From 70b5ab91e5feef32abadcb1c35006b65b2681a98 Mon Sep 17 00:00:00 2001 From: Joakim Date: Fri, 10 Jul 2020 19:00:39 +0300 Subject: [PATCH 01/22] KoaJS: Initial state --- KoaResource.js | 1002 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1002 insertions(+) create mode 100644 KoaResource.js diff --git a/KoaResource.js b/KoaResource.js new file mode 100644 index 0000000..1f3ee33 --- /dev/null +++ b/KoaResource.js @@ -0,0 +1,1002 @@ +'use strict'; + +const paginate = require('node-paginate-anything'); +const jsonpatch = require('fast-json-patch'); +const mongodb = require('mongodb'); +const moment = require('moment'); +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) { + res.status(400).json({ + status: 400, + 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(); + } + + /** + * 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 = new mongodb.ObjectID(value); + } + catch (err) { + console.warn(`Invalid ObjectID: ${value}`); + } + } + + return value; + } + + /** + * Get the find query for the index. + * + * @param req + * @returns {Object} + */ + getFindQuery(req, options) { + 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; + + // 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) { + findQuery[filter.name] = regex; + } + return; + } // See if there is a selector. + else if (filter.selector) { + // Init the filter. + if (!Object.prototype.hasOwnProperty.call(findQuery, filter.name)) { + findQuery[filter.name] = {}; + } + + 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'].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); + } + + findQuery[filter.name][`$${filter.selector}`] = value; + return; + } + else { + // Set the find query to this value. + value = Resource.getQueryValue(filter.name, value, param, options, filter.selector); + findQuery[filter.name] = value; + return; + } + } + + if (!options.queryFilter) { + // Set the find query to this value. + findQuery[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 { + countDocuments(cb) { + query.model.aggregate(stages).exec((err, items) => { + if (err) { + return cb(err); + } + return cb(null, 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, (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 find query. + const findQuery = this.getFindQuery(req); + + // Get the query object. + const countQuery = req.countQuery || req.modelQuery || req.model || this.model; + const query = req.modelQuery || req.model || this.model; + + // First get the total count. + this.countQuery(countQuery.find(findQuery), query.pipeline).countDocuments((err, count) => { + if (err) { + debug.index(err); + return Resource.setResponse(res, { status: 400, error: err }, next); + } + + // Get the default limit. + const defaults = { limit: 10, skip: 0 }; + 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, + () => this.indexQuery(queryExec, query.pipeline).exec((err, items) => { + if (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); + } + + debug.index(items); + options.hooks.index.after.call( + this, + req, + res, + items, + Resource.setResponse.bind(Resource, res, { status: res.statusCode, item: items }, 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, + () => { + query.where(search).lean().exec((err, item) => { + if (err) return Resource.setResponse(res, { status: 400, error: err }, next); + 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) + } + ); + }); + } + ); + }, 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}`, (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); + query.exec((err, item) => { + if (err) return Resource.setResponse(res, { status: 400, error: err }, next); + if (!item) return Resource.setResponse(res, { status: 404 }, next); + return Resource.setResponse(res, { status: 200, item }, 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, + () => { + const writeOptions = req.writeOptions || {}; + model.save(writeOptions, (err, item) => { + if (err) { + debug.post(err); + return Resource.setResponse(res, { status: 400, error: err }, next); + } + + 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) + ); + }); + } + ); + }, 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`, (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; + + query.findOne({ _id: req.params[`${this.name}Id`] }, (err, item) => { + if (err) { + debug.put(err); + return Resource.setResponse(res, { status: 400, error: err }, next); + } + 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, + () => { + const writeOptions = req.writeOptions || {}; + item.save(writeOptions, (err, item) => { + if (err) { + debug.put(err); + return Resource.setResponse(res, { status: 400, error: err }, next); + } + + return options.hooks.put.after.call( + this, + req, + res, + item, + Resource.setResponse.bind(Resource, res, { status: 200, item }, 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`, (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 || {}; + query.findOne({ '_id': req.params[`${this.name}Id`] }, (err, item) => { + if (err) return Resource.setResponse(res, { status: 400, error: err }, next); + 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); + } + } + item.save(writeOptions, (err, item) => { + if (err) return Resource.setResponse(res, { status: 400, error: err }, next); + return Resource.setResponse(res, { status: 200, item }, 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`, (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; + query.findOne({ '_id': req.params[`${this.name}Id`] }, (err, item) => { + if (err) { + debug.delete(err); + return Resource.setResponse(res, { status: 400, error: err }, next); + } + if (!item) { + debug.delete(`No ${this.name} found with ${this.name}Id: ${req.params[`${this.name}Id`]}`); + return Resource.setResponse(res, { status: 404, error: err }, next); + } + if (req.skipDelete) { + return Resource.setResponse(res, { status: 204, item, deleted: true }, next); + } + + options.hooks.delete.before.call( + this, + req, + res, + item, + () => { + const writeOptions = req.writeOptions || {}; + item.remove(writeOptions, (err) => { + if (err) { + debug.delete(err); + return Resource.setResponse(res, { status: 400, error: err }, next); + } + + debug.delete(item); + options.hooks.delete.after.call( + this, + req, + res, + item, + Resource.setResponse.bind(Resource, res, { status: 204, item, deleted: true }, 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; From 0afa100bde01d02f2795d86b2d21874f4c30988b Mon Sep 17 00:00:00 2001 From: Joakim Date: Fri, 10 Jul 2020 19:02:06 +0300 Subject: [PATCH 02/22] Koa: Remove register reverse compatibility --- KoaResource.js | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 1f3ee33..82c6cb3 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -32,20 +32,6 @@ class Resource { 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. From 7db6dd67713012009b645a04b48bc3f0138e2ac0 Mon Sep 17 00:00:00 2001 From: Joakim Date: Fri, 10 Jul 2020 20:07:13 +0300 Subject: [PATCH 03/22] Koa: swithc req and res to ctx and use koa-router --- KoaResource.js | 335 ++++++++++++++++++++++++------------------------- 1 file changed, 162 insertions(+), 173 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 82c6cb3..1dda24c 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -1,5 +1,8 @@ 'use strict'; +const compose = require('koa-compose'); +const Router = require('@koa/router'); + const paginate = require('node-paginate-anything'); const jsonpatch = require('fast-json-patch'); const mongodb = require('mongodb'); @@ -20,6 +23,7 @@ const utils = require('./utils'); class Resource { constructor(app, route, modelName, model, options) { this.app = app; + this.router = new Router(); this.options = options || {}; if (this.options.convertIds === true) { this.options.convertIds = /(^|\.)_id$/; @@ -32,7 +36,6 @@ class Resource { this._swagger = null; } - /** * 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 @@ -41,7 +44,7 @@ class Resource { * @return {function(...[*]=)} */ stackProcessor(stack) { - return (req, res, done) => { + return (ctx, done) => { let layer = 0; (function next(err) { const fn = stack[layer++]; @@ -52,26 +55,24 @@ class Resource { if (err) { switch (fn.length) { case 4: - fn(err, req, res, next); + fn(err, ctx, next); break; case 2: fn(err, next); break; default: - next(err); - break; + throw err; } } else { switch (fn.length) { case 3: - fn(req, res, next); + fn(ctx, next); break; case 1: fn(next); break; default: - next(); break; } } @@ -83,8 +84,8 @@ class Resource { /** * Register a new callback but add before and after options to the middleware. * - * @param method - * @param path + * @param method, string, GET, POST, PUT, PATCH, DEL + * @param path, string, url path to the resource * @param callback * @param last * @param options @@ -108,48 +109,43 @@ class Resource { routeStack = [...routeStack, last.bind(this)]; // Add a fallback error handler. - const error = (err, req, res, next) => { - if (err) { - res.status(400).json({ - status: 400, - message: err.message || err, - }); - } - else { - return next(); + /* const error = async (ctx, next) => { + try { + await next() + } catch (err) { + ctx.status = 400 + ctx.body = err.message || err + ctx.app.emit('error', err, ctx); } - }; - - routeStack = [...routeStack, error.bind(this)] - + } */ // Declare the resourcejs object on the app. - if (!this.app.resourcejs) { - this.app.resourcejs = {}; + if (!this.app.context.resourcejs) { + this.app.context.resourcejs = {}; } - if (!this.app.resourcejs[path]) { - this.app.resourcejs[path] = {}; + if (!this.app.context.resourcejs[path]) { + this.app.context.resourcejs[path] = {}; } // Add a stack processor so this stack can be executed independently of Express. - this.app.resourcejs[path][method] = this.stackProcessor(routeStack); + this.app.context.resourcejs[path][method] = this.stackProcessor(routeStack); // Apply these callbacks to the application. switch (method) { case 'get': - this.app.get(path, routeStack); + this.router.get(path, routeStack); break; case 'post': - this.app.post(path, routeStack); + this.router.post(path, routeStack); break; case 'put': - this.app.put(path, routeStack); + this.router.put(path, routeStack); break; case 'patch': - this.app.patch(path, routeStack); + this.router.patch(path, routeStack); break; case 'delete': - this.app.delete(path, routeStack); + this.router.delete(path, routeStack); break; } } @@ -163,56 +159,59 @@ class Resource { * @param next * The next middleware */ - static respond(req, res, next) { - if (req.noResponse || res.headerSent || res.headersSent) { + static respond(ctx, next) { + if (ctx.headerSent) { debug.respond('Skipping'); return next(); } - if (res.resource) { - switch (res.resource.status) { + if (ctx.resource) { + switch (ctx.resource.status) { case 404: - res.status(404).json({ + ctx.status = 404; + ctx.body = { 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]; + for (const property in ctx.resource.error.errors) { + // eslint-disable-next-line max-depth + if (Object.prototype.hasOwnProperty.call(ctx.resource.error.errors, property)) { + const error = ctx.resource.error.errors[property]; const { path, name, message } = error; - res.resource.error.errors[property] = { path, name, message }; + ctx.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, - }); + ctx.status = ctx.resource.status; + ctx.body = { + status: ctx.resource.status, + message: ctx.resource.error.message, + errors: ctx.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) { + debug.respond(`204 -> ${ctx.__rMethod}`); + switch (ctx.__rMethod) { case 'index': - res.status(200).json([]); + ctx.status = 200; + ctx.body = []; break; default: - res.status(200).json({}); + ctx.status = 200; + ctx.body = {}; break; } break; default: - res.status(res.resource.status).json(res.resource.item); + ctx.status = ctx.resource.status; + ctx.body = ctx.resource.item; break; } } - - next(); } /** @@ -280,7 +279,7 @@ class Resource { utils.set( methodOptions, path, - utils.get(options, path, (req, res, item, next) => next()) + utils.get(options, path, (ctx, item, next) => next()) ); }); @@ -312,8 +311,8 @@ class Resource { * @param name * @returns {*} */ - static getParamQuery(req, name) { - if (!Object.prototype.hasOwnProperty.call(req.query, name)) { + static getParamQuery(ctx, name) { + if (!Object.prototype.hasOwnProperty.call(ctx.query, name)) { switch (name) { case 'populate': return ''; @@ -322,11 +321,11 @@ class Resource { } } - if (name === 'populate' && utils.isObjectLike(req.query[name])) { - return req.query[name]; + if (name === 'populate' && utils.isObjectLike(ctx.query[name])) { + return ctx.query[name]; } else { - const query = ( Array.isArray(req.query[name]) ? req.query[name].join(',') : req.query[name] ); + const query = ( Array.isArray(ctx.query[name]) ? ctx.query[name].join(',') : ctx.query[name] ); // Generate string of spaced unique keys return (query && typeof query === 'string') ? [...new Set(query.match(/[^, ]+/g))].join(' ') : null; } @@ -390,12 +389,12 @@ class Resource { * @param req * @returns {Object} */ - getFindQuery(req, options) { + getFindQuery(ctx, options) { 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; + const { limit, skip, select, sort, populate, ...filters } = ctx.query; // Iterate through each filter. Object.entries(filters).forEach(([name, value]) => { @@ -525,53 +524,53 @@ class Resource { index(options) { options = Resource.getMethodOptions('index', options); this.methods.push('index'); - this._register('get', this.route, (req, res, next) => { + this._register('get', this.route, (ctx, next) => { // Store the internal method for response manipulation. - req.__rMethod = 'index'; + ctx.__rMethod = 'index'; // Allow before handlers the ability to disable resource CRUD. - if (req.skipResource) { + if (ctx.skipResource) { debug.index('Skipping Resource'); return next(); } // Get the find query. - const findQuery = this.getFindQuery(req); + const findQuery = this.getFindQuery(ctx); // Get the query object. - const countQuery = req.countQuery || req.modelQuery || req.model || this.model; - const query = req.modelQuery || req.model || this.model; + const countQuery = ctx.countQuery || ctx.modelQuery || ctx.model || this.model; + const query = ctx.modelQuery || ctx.model || this.model; // First get the total count. this.countQuery(countQuery.find(findQuery), query.pipeline).countDocuments((err, count) => { if (err) { debug.index(err); - return Resource.setResponse(res, { status: 400, error: err }, next); + return Resource.setResponse(ctx, { status: 400, error: err }, next); } // Get the default limit. const defaults = { limit: 10, skip: 0 }; - 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 + let { limit, skip } = ctx.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)}`; + if (reqQuery.skip && !ctx.headers.range) { + ctx.headers['range-unit'] = 'items'; + ctx.headers.range = `${reqQuery.skip}-${reqQuery.skip + (reqQuery.limit - 1)}`; } // Get the page range. - const pageRange = paginate(req, res, count, reqQuery.limit) || { + const pageRange = paginate(ctx, 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) { + if (ctx.headers.range) { reqQuery.limit = pageRange.limit; reqQuery.skip = pageRange.skip; } @@ -581,11 +580,11 @@ class Resource { .find(findQuery) .limit(reqQuery.limit) .skip(reqQuery.skip) - .select(Resource.getParamQuery(req, 'select')) - .sort(Resource.getParamQuery(req, 'sort')); + .select(Resource.getParamQuery(ctx, 'select')) + .sort(Resource.getParamQuery(ctx, 'sort')); // Only call populate if they provide a populate query. - const populate = Resource.getParamQuery(req, 'populate'); + const populate = Resource.getParamQuery(ctx, 'populate'); if (populate) { debug.index(`Populate: ${populate}`); queryExec.populate(populate); @@ -593,8 +592,7 @@ class Resource { options.hooks.index.before.call( this, - req, - res, + ctx, findQuery, () => this.indexQuery(queryExec, query.pipeline).exec((err, items) => { if (err) { @@ -606,16 +604,15 @@ class Resource { debug.index(err.message); } - return Resource.setResponse(res, { status: 400, error: err }, next); + return Resource.setResponse(ctx, { status: 400, error: err }, next); } debug.index(items); options.hooks.index.after.call( this, - req, - res, + ctx, items, - Resource.setResponse.bind(Resource, res, { status: res.statusCode, item: items }, next) + Resource.setResponse.bind(Resource, ctx, { status: ctx.statusCode, item: items }, next) ); }) ); @@ -630,19 +627,19 @@ class Resource { get(options) { options = Resource.getMethodOptions('get', options); this.methods.push('get'); - this._register('get', `${this.route}/:${this.name}Id`, (req, res, next) => { + this._register('get', `${this.route}/:${this.name}Id`, (ctx, next) => { // Store the internal method for response manipulation. - req.__rMethod = 'get'; - if (req.skipResource) { + ctx.__rMethod = 'get'; + if (ctx.skipResource) { debug.get('Skipping Resource'); return next(); } - const query = (req.modelQuery || req.model || this.model).findOne(); - const search = { '_id': req.params[`${this.name}Id`] }; + const query = (ctx.modelQuery || ctx.model || this.model).findOne(); + const search = { '_id': ctx.params[`${this.name}Id`] }; // Only call populate if they provide a populate query. - const populate = Resource.getParamQuery(req, 'populate'); + const populate = Resource.getParamQuery(ctx, 'populate'); if (populate) { debug.get(`Populate: ${populate}`); query.populate(populate); @@ -650,22 +647,20 @@ class Resource { options.hooks.get.before.call( this, - req, - res, + ctx, search, () => { query.where(search).lean().exec((err, item) => { - if (err) return Resource.setResponse(res, { status: 400, error: err }, next); - if (!item) return Resource.setResponse(res, { status: 404 }, next); + if (err) return Resource.setResponse(ctx, { status: 400, error: err }, next); + if (!item) return Resource.setResponse(ctx, { status: 404 }, next); return options.hooks.get.after.call( this, - req, - res, + ctx, item, () => { // Allow them to only return specified fields. - const select = Resource.getParamQuery(req, 'select'); + const select = Resource.getParamQuery(ctx, 'select'); if (select) { const newItem = {}; // Always include the _id. @@ -674,13 +669,13 @@ class Resource { } select.split(' ').map(key => { key = key.trim(); - if (item.hasOwnProperty(key)) { + if (Object.prototype.hasOwnProperty.call(item,key)) { newItem[key] = item[key]; } }); item = newItem; } - Resource.setResponse(res, { status: 200, item: item }, next) + Resource.setResponse(ctx, { status: 200, item: item }, next); } ); }); @@ -700,20 +695,20 @@ class Resource { const path = options.path; options = Resource.getMethodOptions('virtual', options); this.methods.push(`virtual/${path}`); - this._register('get', `${this.route}/virtual/${path}`, (req, res, next) => { + this._register('get', `${this.route}/virtual/${path}`, (ctx, next) => { // Store the internal method for response manipulation. - req.__rMethod = 'virtual'; + ctx.__rMethod = 'virtual'; - if (req.skipResource) { + if (ctx.skipResource) { debug.virtual('Skipping Resource'); return next(); } - const query = req.modelQuery || req.model; - if (!query) return Resource.setResponse(res, { status: 404 }, next); + const query = ctx.modelQuery || ctx.model; + if (!query) return Resource.setResponse(ctx, { status: 404 }, next); query.exec((err, item) => { - if (err) return Resource.setResponse(res, { status: 400, error: err }, next); - if (!item) return Resource.setResponse(res, { status: 404 }, next); - return Resource.setResponse(res, { status: 200, item }, next); + if (err) return Resource.setResponse(ctx, { status: 400, error: err }, next); + if (!item) return Resource.setResponse(ctx, { status: 404 }, next); + return Resource.setResponse(ctx, { status: 200, item }, next); }); }, Resource.respond, options); return this; @@ -725,38 +720,36 @@ class Resource { post(options) { options = Resource.getMethodOptions('post', options); this.methods.push('post'); - this._register('post', this.route, (req, res, next) => { + this._register('post', this.route, (ctx, next) => { // Store the internal method for response manipulation. - req.__rMethod = 'post'; + ctx.__rMethod = 'post'; - if (req.skipResource) { + if (ctx.skipResource) { debug.post('Skipping Resource'); return next(); } - const Model = req.model || this.model; - const model = new Model(req.body); + const Model = ctx.model || this.model; + const model = new Model(ctx.request.body); options.hooks.post.before.call( this, - req, - res, - req.body, + ctx, + ctx.request.body, () => { - const writeOptions = req.writeOptions || {}; + const writeOptions = ctx.writeOptions || {}; model.save(writeOptions, (err, item) => { if (err) { debug.post(err); - return Resource.setResponse(res, { status: 400, error: err }, next); + return Resource.setResponse(ctx, { status: 400, error: err }, next); } debug.post(item); // Trigger any after hooks before responding. return options.hooks.post.after.call( this, - req, - res, + ctx, item, - Resource.setResponse.bind(Resource, res, { status: 201, item }, next) + Resource.setResponse.bind(Resource, ctx, { status: 201, item }, next) ); }); } @@ -771,49 +764,47 @@ class Resource { put(options) { options = Resource.getMethodOptions('put', options); this.methods.push('put'); - this._register('put', `${this.route}/:${this.name}Id`, (req, res, next) => { + this._register('put', `${this.route}/:${this.name}Id`, (ctx, next) => { // Store the internal method for response manipulation. - req.__rMethod = 'put'; + ctx.__rMethod = 'put'; - if (req.skipResource) { + if (ctx.skipResource) { debug.put('Skipping Resource'); return next(); } // Remove __v field - const { __v, ...update} = req.body; - const query = req.modelQuery || req.model || this.model; + const { __v, ...update } = ctx.request.body; + const query = ctx.modelQuery || ctx.model || this.model; - query.findOne({ _id: req.params[`${this.name}Id`] }, (err, item) => { + query.findOne({ _id: ctx.params[`${this.name}Id`] }, (err, item) => { if (err) { debug.put(err); - return Resource.setResponse(res, { status: 400, error: err }, next); + return Resource.setResponse(ctx, { status: 400, error: err }, next); } if (!item) { - debug.put(`No ${this.name} found with ${this.name}Id: ${req.params[`${this.name}Id`]}`); - return Resource.setResponse(res, { status: 404 }, next); + debug.put(`No ${this.name} found with ${this.name}Id: ${ctx.params[`${this.name}Id`]}`); + return Resource.setResponse(ctx, { status: 404 }, next); } item.set(update); options.hooks.put.before.call( this, - req, - res, + ctx, item, () => { - const writeOptions = req.writeOptions || {}; + const writeOptions = ctx.writeOptions || {}; item.save(writeOptions, (err, item) => { if (err) { debug.put(err); - return Resource.setResponse(res, { status: 400, error: err }, next); + return Resource.setResponse(ctx, { status: 400, error: err }, next); } return options.hooks.put.after.call( this, - req, - res, + ctx, item, - Resource.setResponse.bind(Resource, res, { status: 200, item }, next) + Resource.setResponse.bind(Resource, ctx, { status: 200, item }, next) ); }); }); @@ -828,22 +819,22 @@ class Resource { patch(options) { options = Resource.getMethodOptions('patch', options); this.methods.push('patch'); - this._register('patch', `${this.route}/:${this.name}Id`, (req, res, next) => { + this._register('patch', `${this.route}/:${this.name}Id`, (ctx, next) => { // Store the internal method for response manipulation. - req.__rMethod = 'patch'; + ctx.__rMethod = 'patch'; - if (req.skipResource) { + if (ctx.skipResource) { debug.patch('Skipping Resource'); return next(); } - const query = req.modelQuery || req.model || this.model; - const writeOptions = req.writeOptions || {}; - query.findOne({ '_id': req.params[`${this.name}Id`] }, (err, item) => { - if (err) return Resource.setResponse(res, { status: 400, error: err }, next); - if (!item) return Resource.setResponse(res, { status: 404, error: err }, next); + const query = ctx.modelQuery || ctx.model || this.model; + const writeOptions = ctx.writeOptions || {}; + query.findOne({ '_id': ctx.params[`${this.name}Id`] }, (err, item) => { + if (err) return Resource.setResponse(ctx, { status: 400, error: err }, next); + if (!item) return Resource.setResponse(ctx, { status: 404, error: err }, next); // Ensure patches is an array - const patches = [].concat(req.body); + const patches = [].concat(ctx.request.body); let patchFail = null; try { patches.forEach((patch) => { @@ -851,7 +842,7 @@ class Resource { patchFail = patch; const success = jsonpatch.applyOperation(item, patch, true); if (!success || !success.test) { - return Resource.setResponse(res, { + return Resource.setResponse(ctx, { status: 412, name: 'Precondition Failed', message: 'A json-patch test op has failed. No changes have been applied to the document', @@ -867,7 +858,7 @@ class Resource { switch (err.name) { // Check whether JSON PATCH error case 'TEST_OPERATION_FAILED': - return Resource.setResponse(res, { + return Resource.setResponse(ctx, { status: 412, name: 'Precondition Failed', message: 'A json-patch test op has failed. No changes have been applied to the document', @@ -890,19 +881,19 @@ class Resource { name: err.name, message: err.toString(), }]; - return Resource.setResponse(res, { + return Resource.setResponse(ctx, { status: 400, item, error: err, }, next); // Something else than JSON PATCH default: - return Resource.setResponse(res, { status: 400, item, error: err }, next); + return Resource.setResponse(ctx, { status: 400, item, error: err }, next); } } item.save(writeOptions, (err, item) => { - if (err) return Resource.setResponse(res, { status: 400, error: err }, next); - return Resource.setResponse(res, { status: 200, item }, next); + if (err) return Resource.setResponse(ctx, { status: 400, error: err }, next); + return Resource.setResponse(ctx, { status: 200, item }, next); }); }); }, Resource.respond, options); @@ -915,49 +906,47 @@ class Resource { delete(options) { options = Resource.getMethodOptions('delete', options); this.methods.push('delete'); - this._register('delete', `${this.route}/:${this.name}Id`, (req, res, next) => { + this._register('delete', `${this.route}/:${this.name}Id`, (ctx, next) => { // Store the internal method for response manipulation. - req.__rMethod = 'delete'; + ctx.__rMethod = 'delete'; - if (req.skipResource) { + if (ctx.skipResource) { debug.delete('Skipping Resource'); return next(); } - const query = req.modelQuery || req.model || this.model; - query.findOne({ '_id': req.params[`${this.name}Id`] }, (err, item) => { + const query = ctx.modelQuery || ctx.model || this.model; + query.findOne({ '_id': ctx.params[`${this.name}Id`] }, (err, item) => { if (err) { debug.delete(err); - return Resource.setResponse(res, { status: 400, error: err }, next); + return Resource.setResponse(ctx, { status: 400, error: err }, next); } if (!item) { - debug.delete(`No ${this.name} found with ${this.name}Id: ${req.params[`${this.name}Id`]}`); - return Resource.setResponse(res, { status: 404, error: err }, next); + debug.delete(`No ${this.name} found with ${this.name}Id: ${ctx.params[`${this.name}Id`]}`); + return Resource.setResponse(ctx, { status: 404, error: err }, next); } - if (req.skipDelete) { - return Resource.setResponse(res, { status: 204, item, deleted: true }, next); + if (ctx.skipDelete) { + return Resource.setResponse(ctx, { status: 204, item, deleted: true }, next); } options.hooks.delete.before.call( this, - req, - res, + ctx, item, () => { - const writeOptions = req.writeOptions || {}; + const writeOptions = ctx.writeOptions || {}; item.remove(writeOptions, (err) => { if (err) { debug.delete(err); - return Resource.setResponse(res, { status: 400, error: err }, next); + return Resource.setResponse(ctx, { status: 400, error: err }, next); } debug.delete(item); options.hooks.delete.after.call( this, - req, - res, + ctx, item, - Resource.setResponse.bind(Resource, res, { status: 204, item, deleted: true }, next) + Resource.setResponse.bind(Resource, ctx, { status: 204, item, deleted: true }, next) ); }); } From 47e8d93495145ed792442f72ee0798cf42fc6e75 Mon Sep 17 00:00:00 2001 From: Joakim Date: Sat, 11 Jul 2020 13:19:39 +0300 Subject: [PATCH 04/22] Koa: Add copy of test.js as test base --- package-lock.json | 558 +++++++--- package.json | 8 +- test/testKoa.js | 2649 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 3040 insertions(+), 175 deletions(-) create mode 100644 test/testKoa.js diff --git a/package-lock.json b/package-lock.json index 2b0473d..accdb8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -297,6 +297,47 @@ "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", "dev": true }, + "@koa/router": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-9.3.1.tgz", + "integrity": "sha512-OOy4pOEO+Zz5vy+zqc8mWRGKYIpDqjgbVTF/U41fCwBwVWHGmkedvcJ9V5MLI7Ivy0iTv8o0XLDtGWtYHquvxg==", + "requires": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "koa-compose": "^4.1.0", + "methods": "^1.1.2", + "path-to-regexp": "^6.1.0" + }, + "dependencies": { + "http-errors": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz", + "integrity": "sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "path-to-regexp": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.1.0.tgz", + "integrity": "sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==" + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + } + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -332,7 +373,6 @@ "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, "requires": { "mime-types": "~2.1.24", "negotiator": "0.6.2" @@ -379,9 +419,9 @@ "dev": true }, "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true }, "ansi-styles": { @@ -393,6 +433,11 @@ "color-convert": "^1.9.0" } }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, "anymatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", @@ -559,9 +604,9 @@ "dev": true }, "bson": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/bson/-/bson-4.5.1.tgz", - "integrity": "sha512-XqFP74pbTVLyLy5KFxVfTUyRrC1mgOlmu/iXHfXqfCKT59jyP9lwbotGfbN59cHBRbJSamZNkrSopjv+N0SqAA==", + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.6.1.tgz", + "integrity": "sha512-I1LQ7Hz5zgwR4QquilLNZwbhPw0Apx7i7X9kGMBTsqPdml/03Q9NBtD9nt/19ahjlphktQImrnderxqpzeVDjw==", "dev": true, "requires": { "buffer": "^5.6.0" @@ -579,8 +624,16 @@ "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==", - "dev": true + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } }, "caching-transform": { "version": "4.0.0", @@ -680,9 +733,9 @@ "dev": true }, "chokidar": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", - "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "requires": { "anymatch": "~3.1.2", @@ -712,6 +765,22 @@ "wrap-ansi": "^7.0.0" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "co-body": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.0.0.tgz", + "integrity": "sha512-9ZIcixguuuKIptnY8yemEOuhb71L/lLf+Rl5JfJEUiDNJk0e02MBt7BPxR2GEh5mw8dPthQYR4jPI/BnS1MQgw==", + "requires": { + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + } + }, "color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -758,7 +827,6 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", - "dev": true, "requires": { "safe-buffer": "5.1.2" }, @@ -766,16 +834,14 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } }, "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, "convert-source-map": { "version": "1.7.0", @@ -812,6 +878,27 @@ "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", "dev": true }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, + "copy-to": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/copy-to/-/copy-to-2.0.1.tgz", + "integrity": "sha1-JoD7uAaKSNCGVrYJgJK9r8kG9KU=" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -865,6 +952,11 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -886,6 +978,11 @@ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", "dev": true }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, "denque": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz", @@ -894,14 +991,12 @@ "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" }, "destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, "diff": { "version": "5.0.0", @@ -931,8 +1026,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", - "dev": true + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, "emoji-regex": { "version": "8.0.0", @@ -943,8 +1037,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, "enquirer": { "version": "2.3.6", @@ -970,8 +1063,7 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", - "dev": true + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" }, "escape-string-regexp": { "version": "1.0.5", @@ -1406,8 +1498,7 @@ "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" }, "fromentries": { "version": "1.2.0", @@ -1589,11 +1680,19 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + } + }, "http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "dev": true, "requires": { "depd": "~1.1.2", "inherits": "2.0.3", @@ -1617,7 +1716,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3" } @@ -1655,6 +1753,11 @@ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true }, + "inflation": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", + "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1668,7 +1771,12 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", "dev": true }, "ipaddr.js": { @@ -1698,6 +1806,11 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -1901,12 +2014,6 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -1935,23 +2042,117 @@ } }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "dev": true, "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" + }, + "dependencies": { + "json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + } } }, "kareem": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.2.tgz", - "integrity": "sha512-STHz9P7X2L4Kwn72fA4rGyqyXdmrMSdxqHx9IXon/FXluXieaFA6KJ2upcHAHxQPQ0LeM/OjLrhFxifHewOALQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/kareem/-/kareem-2.3.3.tgz", + "integrity": "sha512-uESCXM2KdtOQ8LOvKyTUXEeg0MkYp4wGglTIpGcYHvjJcS5sn2Wkfrfit8m4xSbaNDAw2KdI9elgkOxZbrFYbg==", "dev": true }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "requires": { + "tsscmp": "1.0.6" + } + }, + "koa": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.13.0.tgz", + "integrity": "sha512-i/XJVOfPw7npbMv67+bOeXr3gPqOAw6uh5wFyNs3QvJ47tUx3M3V9rIE0//WytY42MKz4l/MXKyGkQ2LQTfLUQ==", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "koa-bodyparser": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/koa-bodyparser/-/koa-bodyparser-4.3.0.tgz", + "integrity": "sha512-uyV8G29KAGwZc4q/0WUAjH+Tsmuv9ImfBUF2oZVyZtaeo0husInagyn/JH85xMSxM0hEk/mbCII5ubLDuqW/Rw==", + "requires": { + "co-body": "^6.0.0", + "copy-to": "^2.0.1" + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, "lcov-parse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", @@ -2052,8 +2253,7 @@ "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", - "dev": true + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" }, "memory-pager": { "version": "1.5.0", @@ -2070,8 +2270,7 @@ "methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" }, "mime": { "version": "1.6.0", @@ -2082,14 +2281,12 @@ "mime-db": { "version": "1.42.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.42.0.tgz", - "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==", - "dev": true + "integrity": "sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==" }, "mime-types": { "version": "2.1.25", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.25.tgz", "integrity": "sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==", - "dev": true, "requires": { "mime-db": "1.42.0" } @@ -2110,33 +2307,32 @@ "dev": true }, "mocha": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.0.tgz", - "integrity": "sha512-Kjg/XxYOFFUi0h/FwMOeb6RoroiZ+P1yOfya6NK7h3dNhahrJx1r2XIT3ge4ZQvJM86mdjNA+W5phqRQh7DwCg==", + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.2.0.tgz", + "integrity": "sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==", "dev": true, "requires": { "@ungap/promise-all-settled": "1.1.2", "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", - "chokidar": "3.5.2", - "debug": "4.3.1", + "chokidar": "3.5.3", + "debug": "4.3.3", "diff": "5.0.0", "escape-string-regexp": "4.0.0", "find-up": "5.0.0", - "glob": "7.1.7", + "glob": "7.2.0", "growl": "1.10.5", "he": "1.2.0", "js-yaml": "4.1.0", "log-symbols": "4.1.0", "minimatch": "3.0.4", "ms": "2.1.3", - "nanoid": "3.1.23", + "nanoid": "3.2.0", "serialize-javascript": "6.0.0", "strip-json-comments": "3.1.1", "supports-color": "8.1.1", "which": "2.0.2", - "wide-align": "1.1.3", - "workerpool": "6.1.5", + "workerpool": "6.2.0", "yargs": "16.2.0", "yargs-parser": "20.2.4", "yargs-unparser": "2.0.0" @@ -2149,9 +2345,9 @@ "dev": true }, "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", + "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", "dev": true, "requires": { "ms": "2.1.2" @@ -2182,9 +2378,9 @@ } }, "glob": { - "version": "7.1.7", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", - "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", @@ -2290,37 +2486,89 @@ } }, "mongoose": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.0.2.tgz", - "integrity": "sha512-4zrCcScAItE9aefaPMeG9+IPLaJJ2c8sFLXlLvTVPdArZija10rto61qkzIxmhf6zkDmW2W9bh3DS59KuTPCfQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.2.1.tgz", + "integrity": "sha512-VxY1wvlc4uBQKyKNVDoEkTU3/ayFOD//qVXYP+sFyvTRbAj9/M53UWTERd84pWogs2TqAC6DTvZbxCs2LoOd3Q==", "dev": true, "requires": { "bson": "^4.2.2", - "kareem": "2.3.2", - "mongodb": "4.1.1", - "mpath": "0.8.3", - "mquery": "4.0.0", + "kareem": "2.3.3", + "mongodb": "4.3.1", + "mpath": "0.8.4", + "mquery": "4.0.2", "ms": "2.1.2", - "regexp-clone": "1.0.0", - "sift": "13.5.2", - "sliced": "1.0.1" + "sift": "13.5.2" + }, + "dependencies": { + "denque": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.0.1.tgz", + "integrity": "sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ==", + "dev": true + }, + "mongodb": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.3.1.tgz", + "integrity": "sha512-sNa8APSIk+r4x31ZwctKjuPSaeKuvUeNb/fu/3B6dRM02HpEgig7hTHM8A/PJQTlxuC/KFWlDlQjhsk/S43tBg==", + "dev": true, + "requires": { + "bson": "^4.6.1", + "denque": "^2.0.1", + "mongodb-connection-string-url": "^2.4.1", + "saslprep": "^1.0.3", + "socks": "^2.6.1" + } + }, + "mongodb-connection-string-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-2.4.2.tgz", + "integrity": "sha512-mZUXF6nUzRWk5J3h41MsPv13ukWlH4jOMSk6astVeoZ1EbdTJyF5I3wxKkvqBAOoVtzLgyEYUvDjrGdcPlKjAw==", + "dev": true, + "requires": { + "@types/whatwg-url": "^8.2.1", + "whatwg-url": "^11.0.0" + } + }, + "tr46": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz", + "integrity": "sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true + }, + "whatwg-url": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz", + "integrity": "sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==", + "dev": true, + "requires": { + "tr46": "^3.0.0", + "webidl-conversions": "^7.0.0" + } + } } }, "mpath": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.3.tgz", - "integrity": "sha512-eb9rRvhDltXVNL6Fxd2zM9D4vKBxjVVQNLNijlj7uoXUy19zNDsIif5zR+pWmPCWNKwAtqyo4JveQm4nfD5+eA==", + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.8.4.tgz", + "integrity": "sha512-DTxNZomBcTWlrMW76jy1wvV37X/cNNxPW1y2Jzd4DZkAaC5ZGsm8bfGfNOthcDuRJujXLqiuS6o3Tpy0JEoh7g==", "dev": true }, "mquery": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.0.tgz", - "integrity": "sha512-nGjm89lHja+T/b8cybAby6H0YgA4qYC/lx6UlwvHGqvTq8bDaNeCwl1sY8uRELrNbVWJzIihxVd+vphGGn1vBw==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/mquery/-/mquery-4.0.2.tgz", + "integrity": "sha512-oAVF0Nil1mT3rxty6Zln4YiD6x6QsUWYz927jZzjMxOK2aqmhEz5JQ7xmrKK7xRFA2dwV+YaOpKU/S+vfNqKxA==", "dev": true, "requires": { - "debug": "4.x", - "regexp-clone": "^1.0.0", - "sliced": "1.0.1" + "debug": "4.x" } }, "ms": { @@ -2329,9 +2577,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "nanoid": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz", + "integrity": "sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==", "dev": true }, "natural-compare": { @@ -2343,8 +2591,7 @@ "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "node-paginate-anything": { "version": "1.0.0", @@ -2533,7 +2780,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, "requires": { "ee-first": "1.1.1" } @@ -2547,6 +2793,11 @@ "wrappy": "1" } }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -2618,8 +2869,7 @@ "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" }, "path-exists": { "version": "4.0.0", @@ -2658,9 +2908,9 @@ "dev": true }, "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, "pkg-dir": { @@ -2717,8 +2967,7 @@ "qs": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, "randombytes": { "version": "2.1.0", @@ -2738,7 +2987,6 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "dev": true, "requires": { "bytes": "3.1.0", "http-errors": "1.7.2", @@ -2766,12 +3014,6 @@ "picomatch": "^2.2.1" } }, - "regexp-clone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", - "integrity": "sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw==", - "dev": true - }, "regexpp": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", @@ -2874,8 +3116,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "saslprep": { "version": "1.0.3", @@ -2968,8 +3209,7 @@ "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==", - "dev": true + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, "shebang-command": { "version": "2.0.0", @@ -3046,12 +3286,22 @@ } } }, - "sliced": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", - "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=", + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true }, + "socks": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.6.2.tgz", + "integrity": "sha512-zDZhHhZRY9PxRruRMR7kMhnf3I8hDs4S3f9RecfnGxvcBHQcKcIH/oUcEWffsfl1XxdYlA7nnlGbbTvPz9D8gA==", + "dev": true, + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.2.0" + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -3118,8 +3368,7 @@ "statuses": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=", - "dev": true + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" }, "string-width": { "version": "4.2.2", @@ -3306,8 +3555,7 @@ "toidentifier": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==", - "dev": true + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "tough-cookie": { "version": "2.5.0", @@ -3327,6 +3575,11 @@ "punycode": "^2.1.1" } }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -3361,7 +3614,6 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, "requires": { "media-typer": "0.3.0", "mime-types": "~2.1.24" @@ -3379,8 +3631,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", - "dev": true + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" }, "uri-js": { "version": "4.4.1", @@ -3418,8 +3669,7 @@ "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=", - "dev": true + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "verror": { "version": "1.10.0", @@ -3461,48 +3711,6 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -3510,9 +3718,9 @@ "dev": true }, "workerpool": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", - "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.0.tgz", + "integrity": "sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==", "dev": true }, "wrap-ansi": { @@ -3572,8 +3780,7 @@ }, "y18n": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", + "resolved": "", "dev": true }, "yallist": { @@ -3624,9 +3831,9 @@ }, "dependencies": { "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true }, "decamelize": { @@ -3637,6 +3844,11 @@ } } }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" + }, "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index bc08079..e852487 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,12 @@ }, "homepage": "https://github.com/travist/resourcejs", "dependencies": { + "@koa/router": "^9.3.1", "debug": "^4.3.2", "fast-json-patch": "^3.1.0", + "koa": "^2.13.0", + "koa-bodyparser": "^4.3.0", + "koa-compose": "^4.1.0", "moment": "^2.29.1", "mongodb": "^4.1.1", "node-paginate-anything": "^1.0.0", @@ -39,8 +43,8 @@ "eslint": "^7.32.0", "eslint-config-formio": "^1.1.4", "express": "^4.17.1", - "mocha": "^9.1.0", - "mongoose": "^6.0.2", + "mocha": "^9.2.0", + "mongoose": "^6.2.1", "nyc": "^15.1.0", "supertest": "^6.1.6" }, diff --git a/test/testKoa.js b/test/testKoa.js new file mode 100644 index 0000000..4567a19 --- /dev/null +++ b/test/testKoa.js @@ -0,0 +1,2649 @@ +/* eslint-disable no-prototype-builtins */ +'use strict'; + +const express = require('express'); +const bodyParser = require('body-parser'); +const request = require('supertest'); +const assert = require('assert'); +const moment = require('moment'); +const mongoose = require('mongoose'); +const Resource = require('../Resource'); +const app = express(); +const _ = require('lodash'); +const MongoClient = require('mongodb').MongoClient; +const ObjectId = require('mongodb').ObjectID; +const chance = (new require('chance'))(); + +const baseTestDate = moment.utc('2018-04-12T12:00:00.000Z'); +const testDates = [ + baseTestDate, // actual + moment(baseTestDate).subtract(1, 'day'), // oneDayAgo + moment(baseTestDate).subtract(1, 'month'), // oneMonthAgo + moment(baseTestDate).subtract(1, 'year'), // oneYearAgo +]; + +// Use the body parser. +app.use(bodyParser.urlencoded({ extended: true })); +app.use(bodyParser.json()); + +// An object to store handler events. +let handlers = {}; + +// The raw connection to mongo, for consistency checks with mongoose. +let db = null; + +/** + * Updates the reference for the handler invocation using the given sequence and method. + * + * @param entity + * The entity this handler is associated with. + * @param sequence + * The sequence of invocation: `before` or `after`. + * @param req + * The express request to manipulate. + */ +function setInvoked(entity, sequence, req) { + // Get the url fragments, to determine if this request is a get or index. + const parts = req.url.split('/'); + parts.shift(); // Remove empty string element. + + let method = req.method.toLowerCase(); + if (method === 'get' && (parts.length % 2 === 0)) { + method = 'index'; + } + + if (!handlers.hasOwnProperty(entity)) { + handlers[entity] = {}; + } + if (!handlers[entity].hasOwnProperty(sequence)) { + handlers[entity][sequence] = {}; + } + + handlers[entity][sequence][method] = true; +} + +/** + * Determines if the handler for the sequence and method was invoked. + * + * @param entity + * The entity this handler is associated with. + * @param sequence + * The sequence of invocation: `before` or `after`. + * @param method + * The HTTP method for the invocation: `post`, `get`, `put`, `delete`, or `patch` + * + * @return + * If the given handler was invoked or not. + */ +function wasInvoked(entity, sequence, method) { + if ( + handlers.hasOwnProperty(entity) + && handlers[entity].hasOwnProperty(sequence) + && handlers[entity][sequence].hasOwnProperty(method) + ) { + return handlers[entity][sequence][method]; + } + else { + return false; + } +} + +describe('Connect to MongoDB', () => { + it('Connect to MongoDB', () => mongoose.connect('mongodb://localhost:27017/test', { + useCreateIndex: true, + useUnifiedTopology: true, + useNewUrlParser: true, + })); + + it('Drop test database', () => mongoose.connection.db.dropDatabase()); + + it('Should connect MongoDB without mongoose', () => MongoClient.connect('mongodb://localhost:27017', { + useCreateIndex: true, + useUnifiedTopology: true, + useNewUrlParser: true, + }) + .then((client) => db = client.db('test'))); +}); + +describe('Build Resources for following tests', () => { + it('Build the /test/ref endpoints', () => { + // Create the schema. + const RefSchema = new mongoose.Schema({ + data: String, + }, { collection: 'ref' }); + + // Create the model. + const RefModel = mongoose.model('ref', RefSchema); + + // Create the REST resource and continue. + const test = Resource(app, '/test', 'ref', RefModel).rest(); + const testSwaggerio = require('./snippets/testSwaggerio.json'); + const swaggerio = test.swagger(); + assert.equal(Object.values(swaggerio).length, 2); + assert.ok(swaggerio.definitions); + assert.ok(swaggerio.definitions.ref); + assert.equal(swaggerio.definitions.ref.title, 'ref'); + assert.equal(Object.values(swaggerio.paths).length, 2); + assert.deepEqual(swaggerio, testSwaggerio); + }); + + it('Build the /test/resource1 endpoints', () => { + // Create the schema. + const R1SubdocumentSchema = new mongoose.Schema({ + label: { + type: String, + }, + data: [{ + type: mongoose.Schema.Types.ObjectId, + ref: 'ref', + }], + }, { _id: false }); + + const Resource1Schema = new mongoose.Schema({ + title: { + type: String, + required: true, + }, + name: { + type: String, + }, + age: { + type: Number, + }, + description: { + type: String, + }, + list: [R1SubdocumentSchema], + list2: [String], + }); + + // Create the model. + const Resource1Model = mongoose.model('resource1', Resource1Schema); + + // Create the REST resource and continue. + const resource1 = Resource(app, '/test', 'resource1', Resource1Model).rest({ + afterDelete(req, res, next) { + // Check that the delete item is still being returned via resourcejs. + assert.notEqual(res.resource.item, {}); + assert.notEqual(res.resource.item, []); + assert.equal(res.resource.status, 204); + assert.equal(res.statusCode, 200); + next(); + }, + }); + const resource1Swaggerio = require('./snippets/resource1Swaggerio.json'); + const swaggerio = resource1.swagger(); + assert.equal(Object.values(swaggerio).length, 2); + assert.ok(swaggerio.definitions); + assert.ok(swaggerio.definitions.resource1); + assert.equal(swaggerio.definitions.resource1.title, 'resource1'); + assert.equal(Object.values(swaggerio.paths).length, 2); + assert.deepEqual(swaggerio, resource1Swaggerio); + }); + + it('Build the /test/resource2 endpoints', () => { + // Create the schema. + const Resource2Schema = new mongoose.Schema({ + title: { + type: String, + required: true, + }, + age: { + type: Number, + }, + married: { + type: Boolean, + default: false, + }, + updated: { + type: Number, + default: null, + }, + description: { + type: String, + }, + }); + + // Create the model. + const Resource2Model = mongoose.model('resource2', Resource2Schema); + + // Create the REST resource and continue. + const resource2 = Resource(app, '/test', 'resource2', Resource2Model).rest({ + // Register before/after global handlers. + before(req, res, next) { + // Store the invoked handler and continue. + setInvoked('resource2', 'before', req); + next(); + }, + beforePost(req, res, next) { + // Store the invoked handler and continue. + setInvoked('resource2', 'beforePost', req); + next(); + }, + after(req, res, next) { + // Store the invoked handler and continue. + setInvoked('resource2', 'after', req); + next(); + }, + afterPost(req, res, next) { + // Store the invoked handler and continue. + setInvoked('resource2', 'afterPost', req); + next(); + }, + }); + const resource2Swaggerio = require('./snippets/resource2Swaggerio.json'); + const swaggerio = resource2.swagger(); + assert.equal(Object.values(swaggerio).length, 2); + assert.ok(swaggerio.definitions); + assert.ok(swaggerio.definitions.resource2); + assert.equal(swaggerio.definitions.resource2.title, 'resource2'); + assert.equal(Object.values(swaggerio.paths).length, 2); + assert.deepEqual(swaggerio, resource2Swaggerio); + }); + + it('Build the /test/date endpoints and fill it with data', () => { + const Schema = new mongoose.Schema({ + date: { + type: Date, + }, + }); + + // Create the model. + const Model = mongoose.model('date', Schema); + + const date = Resource(app, '/test', 'date', Model).rest(); + const resource3Swaggerio = require('./snippets/dateSwaggerio.json'); + const swaggerio = date.swagger(); + assert.equal(Object.values(swaggerio).length, 2); + assert.ok(swaggerio.definitions); + assert.ok(swaggerio.definitions.date); + assert.equal(swaggerio.definitions.date.title, 'date'); + assert.equal(Object.values(swaggerio.paths).length, 2); + assert.deepEqual(swaggerio, resource3Swaggerio); + return Promise.all(testDates.map((date) => request(app) + .post('/test/date') + .send({ + date: date.toDate(), + }))); + }); + + it('Build the /test/resource1/:resource1Id/nested1 endpoints', () => { + // Create the schema. + const Nested1Schema = new mongoose.Schema({ + resource1: { + type: mongoose.Schema.Types.ObjectId, + ref: 'resource1', + index: true, + required: true, + }, + title: { + type: String, + required: true, + }, + age: { + type: Number, + }, + description: { + type: String, + }, + }); + + // Create the model. + const Nested1Model = mongoose.model('nested1', Nested1Schema); + + // Create the REST resource and continue. + const nested1 = Resource(app, '/test/resource1/:resource1Id', 'nested1', Nested1Model).rest({ + // Register before global handlers to set the resource1 variable. + before(req, res, next) { + req.body.resource1 = req.params.resource1Id; + next(); + }, + }); + const nested1Swaggerio = require('./snippets/nested1Swaggerio.json'); + const swaggerio = nested1.swagger(); + assert.equal(Object.values(swaggerio).length, 2); + assert.ok(swaggerio.definitions); + assert.ok(swaggerio.definitions.nested1); + assert.equal(swaggerio.definitions.nested1.title, 'nested1'); + assert.equal(Object.values(swaggerio.paths).length, 2); + assert.deepEqual(swaggerio, nested1Swaggerio); + }); + + it('Build the /test/resource2/:resource2Id/nested2 endpoints', () => { + // Create the schema. + const Nested2Schema = new mongoose.Schema({ + resource2: { + type: mongoose.Schema.Types.ObjectId, + ref: 'resource2', + index: true, + required: true, + }, + title: { + type: String, + required: true, + }, + age: { + type: Number, + }, + description: { + type: String, + }, + }); + + // Create the model. + const Nested2Model = mongoose.model('nested2', Nested2Schema); + + // Create the REST resource and continue. + const nested2 = Resource(app, '/test/resource2/:resource2Id', 'nested2', Nested2Model).rest({ + // Register before/after global handlers. + before(req, res, next) { + req.body.resource2 = req.params.resource2Id; + req.modelQuery = this.model.where('resource2', req.params.resource2Id); + + // Store the invoked handler and continue. + setInvoked('nested2', 'before', req); + next(); + }, + after(req, res, next) { + // Store the invoked handler and continue. + setInvoked('nested2', 'after', req); + next(); + }, + }); + const nested2Swaggerio = require('./snippets/nested2Swaggerio.json'); + const swaggerio = nested2.swagger(); + assert.equal(Object.values(swaggerio).length, 2); + assert.ok(swaggerio.definitions); + assert.ok(swaggerio.definitions.nested2); + assert.equal(swaggerio.definitions.nested2.title, 'nested2'); + assert.equal(Object.values(swaggerio.paths).length, 2); + assert.deepEqual(swaggerio, nested2Swaggerio); + }); + + it('Build the /test/resource3 endpoints', () => { + // Create the schema. + const Resource3Schema = new mongoose.Schema({ + title: String, + writeOption: String, + }); + + Resource3Schema.pre('save', function(next, options) { + if (options && options.writeSetting) { + return next(); + } + + next(new Error('Save options not passed to middleware')); + }); + + Resource3Schema.pre('remove', function(next, options) { + if (options && options.writeSetting) { + return next(); + } + + return next(new Error('DeleteOptions not passed to middleware')); + }); + + // Create the model. + const Resource3Model = mongoose.model('resource3', Resource3Schema); + + // Create the REST resource and continue. + const resource3 = Resource(app, '/test', 'resource3', Resource3Model).rest({ + before(req, res, next) { + // This setting should be passed down to the underlying `save()` command + req.writeOptions = { writeSetting: true }; + + next(); + }, + }); + const resource3Swaggerio = require('./snippets/resource3Swaggerio.json'); + const swaggerio = resource3.swagger(); + assert.equal(Object.values(swaggerio).length, 2); + assert.ok(swaggerio.definitions); + assert.ok(swaggerio.definitions.resource3); + assert.equal(swaggerio.definitions.resource3.title, 'resource3'); + assert.equal(Object.values(swaggerio.paths).length, 2); + assert.deepEqual(swaggerio, resource3Swaggerio); + }); + + it('Build the /test/resource4 endpoints', () => { + // Create the schema. + const Resource4Schema = new mongoose.Schema({ + title: String, + writeOption: String, + }); + + // Create the model. + const Resource4Model = mongoose.model('resource4', Resource4Schema); + + const doc = new Resource4Model({ title: 'Foo' }); + doc.save(); + + // Create the REST resource and continue. + const resource4 = Resource(app, '/test', 'resource4', Resource4Model) + .rest({ + beforePatch(req, res, next) { + req.modelQuery = { + findOne: function findOne(_, callback) { + callback(new Error('failed'), undefined); + }, + }; + next(); + }, + }) + .virtual({ + path: 'undefined_query', + before: function(req, res, next) { + req.modelQuery = undefined; + return next(); + }, + }) + .virtual({ + path: 'defined', + before: function(req, res, next) { + req.modelQuery = Resource4Model.aggregate([ + { $group: { _id: null, titles: { $sum: '$title' } } }, + ]); + return next(); + }, + }) + .virtual({ + path: 'error', + before: function(req, res, next) { + req.modelQuery = { + exec: function exec(callback) { + callback(new Error('Failed'), undefined); + }, + }; + return next(); + }, + }) + .virtual({ + path: 'empty', + before: function(req, res, next) { + req.modelQuery = { + exec: function exec(callback) { + callback(undefined, undefined); + }, + }; + return next(); + }, + }); + const resource4Swaggerio = require('./snippets/resource4Swaggerio.json'); + const swaggerio = resource4.swagger(); + assert.equal(Object.values(swaggerio).length, 2); + assert.ok(swaggerio.definitions); + assert.ok(swaggerio.definitions.resource4); + assert.equal(swaggerio.definitions.resource4.title, 'resource4'); + assert.equal(Object.values(swaggerio.paths).length, 6); + assert.deepEqual(swaggerio, resource4Swaggerio); + }); + + it('Build the /test/skip endpoints', () => { + // Create the schema. + const SkipSchema = new mongoose.Schema({ + title: String, + }); + + // Create the model. + const SkipModel = mongoose.model('skip', SkipSchema); + + // Create the REST resource and continue. + const skipResource = Resource(app, '/test', 'skip', SkipModel) + .rest({ + before(req, res, next) { + req.skipResource = true; + next(); + }, + }) + .virtual({ + path: 'resource', + before: function(req, res, next) { + req.skipResource = true; + return next(); + }, + }); + const skipSwaggerio = require('./snippets/skipSwaggerio.json'); + const swaggerio = skipResource.swagger(); + assert.equal(Object.values(swaggerio).length, 2); + assert.ok(swaggerio.definitions); + assert.ok(swaggerio.definitions.skip); + assert.equal(swaggerio.definitions.skip.title, 'skip'); + assert.equal(Object.values(swaggerio.paths).length, 3); + assert.deepEqual(swaggerio, skipSwaggerio); + }); +}); + +describe('Test skipResource', () => { + const resource = {}; + it('/GET empty list', () => request(app) + .get('/test/skip') + .expect('Content-Type', /text\/html/) + .expect(404) + .then((res) => { + const response = res.text; + const expected = 'Cannot GET /test/skip'; + assert(response.includes(expected), 'Response not found.'); + })); + + it('/POST Create new resource', () => request(app) + .post('/test/skip') + .send({ + title: 'Test1', + description: '12345678', + }) + .expect('Content-Type', /text\/html/) + .expect(404) + .then((res) => { + const response = res.text; + const expected = 'Cannot POST /test/skip'; + assert(response.includes(expected), 'Response not found.'); + })); + + it('/GET The new resource', () => request(app) + .get(`/test/skip/${resource._id}`) + .expect('Content-Type', /text\/html/) + .expect(404) + .then((res) => { + const response = res.text; + const expected = `Cannot GET /test/skip/${resource._id}`; + assert(response.includes(expected), 'Response not found.'); + })); + + it('/PUT Change data on the resource', () => request(app) + .put(`/test/skip/${resource._id}`) + .send({ + title: 'Test2', + }) + .expect('Content-Type', /text\/html/) + .expect(404) + .then((res) => { + const response = res.text; + const expected = `Cannot PUT /test/skip/${resource._id}`; + assert(response.includes(expected), 'Response not found.'); + })); + + it('/PATCH Change data on the resource', () => request(app) + .patch(`/test/skip/${resource._id}`) + .expect('Content-Type', /text\/html/) + .expect(404) + .then((res) => { + const response = res.text; + const expected = `Cannot PATCH /test/skip/${resource._id}`; + assert(response.includes(expected), 'Response not found.'); + })); + + it('/DELETE the resource', () => request(app) + .delete(`/test/skip/${resource._id}`) + .expect('Content-Type', /text\/html/) + .expect(404) + .then((res) => { + const response = res.text; + const expected = `Cannot DELETE /test/skip/${resource._id}`; + assert(response.includes(expected), 'Response not found.'); + })); + + it('/VIRTUAL the resource', () => request(app) + .get('/test/skip/virtual/resource') + .expect('Content-Type', /text\/html/) + .expect(404) + .then((res) => { + const response = res.text; + const expected = 'Cannot GET /test/skip/virtual/resource'; + assert(response.includes(expected), 'Response not found.'); + })); +}); + +describe('Test Virtual resource and Patch errors', () => { + it('/VIRTUAL undefined resource query', () => request(app) + .get('/test/resource4/virtual/undefined_query') + .expect('Content-Type', /json/) + .expect(404) + .then((res) => { + assert.equal(res.body.errors[0], 'Resource not found'); + })); + + it('/VIRTUAL resource query', () => request(app) + .get('/test/resource4/virtual/defined') + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response[0]._id, null); + assert.equal(response[0].titles, 0); + })); + + it('/VIRTUAL errorous resource query', () => request(app) + .get('/test/resource4/virtual/error') + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + const response = res.body; + assert.equal(response.message, 'Failed'); + })); + + it('/VIRTUAL empty resource response', () => request(app) + .get('/test/resource4/virtual/empty') + .expect('Content-Type', /json/) + .expect(404) + .then((res) => { + const response = res.body; + assert.equal(response.errors[0], 'Resource not found'); + })); + + it('/PATCH with errorous modelquery', () => request(app) + .patch('/test/resource4/1234') + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + const response = res.body; + assert.equal(response.message, 'failed'); + })); +}); + +describe('Test single resource CRUD capabilities', () => { + let resource = {}; + + it('/GET empty list', () => request(app) + .get('/test/resource1') + .expect('Content-Type', /json/) + .expect('Content-Range', '*/0') + .expect(200) + .then((res) => { + assert.equal(res.hasOwnProperty('body'), true); + assert.deepEqual(res.body, []); + })); + + it('/POST Create new resource', () => request(app) + .post('/test/resource1') + .send({ + title: 'Test1', + description: '12345678', + }) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + resource = res.body; + assert.equal(resource.title, 'Test1'); + assert.equal(resource.description, '12345678'); + assert(resource.hasOwnProperty('_id'), 'Resource ID not found'); + })); + + it('/GET The new resource', () => request(app) + .get(`/test/resource1/${resource._id}`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + assert.equal(res.body.title, resource.title); + assert.equal(res.body.description, resource.description); + assert.equal(res.body._id, resource._id); + })); + + it('/PUT Change data on the resource', () => request(app) + .put(`/test/resource1/${resource._id}`) + .send({ + title: 'Test2', + }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + assert.equal(res.body.title, 'Test2'); + assert.equal(res.body.description, resource.description); + assert.equal(res.body._id, resource._id); + resource = res.body; + })); + + it('/PATCH Change data on the resource', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'replace', 'path': '/title', 'value': 'Test3' }]) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + assert.equal(res.body.title, 'Test3'); + resource = res.body; + })); + + it('/PATCH Reject update due to failed test op', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([ + { 'op': 'test', 'path': '/title', 'value': 'not-the-title' }, + { 'op': 'replace', 'path': '/title', 'value': 'Test4' }, + ]) + .expect('Content-Type', /json/) + .expect(412) + .then((res) => { + assert.equal(res.body.title, 'Test3'); + resource = res.body; + })); + + it('/PATCH Reject update due to incorrect patch operation', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'does-not-exist', 'path': '/title', 'value': 'Test4' }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_OP_INVALID'); + })); + + it('/PATCH Should not care whether patch is array or not', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send({ 'op': 'test', 'path': '/title', 'value': 'Test3' }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + assert.equal(res.body.title, 'Test3'); + })); + + it('/PATCH Reject update due to incorrect patch object', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send(['invalid-patch']) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_NOT_AN_OBJECT'); + })); + + it('/PATCH Reject update due to incorrect patch value', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'replace', 'path': '/title', 'value': undefined }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_VALUE_REQUIRED'); + })); + + it('/PATCH Reject update due to incorrect patch add path', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'add', 'path': '/path/does/not/exist', 'value': 'Test4' }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_PATH_CANNOT_ADD'); + })); + + it('/PATCH Reject update due to incorrect patch path', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'replace', 'path': '/path/does/not/exist', 'value': 'Test4' }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_PATH_UNRESOLVABLE'); + })); + + it('/PATCH Reject update due to incorrect patch path', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'replace', 'path': 1, 'value': 'Test4' }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_PATH_INVALID'); + })); + + it('/PATCH Reject update due to incorrect patch path', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'add', 'path': '/path/does/not/exist', 'value': 'Test4' }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_PATH_CANNOT_ADD'); + })); + + it('/PATCH Reject update due to incorrect patch path', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'move', 'from': '/path/does/not/exist', 'path': '/path/does/not/exist', 'value': 'Test4' }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_FROM_UNRESOLVABLE'); + })); + + it('/PATCH Reject update due to incorrect patch array', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'add', 'path': '/list/invalidindex', 'value': '2' }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_PATH_ILLEGAL_ARRAY_INDEX'); + })); + + it('/PATCH Reject update due to incorrect patch array', () => request(app) + .patch(`/test/resource1/${resource._id}`) + .send([{ 'op': 'add', 'path': '/list/9999', 'value': '2' }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + assert.equal(res.body.errors[0].name, 'OPERATION_VALUE_OUT_OF_BOUNDS'); + })); + + it('/GET The changed resource', () => request(app) + .get(`/test/resource1/${resource._id}`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + assert.equal(res.body.title, resource.title); + assert.equal(res.body.description, resource.description); + assert.equal(res.body._id, resource._id); + })); + + it('/GET index of resources', () => request(app) + .get('/test/resource1') + .expect('Content-Type', /json/) + .expect('Content-Range', '0-0/1') + .expect(200) + .then((res) => { + assert.equal(res.body.length, 1); + assert.equal(res.body[0].title, 'Test3'); + assert.equal(res.body[0].description, resource.description); + assert.equal(res.body[0]._id, resource._id); + })); + + it('Cannot /POST to an existing resource', () => request(app) + .post(`/test/resource1/${resource._id}`) + .expect('Content-Type', /text\/html/) + .expect(404) + .then((res) => { + const response = res.text; + const expected = `Cannot POST /test/resource1/${resource._id}`; + assert(response.includes(expected), 'Response not found.'); + })); + + it('/DELETE the resource', () => request(app) + .delete(`/test/resource1/${resource._id}`) + .expect(200) + .then((res) => { + assert.deepEqual(res.body, {}); + })); + + it('/GET empty list', () => request(app) + .get('/test/resource1') + .expect('Content-Type', /json/) + .expect('Content-Range', '*/0') + .expect(200) + .then((res) => { + assert.equal(res.hasOwnProperty('body'), true); + assert.deepEqual(res.body, []); + })); + + describe('Test single resource subdocument updates', () => { + // Ensure that resource reference is empty. + resource = {}; + let doc1 = null; + let doc2 = null; + + describe('Bootstrap', () => { + it('Should create a reference doc with mongoose', () => { + const doc = { data: 'test1' }; + + return request(app) + .post('/test/ref') + .send(doc) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = _.omit(res.body, '__v'); + assert.equal(response.data, doc.data); + doc1 = response; + }); + }); + + it('Should be able to create a reference doc directly with mongo', () => { + const doc = { data: 'test2' }; + const compare = _.clone(doc); + + const ref = db.collection('ref'); + ref.insertOne(doc, (err, result) => { + const response = result.ops[0]; + assert.deepEqual(_.omit(response, '_id'), compare); + response._id = response._id.toString(); + doc2 = response; + }); + }); + + it('Should be able to directly create a resource with subdocuments using mongo', () => { + // Set the resource collection for direct mongo queries. + const resource1 = db.collection('resource1'); + + const tmp = { + title: 'Test2', + description: '987654321', + list: [ + { label: 'one', data: [doc1._id] }, + ], + }; + const compare = _.clone(tmp); + + return resource1.insertOne(tmp).then((result) => { + resource = result.ops[0]; + assert.deepEqual(_.omit(resource, '_id'), compare); + }); + }); + }); + + describe('Subdocument Tests', () => { + it('/PUT to a resource with subdocuments should not mangle the subdocuments', () => { + const two = { label: 'two', data: [doc2._id] }; + + return request(app) + .put(`/test/resource1/${resource._id}`) + .send({ list: resource.list.concat(two) }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, resource.title); + assert.equal(response.description, resource.description); + assert.equal(response._id, resource._id); + assert.deepEqual(response.list, resource.list.concat(two)); + resource = response; + }); + }); + + it('Manual DB updates to a resource with subdocuments should not mangle the subdocuments', () => { + const updates = [ + { label: '1', data: [doc1._id] }, + { label: '2', data: [doc2._id] }, + { label: '3', data: [doc1._id, doc2._id] }, + ]; + + const resource1 = db.collection('resource1'); + resource1.findOneAndUpdate( + { _id: ObjectId(resource._id) }, + { $set: { list: updates } }, + { returnOriginal: false }, + (err, doc) => { + const response = doc.value; + assert.equal(response.title, resource.title); + assert.equal(response.description, resource.description); + assert.equal(response._id, resource._id); + assert.deepEqual(response.list, updates); + resource = response; + }); + }); + + it('/PUT to a resource subdocument should not mangle the subdocuments', () => { + // Update a subdocument property. + const update = _.clone(resource.list); + return request(app) + .put(`/test/resource1/${resource._id}`) + .send({ list: update }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, resource.title); + assert.equal(response.description, resource.description); + assert.equal(response._id, resource._id); + assert.deepEqual(response.list, update); + resource = response; + }); + }); + + it('/PUT to a top-level property should not mangle the other collection properties', () => { + const tempTitle = 'an update without docs'; + + return request(app) + .put(`/test/resource1/${resource._id}`) + .send({ title: tempTitle }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, tempTitle); + assert.equal(response.description, resource.description); + assert.equal(response._id, resource._id); + assert.deepEqual(response.list, resource.list); + resource = response; + }); + }); + }); + + // Remove the test resource. + describe('Subdocument cleanup', () => { + it('Should remove the test resource', () => { + const resource1 = db.collection('resource1'); + resource1.findOneAndDelete({ _id: ObjectId(resource._id) }); + }); + + it('Should remove the test ref resources', () => { + const ref = db.collection('ref'); + ref.findOneAndDelete({ _id: ObjectId(doc1._id) }); + ref.findOneAndDelete({ _id: ObjectId(doc2._id) }); + }); + }); + }); +}); + +let refDoc1Content = null; +let refDoc1Response = null; +const resourceNames = []; +// eslint-disable-next-line max-statements +function testSearch(testPath) { + it('Should populate', () => request(app) + .get(`${testPath}?name=noage&populate=list.data`) + .then((res) => { + const response = res.body; + + // Check statusCode + assert.equal(res.statusCode, 200); + + // Check main resource + assert.equal(response[0].title, 'No Age'); + assert.equal(response[0].description, 'No age'); + assert.equal(response[0].name, 'noage'); + assert.equal(response[0].list.length, 1); + + // Check populated resource + assert.equal(response[0].list[0].label, '1'); + assert.equal(response[0].list[0].data.length, 1); + assert.equal(response[0].list[0].data[0]._id, refDoc1Response._id); + assert.equal(response[0].list[0].data[0].data, refDoc1Content.data); + })); + + it('Should ignore empty populate query parameter', () => request(app) + .get(`${testPath}?name=noage&populate=`) + .then((res) => { + const response = res.body; + + // Check statusCode + assert.equal(res.statusCode, 200); + + // Check main resource + assert.equal(response[0].title, 'No Age'); + assert.equal(response[0].description, 'No age'); + assert.equal(response[0].name, 'noage'); + assert.equal(response[0].list.length, 1); + + // Check populated resource + assert.equal(response[0].list[0].label, '1'); + assert.equal(response[0].list[0].data.length, 1); + assert.equal(response[0].list[0].data[0], refDoc1Response._id); + })); + + it('Should not populate paths that are not a reference', () => request(app) + .get(`${testPath}?name=noage&populate=list2`) + .then((res) => { + const response = res.body; + + // Check statusCode + assert.equal(res.statusCode, 200); + + // Check main resource + assert.equal(response[0].title, 'No Age'); + assert.equal(response[0].description, 'No age'); + assert.equal(response[0].name, 'noage'); + assert.equal(response[0].list.length, 1); + + // Check populated resource + assert.equal(response[0].list[0].label, '1'); + assert.equal(response[0].list[0].data.length, 1); + assert.equal(response[0].list[0].data[0], refDoc1Response._id); + })); + + it('Should populate with options', () => request(app) + .get(`${testPath}?name=noage&populate[path]=list.data`) + .then((res) => { + const response = res.body; + + // Check statusCode + assert.equal(res.statusCode, 200); + + // Check main resource + assert.equal(response[0].title, 'No Age'); + assert.equal(response[0].description, 'No age'); + assert.equal(response[0].name, 'noage'); + assert.equal(response[0].list.length, 1); + + // Check populated resource + assert.equal(response[0].list[0].label, '1'); + assert.equal(response[0].list[0].data.length, 1); + assert.equal(response[0].list[0].data[0]._id, refDoc1Response._id); + assert.equal(response[0].list[0].data[0].data, refDoc1Content.data); + })); + + it('Should limit 10', () => request(app) + .get(testPath) + .expect('Content-Type', /json/) + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + let age = 0; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, `Description of test age ${age}`); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should accept a change in limit', () => request(app) + .get(`${testPath}?limit=5`) + .expect('Content-Type', /json/) + .expect('Content-Range', '0-4/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 5); + let age = 0; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, `Description of test age ${age}`); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should be able to skip and limit', () => request(app) + .get(`${testPath}?limit=5&skip=4`) + .expect('Content-Type', /json/) + .expect('Content-Range', '4-8/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 5); + let age = 4; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, `Description of test age ${age}`); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should default negative limit to 10', () => request(app) + .get(`${testPath}?limit=-5&skip=4`) + .expect('Content-Type', /json/) + .expect('Content-Range', '4-13/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + let age = 4; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, `Description of test age ${age}`); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should default negative skip to 0', () => request(app) + .get(`${testPath}?limit=5&skip=-4`) + .expect('Content-Type', /json/) + .expect('Content-Range', '0-4/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 5); + let age = 0; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, `Description of test age ${age}`); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should default negative skip and negative limit to 0 and 10', () => request(app) + .get(`${testPath}?limit=-5&skip=-4`) + .expect('Content-Type', /json/) + .expect('Content-Range', '0-9/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + let age = 0; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, `Description of test age ${age}`); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should default non numeric limit to 10', () => request(app) + .get(`${testPath}?limit=badlimit&skip=4`) + .expect('Content-Type', /json/) + .expect('Content-Range', '4-13/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + let age = 4; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, `Description of test age ${age}`); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should default non numeric skip to 0', () => request(app) + .get(`${testPath}?limit=5&skip=badskip`) + .expect('Content-Type', /json/) + .expect('Content-Range', '0-4/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 5); + let age = 0; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, `Description of test age ${age}`); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should be able to select fields', () => request(app) + .get(`${testPath}?limit=10&skip=10&select=title,age`) + .expect('Content-Type', /json/) + .expect('Content-Range', '10-19/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + let age = 10; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, undefined); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should be able to select fields with multiple select queries', () => request(app) + .get(`${testPath}?limit=10&skip=10&select=title&select=age`) + .expect('Content-Type', /json/) + .expect('Content-Range', '10-19/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + let age = 10; + response.forEach((resource) => { + assert.equal(resource.title, `Test Age ${age}`); + assert.equal(resource.description, undefined); + assert.equal(resource.age, age); + age++; + }); + })); + + it('Should be able to sort', () => request(app) + .get(`${testPath}?select=age&sort=-age`) + .expect('Content-Type', /json/) + .expect('Content-Range', '0-9/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + let age = 24; + response.forEach((resource) => { + assert.equal(resource.title, undefined); + assert.equal(resource.description, undefined); + assert.equal(resource.age, age); + age--; + }); + })); + + it('Should paginate with a sort', () => request(app) + .get(`${testPath}?limit=5&skip=5&select=age&sort=-age`) + .expect('Content-Type', /json/) + .expect('Content-Range', '5-9/26') + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 5); + let age = 19; + response.forEach((resource) => { + assert.equal(resource.title, undefined); + assert.equal(resource.description, undefined); + assert.equal(resource.age, age); + age--; + }); + })); + + it('Should be able to find', () => request(app) + .get(`${testPath}?limit=5&select=age&age=5`) + .expect('Content-Type', /json/) + .expect('Content-Range', '0-0/1') + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, undefined); + assert.equal(response[0].description, undefined); + assert.equal(response[0].age, 5); + })); + + it('eq search selector', () => request(app) + .get(`${testPath}?age__eq=5`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + response.forEach((resource) => { + assert.equal(resource.age, 5); + }); + })); + + it('equals (alternative) search selector', () => request(app) + .get(`${testPath}?age=5`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + response.forEach((resource) => { + assert.equal(resource.age, 5); + }); + })); + + it('ne search selector', () => request(app) + .get(`${testPath}?age__ne=5&limit=100`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 25); + response.forEach((resource) => { + assert.notEqual(resource.age, 5); + }); + })); + + it('in search selector', () => request(app) + .get(`${testPath}?title__in=Test Age 1,Test Age 5,Test Age 9,Test Age 20`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 4); + response.forEach((resource) => { + let found = false; + + [1, 5, 9, 20].forEach((a) => { + if (resource.age && resource.age === a) { + found = true; + } + }); + + assert(found); + }); + })); + + it('nin search selector', () => request(app) + .get(`${testPath}?title__nin=Test Age 1,Test Age 5`) + .expect('Content-Type', /json/) + .expect(206) + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + response.forEach((resource) => { + let found = false; + + [1, 5].forEach((a) => { + if (resource.age && resource.age === a) { + found = true; + } + }); + + assert(!found); + }); + })); + + it('exists=false search selector', () => request(app) + .get(`${testPath}?age__exists=false`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].name, 'noage'); + })); + + it('exists=0 search selector', () => request(app) + .get(`${testPath}?age__exists=0`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].name, 'noage'); + })); + + it('exists=true search selector', () => request(app) + .get(`${testPath}?age__exists=true&limit=1000`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 25); + response.forEach((resource) => { + assert(resource.name !== 'noage', 'No age should be found.'); + }); + })); + + it('exists=1 search selector', () => request(app) + .get(`${testPath}?age__exists=true&limit=1000`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 25); + response.forEach((resource) => { + assert(resource.name !== 'noage', 'No age should be found.'); + }); + })); + + it('lt search selector', () => request(app) + .get(`${testPath}?age__lt=5`) + .expect('Content-Range', '0-4/5') + .then((res) => { + const response = res.body; + assert.equal(response.length, 5); + response.forEach((resource) => { + assert.ok(resource.age < 5); + }); + })); + + it('lte search selector', () => request(app) + .get(`${testPath}?age__lte=5`) + .expect('Content-Range', '0-5/6') + .then((res) => { + const response = res.body; + assert.equal(response.length, 6); + response.forEach((resource) => { + assert.ok(resource.age <= 5); + }); + })); + + it('gt search selector', () => request(app) + .get(`${testPath}?age__gt=5`) + .expect('Content-Range', '0-9/19') + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + response.forEach((resource) => { + assert.ok(resource.age > 5); + }); + })); + + it('gte search selector', () => request(app) + .get(`${testPath}?age__gte=5`) + .expect('Content-Range', '0-9/20') + .then((res) => { + const response = res.body; + assert.equal(response.length, 10); + response.forEach((resource) => { + assert.ok(resource.age >= 5); + }); + })); + + it('regex search selector', () => request(app) + .get(`${testPath}?title__regex=/.*Age [0-1]?[0-3]$/g`) + .expect('Content-Range', '0-7/8') + .then((res) => { + const response = res.body; + const valid = [0, 1, 2, 3, 10, 11, 12, 13]; + assert.equal(response.length, valid.length); + response.forEach((resource) => { + assert.ok(valid.includes(resource.age)); + }); + })); + + it('regex search selector should be case insensitive', () => { + const name = resourceNames[0].toString(); + + return request(app) + .get(`${testPath}?name__regex=${name.toUpperCase()}`) + .then((res) => { + const uppercaseResponse = res.body; + return request(app) + .get(`/test/resource1?name__regex=${name.toLowerCase()}`) + .then((res) => { + const lowercaseResponse = res.body; + assert.equal(uppercaseResponse.length, lowercaseResponse.length); + }); + }); + }); +} + +describe('Test single resource search capabilities', () => { + let singleResource1Id = undefined; + it('Should create a reference doc with mongoose', () => { + refDoc1Content = { data: 'test1' }; + return request(app) + .post('/test/ref') + .send(refDoc1Content) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = _.omit(res.body, '__v'); + assert.equal(response.data, refDoc1Content.data); + refDoc1Response = response; + }); + }); + + it('Create a full index of resources', () => _.range(25).reduce((promise, age) => { + const name = (chance.name()).toUpperCase(); + resourceNames.push(name); + return promise.then(() => request(app) + .post('/test/resource1') + .send({ + title: `Test Age ${age}`, + description: `Description of test age ${age}`, + name, + age, + }) + .then((res) => { + const response = res.body; + assert.equal(response.title, `Test Age ${age}`); + assert.equal(response.description, `Description of test age ${age}`); + assert.equal(response.age, age); + })); + }, Promise.resolve()) + .then(() => { + const refList = [{ label: '1', data: [refDoc1Response._id] }]; + + // Insert a record with no age. + return request(app) + .post('/test/resource1') + .send({ + title: 'No Age', + name: 'noage', + description: 'No age', + list: refList, + }) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'No Age'); + assert.equal(response.description, 'No age'); + assert.equal(response.name, 'noage'); + assert(!response.hasOwnProperty('age'), 'Age should not be found.'); + + singleResource1Id = res.body._id; + }); + })); + + testSearch('/test/resource1'); + + it('Should allow population on single object GET request', () => request(app) + .get(`/test/resource1/${singleResource1Id}?populate=list.data`) + .then((res) => { + const response = res.body; + + // Check statusCode + assert.equal(res.statusCode, 200); + + // Check main resource + assert.equal(response.title, 'No Age'); + assert.equal(response.description, 'No age'); + assert.equal(response.name, 'noage'); + assert.equal(response.list.length, 1); + + // Check populated resource + assert.equal(response.list[0].label, '1'); + assert.equal(response.list[0].data.length, 1); + assert.equal(response.list[0].data[0]._id, refDoc1Response._id); + assert.equal(response.list[0].data[0].data, refDoc1Content.data); + })); + + it('Create an aggregation path', () => { + Resource(app, '', 'aggregation', mongoose.model('resource1')).rest({ + beforeIndex(req, res, next) { + req.modelQuery = mongoose.model('resource1'); + req.modelQuery.pipeline = []; + next(); + }, + }); + }); + + testSearch('/aggregation'); +}); + +describe('Test dates search capabilities', () => { + it('Should search by ISO date', () => { + const isoString = testDates[0].toISOString(); + + return Promise.all([ + request(app) + .get(`/test/date?date=${isoString}`) + .then(({ body: response }) => assert.equal(response.length, 1)), + request(app) + .get(`/test/date?date__lt=${isoString}`) + .then(({ body: response }) => assert.equal(response.length, 3)), + request(app) + .get(`/test/date?date__lte=${isoString}`) + .then(({ body: response }) => assert.equal(response.length, 4)), + request(app) + .get(`/test/date?date__gte=${isoString}`) + .then(({ body: response }) => assert.equal(response.length, 1)), + request(app) + .get(`/test/date?date__gt=${isoString}`) + .then(({ body: response }) => assert.equal(response.length, 0)), + request(app) + .get(`/test/date?date__ne=${isoString}`) + .then(({ body: response }) => assert.equal(response.length, 3)), + ]); + }); + + it('Should search by YYYY-MM-DD format', () => { + const search = testDates[0].format('YYYY-MM-DD'); + + return Promise.all([ + request(app) + .get(`/test/date?date=${search}`) + .then(({ body: response }) => assert.equal(response.length, 0)), + request(app) + .get(`/test/date?date__lt=${search}`) + .then(({ body: response }) => assert.equal(response.length, 3)), + request(app) + .get(`/test/date?date__lte=${search}`) + .then(({ body: response }) => assert.equal(response.length, 3)), + request(app) + .get(`/test/date?date__gte=${search}`) + .then(({ body: response }) => assert.equal(response.length, 1)), + request(app) + .get(`/test/date?date__gt=${search}`) + .then(({ body: response }) => assert.equal(response.length, 1)), + request(app) + .get(`/test/date?date__ne=${search}`) + .then(({ body: response }) => assert.equal(response.length, 4)), + ]); + }); + + it('Should search by YYYY-MM format', () => { + const search = testDates[0].format('YYYY-MM'); + + return Promise.all([ + request(app) + .get(`/test/date?date=${search}`) + .then(({ body: response }) => assert.equal(response.length, 0)), + request(app) + .get(`/test/date?date__lt=${search}`) + .then(({ body: response }) => assert.equal(response.length, 2)), + request(app) + .get(`/test/date?date__lte=${search}`) + .then(({ body: response }) => assert.equal(response.length, 2)), + request(app) + .get(`/test/date?date__gte=${search}`) + .then(({ body: response }) => assert.equal(response.length, 2)), + request(app) + .get(`/test/date?date__gt=${search}`) + .then(({ body: response }) => assert.equal(response.length, 2)), + request(app) + .get(`/test/date?date__ne=${search}`) + .then(({ body: response }) => assert.equal(response.length, 4)), + ]); + }); + + it('Should search by YYYY format', () => { + const search = testDates[0].format('YYYY'); + + return Promise.all([ + request(app) + .get(`/test/date?date=${search}`) + .then(({ body: response }) => assert.equal(response.length, 0)), + request(app) + .get(`/test/date?date__lt=${search}`) + .then(({ body: response }) => assert.equal(response.length, 1)), + request(app) + .get(`/test/date?date__lte=${search}`) + .then(({ body: response }) => assert.equal(response.length, 1)), + request(app) + .get(`/test/date?date__gte=${search}`) + .then(({ body: response }) => assert.equal(response.length, 3)), + request(app) + .get(`/test/date?date__gt=${search}`) + .then(({ body: response }) => assert.equal(response.length, 3)), + request(app) + .get(`/test/date?date__ne=${search}`) + .then(({ body: response }) => assert.equal(response.length, 4)), + ]); + }); + + it('Should search by timestamp', () => { + const search = testDates[0].format('x'); + + return Promise.all([ + request(app) + .get(`/test/date?date=${search}`) + .then(({ body: response }) => assert.equal(response.length, 1)), + request(app) + .get(`/test/date?date__lt=${search}`) + .then(({ body: response }) => assert.equal(response.length, 3)), + request(app) + .get(`/test/date?date__lte=${search}`) + .then(({ body: response }) => assert.equal(response.length, 4)), + request(app) + .get(`/test/date?date__gte=${search}`) + .then(({ body: response }) => assert.equal(response.length, 1)), + request(app) + .get(`/test/date?date__gt=${search}`) + .then(({ body: response }) => assert.equal(response.length, 0)), + request(app) + .get(`/test/date?date__ne=${search}`) + .then(({ body: response }) => assert.equal(response.length, 3)), + ]); + }); +}); + +describe('Test single resource handlers capabilities', () => { + // Store the resource being mutated. + let resource = {}; + + it('A POST request should invoke the global handlers and method handlers', () => request(app) + .post('/test/resource2') + .send({ + title: 'Test1', + description: '12345678', + }) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Test1'); + assert.equal(response.description, '12345678'); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('resource2', 'before', 'post'), true); + assert.equal(wasInvoked('resource2', 'after', 'post'), true); + assert.equal(wasInvoked('resource2', 'beforePost', 'post'), true); + assert.equal(wasInvoked('resource2', 'afterPost', 'post'), true); + + // Store the resource and continue. + resource = response; + })); + + it('A GET request should invoke the global handlers', () => request(app) + .get(`/test/resource2/${resource._id}`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Test1'); + assert.equal(response.description, '12345678'); + assert(response.hasOwnProperty('_id'), 'Resource ID not found'); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('resource2', 'before', 'get'), true); + assert.equal(wasInvoked('resource2', 'after', 'get'), true); + + // Confirm that POST method handlers were NOT called + assert.equal(wasInvoked('resource2', 'beforePost', 'get'), false); + assert.equal(wasInvoked('resource2', 'afterPost', 'get'), false); + + // Store the resource and continue. + resource = response; + })); + + it('Should allow you to use select to select certain fields.', () => request(app) + .get(`/test/resource2/${resource._id}?select=title`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Test1'); + assert.equal(response.description, undefined); + })); + + it('A PUT request should invoke the global handlers', () => request(app) + .put(`/test/resource2/${resource._id}`) + .send({ + title: 'Test1 - Updated', + }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Test1 - Updated'); + assert.equal(response.description, '12345678'); + assert(response.hasOwnProperty('_id'), 'Resource ID not found'); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('resource2', 'before', 'put'), true); + assert.equal(wasInvoked('resource2', 'after', 'put'), true); + + // Store the resource and continue. + resource = response; + })); + + it('A GET (Index) request should invoke the global handlers', () => request(app) + .get('/test/resource2') + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'Test1 - Updated'); + assert.equal(response[0].description, '12345678'); + assert(response[0].hasOwnProperty('_id'), 'Resource ID not found'); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('resource2', 'before', 'index'), true); + assert.equal(wasInvoked('resource2', 'after', 'index'), true); + + // Store the resource and continue. + resource = response[0]; + })); + + it('A DELETE request should invoke the global handlers', () => request(app) + .delete(`/test/resource2/${resource._id}`) + .expect(200) + .then((res) => { + const response = res.body; + assert.deepEqual(response, {}); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('resource2', 'before', 'delete'), true); + assert.equal(wasInvoked('resource2', 'after', 'delete'), true); + + // Store the resource and continue. + resource = response; + })); +}); + +describe('Handle native data formats', () => { + it('Should create a new resource with boolean and string values set.', () => request(app) + .post('/test/resource2') + .send({ + title: 'null', + description: 'false', + married: true, + }) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = res.body; + assert.equal(response.married, true); + assert.equal(response.title, 'null'); + assert.equal(response.description, 'false'); + })); + + it('Should find the record when filtering the title as "null"', () => request(app) + .get('/test/resource2?title=null') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'null'); + })); + + it('Should find the record when filtering the description as "false"', () => request(app) + .get('/test/resource2?description=false') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'null'); + assert.equal(response[0].description, 'false'); + })); + + it('Should find the record when filtering the description as "true"', () => request(app) + .get('/test/resource2?description=true') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 0); + })); + + it('Should find the record when filtering the updated property as null with strict equality', () => request(app) + .get('/test/resource2?updated__eq=null') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'null'); + assert.equal(response[0].updated, null); + })); + + it('Should still find the null values based on string if explicitely provided "null"', () => request(app) + .get('/test/resource2?title__eq="null"') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'null'); + })); + + it('Should find the boolean false values based on equality', () => request(app) + .get('/test/resource2?description__eq=false') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'null'); + assert.equal(response[0].married, true); + })); + + it('Should find the boolean true values based on equality', () => request(app) + .get('/test/resource2?married__eq=true') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'null'); + assert.equal(response[0].married, true); + })); + + it('Should still find the boolean values based on string if explicitely provided', () => request(app) + .get('/test/resource2?description__eq=%22false%22') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'null'); + assert.equal(response[0].married, true); + })); + + it('Should still find the boolean values based on string if explicitely provided', () => request(app) + .get('/test/resource2?married__eq=%22true%22') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'null'); + assert.equal(response[0].married, true); + })); + + it('Should CAST a boolean to find the boolean values based on equals', () => request(app) + .get('/test/resource2?married=true') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, 'null'); + assert.equal(response[0].married, true); + })); + + it('Should CAST a boolean to find the boolean values based on equals', () => request(app) + .get('/test/resource2?married=false') + .send() + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 0); + })); +}); + +describe('Test writeOptions capabilities', () => { + let resource = {}; + + it('/POST a new resource3 with options', () => request(app) + .post('/test/resource3') + .send({ title: 'Test1' }) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Test1'); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + resource = response; + })); + + it('/PUT an update with options', () => request(app) + .put(`/test/resource3/${resource._id}`) + .send({ title: 'Test1 - Updated' }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Test1 - Updated'); + assert(response.hasOwnProperty('_id'), 'Resource ID not found'); + })); + + it('/PATCH an update with options', () => request(app) + .patch(`/test/resource3/${resource._id}`) + .send([{ 'op': 'replace', 'path': '/title', 'value': 'Test1 - Updated Again' }]) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Test1 - Updated Again'); + assert(response.hasOwnProperty('_id'), 'Resource ID not found'); + })); + + it('/DELETE a resource3 with options', () => request(app) + .delete(`/test/resource3/${resource._id}`) + .expect(200) + .then((res) => { + const response = res.body; + assert.deepEqual(response, {}); + })); +}); + +describe('Test nested resource CRUD capabilities', () => { + let resource = {}; + let nested = {}; + + it('/POST a new parent resource', () => request(app) + .post('/test/resource1') + .send({ + title: 'Test1', + description: '123456789', + }) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Test1'); + assert.equal(response.description, '123456789'); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + resource = response; + })); + + it('/GET an empty list of nested resources', () => request(app) + .get(`/test/resource1/${resource._id}/nested1`) + .expect('Content-Type', /json/) + .expect('Content-Range', '*/0') + .expect(200) + .then((res) => { + assert.equal(res.hasOwnProperty('body'), true); + assert.deepEqual(res.body, []); + })); + + it('/POST a new nested resource', () => request(app) + .post(`/test/resource1/${resource._id}/nested1`) + .send({ + title: 'Nest1', + description: '987654321', + }) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Nest1'); + assert.equal(response.description, '987654321'); + assert(response.hasOwnProperty('resource1'), 'The response must contain the parent object `_id`'); + assert.equal(response.resource1, resource._id); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + nested = response; + })); + + it('/GET the list of nested resources', () => request(app) + .get(`/test/resource1/${resource._id}/nested1/${nested._id}`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, nested.title); + assert.equal(response.description, nested.description); + assert(response.hasOwnProperty('resource1'), 'The response must contain the parent object `_id`'); + assert.equal(response.resource1, resource._id); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + assert.equal(response._id, nested._id); + })); + + it('/PUT the nested resource', () => request(app) + .put(`/test/resource1/${resource._id}/nested1/${nested._id}`) + .send({ + title: 'Nest1 - Updated1', + }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Nest1 - Updated1'); + assert.equal(response.description, nested.description); + assert(response.hasOwnProperty('resource1'), 'The response must contain the parent object `_id`'); + assert.equal(response.resource1, resource._id); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + assert.equal(response._id, nested._id); + nested = response; + })); + + it('/PATCH data on the nested resource', () => request(app) + .patch(`/test/resource1/${resource._id}/nested1/${nested._id}`) + .send([{ 'op': 'replace', 'path': '/title', 'value': 'Nest1 - Updated2' }]) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Nest1 - Updated2'); + assert.equal(response.description, nested.description); + assert(response.hasOwnProperty('resource1'), 'The response must contain the parent object `_id`'); + assert.equal(response.resource1, resource._id); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + assert.equal(response._id, nested._id); + nested = response; + })); + + it('/PATCH rejection on the nested resource due to failed test op', () => request(app) + .patch(`/test/resource1/${resource._id}/nested1/${nested._id}`) + .send([ + { 'op': 'test', 'path': '/title', 'value': 'not-the-title' }, + { 'op': 'replace', 'path': '/title', 'value': 'Nest1 - Updated3' }, + ]) + .expect('Content-Type', /json/) + .expect(412) + .then((res) => { + const response = res.body; + assert.equal(response.title, nested.title); + assert.equal(response.description, nested.description); + assert(response.hasOwnProperty('resource1'), 'The response must contain the parent object `_id`'); + assert.equal(response.resource1, resource._id); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + assert.equal(response._id, nested._id); + })); + + it('/GET the nested resource with patch changes', () => request(app) + .get(`/test/resource1/${resource._id}/nested1/${nested._id}`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, nested.title); + assert.equal(response.description, nested.description); + assert(response.hasOwnProperty('resource1'), 'The response must contain the parent object `_id`'); + assert.equal(response.resource1, resource._id); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + assert.equal(response._id, nested._id); + })); + + it('/GET index of nested resources', () => request(app) + .get(`/test/resource1/${resource._id}/nested1`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, nested.title); + assert.equal(response[0].description, nested.description); + assert(response[0].hasOwnProperty('resource1'), 'The response must contain the parent object `_id`'); + assert.equal(response[0].resource1, resource._id); + assert(response[0].hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + assert.equal(response[0]._id, nested._id); + })); + + it('Cannot /POST to an existing nested resource', () => request(app) + .post(`/test/resource1/${resource._id}/nested1/${nested._id}`) + .expect('Content-Type', /text\/html/) + .expect(404) + .then((res) => { + const response = res.text; + const expected = `Cannot POST /test/resource1/${resource._id}/nested1/${nested._id}`; + assert(response.includes(expected), 'Response not found.'); + })); + + it('/DELETE the nested resource', () => request(app) + .delete(`/test/resource1/${resource._id}/nested1/${nested._id}`) + .expect(200) + .then((res) => { + const response = res.body; + assert.deepEqual(response, {}); + })); + + it('/GET an empty list of nested resources', () => request(app) + .get(`/test/resource1/${resource._id}/nested1/`) + .expect('Content-Type', /json/) + .expect('Content-Range', '*/0') + .expect(200) + .then((res) => { + assert.equal(res.hasOwnProperty('body'), true); + assert.deepEqual(res.body, []); + })); +}); + +describe('Test nested resource handlers capabilities', () => { + // Store the resources being mutated. + let resource = {}; + let nested = {}; + + it('/POST a new parent resource', () => request(app) + .post('/test/resource2') + .send({ + title: 'Test2', + description: '987654321', + }) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Test2'); + assert.equal(response.description, '987654321'); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + resource = response; + })); + + it('Reset the history of the global handlers', () => { + handlers = {}; + }); + + it('A POST request to a child resource should invoke the global handlers', () => request(app) + .post(`/test/resource2/${resource._id}/nested2`) + .send({ + title: 'Nest2', + description: '987654321', + }) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Nest2'); + assert.equal(response.description, '987654321'); + assert(response.hasOwnProperty('resource2'), 'The response must contain the parent object `_id`'); + assert.equal(response.resource2, resource._id); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('nested2', 'before', 'post'), true); + assert.equal(wasInvoked('nested2', 'after', 'post'), true); + + // Store the resource and continue. + nested = response; + })); + + it('A GET request to a child resource should invoke the global handlers', () => request(app) + .get(`/test/resource2/${resource._id}/nested2/${nested._id}`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, nested.title); + assert.equal(response.description, nested.description); + assert(response.hasOwnProperty('resource2'), 'The response must contain the parent object `_id`'); + assert.equal(response.resource2, resource._id); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + assert.equal(response._id, nested._id); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('nested2', 'before', 'get'), true); + assert.equal(wasInvoked('nested2', 'after', 'get'), true); + })); + + it('A PUT request to a child resource should invoke the global handlers', () => request(app) + .put(`/test/resource2/${resource._id}/nested2/${nested._id}`) + .send({ + title: 'Nest2 - Updated', + }) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.title, 'Nest2 - Updated'); + assert.equal(response.description, nested.description); + assert(response.hasOwnProperty('resource2'), 'The response must contain the parent object `_id`'); + assert.equal(response.resource2, resource._id); + assert(response.hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + assert.equal(response._id, nested._id); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('nested2', 'before', 'put'), true); + assert.equal(wasInvoked('nested2', 'after', 'put'), true); + + // Store the resource and continue. + nested = response; + })); + + it('A GET (Index) request to a child resource should invoke the global handlers', () => request(app) + .get(`/test/resource2/${resource._id}/nested2`) + .expect('Content-Type', /json/) + .expect(200) + .then((res) => { + const response = res.body; + assert.equal(response.length, 1); + assert.equal(response[0].title, nested.title); + assert.equal(response[0].description, nested.description); + assert(response[0].hasOwnProperty('resource2'), 'The response must contain the parent object `_id`'); + assert.equal(response[0].resource2, resource._id); + assert(response[0].hasOwnProperty('_id'), 'The response must contain the mongo object `_id`'); + assert.equal(response[0]._id, nested._id); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('nested2', 'before', 'index'), true); + assert.equal(wasInvoked('nested2', 'after', 'index'), true); + })); + + it('A DELETE request to a child resource should invoke the global handlers', () => request(app) + .delete(`/test/resource2/${resource._id}/nested2/${nested._id}`) + .expect(200) + .then((res) => { + const response = res.body; + assert.deepEqual(response, {}); + + // Confirm that the handlers were called. + assert.equal(wasInvoked('nested2', 'before', 'delete'), true); + assert.equal(wasInvoked('nested2', 'after', 'delete'), true); + + // Store the resource and continue. + resource = response; + })); +}); + +describe('Test mount variations', () => { + before(() => { + // Create the REST resource and continue. + Resource(app, '', 'testindex', mongoose.model('testindex', new mongoose.Schema({ + data: { + type: String, + required: true, + }, + }))).index(); + }); + + it('/GET empty list', () => request(app) + .get('/testindex') + .expect('Content-Type', /json/) + .expect('Content-Range', '*/0') + .expect(200) + .then((res) => { + assert.equal(res.hasOwnProperty('body'), true); + assert.deepEqual(res.body, []); + })); + + it('/POST should be 404', () => request(app) + .post('/testindex') + .send({ + title: 'Test1', + description: '12345678', + }) + .expect(404)); + + it('/GET should be 404', () => request(app) + .get('/testindex/234234234') + .expect(404)); + + it('/PUT should be 404', () => request(app) + .put('/testindex/234234234') + .send({ + title: 'Test2', + }) + .expect(404)); + + it('/PATCH should be 404', () => request(app) + .patch('/testindex/234234234') + .send([{ 'op': 'replace', 'path': '/title', 'value': 'Test3' }]) + .expect(404)); + + it('/VIRTUAL should be 404', () => request(app) + .get('/testindex/234234234/virtual') + .send() + .expect(404)); + + it('/DELETE the resource', () => request(app) + .delete('/testindex/234234234') + .expect(404)); +}); + +describe('Test before hooks', () => { + let calls = []; + let sub; + + before(() => { + // Create the schema. + const hookSchema = new mongoose.Schema({ + data: { + type: String, + required: true, + }, + }); + + // Create the model. + const hookModel = mongoose.model('hook', hookSchema); + + // Create the REST resource and continue. + Resource(app, '', 'hook', hookModel).rest({ + hooks: { + post: { + before(req, res, item, next) { + assert.equal(calls.length, 0); + calls.push('before'); + next(); + }, + after(req, res, item, next) { + assert.equal(calls.length, 1); + assert.deepEqual(calls, ['before']); + calls.push('after'); + next(); + }, + }, + get: { + before(req, res, item, next) { + assert.equal(calls.length, 0); + calls.push('before'); + next(); + }, + after(req, res, item, next) { + assert.equal(calls.length, 1); + assert.deepEqual(calls, ['before']); + calls.push('after'); + next(); + }, + }, + put: { + before(req, res, item, next) { + assert.equal(calls.length, 0); + calls.push('before'); + next(); + }, + after(req, res, item, next) { + assert.equal(calls.length, 1); + assert.deepEqual(calls, ['before']); + calls.push('after'); + next(); + }, + }, + delete: { + before(req, res, item, next) { + assert.equal(calls.length, 0); + calls.push('before'); + next(); + }, + after(req, res, item, next) { + assert.equal(calls.length, 1); + assert.deepEqual(calls, ['before']); + calls.push('after'); + next(); + }, + }, + index: { + before(req, res, item, next) { + assert.equal(calls.length, 0); + calls.push('before'); + next(); + }, + after(req, res, item, next) { + assert.equal(calls.length, 1); + assert.deepEqual(calls, ['before']); + calls.push('after'); + next(); + }, + }, + }, + }); + }); + + describe('post hooks', () => { + beforeEach(() => { + calls = []; + }); + + it('Bootstrap some test resources', () => request(app) + .post('/hook') + .send({ + data: chance.word(), + }) + .expect('Content-Type', /json/) + .expect(201) + .then((res) => { + const response = res.body; + sub = response; + assert(calls.length === 2); + assert.equal(calls[0], 'before'); + assert.equal(calls[1], 'after'); + })); + + it('test required validation', () => request(app) + .post('/hook') + .send({}) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + const response = res.body; + assert(calls.length === 1); + assert.equal(calls[0], 'before'); + assert(_.get(response, 'message'), 'hook validation failed'); + })); + }); + + describe('get hooks', () => { + beforeEach(() => { + calls = []; + }); + + it('Call hooks are called in order', () => request(app) + .get(`/hook/${sub._id}`) + .expect('Content-Type', /json/) + .expect(200) + .then(() => { + assert(calls.length === 2); + assert.equal(calls[0], 'before'); + assert.equal(calls[1], 'after'); + })); + + it('test undefined resource', () => request(app) + .get(`/hook/${undefined}`) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + const response = res.body; + assert(calls.length === 1); + assert.equal(calls[0], 'before'); + assert.equal(_.get(response, 'message'), 'Cast to ObjectId failed for value "undefined" at path "_id" for model "hook"'); + })); + + it('test unknown resource', () => request(app) + .get('/hook/000000000000000000000000') + .expect('Content-Type', /json/) + .expect(404) + .then((res) => { + const response = res.body; + assert(calls.length === 1); + assert.equal(calls[0], 'before'); + assert.equal(_.get(response, 'errors[0]'), 'Resource not found'); + })); + }); + + describe('put hooks', () => { + beforeEach(() => { + calls = []; + }); + + it('Call hooks are called in order', () => request(app) + .put(`/hook/${sub._id}`) + .send({ + data: chance.word(), + }) + .expect('Content-Type', /json/) + .expect(200) + .then(() => { + assert(calls.length === 2); + assert.equal(calls[0], 'before'); + assert.equal(calls[1], 'after'); + })); + + it('test undefined resource', () => request(app) + .put(`/hook/${undefined}`) + .send({ + data: chance.word(), + }) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + const response = res.body; + assert(calls.length === 0); + assert.equal(_.get(response, 'message'), 'Cast to ObjectId failed for value "undefined" at path "_id" for model "hook"'); + })); + + it('test unknown resource', () => request(app) + .put('/hook/000000000000000000000000') + .send({ + data: chance.word(), + }) + .expect('Content-Type', /json/) + .expect(404) + .then((res) => { + const response = res.body; + assert(calls.length === 0); + assert.equal(_.get(response, 'errors[0]'), 'Resource not found'); + })); + }); + + describe('delete hooks', () => { + beforeEach(() => { + calls = []; + }); + + it('Call hooks are called in order', () => request(app) + .delete(`/hook/${sub._id}`) + .expect('Content-Type', /json/) + .expect(200) + .then(() => { + assert(calls.length === 2); + assert.equal(calls[0], 'before'); + assert.equal(calls[1], 'after'); + })); + + it('test undefined resource', () => request(app) + .delete(`/hook/${undefined}`) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + const response = res.body; + assert(calls.length === 0); + assert.equal(_.get(response, 'message'), 'Cast to ObjectId failed for value "undefined" at path "_id" for model "hook"'); + })); + + it('test unknown resource', () => request(app) + .delete('/hook/000000000000000000000000') + .expect('Content-Type', /json/) + .expect(404) + .then((res) => { + const response = res.body; + assert(calls.length === 0); + assert.equal(_.get(response, 'errors[0]'), 'Resource not found'); + })); + }); + + describe('index hooks', () => { + beforeEach(() => { + calls = []; + }); + + it('Call hooks are called in order', () => request(app) + .get('/hook') + .expect('Content-Type', /json/) + .expect(200) + .then(() => { + assert(calls.length === 2); + assert.equal(calls[0], 'before'); + assert.equal(calls[1], 'after'); + })); + }); +}); + +describe('Test Swagger.io', () => { + +}); From dbf6a952c3968f41689feef6a49ff5ce6a58eeae Mon Sep 17 00:00:00 2001 From: Joakim Date: Sat, 11 Jul 2020 13:22:08 +0300 Subject: [PATCH 05/22] test(Koa): Change test environment (Express > Koa) --- test/testKoa.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/testKoa.js b/test/testKoa.js index 4567a19..727d13e 100644 --- a/test/testKoa.js +++ b/test/testKoa.js @@ -1,14 +1,14 @@ /* eslint-disable no-prototype-builtins */ 'use strict'; -const express = require('express'); -const bodyParser = require('body-parser'); +const Koa = require('koa'); +const bodyParser = require('koa-bodyparser'); const request = require('supertest'); const assert = require('assert'); const moment = require('moment'); const mongoose = require('mongoose'); const Resource = require('../Resource'); -const app = express(); +const app = new Koa(); const _ = require('lodash'); const MongoClient = require('mongodb').MongoClient; const ObjectId = require('mongodb').ObjectID; @@ -23,8 +23,7 @@ const testDates = [ ]; // Use the body parser. -app.use(bodyParser.urlencoded({ extended: true })); -app.use(bodyParser.json()); +app.use(bodyParser()); // An object to store handler events. let handlers = {}; From d529cc8d95ea59a00579e682ef122f039ee20e93 Mon Sep 17 00:00:00 2001 From: Sefriol Date: Sat, 11 Jul 2020 15:53:57 +0300 Subject: [PATCH 06/22] Koa: Add Compose and routes to app --- KoaResource.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/KoaResource.js b/KoaResource.js index 1dda24c..bf34e94 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -129,6 +129,7 @@ class Resource { // Add a stack processor so this stack can be executed independently of Express. this.app.context.resourcejs[path][method] = this.stackProcessor(routeStack); + routeStack = compose(routeStack); // Apply these callbacks to the application. switch (method) { @@ -148,6 +149,7 @@ class Resource { this.router.delete(path, routeStack); break; } + this.app.use(this.router.routes(), this.router.allowedMethods()); } /** From e5590617a15a5806404d90532f22a3d7983a52ad Mon Sep 17 00:00:00 2001 From: Sefriol Date: Sun, 12 Jul 2020 15:26:25 +0300 Subject: [PATCH 07/22] Koa: Testing get based on koa middleware --- .eslintrc.json | 2 +- .mocharc.js | 2 +- KoaResource.js | 162 +++++++++++------ package.json | 3 +- test/testKoa.js | 448 ++++++++++++++++++++++++------------------------ 5 files changed, 340 insertions(+), 277 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 2614f13..d95aa81 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "extends": "formio", "parserOptions": { - "ecmaVersion": 2015 + "ecmaVersion": 2020 }, "env": { "browser": true, diff --git a/.mocharc.js b/.mocharc.js index 01ac034..ac893cd 100644 --- a/.mocharc.js +++ b/.mocharc.js @@ -14,5 +14,5 @@ module.exports = { slow: 75, timeout: 2000, ui: 'bdd', - 'watch-files': ['test/**/*.js'] + 'watch-files': ['test/**/testKoa.js'] }; \ No newline at end of file diff --git a/KoaResource.js b/KoaResource.js index bf34e94..23815e0 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -152,6 +152,17 @@ class Resource { this.app.use(this.router.routes(), this.router.allowedMethods()); } + _generateMiddleware(options, position) { + let routeStack = []; + + if (options && options[position]) { + const before = options[position].map((m) => m.bind(this)); + routeStack = [...routeStack, ...before]; + } + console.log(routeStack) + return compose(routeStack); + } + /** * Sets the different responses and calls the next middleware for * execution. @@ -226,7 +237,7 @@ class Resource { */ static setResponse(res, resource, next) { res.resource = resource; - next(); + // next(); } /** @@ -248,7 +259,7 @@ class Resource { // Uppercase the method. method = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase(); const methodOptions = { methodOptions: true }; - + console.log(options.before?.toString()) // Find all of the options that may have been passed to the rest method. const beforeHandlers = options.before ? ( @@ -629,61 +640,110 @@ class Resource { get(options) { options = Resource.getMethodOptions('get', options); this.methods.push('get'); - this._register('get', `${this.route}/:${this.name}Id`, (ctx, next) => { - // Store the internal method for response manipulation. - ctx.__rMethod = 'get'; - if (ctx.skipResource) { - debug.get('Skipping Resource'); - return next(); - } + const middlewares = compose([ + async(ctx, next) => { + console.log('test1') + return await next(); + // Store the internal method for response manipulation. + ctx.__rMethod = 'get'; + if (ctx.skipResource) { + debug.get('Skipping Resource'); + return await next(); + } + ctx.modelQuery = (ctx.modelQuery || ctx.model || this.model).findOne(); + ctx.search = { '_id': ctx.params[`${this.name}Id`] }; - const query = (ctx.modelQuery || ctx.model || this.model).findOne(); - const search = { '_id': ctx.params[`${this.name}Id`] }; + // Only call populate if they provide a populate query. + const populate = Resource.getParamQuery(ctx, 'populate'); + if (populate) { + debug.get(`Populate: ${populate}`); + ctx.modelQuery.populate(populate); + } + return await next(); + }, + this._generateMiddleware.call(this, 'get', `${this.route}/:${this.name}Id`, options, 'before'), + async(ctx, next) => { + console.log('test2') + return await next(); + ctx.item = await ctx.modelQuery.where(ctx.search).lean().exec(); + if (!ctx.item) { + Resource.setResponse(ctx, { status: 404 }, next); + } + return await next(); + }, + this._generateMiddleware.call(this, 'get', `${this.route}/:${this.name}Id`, options, 'after'), + async(ctx, next) => { + console.log('test3') + return await next(); + // Allow them to only return specified fields. + const select = Resource.getParamQuery(ctx, 'select'); + if (select) { + const newItem = {}; + // Always include the _id. + if (ctx.item._id) { + newItem._id = ctx.item._id; + } + select.split(' ').map(key => { + key = key.trim(); + if (Object.prototype.hasOwnProperty.call(ctx.item,key)) { + newItem[key] = ctx.item[key]; + } + }); + ctx.item = newItem; + } + Resource.setResponse(ctx, { status: 200, item: ctx.item }, next); + return await next(); + }, + Resource.respond, + ]); + console.log(middlewares.toString()) + this.router.get(`${this.route}/:${this.name}Id`, middlewares); + this.app.use(this.router.routes(), this.router.allowedMethods()); +/* this._register('get', `${this.route}/:${this.name}Id`, async(ctx, next) => { + try { + // Store the internal method for response manipulation. + ctx.__rMethod = 'get'; + if (ctx.skipResource) { + debug.get('Skipping Resource'); + return next(); + } - // Only call populate if they provide a populate query. - const populate = Resource.getParamQuery(ctx, 'populate'); - if (populate) { - debug.get(`Populate: ${populate}`); - query.populate(populate); - } + const query = (ctx.modelQuery || ctx.model || this.model).findOne(); + ctx.search = { '_id': ctx.params[`${this.name}Id`] }; - options.hooks.get.before.call( - this, - ctx, - search, - () => { - query.where(search).lean().exec((err, item) => { - if (err) return Resource.setResponse(ctx, { status: 400, error: err }, next); - if (!item) return Resource.setResponse(ctx, { status: 404 }, next); + // Only call populate if they provide a populate query. + const populate = Resource.getParamQuery(ctx, 'populate'); + if (populate) { + debug.get(`Populate: ${populate}`); + query.populate(populate); + } + await next(); // options.hooks.get.before.call(this, ctx, search), - return options.hooks.get.after.call( - this, - ctx, - item, - () => { - // Allow them to only return specified fields. - const select = Resource.getParamQuery(ctx, 'select'); - if (select) { - const newItem = {}; - // Always include the _id. - if (item._id) { - newItem._id = item._id; - } - select.split(' ').map(key => { - key = key.trim(); - if (Object.prototype.hasOwnProperty.call(item,key)) { - newItem[key] = item[key]; - } - }); - item = newItem; - } - Resource.setResponse(ctx, { status: 200, item: item }, next); - } - ); + let item = await query.where(ctx.search).lean().exec(); + if (!item) return Resource.setResponse(ctx, { status: 404 }, next); + + await next(); // options.hooks.get.after.call(this, ctx, item); + // Allow them to only return specified fields. + const select = Resource.getParamQuery(ctx, 'select'); + if (select) { + const newItem = {}; + // Always include the _id. + if (item._id) { + newItem._id = item._id; + } + select.split(' ').map(key => { + key = key.trim(); + if (Object.prototype.hasOwnProperty.call(item,key)) { + newItem[key] = item[key]; + } }); + item = newItem; } - ); - }, Resource.respond, options); + Resource.setResponse(ctx, { status: 200, item: item }, next); + } catch (error) { + return Resource.setResponse(ctx, { status: 400, error }, next); + } + }, Resource.respond, options); */ return this; } diff --git a/package.json b/package.json index e852487..6cb639d 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "A simple Express library to reflect Mongoose models to a REST interface.", "main": "Resource.js", "scripts": { - "test": "mocha --exit", + "test": "nyc mocha --exit", + "test:koa": "nyc mocha ./test/testKoa.js --exit", "coverage": "nyc --reporter=lcov --report-dir=./coverage npm run test", "lint": "eslint Resource.js" }, diff --git a/test/testKoa.js b/test/testKoa.js index 727d13e..387749e 100644 --- a/test/testKoa.js +++ b/test/testKoa.js @@ -7,8 +7,9 @@ const request = require('supertest'); const assert = require('assert'); const moment = require('moment'); const mongoose = require('mongoose'); -const Resource = require('../Resource'); +const Resource = require('../KoaResource'); const app = new Koa(); +const server = app.listen(); const _ = require('lodash'); const MongoClient = require('mongodb').MongoClient; const ObjectId = require('mongodb').ObjectID; @@ -41,12 +42,12 @@ let db = null; * @param req * The express request to manipulate. */ -function setInvoked(entity, sequence, req) { +function setInvoked(entity, sequence, ctx) { // Get the url fragments, to determine if this request is a get or index. - const parts = req.url.split('/'); + const parts = ctx.url.split('/'); parts.shift(); // Remove empty string element. - let method = req.method.toLowerCase(); + let method = ctx.method.toLowerCase(); if (method === 'get' && (parts.length % 2 === 0)) { method = 'index'; } @@ -161,13 +162,13 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const resource1 = Resource(app, '/test', 'resource1', Resource1Model).rest({ - afterDelete(req, res, next) { + afterDelete(ctx, next) { // Check that the delete item is still being returned via resourcejs. - assert.notEqual(res.resource.item, {}); - assert.notEqual(res.resource.item, []); - assert.equal(res.resource.status, 204); - assert.equal(res.statusCode, 200); - next(); + assert.notEqual(ctx.resource.item, {}); + assert.notEqual(ctx.resource.item, []); + assert.equal(ctx.resource.status, 204); + assert.equal(ctx.statusCode, 200); + // next(); }, }); const resource1Swaggerio = require('./snippets/resource1Swaggerio.json'); @@ -209,24 +210,24 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const resource2 = Resource(app, '/test', 'resource2', Resource2Model).rest({ // Register before/after global handlers. - before(req, res, next) { + before(ctx, next) { // Store the invoked handler and continue. - setInvoked('resource2', 'before', req); + setInvoked('resource2', 'before', ctx); next(); }, - beforePost(req, res, next) { + beforePost(ctx, next) { // Store the invoked handler and continue. - setInvoked('resource2', 'beforePost', req); + setInvoked('resource2', 'beforePost', ctx); next(); }, - after(req, res, next) { + after(ctx, next) { // Store the invoked handler and continue. - setInvoked('resource2', 'after', req); + setInvoked('resource2', 'after', ctx); next(); }, - afterPost(req, res, next) { + afterPost(ctx, next) { // Store the invoked handler and continue. - setInvoked('resource2', 'afterPost', req); + setInvoked('resource2', 'afterPost', ctx); next(); }, }); @@ -259,7 +260,7 @@ describe('Build Resources for following tests', () => { assert.equal(swaggerio.definitions.date.title, 'date'); assert.equal(Object.values(swaggerio.paths).length, 2); assert.deepEqual(swaggerio, resource3Swaggerio); - return Promise.all(testDates.map((date) => request(app) + return Promise.all(testDates.map((date) => request(server) .post('/test/date') .send({ date: date.toDate(), @@ -293,8 +294,8 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const nested1 = Resource(app, '/test/resource1/:resource1Id', 'nested1', Nested1Model).rest({ // Register before global handlers to set the resource1 variable. - before(req, res, next) { - req.body.resource1 = req.params.resource1Id; + before(ctx, next) { + ctx.request.body.resource1 = ctx.params.resource1Id; next(); }, }); @@ -335,17 +336,17 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const nested2 = Resource(app, '/test/resource2/:resource2Id', 'nested2', Nested2Model).rest({ // Register before/after global handlers. - before(req, res, next) { - req.body.resource2 = req.params.resource2Id; - req.modelQuery = this.model.where('resource2', req.params.resource2Id); + before(ctx, next) { + ctx.request.body.resource2 = ctx.params.resource2Id; + ctx.modelQuery = this.model.where('resource2', ctx.params.resource2Id); // Store the invoked handler and continue. - setInvoked('nested2', 'before', req); + setInvoked('nested2', 'before', ctx); next(); }, - after(req, res, next) { + after(ctx, next) { // Store the invoked handler and continue. - setInvoked('nested2', 'after', req); + setInvoked('nested2', 'after', ctx); next(); }, }); @@ -387,9 +388,9 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const resource3 = Resource(app, '/test', 'resource3', Resource3Model).rest({ - before(req, res, next) { + before(ctx, next) { // This setting should be passed down to the underlying `save()` command - req.writeOptions = { writeSetting: true }; + ctx.writeOptions = { writeSetting: true }; next(); }, @@ -420,8 +421,8 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const resource4 = Resource(app, '/test', 'resource4', Resource4Model) .rest({ - beforePatch(req, res, next) { - req.modelQuery = { + beforePatch(ctx, next) { + ctx.modelQuery = { findOne: function findOne(_, callback) { callback(new Error('failed'), undefined); }, @@ -431,15 +432,15 @@ describe('Build Resources for following tests', () => { }) .virtual({ path: 'undefined_query', - before: function(req, res, next) { - req.modelQuery = undefined; + before: function(ctx, next) { + ctx.modelQuery = undefined; return next(); }, }) .virtual({ path: 'defined', - before: function(req, res, next) { - req.modelQuery = Resource4Model.aggregate([ + before: function(ctx, next) { + ctx.modelQuery = Resource4Model.aggregate([ { $group: { _id: null, titles: { $sum: '$title' } } }, ]); return next(); @@ -447,8 +448,8 @@ describe('Build Resources for following tests', () => { }) .virtual({ path: 'error', - before: function(req, res, next) { - req.modelQuery = { + before: function(ctx, next) { + ctx.modelQuery = { exec: function exec(callback) { callback(new Error('Failed'), undefined); }, @@ -458,8 +459,8 @@ describe('Build Resources for following tests', () => { }) .virtual({ path: 'empty', - before: function(req, res, next) { - req.modelQuery = { + before: function(ctx, next) { + ctx.modelQuery = { exec: function exec(callback) { callback(undefined, undefined); }, @@ -489,16 +490,17 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const skipResource = Resource(app, '/test', 'skip', SkipModel) .rest({ - before(req, res, next) { - req.skipResource = true; - next(); + before: async (ctx, next) => { + console.log(ctx, 'test1.1') + ctx.skipResource = true; + return await next(); }, }) .virtual({ path: 'resource', - before: function(req, res, next) { - req.skipResource = true; - return next(); + before: (ctx, next) => { + ctx.skipResource = true; + // return next(); }, }); const skipSwaggerio = require('./snippets/skipSwaggerio.json'); @@ -514,9 +516,9 @@ describe('Build Resources for following tests', () => { describe('Test skipResource', () => { const resource = {}; - it('/GET empty list', () => request(app) + it('/GET empty list', () => request(server) .get('/test/skip') - .expect('Content-Type', /text\/html/) + //.expect('Content-Type', /text\/html/) .expect(404) .then((res) => { const response = res.text; @@ -524,7 +526,7 @@ describe('Test skipResource', () => { assert(response.includes(expected), 'Response not found.'); })); - it('/POST Create new resource', () => request(app) + it('/POST Create new resource', () => request(server) .post('/test/skip') .send({ title: 'Test1', @@ -538,7 +540,7 @@ describe('Test skipResource', () => { assert(response.includes(expected), 'Response not found.'); })); - it('/GET The new resource', () => request(app) + it('/GET The new resource', () => request(server) .get(`/test/skip/${resource._id}`) .expect('Content-Type', /text\/html/) .expect(404) @@ -548,7 +550,7 @@ describe('Test skipResource', () => { assert(response.includes(expected), 'Response not found.'); })); - it('/PUT Change data on the resource', () => request(app) + it('/PUT Change data on the resource', () => request(server) .put(`/test/skip/${resource._id}`) .send({ title: 'Test2', @@ -561,7 +563,7 @@ describe('Test skipResource', () => { assert(response.includes(expected), 'Response not found.'); })); - it('/PATCH Change data on the resource', () => request(app) + it('/PATCH Change data on the resource', () => request(server) .patch(`/test/skip/${resource._id}`) .expect('Content-Type', /text\/html/) .expect(404) @@ -571,7 +573,7 @@ describe('Test skipResource', () => { assert(response.includes(expected), 'Response not found.'); })); - it('/DELETE the resource', () => request(app) + it('/DELETE the resource', () => request(server) .delete(`/test/skip/${resource._id}`) .expect('Content-Type', /text\/html/) .expect(404) @@ -581,7 +583,7 @@ describe('Test skipResource', () => { assert(response.includes(expected), 'Response not found.'); })); - it('/VIRTUAL the resource', () => request(app) + it('/VIRTUAL the resource', () => request(server) .get('/test/skip/virtual/resource') .expect('Content-Type', /text\/html/) .expect(404) @@ -593,7 +595,7 @@ describe('Test skipResource', () => { }); describe('Test Virtual resource and Patch errors', () => { - it('/VIRTUAL undefined resource query', () => request(app) + it('/VIRTUAL undefined resource query', () => request(server) .get('/test/resource4/virtual/undefined_query') .expect('Content-Type', /json/) .expect(404) @@ -601,7 +603,7 @@ describe('Test Virtual resource and Patch errors', () => { assert.equal(res.body.errors[0], 'Resource not found'); })); - it('/VIRTUAL resource query', () => request(app) + it('/VIRTUAL resource query', () => request(server) .get('/test/resource4/virtual/defined') .expect('Content-Type', /json/) .expect(200) @@ -611,7 +613,7 @@ describe('Test Virtual resource and Patch errors', () => { assert.equal(response[0].titles, 0); })); - it('/VIRTUAL errorous resource query', () => request(app) + it('/VIRTUAL errorous resource query', () => request(server) .get('/test/resource4/virtual/error') .expect('Content-Type', /json/) .expect(400) @@ -620,7 +622,7 @@ describe('Test Virtual resource and Patch errors', () => { assert.equal(response.message, 'Failed'); })); - it('/VIRTUAL empty resource response', () => request(app) + it('/VIRTUAL empty resource response', () => request(server) .get('/test/resource4/virtual/empty') .expect('Content-Type', /json/) .expect(404) @@ -629,7 +631,7 @@ describe('Test Virtual resource and Patch errors', () => { assert.equal(response.errors[0], 'Resource not found'); })); - it('/PATCH with errorous modelquery', () => request(app) + it('/PATCH with errorous modelquery', () => request(server) .patch('/test/resource4/1234') .expect('Content-Type', /json/) .expect(400) @@ -642,7 +644,7 @@ describe('Test Virtual resource and Patch errors', () => { describe('Test single resource CRUD capabilities', () => { let resource = {}; - it('/GET empty list', () => request(app) + it('/GET empty list', () => request(server) .get('/test/resource1') .expect('Content-Type', /json/) .expect('Content-Range', '*/0') @@ -652,7 +654,7 @@ describe('Test single resource CRUD capabilities', () => { assert.deepEqual(res.body, []); })); - it('/POST Create new resource', () => request(app) + it('/POST Create new resource', () => request(server) .post('/test/resource1') .send({ title: 'Test1', @@ -667,7 +669,7 @@ describe('Test single resource CRUD capabilities', () => { assert(resource.hasOwnProperty('_id'), 'Resource ID not found'); })); - it('/GET The new resource', () => request(app) + it('/GET The new resource', () => request(server) .get(`/test/resource1/${resource._id}`) .expect('Content-Type', /json/) .expect(200) @@ -677,7 +679,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body._id, resource._id); })); - it('/PUT Change data on the resource', () => request(app) + it('/PUT Change data on the resource', () => request(server) .put(`/test/resource1/${resource._id}`) .send({ title: 'Test2', @@ -691,7 +693,7 @@ describe('Test single resource CRUD capabilities', () => { resource = res.body; })); - it('/PATCH Change data on the resource', () => request(app) + it('/PATCH Change data on the resource', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'replace', 'path': '/title', 'value': 'Test3' }]) .expect('Content-Type', /json/) @@ -701,7 +703,7 @@ describe('Test single resource CRUD capabilities', () => { resource = res.body; })); - it('/PATCH Reject update due to failed test op', () => request(app) + it('/PATCH Reject update due to failed test op', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([ { 'op': 'test', 'path': '/title', 'value': 'not-the-title' }, @@ -714,7 +716,7 @@ describe('Test single resource CRUD capabilities', () => { resource = res.body; })); - it('/PATCH Reject update due to incorrect patch operation', () => request(app) + it('/PATCH Reject update due to incorrect patch operation', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'does-not-exist', 'path': '/title', 'value': 'Test4' }]) .expect('Content-Type', /json/) @@ -723,7 +725,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_OP_INVALID'); })); - it('/PATCH Should not care whether patch is array or not', () => request(app) + it('/PATCH Should not care whether patch is array or not', () => request(server) .patch(`/test/resource1/${resource._id}`) .send({ 'op': 'test', 'path': '/title', 'value': 'Test3' }) .expect('Content-Type', /json/) @@ -732,7 +734,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.title, 'Test3'); })); - it('/PATCH Reject update due to incorrect patch object', () => request(app) + it('/PATCH Reject update due to incorrect patch object', () => request(server) .patch(`/test/resource1/${resource._id}`) .send(['invalid-patch']) .expect('Content-Type', /json/) @@ -741,7 +743,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_NOT_AN_OBJECT'); })); - it('/PATCH Reject update due to incorrect patch value', () => request(app) + it('/PATCH Reject update due to incorrect patch value', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'replace', 'path': '/title', 'value': undefined }]) .expect('Content-Type', /json/) @@ -750,7 +752,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_VALUE_REQUIRED'); })); - it('/PATCH Reject update due to incorrect patch add path', () => request(app) + it('/PATCH Reject update due to incorrect patch add path', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'add', 'path': '/path/does/not/exist', 'value': 'Test4' }]) .expect('Content-Type', /json/) @@ -759,7 +761,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_PATH_CANNOT_ADD'); })); - it('/PATCH Reject update due to incorrect patch path', () => request(app) + it('/PATCH Reject update due to incorrect patch path', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'replace', 'path': '/path/does/not/exist', 'value': 'Test4' }]) .expect('Content-Type', /json/) @@ -768,7 +770,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_PATH_UNRESOLVABLE'); })); - it('/PATCH Reject update due to incorrect patch path', () => request(app) + it('/PATCH Reject update due to incorrect patch path', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'replace', 'path': 1, 'value': 'Test4' }]) .expect('Content-Type', /json/) @@ -777,7 +779,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_PATH_INVALID'); })); - it('/PATCH Reject update due to incorrect patch path', () => request(app) + it('/PATCH Reject update due to incorrect patch path', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'add', 'path': '/path/does/not/exist', 'value': 'Test4' }]) .expect('Content-Type', /json/) @@ -786,7 +788,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_PATH_CANNOT_ADD'); })); - it('/PATCH Reject update due to incorrect patch path', () => request(app) + it('/PATCH Reject update due to incorrect patch path', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'move', 'from': '/path/does/not/exist', 'path': '/path/does/not/exist', 'value': 'Test4' }]) .expect('Content-Type', /json/) @@ -795,7 +797,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_FROM_UNRESOLVABLE'); })); - it('/PATCH Reject update due to incorrect patch array', () => request(app) + it('/PATCH Reject update due to incorrect patch array', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'add', 'path': '/list/invalidindex', 'value': '2' }]) .expect('Content-Type', /json/) @@ -804,7 +806,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_PATH_ILLEGAL_ARRAY_INDEX'); })); - it('/PATCH Reject update due to incorrect patch array', () => request(app) + it('/PATCH Reject update due to incorrect patch array', () => request(server) .patch(`/test/resource1/${resource._id}`) .send([{ 'op': 'add', 'path': '/list/9999', 'value': '2' }]) .expect('Content-Type', /json/) @@ -813,7 +815,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body.errors[0].name, 'OPERATION_VALUE_OUT_OF_BOUNDS'); })); - it('/GET The changed resource', () => request(app) + it('/GET The changed resource', () => request(server) .get(`/test/resource1/${resource._id}`) .expect('Content-Type', /json/) .expect(200) @@ -823,7 +825,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body._id, resource._id); })); - it('/GET index of resources', () => request(app) + it('/GET index of resources', () => request(server) .get('/test/resource1') .expect('Content-Type', /json/) .expect('Content-Range', '0-0/1') @@ -835,7 +837,7 @@ describe('Test single resource CRUD capabilities', () => { assert.equal(res.body[0]._id, resource._id); })); - it('Cannot /POST to an existing resource', () => request(app) + it('Cannot /POST to an existing resource', () => request(server) .post(`/test/resource1/${resource._id}`) .expect('Content-Type', /text\/html/) .expect(404) @@ -845,14 +847,14 @@ describe('Test single resource CRUD capabilities', () => { assert(response.includes(expected), 'Response not found.'); })); - it('/DELETE the resource', () => request(app) + it('/DELETE the resource', () => request(server) .delete(`/test/resource1/${resource._id}`) .expect(200) .then((res) => { assert.deepEqual(res.body, {}); })); - it('/GET empty list', () => request(app) + it('/GET empty list', () => request(server) .get('/test/resource1') .expect('Content-Type', /json/) .expect('Content-Range', '*/0') @@ -872,7 +874,7 @@ describe('Test single resource CRUD capabilities', () => { it('Should create a reference doc with mongoose', () => { const doc = { data: 'test1' }; - return request(app) + return request(server) .post('/test/ref') .send(doc) .expect('Content-Type', /json/) @@ -921,7 +923,7 @@ describe('Test single resource CRUD capabilities', () => { it('/PUT to a resource with subdocuments should not mangle the subdocuments', () => { const two = { label: 'two', data: [doc2._id] }; - return request(app) + return request(server) .put(`/test/resource1/${resource._id}`) .send({ list: resource.list.concat(two) }) .expect('Content-Type', /json/) @@ -961,7 +963,7 @@ describe('Test single resource CRUD capabilities', () => { it('/PUT to a resource subdocument should not mangle the subdocuments', () => { // Update a subdocument property. const update = _.clone(resource.list); - return request(app) + return request(server) .put(`/test/resource1/${resource._id}`) .send({ list: update }) .expect('Content-Type', /json/) @@ -979,7 +981,7 @@ describe('Test single resource CRUD capabilities', () => { it('/PUT to a top-level property should not mangle the other collection properties', () => { const tempTitle = 'an update without docs'; - return request(app) + return request(server) .put(`/test/resource1/${resource._id}`) .send({ title: tempTitle }) .expect('Content-Type', /json/) @@ -1016,7 +1018,7 @@ let refDoc1Response = null; const resourceNames = []; // eslint-disable-next-line max-statements function testSearch(testPath) { - it('Should populate', () => request(app) + it('Should populate', () => request(server) .get(`${testPath}?name=noage&populate=list.data`) .then((res) => { const response = res.body; @@ -1037,7 +1039,7 @@ function testSearch(testPath) { assert.equal(response[0].list[0].data[0].data, refDoc1Content.data); })); - it('Should ignore empty populate query parameter', () => request(app) + it('Should ignore empty populate query parameter', () => request(server) .get(`${testPath}?name=noage&populate=`) .then((res) => { const response = res.body; @@ -1057,7 +1059,7 @@ function testSearch(testPath) { assert.equal(response[0].list[0].data[0], refDoc1Response._id); })); - it('Should not populate paths that are not a reference', () => request(app) + it('Should not populate paths that are not a reference', () => request(server) .get(`${testPath}?name=noage&populate=list2`) .then((res) => { const response = res.body; @@ -1077,7 +1079,7 @@ function testSearch(testPath) { assert.equal(response[0].list[0].data[0], refDoc1Response._id); })); - it('Should populate with options', () => request(app) + it('Should populate with options', () => request(server) .get(`${testPath}?name=noage&populate[path]=list.data`) .then((res) => { const response = res.body; @@ -1098,7 +1100,7 @@ function testSearch(testPath) { assert.equal(response[0].list[0].data[0].data, refDoc1Content.data); })); - it('Should limit 10', () => request(app) + it('Should limit 10', () => request(server) .get(testPath) .expect('Content-Type', /json/) .expect(206) @@ -1114,7 +1116,7 @@ function testSearch(testPath) { }); })); - it('Should accept a change in limit', () => request(app) + it('Should accept a change in limit', () => request(server) .get(`${testPath}?limit=5`) .expect('Content-Type', /json/) .expect('Content-Range', '0-4/26') @@ -1131,7 +1133,7 @@ function testSearch(testPath) { }); })); - it('Should be able to skip and limit', () => request(app) + it('Should be able to skip and limit', () => request(server) .get(`${testPath}?limit=5&skip=4`) .expect('Content-Type', /json/) .expect('Content-Range', '4-8/26') @@ -1148,7 +1150,7 @@ function testSearch(testPath) { }); })); - it('Should default negative limit to 10', () => request(app) + it('Should default negative limit to 10', () => request(server) .get(`${testPath}?limit=-5&skip=4`) .expect('Content-Type', /json/) .expect('Content-Range', '4-13/26') @@ -1165,7 +1167,7 @@ function testSearch(testPath) { }); })); - it('Should default negative skip to 0', () => request(app) + it('Should default negative skip to 0', () => request(server) .get(`${testPath}?limit=5&skip=-4`) .expect('Content-Type', /json/) .expect('Content-Range', '0-4/26') @@ -1182,7 +1184,7 @@ function testSearch(testPath) { }); })); - it('Should default negative skip and negative limit to 0 and 10', () => request(app) + it('Should default negative skip and negative limit to 0 and 10', () => request(server) .get(`${testPath}?limit=-5&skip=-4`) .expect('Content-Type', /json/) .expect('Content-Range', '0-9/26') @@ -1199,7 +1201,7 @@ function testSearch(testPath) { }); })); - it('Should default non numeric limit to 10', () => request(app) + it('Should default non numeric limit to 10', () => request(server) .get(`${testPath}?limit=badlimit&skip=4`) .expect('Content-Type', /json/) .expect('Content-Range', '4-13/26') @@ -1216,7 +1218,7 @@ function testSearch(testPath) { }); })); - it('Should default non numeric skip to 0', () => request(app) + it('Should default non numeric skip to 0', () => request(server) .get(`${testPath}?limit=5&skip=badskip`) .expect('Content-Type', /json/) .expect('Content-Range', '0-4/26') @@ -1233,7 +1235,7 @@ function testSearch(testPath) { }); })); - it('Should be able to select fields', () => request(app) + it('Should be able to select fields', () => request(server) .get(`${testPath}?limit=10&skip=10&select=title,age`) .expect('Content-Type', /json/) .expect('Content-Range', '10-19/26') @@ -1250,7 +1252,7 @@ function testSearch(testPath) { }); })); - it('Should be able to select fields with multiple select queries', () => request(app) + it('Should be able to select fields with multiple select queries', () => request(server) .get(`${testPath}?limit=10&skip=10&select=title&select=age`) .expect('Content-Type', /json/) .expect('Content-Range', '10-19/26') @@ -1267,7 +1269,7 @@ function testSearch(testPath) { }); })); - it('Should be able to sort', () => request(app) + it('Should be able to sort', () => request(server) .get(`${testPath}?select=age&sort=-age`) .expect('Content-Type', /json/) .expect('Content-Range', '0-9/26') @@ -1284,7 +1286,7 @@ function testSearch(testPath) { }); })); - it('Should paginate with a sort', () => request(app) + it('Should paginate with a sort', () => request(server) .get(`${testPath}?limit=5&skip=5&select=age&sort=-age`) .expect('Content-Type', /json/) .expect('Content-Range', '5-9/26') @@ -1301,7 +1303,7 @@ function testSearch(testPath) { }); })); - it('Should be able to find', () => request(app) + it('Should be able to find', () => request(server) .get(`${testPath}?limit=5&select=age&age=5`) .expect('Content-Type', /json/) .expect('Content-Range', '0-0/1') @@ -1314,7 +1316,7 @@ function testSearch(testPath) { assert.equal(response[0].age, 5); })); - it('eq search selector', () => request(app) + it('eq search selector', () => request(server) .get(`${testPath}?age__eq=5`) .expect('Content-Type', /json/) .expect(200) @@ -1326,7 +1328,7 @@ function testSearch(testPath) { }); })); - it('equals (alternative) search selector', () => request(app) + it('equals (alternative) search selector', () => request(server) .get(`${testPath}?age=5`) .expect('Content-Type', /json/) .expect(200) @@ -1338,7 +1340,7 @@ function testSearch(testPath) { }); })); - it('ne search selector', () => request(app) + it('ne search selector', () => request(server) .get(`${testPath}?age__ne=5&limit=100`) .expect('Content-Type', /json/) .expect(200) @@ -1350,7 +1352,7 @@ function testSearch(testPath) { }); })); - it('in search selector', () => request(app) + it('in search selector', () => request(server) .get(`${testPath}?title__in=Test Age 1,Test Age 5,Test Age 9,Test Age 20`) .expect('Content-Type', /json/) .expect(200) @@ -1370,7 +1372,7 @@ function testSearch(testPath) { }); })); - it('nin search selector', () => request(app) + it('nin search selector', () => request(server) .get(`${testPath}?title__nin=Test Age 1,Test Age 5`) .expect('Content-Type', /json/) .expect(206) @@ -1390,7 +1392,7 @@ function testSearch(testPath) { }); })); - it('exists=false search selector', () => request(app) + it('exists=false search selector', () => request(server) .get(`${testPath}?age__exists=false`) .expect('Content-Type', /json/) .expect(200) @@ -1400,7 +1402,7 @@ function testSearch(testPath) { assert.equal(response[0].name, 'noage'); })); - it('exists=0 search selector', () => request(app) + it('exists=0 search selector', () => request(server) .get(`${testPath}?age__exists=0`) .expect('Content-Type', /json/) .expect(200) @@ -1410,7 +1412,7 @@ function testSearch(testPath) { assert.equal(response[0].name, 'noage'); })); - it('exists=true search selector', () => request(app) + it('exists=true search selector', () => request(server) .get(`${testPath}?age__exists=true&limit=1000`) .expect('Content-Type', /json/) .expect(200) @@ -1422,7 +1424,7 @@ function testSearch(testPath) { }); })); - it('exists=1 search selector', () => request(app) + it('exists=1 search selector', () => request(server) .get(`${testPath}?age__exists=true&limit=1000`) .expect('Content-Type', /json/) .expect(200) @@ -1434,7 +1436,7 @@ function testSearch(testPath) { }); })); - it('lt search selector', () => request(app) + it('lt search selector', () => request(server) .get(`${testPath}?age__lt=5`) .expect('Content-Range', '0-4/5') .then((res) => { @@ -1445,7 +1447,7 @@ function testSearch(testPath) { }); })); - it('lte search selector', () => request(app) + it('lte search selector', () => request(server) .get(`${testPath}?age__lte=5`) .expect('Content-Range', '0-5/6') .then((res) => { @@ -1456,7 +1458,7 @@ function testSearch(testPath) { }); })); - it('gt search selector', () => request(app) + it('gt search selector', () => request(server) .get(`${testPath}?age__gt=5`) .expect('Content-Range', '0-9/19') .then((res) => { @@ -1467,7 +1469,7 @@ function testSearch(testPath) { }); })); - it('gte search selector', () => request(app) + it('gte search selector', () => request(server) .get(`${testPath}?age__gte=5`) .expect('Content-Range', '0-9/20') .then((res) => { @@ -1478,7 +1480,7 @@ function testSearch(testPath) { }); })); - it('regex search selector', () => request(app) + it('regex search selector', () => request(server) .get(`${testPath}?title__regex=/.*Age [0-1]?[0-3]$/g`) .expect('Content-Range', '0-7/8') .then((res) => { @@ -1493,11 +1495,11 @@ function testSearch(testPath) { it('regex search selector should be case insensitive', () => { const name = resourceNames[0].toString(); - return request(app) + return request(server) .get(`${testPath}?name__regex=${name.toUpperCase()}`) .then((res) => { const uppercaseResponse = res.body; - return request(app) + return request(server) .get(`/test/resource1?name__regex=${name.toLowerCase()}`) .then((res) => { const lowercaseResponse = res.body; @@ -1511,7 +1513,7 @@ describe('Test single resource search capabilities', () => { let singleResource1Id = undefined; it('Should create a reference doc with mongoose', () => { refDoc1Content = { data: 'test1' }; - return request(app) + return request(server) .post('/test/ref') .send(refDoc1Content) .expect('Content-Type', /json/) @@ -1526,7 +1528,7 @@ describe('Test single resource search capabilities', () => { it('Create a full index of resources', () => _.range(25).reduce((promise, age) => { const name = (chance.name()).toUpperCase(); resourceNames.push(name); - return promise.then(() => request(app) + return promise.then(() => request(server) .post('/test/resource1') .send({ title: `Test Age ${age}`, @@ -1545,7 +1547,7 @@ describe('Test single resource search capabilities', () => { const refList = [{ label: '1', data: [refDoc1Response._id] }]; // Insert a record with no age. - return request(app) + return request(server) .post('/test/resource1') .send({ title: 'No Age', @@ -1566,7 +1568,7 @@ describe('Test single resource search capabilities', () => { testSearch('/test/resource1'); - it('Should allow population on single object GET request', () => request(app) + it('Should allow population on single object GET request', () => request(server) .get(`/test/resource1/${singleResource1Id}?populate=list.data`) .then((res) => { const response = res.body; @@ -1589,9 +1591,9 @@ describe('Test single resource search capabilities', () => { it('Create an aggregation path', () => { Resource(app, '', 'aggregation', mongoose.model('resource1')).rest({ - beforeIndex(req, res, next) { - req.modelQuery = mongoose.model('resource1'); - req.modelQuery.pipeline = []; + beforeIndex(ctx, next) { + ctx.modelQuery = mongoose.model('resource1'); + ctx.modelQuery.pipeline = []; next(); }, }); @@ -1605,22 +1607,22 @@ describe('Test dates search capabilities', () => { const isoString = testDates[0].toISOString(); return Promise.all([ - request(app) + request(server) .get(`/test/date?date=${isoString}`) .then(({ body: response }) => assert.equal(response.length, 1)), - request(app) + request(server) .get(`/test/date?date__lt=${isoString}`) .then(({ body: response }) => assert.equal(response.length, 3)), - request(app) + request(server) .get(`/test/date?date__lte=${isoString}`) .then(({ body: response }) => assert.equal(response.length, 4)), - request(app) + request(server) .get(`/test/date?date__gte=${isoString}`) .then(({ body: response }) => assert.equal(response.length, 1)), - request(app) + request(server) .get(`/test/date?date__gt=${isoString}`) .then(({ body: response }) => assert.equal(response.length, 0)), - request(app) + request(server) .get(`/test/date?date__ne=${isoString}`) .then(({ body: response }) => assert.equal(response.length, 3)), ]); @@ -1630,22 +1632,22 @@ describe('Test dates search capabilities', () => { const search = testDates[0].format('YYYY-MM-DD'); return Promise.all([ - request(app) + request(server) .get(`/test/date?date=${search}`) .then(({ body: response }) => assert.equal(response.length, 0)), - request(app) + request(server) .get(`/test/date?date__lt=${search}`) .then(({ body: response }) => assert.equal(response.length, 3)), - request(app) + request(server) .get(`/test/date?date__lte=${search}`) .then(({ body: response }) => assert.equal(response.length, 3)), - request(app) + request(server) .get(`/test/date?date__gte=${search}`) .then(({ body: response }) => assert.equal(response.length, 1)), - request(app) + request(server) .get(`/test/date?date__gt=${search}`) .then(({ body: response }) => assert.equal(response.length, 1)), - request(app) + request(server) .get(`/test/date?date__ne=${search}`) .then(({ body: response }) => assert.equal(response.length, 4)), ]); @@ -1655,22 +1657,22 @@ describe('Test dates search capabilities', () => { const search = testDates[0].format('YYYY-MM'); return Promise.all([ - request(app) + request(server) .get(`/test/date?date=${search}`) .then(({ body: response }) => assert.equal(response.length, 0)), - request(app) + request(server) .get(`/test/date?date__lt=${search}`) .then(({ body: response }) => assert.equal(response.length, 2)), - request(app) + request(server) .get(`/test/date?date__lte=${search}`) .then(({ body: response }) => assert.equal(response.length, 2)), - request(app) + request(server) .get(`/test/date?date__gte=${search}`) .then(({ body: response }) => assert.equal(response.length, 2)), - request(app) + request(server) .get(`/test/date?date__gt=${search}`) .then(({ body: response }) => assert.equal(response.length, 2)), - request(app) + request(server) .get(`/test/date?date__ne=${search}`) .then(({ body: response }) => assert.equal(response.length, 4)), ]); @@ -1680,22 +1682,22 @@ describe('Test dates search capabilities', () => { const search = testDates[0].format('YYYY'); return Promise.all([ - request(app) + request(server) .get(`/test/date?date=${search}`) .then(({ body: response }) => assert.equal(response.length, 0)), - request(app) + request(server) .get(`/test/date?date__lt=${search}`) .then(({ body: response }) => assert.equal(response.length, 1)), - request(app) + request(server) .get(`/test/date?date__lte=${search}`) .then(({ body: response }) => assert.equal(response.length, 1)), - request(app) + request(server) .get(`/test/date?date__gte=${search}`) .then(({ body: response }) => assert.equal(response.length, 3)), - request(app) + request(server) .get(`/test/date?date__gt=${search}`) .then(({ body: response }) => assert.equal(response.length, 3)), - request(app) + request(server) .get(`/test/date?date__ne=${search}`) .then(({ body: response }) => assert.equal(response.length, 4)), ]); @@ -1705,22 +1707,22 @@ describe('Test dates search capabilities', () => { const search = testDates[0].format('x'); return Promise.all([ - request(app) + request(server) .get(`/test/date?date=${search}`) .then(({ body: response }) => assert.equal(response.length, 1)), - request(app) + request(server) .get(`/test/date?date__lt=${search}`) .then(({ body: response }) => assert.equal(response.length, 3)), - request(app) + request(server) .get(`/test/date?date__lte=${search}`) .then(({ body: response }) => assert.equal(response.length, 4)), - request(app) + request(server) .get(`/test/date?date__gte=${search}`) .then(({ body: response }) => assert.equal(response.length, 1)), - request(app) + request(server) .get(`/test/date?date__gt=${search}`) .then(({ body: response }) => assert.equal(response.length, 0)), - request(app) + request(server) .get(`/test/date?date__ne=${search}`) .then(({ body: response }) => assert.equal(response.length, 3)), ]); @@ -1731,7 +1733,7 @@ describe('Test single resource handlers capabilities', () => { // Store the resource being mutated. let resource = {}; - it('A POST request should invoke the global handlers and method handlers', () => request(app) + it('A POST request should invoke the global handlers and method handlers', () => request(server) .post('/test/resource2') .send({ title: 'Test1', @@ -1755,7 +1757,7 @@ describe('Test single resource handlers capabilities', () => { resource = response; })); - it('A GET request should invoke the global handlers', () => request(app) + it('A GET request should invoke the global handlers', () => request(server) .get(`/test/resource2/${resource._id}`) .expect('Content-Type', /json/) .expect(200) @@ -1777,7 +1779,7 @@ describe('Test single resource handlers capabilities', () => { resource = response; })); - it('Should allow you to use select to select certain fields.', () => request(app) + it('Should allow you to use select to select certain fields.', () => request(server) .get(`/test/resource2/${resource._id}?select=title`) .expect('Content-Type', /json/) .expect(200) @@ -1787,7 +1789,7 @@ describe('Test single resource handlers capabilities', () => { assert.equal(response.description, undefined); })); - it('A PUT request should invoke the global handlers', () => request(app) + it('A PUT request should invoke the global handlers', () => request(server) .put(`/test/resource2/${resource._id}`) .send({ title: 'Test1 - Updated', @@ -1808,7 +1810,7 @@ describe('Test single resource handlers capabilities', () => { resource = response; })); - it('A GET (Index) request should invoke the global handlers', () => request(app) + it('A GET (Index) request should invoke the global handlers', () => request(server) .get('/test/resource2') .expect('Content-Type', /json/) .expect(200) @@ -1827,7 +1829,7 @@ describe('Test single resource handlers capabilities', () => { resource = response[0]; })); - it('A DELETE request should invoke the global handlers', () => request(app) + it('A DELETE request should invoke the global handlers', () => request(server) .delete(`/test/resource2/${resource._id}`) .expect(200) .then((res) => { @@ -1844,7 +1846,7 @@ describe('Test single resource handlers capabilities', () => { }); describe('Handle native data formats', () => { - it('Should create a new resource with boolean and string values set.', () => request(app) + it('Should create a new resource with boolean and string values set.', () => request(server) .post('/test/resource2') .send({ title: 'null', @@ -1860,7 +1862,7 @@ describe('Handle native data formats', () => { assert.equal(response.description, 'false'); })); - it('Should find the record when filtering the title as "null"', () => request(app) + it('Should find the record when filtering the title as "null"', () => request(server) .get('/test/resource2?title=null') .send() .expect('Content-Type', /json/) @@ -1871,7 +1873,7 @@ describe('Handle native data formats', () => { assert.equal(response[0].title, 'null'); })); - it('Should find the record when filtering the description as "false"', () => request(app) + it('Should find the record when filtering the description as "false"', () => request(server) .get('/test/resource2?description=false') .send() .expect('Content-Type', /json/) @@ -1883,7 +1885,7 @@ describe('Handle native data formats', () => { assert.equal(response[0].description, 'false'); })); - it('Should find the record when filtering the description as "true"', () => request(app) + it('Should find the record when filtering the description as "true"', () => request(server) .get('/test/resource2?description=true') .send() .expect('Content-Type', /json/) @@ -1893,7 +1895,7 @@ describe('Handle native data formats', () => { assert.equal(response.length, 0); })); - it('Should find the record when filtering the updated property as null with strict equality', () => request(app) + it('Should find the record when filtering the updated property as null with strict equality', () => request(server) .get('/test/resource2?updated__eq=null') .send() .expect('Content-Type', /json/) @@ -1905,7 +1907,7 @@ describe('Handle native data formats', () => { assert.equal(response[0].updated, null); })); - it('Should still find the null values based on string if explicitely provided "null"', () => request(app) + it('Should still find the null values based on string if explicitely provided "null"', () => request(server) .get('/test/resource2?title__eq="null"') .send() .expect('Content-Type', /json/) @@ -1916,7 +1918,7 @@ describe('Handle native data formats', () => { assert.equal(response[0].title, 'null'); })); - it('Should find the boolean false values based on equality', () => request(app) + it('Should find the boolean false values based on equality', () => request(server) .get('/test/resource2?description__eq=false') .send() .expect('Content-Type', /json/) @@ -1928,7 +1930,7 @@ describe('Handle native data formats', () => { assert.equal(response[0].married, true); })); - it('Should find the boolean true values based on equality', () => request(app) + it('Should find the boolean true values based on equality', () => request(server) .get('/test/resource2?married__eq=true') .send() .expect('Content-Type', /json/) @@ -1940,7 +1942,7 @@ describe('Handle native data formats', () => { assert.equal(response[0].married, true); })); - it('Should still find the boolean values based on string if explicitely provided', () => request(app) + it('Should still find the boolean values based on string if explicitely provided', () => request(server) .get('/test/resource2?description__eq=%22false%22') .send() .expect('Content-Type', /json/) @@ -1952,7 +1954,7 @@ describe('Handle native data formats', () => { assert.equal(response[0].married, true); })); - it('Should still find the boolean values based on string if explicitely provided', () => request(app) + it('Should still find the boolean values based on string if explicitely provided', () => request(server) .get('/test/resource2?married__eq=%22true%22') .send() .expect('Content-Type', /json/) @@ -1964,7 +1966,7 @@ describe('Handle native data formats', () => { assert.equal(response[0].married, true); })); - it('Should CAST a boolean to find the boolean values based on equals', () => request(app) + it('Should CAST a boolean to find the boolean values based on equals', () => request(server) .get('/test/resource2?married=true') .send() .expect('Content-Type', /json/) @@ -1976,7 +1978,7 @@ describe('Handle native data formats', () => { assert.equal(response[0].married, true); })); - it('Should CAST a boolean to find the boolean values based on equals', () => request(app) + it('Should CAST a boolean to find the boolean values based on equals', () => request(server) .get('/test/resource2?married=false') .send() .expect('Content-Type', /json/) @@ -1990,7 +1992,7 @@ describe('Handle native data formats', () => { describe('Test writeOptions capabilities', () => { let resource = {}; - it('/POST a new resource3 with options', () => request(app) + it('/POST a new resource3 with options', () => request(server) .post('/test/resource3') .send({ title: 'Test1' }) .expect('Content-Type', /json/) @@ -2002,7 +2004,7 @@ describe('Test writeOptions capabilities', () => { resource = response; })); - it('/PUT an update with options', () => request(app) + it('/PUT an update with options', () => request(server) .put(`/test/resource3/${resource._id}`) .send({ title: 'Test1 - Updated' }) .expect('Content-Type', /json/) @@ -2013,7 +2015,7 @@ describe('Test writeOptions capabilities', () => { assert(response.hasOwnProperty('_id'), 'Resource ID not found'); })); - it('/PATCH an update with options', () => request(app) + it('/PATCH an update with options', () => request(server) .patch(`/test/resource3/${resource._id}`) .send([{ 'op': 'replace', 'path': '/title', 'value': 'Test1 - Updated Again' }]) .expect('Content-Type', /json/) @@ -2024,7 +2026,7 @@ describe('Test writeOptions capabilities', () => { assert(response.hasOwnProperty('_id'), 'Resource ID not found'); })); - it('/DELETE a resource3 with options', () => request(app) + it('/DELETE a resource3 with options', () => request(server) .delete(`/test/resource3/${resource._id}`) .expect(200) .then((res) => { @@ -2037,7 +2039,7 @@ describe('Test nested resource CRUD capabilities', () => { let resource = {}; let nested = {}; - it('/POST a new parent resource', () => request(app) + it('/POST a new parent resource', () => request(server) .post('/test/resource1') .send({ title: 'Test1', @@ -2053,7 +2055,7 @@ describe('Test nested resource CRUD capabilities', () => { resource = response; })); - it('/GET an empty list of nested resources', () => request(app) + it('/GET an empty list of nested resources', () => request(server) .get(`/test/resource1/${resource._id}/nested1`) .expect('Content-Type', /json/) .expect('Content-Range', '*/0') @@ -2063,7 +2065,7 @@ describe('Test nested resource CRUD capabilities', () => { assert.deepEqual(res.body, []); })); - it('/POST a new nested resource', () => request(app) + it('/POST a new nested resource', () => request(server) .post(`/test/resource1/${resource._id}/nested1`) .send({ title: 'Nest1', @@ -2081,7 +2083,7 @@ describe('Test nested resource CRUD capabilities', () => { nested = response; })); - it('/GET the list of nested resources', () => request(app) + it('/GET the list of nested resources', () => request(server) .get(`/test/resource1/${resource._id}/nested1/${nested._id}`) .expect('Content-Type', /json/) .expect(200) @@ -2095,7 +2097,7 @@ describe('Test nested resource CRUD capabilities', () => { assert.equal(response._id, nested._id); })); - it('/PUT the nested resource', () => request(app) + it('/PUT the nested resource', () => request(server) .put(`/test/resource1/${resource._id}/nested1/${nested._id}`) .send({ title: 'Nest1 - Updated1', @@ -2113,7 +2115,7 @@ describe('Test nested resource CRUD capabilities', () => { nested = response; })); - it('/PATCH data on the nested resource', () => request(app) + it('/PATCH data on the nested resource', () => request(server) .patch(`/test/resource1/${resource._id}/nested1/${nested._id}`) .send([{ 'op': 'replace', 'path': '/title', 'value': 'Nest1 - Updated2' }]) .expect('Content-Type', /json/) @@ -2129,7 +2131,7 @@ describe('Test nested resource CRUD capabilities', () => { nested = response; })); - it('/PATCH rejection on the nested resource due to failed test op', () => request(app) + it('/PATCH rejection on the nested resource due to failed test op', () => request(server) .patch(`/test/resource1/${resource._id}/nested1/${nested._id}`) .send([ { 'op': 'test', 'path': '/title', 'value': 'not-the-title' }, @@ -2147,7 +2149,7 @@ describe('Test nested resource CRUD capabilities', () => { assert.equal(response._id, nested._id); })); - it('/GET the nested resource with patch changes', () => request(app) + it('/GET the nested resource with patch changes', () => request(server) .get(`/test/resource1/${resource._id}/nested1/${nested._id}`) .expect('Content-Type', /json/) .expect(200) @@ -2161,7 +2163,7 @@ describe('Test nested resource CRUD capabilities', () => { assert.equal(response._id, nested._id); })); - it('/GET index of nested resources', () => request(app) + it('/GET index of nested resources', () => request(server) .get(`/test/resource1/${resource._id}/nested1`) .expect('Content-Type', /json/) .expect(200) @@ -2176,7 +2178,7 @@ describe('Test nested resource CRUD capabilities', () => { assert.equal(response[0]._id, nested._id); })); - it('Cannot /POST to an existing nested resource', () => request(app) + it('Cannot /POST to an existing nested resource', () => request(server) .post(`/test/resource1/${resource._id}/nested1/${nested._id}`) .expect('Content-Type', /text\/html/) .expect(404) @@ -2186,7 +2188,7 @@ describe('Test nested resource CRUD capabilities', () => { assert(response.includes(expected), 'Response not found.'); })); - it('/DELETE the nested resource', () => request(app) + it('/DELETE the nested resource', () => request(server) .delete(`/test/resource1/${resource._id}/nested1/${nested._id}`) .expect(200) .then((res) => { @@ -2194,7 +2196,7 @@ describe('Test nested resource CRUD capabilities', () => { assert.deepEqual(response, {}); })); - it('/GET an empty list of nested resources', () => request(app) + it('/GET an empty list of nested resources', () => request(server) .get(`/test/resource1/${resource._id}/nested1/`) .expect('Content-Type', /json/) .expect('Content-Range', '*/0') @@ -2210,7 +2212,7 @@ describe('Test nested resource handlers capabilities', () => { let resource = {}; let nested = {}; - it('/POST a new parent resource', () => request(app) + it('/POST a new parent resource', () => request(server) .post('/test/resource2') .send({ title: 'Test2', @@ -2230,7 +2232,7 @@ describe('Test nested resource handlers capabilities', () => { handlers = {}; }); - it('A POST request to a child resource should invoke the global handlers', () => request(app) + it('A POST request to a child resource should invoke the global handlers', () => request(server) .post(`/test/resource2/${resource._id}/nested2`) .send({ title: 'Nest2', @@ -2254,7 +2256,7 @@ describe('Test nested resource handlers capabilities', () => { nested = response; })); - it('A GET request to a child resource should invoke the global handlers', () => request(app) + it('A GET request to a child resource should invoke the global handlers', () => request(server) .get(`/test/resource2/${resource._id}/nested2/${nested._id}`) .expect('Content-Type', /json/) .expect(200) @@ -2272,7 +2274,7 @@ describe('Test nested resource handlers capabilities', () => { assert.equal(wasInvoked('nested2', 'after', 'get'), true); })); - it('A PUT request to a child resource should invoke the global handlers', () => request(app) + it('A PUT request to a child resource should invoke the global handlers', () => request(server) .put(`/test/resource2/${resource._id}/nested2/${nested._id}`) .send({ title: 'Nest2 - Updated', @@ -2296,7 +2298,7 @@ describe('Test nested resource handlers capabilities', () => { nested = response; })); - it('A GET (Index) request to a child resource should invoke the global handlers', () => request(app) + it('A GET (Index) request to a child resource should invoke the global handlers', () => request(server) .get(`/test/resource2/${resource._id}/nested2`) .expect('Content-Type', /json/) .expect(200) @@ -2315,7 +2317,7 @@ describe('Test nested resource handlers capabilities', () => { assert.equal(wasInvoked('nested2', 'after', 'index'), true); })); - it('A DELETE request to a child resource should invoke the global handlers', () => request(app) + it('A DELETE request to a child resource should invoke the global handlers', () => request(server) .delete(`/test/resource2/${resource._id}/nested2/${nested._id}`) .expect(200) .then((res) => { @@ -2342,7 +2344,7 @@ describe('Test mount variations', () => { }))).index(); }); - it('/GET empty list', () => request(app) + it('/GET empty list', () => request(server) .get('/testindex') .expect('Content-Type', /json/) .expect('Content-Range', '*/0') @@ -2352,7 +2354,7 @@ describe('Test mount variations', () => { assert.deepEqual(res.body, []); })); - it('/POST should be 404', () => request(app) + it('/POST should be 404', () => request(server) .post('/testindex') .send({ title: 'Test1', @@ -2360,28 +2362,28 @@ describe('Test mount variations', () => { }) .expect(404)); - it('/GET should be 404', () => request(app) + it('/GET should be 404', () => request(server) .get('/testindex/234234234') .expect(404)); - it('/PUT should be 404', () => request(app) + it('/PUT should be 404', () => request(server) .put('/testindex/234234234') .send({ title: 'Test2', }) .expect(404)); - it('/PATCH should be 404', () => request(app) + it('/PATCH should be 404', () => request(server) .patch('/testindex/234234234') .send([{ 'op': 'replace', 'path': '/title', 'value': 'Test3' }]) .expect(404)); - it('/VIRTUAL should be 404', () => request(app) + it('/VIRTUAL should be 404', () => request(server) .get('/testindex/234234234/virtual') .send() .expect(404)); - it('/DELETE the resource', () => request(app) + it('/DELETE the resource', () => request(server) .delete('/testindex/234234234') .expect(404)); }); @@ -2406,12 +2408,12 @@ describe('Test before hooks', () => { Resource(app, '', 'hook', hookModel).rest({ hooks: { post: { - before(req, res, item, next) { + before(ctx, item, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(req, res, item, next) { + after(ctx, item, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); @@ -2419,12 +2421,12 @@ describe('Test before hooks', () => { }, }, get: { - before(req, res, item, next) { + before(ctx, item, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(req, res, item, next) { + after(ctx, item, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); @@ -2432,12 +2434,12 @@ describe('Test before hooks', () => { }, }, put: { - before(req, res, item, next) { + before(ctx, item, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(req, res, item, next) { + after(ctx, item, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); @@ -2445,12 +2447,12 @@ describe('Test before hooks', () => { }, }, delete: { - before(req, res, item, next) { + before(ctx, item, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(req, res, item, next) { + after(ctx, item, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); @@ -2458,12 +2460,12 @@ describe('Test before hooks', () => { }, }, index: { - before(req, res, item, next) { + before(ctx, item, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(req, res, item, next) { + after(ctx, item, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); @@ -2479,7 +2481,7 @@ describe('Test before hooks', () => { calls = []; }); - it('Bootstrap some test resources', () => request(app) + it('Bootstrap some test resources', () => request(server) .post('/hook') .send({ data: chance.word(), @@ -2494,7 +2496,7 @@ describe('Test before hooks', () => { assert.equal(calls[1], 'after'); })); - it('test required validation', () => request(app) + it('test required validation', () => request(server) .post('/hook') .send({}) .expect('Content-Type', /json/) @@ -2512,7 +2514,7 @@ describe('Test before hooks', () => { calls = []; }); - it('Call hooks are called in order', () => request(app) + it('Call hooks are called in order', () => request(server) .get(`/hook/${sub._id}`) .expect('Content-Type', /json/) .expect(200) @@ -2522,7 +2524,7 @@ describe('Test before hooks', () => { assert.equal(calls[1], 'after'); })); - it('test undefined resource', () => request(app) + it('test undefined resource', () => request(server) .get(`/hook/${undefined}`) .expect('Content-Type', /json/) .expect(400) @@ -2533,7 +2535,7 @@ describe('Test before hooks', () => { assert.equal(_.get(response, 'message'), 'Cast to ObjectId failed for value "undefined" at path "_id" for model "hook"'); })); - it('test unknown resource', () => request(app) + it('test unknown resource', () => request(server) .get('/hook/000000000000000000000000') .expect('Content-Type', /json/) .expect(404) @@ -2550,7 +2552,7 @@ describe('Test before hooks', () => { calls = []; }); - it('Call hooks are called in order', () => request(app) + it('Call hooks are called in order', () => request(server) .put(`/hook/${sub._id}`) .send({ data: chance.word(), @@ -2563,7 +2565,7 @@ describe('Test before hooks', () => { assert.equal(calls[1], 'after'); })); - it('test undefined resource', () => request(app) + it('test undefined resource', () => request(server) .put(`/hook/${undefined}`) .send({ data: chance.word(), @@ -2576,7 +2578,7 @@ describe('Test before hooks', () => { assert.equal(_.get(response, 'message'), 'Cast to ObjectId failed for value "undefined" at path "_id" for model "hook"'); })); - it('test unknown resource', () => request(app) + it('test unknown resource', () => request(server) .put('/hook/000000000000000000000000') .send({ data: chance.word(), @@ -2595,7 +2597,7 @@ describe('Test before hooks', () => { calls = []; }); - it('Call hooks are called in order', () => request(app) + it('Call hooks are called in order', () => request(server) .delete(`/hook/${sub._id}`) .expect('Content-Type', /json/) .expect(200) @@ -2605,7 +2607,7 @@ describe('Test before hooks', () => { assert.equal(calls[1], 'after'); })); - it('test undefined resource', () => request(app) + it('test undefined resource', () => request(server) .delete(`/hook/${undefined}`) .expect('Content-Type', /json/) .expect(400) @@ -2615,7 +2617,7 @@ describe('Test before hooks', () => { assert.equal(_.get(response, 'message'), 'Cast to ObjectId failed for value "undefined" at path "_id" for model "hook"'); })); - it('test unknown resource', () => request(app) + it('test unknown resource', () => request(server) .delete('/hook/000000000000000000000000') .expect('Content-Type', /json/) .expect(404) @@ -2631,7 +2633,7 @@ describe('Test before hooks', () => { calls = []; }); - it('Call hooks are called in order', () => request(app) + it('Call hooks are called in order', () => request(server) .get('/hook') .expect('Content-Type', /json/) .expect(200) From c5287e8a555664966faf7662da22779ac4a1a01b Mon Sep 17 00:00:00 2001 From: Sefriol Date: Mon, 13 Jul 2020 16:37:51 +0300 Subject: [PATCH 08/22] refactor(Koa): _register, get and respond - _register and get now use Koa middleware approach - middleware variables are now saved in context.state to avoid conflicts --- KoaResource.js | 209 ++++++++++++++++++++---------------------------- test/testKoa.js | 38 +++++---- 2 files changed, 108 insertions(+), 139 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 23815e0..1e17d17 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -86,38 +86,37 @@ class Resource { * * @param method, string, GET, POST, PUT, PATCH, DEL * @param path, string, url path to the resource - * @param callback - * @param last - * @param options + * @param middlewares, object, contains beforeQuery, afterQuery and select middlewares + * @param options, object, contains before, after and hook handlers */ - _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)]; + _register(method, path, middlewares, options) { + const { beforeQueryMW, afterQueryMW, selectMW } = middlewares; - // The after middleware. - if (options && options.after) { - const after = options.after.map((m) => m.bind(this)); - routeStack = [...routeStack, ...after]; + // The fallback error handler. + const errorMW = async(ctx, next) => { + try { + return await next(); } + catch (err) { + console.log('errorrr', err) + err.status = err.statusCode || err.status || 500; + return await Resource.respond(ctx); + } + }; - routeStack = [...routeStack, last.bind(this)]; + // The before middleware. + const beforeMW = this._generateMiddleware.call(this, options, 'before'); + + const routeStack = compose([ + errorMW, + beforeMW, + beforeQueryMW, + options.hooks.get.before.bind(this), + afterQueryMW, + options.hooks.get.after.bind(this), + selectMW, + ]); - // Add a fallback error handler. - /* const error = async (ctx, next) => { - try { - await next() - } catch (err) { - ctx.status = 400 - ctx.body = err.message || err - ctx.app.emit('error', err, ctx); - } - } */ // Declare the resourcejs object on the app. if (!this.app.context.resourcejs) { this.app.context.resourcejs = {}; @@ -128,8 +127,7 @@ class Resource { } // Add a stack processor so this stack can be executed independently of Express. - this.app.context.resourcejs[path][method] = this.stackProcessor(routeStack); - routeStack = compose(routeStack); + // this.app.context.resourcejs[path][method] = this.stackProcessor(routeStack); // Apply these callbacks to the application. switch (method) { @@ -159,8 +157,11 @@ class Resource { const before = options[position].map((m) => m.bind(this)); routeStack = [...routeStack, ...before]; } - console.log(routeStack) - return compose(routeStack); + routeStack = routeStack.length ? compose(routeStack) : async(ctx, next) => { + console.log(`generated ${position} MW`) + return await next(); + }; + return routeStack; } /** @@ -172,14 +173,15 @@ class Resource { * @param next * The next middleware */ - static respond(ctx, next) { + static async respond(ctx) { + console.log('test4 respond') if (ctx.headerSent) { debug.respond('Skipping'); - return next(); + return; } - if (ctx.resource) { - switch (ctx.resource.status) { + if (ctx.state.resource) { + switch (ctx.state.resource.status) { case 404: ctx.status = 404; ctx.body = { @@ -189,26 +191,26 @@ class Resource { break; case 400: case 500: - for (const property in ctx.resource.error.errors) { + for (const property in ctx.state.resource.error.errors) { // eslint-disable-next-line max-depth - if (Object.prototype.hasOwnProperty.call(ctx.resource.error.errors, property)) { - const error = ctx.resource.error.errors[property]; + if (Object.prototype.hasOwnProperty.call(ctx.state.resource.error.errors, property)) { + const error = ctx.state.resource.error.errors[property]; const { path, name, message } = error; - ctx.resource.error.errors[property] = { path, name, message }; + ctx.state.resource.error.errors[property] = { path, name, message }; } } - ctx.status = ctx.resource.status; + ctx.status = ctx.state.resource.status; ctx.body = { - status: ctx.resource.status, - message: ctx.resource.error.message, - errors: ctx.resource.error.errors, + status: ctx.state.resource.status, + message: ctx.state.resource.error.message, + errors: ctx.state.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 -> ${ctx.__rMethod}`); - switch (ctx.__rMethod) { + debug.respond(`204 -> ${ctx.state.__rMethod}`); + switch (ctx.state.__rMethod) { case 'index': ctx.status = 200; ctx.body = []; @@ -220,11 +222,12 @@ class Resource { } break; default: - ctx.status = ctx.resource.status; - ctx.body = ctx.resource.item; + ctx.status = ctx.state.resource.status; + ctx.body = ctx.state.resource.item; break; } } + console.log(ctx.state.resource, ctx.status, ctx.body) } /** @@ -236,7 +239,7 @@ class Resource { * @param next */ static setResponse(res, resource, next) { - res.resource = resource; + res.state.resource = resource; // next(); } @@ -292,7 +295,10 @@ class Resource { utils.set( methodOptions, path, - utils.get(options, path, (ctx, item, next) => next()) + utils.get(options, path, async(ctx, next) => { + console.log(`${type} hook`) + return await next(); + }) ); }); @@ -640,110 +646,67 @@ class Resource { get(options) { options = Resource.getMethodOptions('get', options); this.methods.push('get'); - const middlewares = compose([ - async(ctx, next) => { + const afterMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + const beforeQueryMW = async(ctx, next) => { // Callback console.log('test1') - return await next(); // Store the internal method for response manipulation. - ctx.__rMethod = 'get'; - if (ctx.skipResource) { + ctx.state.__rMethod = 'get'; + if (ctx.state.skipResource) { + console.log('test1 skip', ctx.state.skipResource) debug.get('Skipping Resource'); - return await next(); + return await afterMW(ctx); } - ctx.modelQuery = (ctx.modelQuery || ctx.model || this.model).findOne(); - ctx.search = { '_id': ctx.params[`${this.name}Id`] }; - + console.log('test1 model') + ctx.state.modelQuery = (ctx.state.modelQuery || ctx.state.model || this.model).findOne(); + ctx.state.search = { '_id': ctx.params[`${this.name}Id`] }; + console.log('test1 populate', ctx.state.search, ctx.params) // Only call populate if they provide a populate query. const populate = Resource.getParamQuery(ctx, 'populate'); if (populate) { debug.get(`Populate: ${populate}`); ctx.modelQuery.populate(populate); } + console.log('test1 next') return await next(); - }, - this._generateMiddleware.call(this, 'get', `${this.route}/:${this.name}Id`, options, 'before'), - async(ctx, next) => { + }; + const afterQueryMW = async(ctx, next) => { // Callback console.log('test2') - return await next(); - ctx.item = await ctx.modelQuery.where(ctx.search).lean().exec(); - if (!ctx.item) { + ctx.state.item = await ctx.state.modelQuery.where(ctx.state.search).lean().exec(); + if (!ctx.state.item) { + console.log('test2 no item') Resource.setResponse(ctx, { status: 404 }, next); } return await next(); - }, - this._generateMiddleware.call(this, 'get', `${this.route}/:${this.name}Id`, options, 'after'), - async(ctx, next) => { + }; + const selectMW = async(ctx, next) => { console.log('test3') - return await next(); // Allow them to only return specified fields. const select = Resource.getParamQuery(ctx, 'select'); if (select) { + console.log('test3 select') const newItem = {}; // Always include the _id. - if (ctx.item._id) { + if (ctx.state.item._id) { newItem._id = ctx.item._id; } select.split(' ').map(key => { key = key.trim(); - if (Object.prototype.hasOwnProperty.call(ctx.item,key)) { + if (Object.prototype.hasOwnProperty.call(ctx.state.item, key)) { newItem[key] = ctx.item[key]; } }); - ctx.item = newItem; + ctx.state.item = newItem; } - Resource.setResponse(ctx, { status: 200, item: ctx.item }, next); + console.log('test3 response') + Resource.setResponse(ctx, { status: 200, item: ctx.state.item }, next); return await next(); - }, - Resource.respond, - ]); - console.log(middlewares.toString()) - this.router.get(`${this.route}/:${this.name}Id`, middlewares); - this.app.use(this.router.routes(), this.router.allowedMethods()); -/* this._register('get', `${this.route}/:${this.name}Id`, async(ctx, next) => { - try { - // Store the internal method for response manipulation. - ctx.__rMethod = 'get'; - if (ctx.skipResource) { - debug.get('Skipping Resource'); - return next(); - } - - const query = (ctx.modelQuery || ctx.model || this.model).findOne(); - ctx.search = { '_id': ctx.params[`${this.name}Id`] }; - - // Only call populate if they provide a populate query. - const populate = Resource.getParamQuery(ctx, 'populate'); - if (populate) { - debug.get(`Populate: ${populate}`); - query.populate(populate); - } - await next(); // options.hooks.get.before.call(this, ctx, search), - - let item = await query.where(ctx.search).lean().exec(); - if (!item) return Resource.setResponse(ctx, { status: 404 }, next); - - await next(); // options.hooks.get.after.call(this, ctx, item); - // Allow them to only return specified fields. - const select = Resource.getParamQuery(ctx, 'select'); - if (select) { - const newItem = {}; - // Always include the _id. - if (item._id) { - newItem._id = item._id; - } - select.split(' ').map(key => { - key = key.trim(); - if (Object.prototype.hasOwnProperty.call(item,key)) { - newItem[key] = item[key]; - } - }); - item = newItem; - } - Resource.setResponse(ctx, { status: 200, item: item }, next); - } catch (error) { - return Resource.setResponse(ctx, { status: 400, error }, next); - } - }, Resource.respond, options); */ + }; + const middlewares = { + beforeQueryMW, + afterQueryMW, + selectMW, + }; + this._register('get',`${this.route}/:${this.name}Id`, middlewares, options); return this; } diff --git a/test/testKoa.js b/test/testKoa.js index 387749e..187bc07 100644 --- a/test/testKoa.js +++ b/test/testKoa.js @@ -89,13 +89,19 @@ function wasInvoked(entity, sequence, method) { } describe('Connect to MongoDB', () => { - it('Connect to MongoDB', () => mongoose.connect('mongodb://localhost:27017/test', { + it('Connect to MongoDB', async() => { + const connection = await mongoose.connect('mongodb://localhost:27017/test', { useCreateIndex: true, useUnifiedTopology: true, useNewUrlParser: true, - })); + }); + assert.ok(connection); + }); - it('Drop test database', () => mongoose.connection.db.dropDatabase()); + it('Drop test database', async() => { + const result = await mongoose.connection.db.dropDatabase(); + assert.ok(result); + }); it('Should connect MongoDB without mongoose', () => MongoClient.connect('mongodb://localhost:27017', { useCreateIndex: true, @@ -490,7 +496,7 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const skipResource = Resource(app, '/test', 'skip', SkipModel) .rest({ - before: async (ctx, next) => { + before: async(ctx, next) => { console.log(ctx, 'test1.1') ctx.skipResource = true; return await next(); @@ -498,9 +504,9 @@ describe('Build Resources for following tests', () => { }) .virtual({ path: 'resource', - before: (ctx, next) => { + before: async(ctx, next) => { ctx.skipResource = true; - // return next(); + return await next(); }, }); const skipSwaggerio = require('./snippets/skipSwaggerio.json'); @@ -2408,12 +2414,12 @@ describe('Test before hooks', () => { Resource(app, '', 'hook', hookModel).rest({ hooks: { post: { - before(ctx, item, next) { + before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(ctx, item, next) { + after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); @@ -2421,12 +2427,12 @@ describe('Test before hooks', () => { }, }, get: { - before(ctx, item, next) { + before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(ctx, item, next) { + after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); @@ -2434,12 +2440,12 @@ describe('Test before hooks', () => { }, }, put: { - before(ctx, item, next) { + before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(ctx, item, next) { + after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); @@ -2447,12 +2453,12 @@ describe('Test before hooks', () => { }, }, delete: { - before(ctx, item, next) { + before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(ctx, item, next) { + after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); @@ -2460,12 +2466,12 @@ describe('Test before hooks', () => { }, }, index: { - before(ctx, item, next) { + before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); next(); }, - after(ctx, item, next) { + after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); From c313f01f14ecc8ea390b7771e0b57d4f4d2ebe92 Mon Sep 17 00:00:00 2001 From: Sefriol Date: Mon, 13 Jul 2020 17:45:48 +0300 Subject: [PATCH 09/22] Koa: Refactor Virtual and fixes: - Add proper register hook methods - Rename Middlewares for clarity --- KoaResource.js | 119 +++++++++++++++++++++++++------------------------ 1 file changed, 61 insertions(+), 58 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 1e17d17..6ebe33a 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -90,7 +90,7 @@ class Resource { * @param options, object, contains before, after and hook handlers */ _register(method, path, middlewares, options) { - const { beforeQueryMW, afterQueryMW, selectMW } = middlewares; + const { beforeQueryMW, QueryMW, afterQueryMW, lastMW } = middlewares; // The fallback error handler. const errorMW = async(ctx, next) => { @@ -111,10 +111,11 @@ class Resource { errorMW, beforeMW, beforeQueryMW, - options.hooks.get.before.bind(this), + options.hooks[method].before.bind(this), + QueryMW, + options.hooks[method].after.bind(this), afterQueryMW, - options.hooks.get.after.bind(this), - selectMW, + lastMW, ]); // Declare the resourcejs object on the app. @@ -131,7 +132,9 @@ class Resource { // Apply these callbacks to the application. switch (method) { + case 'index': case 'get': + case 'virtual': this.router.get(path, routeStack); break; case 'post': @@ -262,7 +265,6 @@ class Resource { // Uppercase the method. method = method.charAt(0).toUpperCase() + method.slice(1).toLowerCase(); const methodOptions = { methodOptions: true }; - console.log(options.before?.toString()) // Find all of the options that may have been passed to the rest method. const beforeHandlers = options.before ? ( @@ -543,29 +545,27 @@ class Resource { index(options) { options = Resource.getMethodOptions('index', options); this.methods.push('index'); - this._register('get', this.route, (ctx, next) => { + const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + const beforeQueryMW = async(ctx, next) => { // Callback + console.log('index: beforeQueryMW') // Store the internal method for response manipulation. - ctx.__rMethod = 'index'; + ctx.state.__rMethod = 'index'; // Allow before handlers the ability to disable resource CRUD. - if (ctx.skipResource) { + if (ctx.state.skipResource) { debug.index('Skipping Resource'); - return next(); + return lastMW(ctx); } // Get the find query. - const findQuery = this.getFindQuery(ctx); + ctx.state.findQuery = this.getFindQuery(ctx); // Get the query object. - const countQuery = ctx.countQuery || ctx.modelQuery || ctx.model || this.model; - const query = ctx.modelQuery || ctx.model || this.model; + const countQuery = ctx.state.countQuery || ctx.state.modelQuery || ctx.state.model || this.model; + ctx.state.query = ctx.state.modelQuery || ctx.state.model || this.model; // First get the total count. - this.countQuery(countQuery.find(findQuery), query.pipeline).countDocuments((err, count) => { - if (err) { - debug.index(err); - return Resource.setResponse(ctx, { status: 400, error: err }, next); - } + const count = await this.countQuery(countQuery.find(ctx.state.findQuery),ctx.state.query.pipeline).countDocuments(); // Get the default limit. const defaults = { limit: 10, skip: 0 }; @@ -595,48 +595,56 @@ class Resource { } // Next get the items within the index. - const queryExec = query - .find(findQuery) + ctx.state.queryExec = ctx.state.query + .find(ctx.state.findQuery) .limit(reqQuery.limit) .skip(reqQuery.skip) .select(Resource.getParamQuery(ctx, 'select')) .sort(Resource.getParamQuery(ctx, 'sort')); // Only call populate if they provide a populate query. - const populate = Resource.getParamQuery(ctx, 'populate'); - if (populate) { - debug.index(`Populate: ${populate}`); - queryExec.populate(populate); + ctx.state.populate = Resource.getParamQuery(ctx, 'populate'); + if (ctx.state.populate) { + debug.index(`Populate: ${ctx.state.populate}`); + ctx.state.queryExec.populate(ctx.state.populate); } - options.hooks.index.before.call( - this, - ctx, - findQuery, - () => this.indexQuery(queryExec, query.pipeline).exec((err, items) => { - if (err) { + return await next(); + }; + const queryMW = async(ctx, next) => { // Callback + console.log('index:afterQueryMW') + try { + const items = await this.indexQuery(ctx.state.queryExec, ctx.state.query.pipeline) + debug.index(items); + ctx.state.item = items; + } + 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`; + if (err.name === 'CastError' && ctx.state.populate) { + err.message = `Cannot populate "${ctx.state.populate}" as it is not a reference in this resource`; debug.index(err.message); } - return Resource.setResponse(ctx, { status: 400, error: err }, next); + Resource.setResponse(ctx, { status: 400, error: err }); + throw err; } - - debug.index(items); - options.hooks.index.after.call( - this, - ctx, - items, - Resource.setResponse.bind(Resource, ctx, { status: ctx.statusCode, item: items }, next) - ); - }) - ); - }); - }, Resource.respond, options); + return await next(); + }; + const setMW = async(ctx, next) => { + console.log('index:setMW') + Resource.setResponse.bind(Resource, ctx, { status: ctx.statusCode, item: ctx.state.items }); + return await next(); + }; + const middlewares = { + beforeQueryMW, + queryMW, + afterQueryMW: setMW, + lastMW, + }; + console.log('index', this.route, middlewares, options) + this._register('index', this.route, middlewares, options); return this; } @@ -646,20 +654,17 @@ class Resource { get(options) { options = Resource.getMethodOptions('get', options); this.methods.push('get'); - const afterMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); const beforeQueryMW = async(ctx, next) => { // Callback - console.log('test1') + console.log('get:beforeQueryMW') // Store the internal method for response manipulation. ctx.state.__rMethod = 'get'; if (ctx.state.skipResource) { - console.log('test1 skip', ctx.state.skipResource) debug.get('Skipping Resource'); - return await afterMW(ctx); + return await lastMW(ctx); } - console.log('test1 model') ctx.state.modelQuery = (ctx.state.modelQuery || ctx.state.model || this.model).findOne(); ctx.state.search = { '_id': ctx.params[`${this.name}Id`] }; - console.log('test1 populate', ctx.state.search, ctx.params) // Only call populate if they provide a populate query. const populate = Resource.getParamQuery(ctx, 'populate'); if (populate) { @@ -669,21 +674,19 @@ class Resource { console.log('test1 next') return await next(); }; - const afterQueryMW = async(ctx, next) => { // Callback - console.log('test2') + const queryMW = async(ctx, next) => { // Callback + console.log('get:queryMW') ctx.state.item = await ctx.state.modelQuery.where(ctx.state.search).lean().exec(); if (!ctx.state.item) { - console.log('test2 no item') Resource.setResponse(ctx, { status: 404 }, next); } return await next(); }; const selectMW = async(ctx, next) => { - console.log('test3') + console.log('get:selectMW') // Allow them to only return specified fields. const select = Resource.getParamQuery(ctx, 'select'); if (select) { - console.log('test3 select') const newItem = {}; // Always include the _id. if (ctx.state.item._id) { @@ -697,14 +700,14 @@ class Resource { }); ctx.state.item = newItem; } - console.log('test3 response') Resource.setResponse(ctx, { status: 200, item: ctx.state.item }, next); return await next(); }; const middlewares = { beforeQueryMW, - afterQueryMW, - selectMW, + queryMW, + afterQueryMW: selectMW, + lastMW, }; this._register('get',`${this.route}/:${this.name}Id`, middlewares, options); return this; From b54c67722dcb0b83a3982d37c2a0cc81026d7f98 Mon Sep 17 00:00:00 2001 From: Sefriol Date: Mon, 13 Jul 2020 23:18:21 +0300 Subject: [PATCH 10/22] Koa: Add pagination --- utils.js | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/utils.js b/utils.js index 8fc00fc..decc2f0 100644 --- a/utils.js +++ b/utils.js @@ -26,4 +26,150 @@ const isEmpty = (obj) => { return !obj || (Object.entries(obj).length === 0 && obj.constructor === Object); }; -module.exports = { zipObject, isObjectLike, isEmpty, get, set }; +/** + * Modify the http response for pagination, return 2 properties to use in a query + * + * @url https://github.com/begriffs/clean_pagination + * @url http://nodejs.org/api/http.html#http_class_http_clientrequest + * @url http://nodejs.org/api/http.html#http_class_http_serverresponse + * + * + * @param {http.ClientRequest} req http request to get headers from + * @param {http.ServerResponse} res http response to complete + * @param {int} totalItems total number of items available, can be Infinity + * @param {int} maxRangeSize + * + * @return {Object} + * .limit Number of items to return + * .skip Zero based position for the first item to return + */ +const paginate = function(req, res, totalItems, maxRangeSize) { + /** + * Parse requested range + */ + function parseRange(hdr) { + var m = hdr && hdr.match(/^(\d+)-(\d*)$/); + if (!m) { + return null; + } + return { + from: parseInt(m[1]), + to: m[2] ? parseInt(m[2]) : Infinity, + }; + } + + res.setHeader('Accept-Ranges', 'items'); + res.setHeader('Range-Unit', 'items'); + res.setHeader('Access-Control-Expose-Headers', 'Content-Range, Accept-Ranges, Range-Unit'); + + maxRangeSize = parseInt(maxRangeSize); + + var range = { + from: 0, + to: (totalItems - 1), + }; + + if ('items' === req.headers['range-unit']) { + var parsedRange = parseRange(req.headers.range); + if (parsedRange) { + range = parsedRange; + } + } + + if ((null !== range.to && range.from > range.to) || (range.from > 0 && range.from >= totalItems)) { + if (totalItems > 0 || range.from !== 0) { + res.statusCode = 416; // Requested range unsatisfiable + } + else { + res.statusCode = 204; // No content + } + res.setHeader('Content-Range', `*/${ totalItems}`); + return; + } + + var availableTo; + var reportTotal; + + if (totalItems < Infinity) { + availableTo = Math.min( + range.to, + totalItems - 1, + range.from + maxRangeSize - 1 + ); + + reportTotal = totalItems; + } + else { + availableTo = Math.min( + range.to, + range.from + maxRangeSize - 1 + ); + + reportTotal = '*'; + } + + res.setHeader('Content-Range', `${range.from }-${ availableTo }/${ reportTotal}`); + + var availableLimit = availableTo - range.from + 1; + + if (0 === availableLimit) { + res.statusCode = 204; // no content + res.setHeader('Content-Range', '*/0'); + return; + } + + if (availableLimit < totalItems) { + res.statusCode = 206; // Partial contents + } + else { + res.statusCode = 200; // OK (all items) + } + + // Links + function buildLink(rel, itemsFrom, itemsTo) { + var to = itemsTo < Infinity ? itemsTo : ''; + return `<${ req.url }>; rel="${ rel }"; items="${ itemsFrom }-${ to }"`; + } + + var requestedLimit = range.to - range.from + 1; + var links = []; + + if (availableTo < totalItems - 1) { + links.push(buildLink('next', + availableTo + 1, + availableTo + requestedLimit + )); + + if (totalItems < Infinity) { + var lastStart = Math.floor((totalItems - 1) / availableLimit) * availableLimit; + + links.push(buildLink('last', + lastStart, + lastStart + requestedLimit - 1 + )); + } + } + + if (range.from > 0) { + var previousFrom = Math.max(0, range.from - Math.min(requestedLimit, maxRangeSize)); + links.push(buildLink('prev', + previousFrom, + previousFrom + requestedLimit - 1 + )); + + links.push(buildLink('first', + 0, + requestedLimit - 1 + )); + } + + res.setHeader('Link', links.join(', ')); + + // return values named from mongoose methods + return { + limit: availableLimit, + skip: range.from, + }; +}; + +module.exports = { zipObject, isObjectLike, isEmpty, get, set, paginate }; From 48a234d4565ac27dd79c47106e3b3427e1b9668f Mon Sep 17 00:00:00 2001 From: Sefriol Date: Tue, 14 Jul 2020 02:13:26 +0300 Subject: [PATCH 11/22] Koa: Refactor most of the methods. --- KoaResource.js | 417 +++++++++++++++++++++++++++++------------------- test/testKoa.js | 42 ++--- utils.js | 54 ++++--- 3 files changed, 300 insertions(+), 213 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 6ebe33a..97004ce 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -3,7 +3,6 @@ const compose = require('koa-compose'); const Router = require('@koa/router'); -const paginate = require('node-paginate-anything'); const jsonpatch = require('fast-json-patch'); const mongodb = require('mongodb'); const moment = require('moment'); @@ -90,7 +89,7 @@ class Resource { * @param options, object, contains before, after and hook handlers */ _register(method, path, middlewares, options) { - const { beforeQueryMW, QueryMW, afterQueryMW, lastMW } = middlewares; + const { beforeQueryMW, queryMW, afterQueryMW, lastMW } = middlewares; // The fallback error handler. const errorMW = async(ctx, next) => { @@ -98,9 +97,10 @@ class Resource { return await next(); } catch (err) { - console.log('errorrr', err) - err.status = err.statusCode || err.status || 500; - return await Resource.respond(ctx); + ctx.status = 400; + ctx.body = { + message: err.message || err, + }; } }; @@ -112,7 +112,7 @@ class Resource { beforeMW, beforeQueryMW, options.hooks[method].before.bind(this), - QueryMW, + queryMW, options.hooks[method].after.bind(this), afterQueryMW, lastMW, @@ -233,19 +233,6 @@ class Resource { console.log(ctx.state.resource, ctx.status, ctx.body) } - /** - * 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.state.resource = resource; - // next(); - } - /** * Returns the method options for a specific method to be executed. * @param method @@ -546,8 +533,9 @@ class Resource { options = Resource.getMethodOptions('index', options); this.methods.push('index'); const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + // eslint-disable-next-line max-statements const beforeQueryMW = async(ctx, next) => { // Callback - console.log('index: beforeQueryMW') + console.log('index: beforeQueryMW'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'index'; @@ -565,7 +553,15 @@ class Resource { ctx.state.query = ctx.state.modelQuery || ctx.state.model || this.model; // First get the total count. - const count = await this.countQuery(countQuery.find(ctx.state.findQuery),ctx.state.query.pipeline).countDocuments(); + let count; + try { + count = await this.countQuery(countQuery.find(ctx.state.findQuery), ctx.state.query.pipeline).countDocuments(); + } + catch (err) { + debug.index(err); + ctx.resource = { status: 400, error: err }; + return await lastMW(ctx); + } // Get the default limit. const defaults = { limit: 10, skip: 0 }; @@ -583,7 +579,7 @@ class Resource { } // Get the page range. - const pageRange = paginate(ctx, count, reqQuery.limit) || { + const pageRange = utils.paginate(ctx, count, reqQuery.limit) || { limit: reqQuery.limit, skip: reqQuery.skip, }; @@ -612,9 +608,9 @@ class Resource { return await next(); }; const queryMW = async(ctx, next) => { // Callback - console.log('index:afterQueryMW') + console.log('index:afterQueryMW'); try { - const items = await this.indexQuery(ctx.state.queryExec, ctx.state.query.pipeline) + const items = await this.indexQuery(ctx.state.queryExec, ctx.state.query.pipeline); debug.index(items); ctx.state.item = items; } @@ -627,14 +623,14 @@ class Resource { debug.index(err.message); } - Resource.setResponse(ctx, { status: 400, error: err }); - throw err; + ctx.state.resource = { status: 400, error: err }; + return await lastMW(ctx); } return await next(); }; const setMW = async(ctx, next) => { - console.log('index:setMW') - Resource.setResponse.bind(Resource, ctx, { status: ctx.statusCode, item: ctx.state.items }); + console.log('index:setMW'); + ctx.state.resource = { status: ctx.statusCode, item: ctx.state.items }; return await next(); }; const middlewares = { @@ -655,8 +651,9 @@ class Resource { options = Resource.getMethodOptions('get', options); this.methods.push('get'); const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + const beforeQueryMW = async(ctx, next) => { // Callback - console.log('get:beforeQueryMW') + console.log('get:beforeQueryMW'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'get'; if (ctx.state.skipResource) { @@ -675,15 +672,23 @@ class Resource { return await next(); }; const queryMW = async(ctx, next) => { // Callback - console.log('get:queryMW') + console.log('get:queryMW'); + try { ctx.state.item = await ctx.state.modelQuery.where(ctx.state.search).lean().exec(); + } + catch (err) { + ctx.state.resource = { status: 400, error: err }; + return await lastMW(ctx); + } if (!ctx.state.item) { - Resource.setResponse(ctx, { status: 404 }, next); + ctx.state.resource = { status: 404 }; + return await lastMW(ctx); } return await next(); }; + const selectMW = async(ctx, next) => { - console.log('get:selectMW') + console.log('get:selectMW'); // Allow them to only return specified fields. const select = Resource.getParamQuery(ctx, 'select'); if (select) { @@ -700,7 +705,7 @@ class Resource { }); ctx.state.item = newItem; } - Resource.setResponse(ctx, { status: 200, item: ctx.state.item }, next); + ctx.state.resource = { status: 200, item: ctx.state.item }; return await next(); }; const middlewares = { @@ -723,22 +728,39 @@ class Resource { const path = options.path; options = Resource.getMethodOptions('virtual', options); this.methods.push(`virtual/${path}`); - this._register('get', `${this.route}/virtual/${path}`, (ctx, next) => { + const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + const beforeQueryMW = async(ctx, next) => await next(); + const queryMW = async(ctx, next) => { + console.log('virtual:queryMW'); // Store the internal method for response manipulation. - ctx.__rMethod = 'virtual'; + ctx.state.__rMethod = 'virtual'; - if (ctx.skipResource) { + if (ctx.state.skipResource) { debug.virtual('Skipping Resource'); - return next(); - } - const query = ctx.modelQuery || ctx.model; - if (!query) return Resource.setResponse(ctx, { status: 404 }, next); - query.exec((err, item) => { - if (err) return Resource.setResponse(ctx, { status: 400, error: err }, next); - if (!item) return Resource.setResponse(ctx, { status: 404 }, next); - return Resource.setResponse(ctx, { status: 200, item }, next); - }); - }, Resource.respond, options); + return await lastMW(ctx); + } + const query = ctx.state.modelQuery || ctx.state.model; + if (!query) { + ctx.state.resource = { status: 404 }; + return await next(); + } + try { + const item = await query.exec(); + if (!item) ctx.state.resource = { status: 404 }; + else ctx.state.resource = { status: 200, item }; + } + catch (err) { + ctx.state.resource = { status: 400, error: err }; + } + return await next(); + }; + const middlewares = { + beforeQueryMW, + queryMW, + afterQueryMW: beforeQueryMW, + lastMW, + }; + this._register('virtual', `${this.route}/virtual/${path}`, middlewares, options); return this; } @@ -748,41 +770,49 @@ class Resource { post(options) { options = Resource.getMethodOptions('post', options); this.methods.push('post'); - this._register('post', this.route, (ctx, next) => { + const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + + const beforeQueryMW = async(ctx, next) => { // Store the internal method for response manipulation. - ctx.__rMethod = 'post'; + ctx.state.__rMethod = 'post'; - if (ctx.skipResource) { + if (ctx.state.skipResource) { debug.post('Skipping Resource'); - return next(); - } - - const Model = ctx.model || this.model; - const model = new Model(ctx.request.body); - options.hooks.post.before.call( - this, - ctx, - ctx.request.body, - () => { - const writeOptions = ctx.writeOptions || {}; - model.save(writeOptions, (err, item) => { - if (err) { + return await lastMW(ctx); + } + + const Model = ctx.state.model || this.model; + ctx.state.model = new Model(ctx.request.body); + console.log('postmodel', ctx.state.model); + return await next(); + }; + + const queryMW = async(ctx, next) => { + const writeOptions = ctx.state.writeOptions || {}; + try { + ctx.state.item = await ctx.state.model.save(writeOptions); + debug.post(ctx.state.item); + } + catch (err) { debug.post(err); - return Resource.setResponse(ctx, { status: 400, error: err }, next); + ctx.state.resource = { status: 400, error: err }; + return await lastMW(ctx); } + return await next(); + }; - debug.post(item); - // Trigger any after hooks before responding. - return options.hooks.post.after.call( - this, - ctx, - item, - Resource.setResponse.bind(Resource, ctx, { status: 201, item }, next) - ); - }); - } - ); - }, Resource.respond, options); + const afterQueryMW = async(ctx, next) => { + ctx.state.resource = { status: 201, item: ctx.state.item }; + return await next(); + }; + + const middlewares = { + beforeQueryMW, + queryMW, + afterQueryMW, + lastMW, + }; + this._register('post', this.route, middlewares, options); return this; } @@ -792,52 +822,62 @@ class Resource { put(options) { options = Resource.getMethodOptions('put', options); this.methods.push('put'); - this._register('put', `${this.route}/:${this.name}Id`, (ctx, next) => { + const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + const beforeQueryMW = async(ctx, next) => { // Store the internal method for response manipulation. - ctx.__rMethod = 'put'; + ctx.state.__rMethod = 'put'; - if (ctx.skipResource) { + if (ctx.state.skipResource) { debug.put('Skipping Resource'); - return next(); + return await lastMW(ctx); } // Remove __v field const { __v, ...update } = ctx.request.body; - const query = ctx.modelQuery || ctx.model || this.model; - - query.findOne({ _id: ctx.params[`${this.name}Id`] }, (err, item) => { - if (err) { + ctx.state.query = ctx.state.modelQuery || ctx.state.model || this.model; + try { + ctx.state.item = await ctx.state.query.findOne({ _id: ctx.params[`${this.name}Id`] }).exec(); + } + catch (err) { debug.put(err); - return Resource.setResponse(ctx, { status: 400, error: err }, next); + ctx.state.resource = { status: 400, error: err }; + return await lastMW(ctx); } - if (!item) { + if (!ctx.state.item) { debug.put(`No ${this.name} found with ${this.name}Id: ${ctx.params[`${this.name}Id`]}`); - return Resource.setResponse(ctx, { status: 404 }, next); + ctx.state.resource = { status: 404 }; + return await lastMW(ctx); } + ctx.state.item.set(update); + return await next(); + }; - item.set(update); - options.hooks.put.before.call( - this, - ctx, - item, - () => { - const writeOptions = ctx.writeOptions || {}; - item.save(writeOptions, (err, item) => { - if (err) { + const queryMW = async(ctx, next) => { + const writeOptions = ctx.state.writeOptions || {}; + try { + ctx.state.item = await ctx.state.item.save(writeOptions); + debug.put(ctx.state.item); + } + catch (err) { debug.put(err); - return Resource.setResponse(ctx, { status: 400, error: err }, next); + ctx.state.resource = { status: 400, error: err }; + return await lastMW(ctx); } + return await next(); + }; - return options.hooks.put.after.call( - this, - ctx, - item, - Resource.setResponse.bind(Resource, ctx, { status: 200, item }, next) - ); - }); - }); - }); - }, Resource.respond, options); + const afterQueryMW = async(ctx, next) => { + ctx.state.resource = { status: 200, item: ctx.state.item }; + return await next(); + }; + + const middlewares = { + beforeQueryMW, + queryMW, + afterQueryMW, + lastMW, + }; + this._register('put', `${this.route}/:${this.name}Id`, middlewares, options); return this; } @@ -847,52 +887,63 @@ class Resource { patch(options) { options = Resource.getMethodOptions('patch', options); this.methods.push('patch'); - this._register('patch', `${this.route}/:${this.name}Id`, (ctx, next) => { + const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + const beforeQueryMW = async(ctx, next) => { // Store the internal method for response manipulation. - ctx.__rMethod = 'patch'; + ctx.state.__rMethod = 'patch'; - if (ctx.skipResource) { + if (ctx.state.skipResource) { debug.patch('Skipping Resource'); - return next(); + return await lastMW(ctx); + } + ctx.state.query = ctx.state.modelQuery || ctx.model || this.model; + try { + ctx.state.item = await ctx.state.query.findOne({ '_id': ctx.params[`${this.name}Id`] }); + } + catch (err) { + ctx.state.resource = { status: 400, error: err }; + return await lastMW(ctx); + } + + if (!ctx.state.item ) { + ctx.state.resource = { status: 404, error: '' }; + return await lastMW(ctx); } - const query = ctx.modelQuery || ctx.model || this.model; - const writeOptions = ctx.writeOptions || {}; - query.findOne({ '_id': ctx.params[`${this.name}Id`] }, (err, item) => { - if (err) return Resource.setResponse(ctx, { status: 400, error: err }, next); - if (!item) return Resource.setResponse(ctx, { status: 404, error: err }, next); // Ensure patches is an array const patches = [].concat(ctx.request.body); let patchFail = null; try { - patches.forEach((patch) => { + patches.forEach(async(patch) => { if (patch.op === 'test') { patchFail = patch; - const success = jsonpatch.applyOperation(item, patch, true); + const success = jsonpatch.applyOperation(ctx.state.item, patch, true); if (!success || !success.test) { - return Resource.setResponse(ctx, { + ctx.state.resource = { status: 412, name: 'Precondition Failed', message: 'A json-patch test op has failed. No changes have been applied to the document', - item, + item: ctx.state.item, patch, - }, next); + }; + return await lastMW(ctx); } } }); - jsonpatch.applyPatch(item, patches, true); + jsonpatch.applyPatch(ctx.state.item, patches, true); } catch (err) { switch (err.name) { // Check whether JSON PATCH error case 'TEST_OPERATION_FAILED': - return Resource.setResponse(ctx, { + ctx.state.resource = { status: 412, name: 'Precondition Failed', message: 'A json-patch test op has failed. No changes have been applied to the document', - item, + item: ctx.state.item, patch: patchFail, - }, next); + }; + return await lastMW(ctx); case 'SEQUENCE_NOT_AN_ARRAY': case 'OPERATION_NOT_AN_OBJECT': case 'OPERATION_OP_INVALID': @@ -909,22 +960,45 @@ class Resource { name: err.name, message: err.toString(), }]; - return Resource.setResponse(ctx, { + ctx.state.resource = { status: 400, - item, + item: ctx.state.item, error: err, - }, next); + }; + return await lastMW(ctx); // Something else than JSON PATCH default: - return Resource.setResponse(ctx, { status: 400, item, error: err }, next); + ctx.state.resource = { status: 400, item: ctx.state.item, error: err }; + return await lastMW(ctx); + } + } + return await next(); + }; + + const queryMW = async(ctx, next) => { + const writeOptions = ctx.state.writeOptions || {}; + try { + ctx.state.item = await ctx.state.item.save(writeOptions); } + catch (err) { + ctx.state.resource = { status: 400, error: err }; + return await lastMW(ctx); } - item.save(writeOptions, (err, item) => { - if (err) return Resource.setResponse(ctx, { status: 400, error: err }, next); - return Resource.setResponse(ctx, { status: 200, item }, next); - }); - }); - }, Resource.respond, options); + return await next(); + }; + + const afterQueryMW = async(ctx, next) => { + ctx.state.resource = { status: 200, item: ctx.state.item }; + return await next(); + }; + + const middlewares = { + beforeQueryMW, + queryMW, + afterQueryMW, + lastMW, + }; + this._register('patch', `${this.route}/:${this.name}Id`, middlewares, options); return this; } @@ -934,53 +1008,64 @@ class Resource { delete(options) { options = Resource.getMethodOptions('delete', options); this.methods.push('delete'); - this._register('delete', `${this.route}/:${this.name}Id`, (ctx, next) => { + const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + const beforeQueryMW = async(ctx, next) => { // Store the internal method for response manipulation. - ctx.__rMethod = 'delete'; + ctx.state.__rMethod = 'delete'; - if (ctx.skipResource) { + if (ctx.state.skipResource) { debug.delete('Skipping Resource'); - return next(); + return await lastMW(ctx); } - const query = ctx.modelQuery || ctx.model || this.model; - query.findOne({ '_id': ctx.params[`${this.name}Id`] }, (err, item) => { - if (err) { + ctx.state.query = ctx.state.modelQuery || ctx.state.model || this.model; + + try { + ctx.state.item = await ctx.state.query.findOne({ '_id': ctx.params[`${this.name}Id`] }); + } + catch (err) { debug.delete(err); - return Resource.setResponse(ctx, { status: 400, error: err }, next); + ctx.state.resource = { status: 400, error: err }; + return await lastMW(ctx); } - if (!item) { + if (!ctx.state.item) { debug.delete(`No ${this.name} found with ${this.name}Id: ${ctx.params[`${this.name}Id`]}`); - return Resource.setResponse(ctx, { status: 404, error: err }, next); + ctx.state.resource = { status: 404, error: '' }; + return await lastMW(ctx); } if (ctx.skipDelete) { - return Resource.setResponse(ctx, { status: 204, item, deleted: true }, next); + ctx.state.resource = { status: 204, item: ctx.state.item, deleted: true }; + return await lastMW(ctx); } + return await next(); + }; - options.hooks.delete.before.call( - this, - ctx, - item, - () => { - const writeOptions = ctx.writeOptions || {}; - item.remove(writeOptions, (err) => { - if (err) { + const queryMW = async(ctx, next) => { + const writeOptions = ctx.state.writeOptions || {}; + try { + ctx.state.item = await ctx.state.item.remove(writeOptions); + } + catch (err) { debug.delete(err); - return Resource.setResponse(ctx, { status: 400, error: err }, next); + ctx.state.resource = { status: 400, error: err }; + return await lastMW(ctx); } + debug.delete(ctx.state.item ); + return await next(); + }; - debug.delete(item); - options.hooks.delete.after.call( - this, - ctx, - item, - Resource.setResponse.bind(Resource, ctx, { status: 204, item, deleted: true }, next) - ); - }); - } - ); - }); - }, Resource.respond, options); + const afterQueryMW = async(ctx, next) => { + ctx.state.resource = { status: 204, item: ctx.state.item, deleted: true }; + return await next(); + }; + + const middlewares = { + beforeQueryMW, + queryMW, + afterQueryMW, + lastMW, + }; + this._register('delete', `${this.route}/:${this.name}Id`, middlewares, options); return this; } diff --git a/test/testKoa.js b/test/testKoa.js index 187bc07..13f14f1 100644 --- a/test/testKoa.js +++ b/test/testKoa.js @@ -170,9 +170,9 @@ describe('Build Resources for following tests', () => { const resource1 = Resource(app, '/test', 'resource1', Resource1Model).rest({ afterDelete(ctx, next) { // Check that the delete item is still being returned via resourcejs. - assert.notEqual(ctx.resource.item, {}); - assert.notEqual(ctx.resource.item, []); - assert.equal(ctx.resource.status, 204); + assert.notEqual(ctx.state.resource.item, {}); + assert.notEqual(ctx.state.resource.item, []); + assert.equal(ctx.state.resource.status, 204); assert.equal(ctx.statusCode, 200); // next(); }, @@ -344,7 +344,7 @@ describe('Build Resources for following tests', () => { // Register before/after global handlers. before(ctx, next) { ctx.request.body.resource2 = ctx.params.resource2Id; - ctx.modelQuery = this.model.where('resource2', ctx.params.resource2Id); + ctx.state.modelQuery = this.model.where('resource2', ctx.params.resource2Id); // Store the invoked handler and continue. setInvoked('nested2', 'before', ctx); @@ -396,7 +396,7 @@ describe('Build Resources for following tests', () => { const resource3 = Resource(app, '/test', 'resource3', Resource3Model).rest({ before(ctx, next) { // This setting should be passed down to the underlying `save()` command - ctx.writeOptions = { writeSetting: true }; + ctx.state.writeOptions = { writeSetting: true }; next(); }, @@ -428,9 +428,9 @@ describe('Build Resources for following tests', () => { const resource4 = Resource(app, '/test', 'resource4', Resource4Model) .rest({ beforePatch(ctx, next) { - ctx.modelQuery = { - findOne: function findOne(_, callback) { - callback(new Error('failed'), undefined); + ctx.state.modelQuery = { + findOne: async function findOne() { + throw new Error('failed'); }, }; next(); @@ -439,14 +439,14 @@ describe('Build Resources for following tests', () => { .virtual({ path: 'undefined_query', before: function(ctx, next) { - ctx.modelQuery = undefined; + ctx.state.modelQuery = undefined; return next(); }, }) .virtual({ path: 'defined', before: function(ctx, next) { - ctx.modelQuery = Resource4Model.aggregate([ + ctx.state.modelQuery = Resource4Model.aggregate([ { $group: { _id: null, titles: { $sum: '$title' } } }, ]); return next(); @@ -455,9 +455,9 @@ describe('Build Resources for following tests', () => { .virtual({ path: 'error', before: function(ctx, next) { - ctx.modelQuery = { - exec: function exec(callback) { - callback(new Error('Failed'), undefined); + ctx.state.modelQuery = { + exec: async function exec() { + throw new Error('Failed'); }, }; return next(); @@ -466,9 +466,9 @@ describe('Build Resources for following tests', () => { .virtual({ path: 'empty', before: function(ctx, next) { - ctx.modelQuery = { - exec: function exec(callback) { - callback(undefined, undefined); + ctx.state.modelQuery = { + exec: async function exec() { + return; }, }; return next(); @@ -497,15 +497,15 @@ describe('Build Resources for following tests', () => { const skipResource = Resource(app, '/test', 'skip', SkipModel) .rest({ before: async(ctx, next) => { - console.log(ctx, 'test1.1') - ctx.skipResource = true; + console.log(ctx, 'test1.1'); + ctx.state.skipResource = true; return await next(); }, }) .virtual({ path: 'resource', before: async(ctx, next) => { - ctx.skipResource = true; + ctx.state.skipResource = true; return await next(); }, }); @@ -1598,8 +1598,8 @@ describe('Test single resource search capabilities', () => { it('Create an aggregation path', () => { Resource(app, '', 'aggregation', mongoose.model('resource1')).rest({ beforeIndex(ctx, next) { - ctx.modelQuery = mongoose.model('resource1'); - ctx.modelQuery.pipeline = []; + ctx.state.modelQuery = mongoose.model('resource1'); + ctx.state.modelQuery.pipeline = []; next(); }, }); diff --git a/utils.js b/utils.js index decc2f0..3c1ab1f 100644 --- a/utils.js +++ b/utils.js @@ -27,6 +27,7 @@ const isEmpty = (obj) => { }; /** + * Fork of https://github.com/polo2ro/node-paginate-anything * Modify the http response for pagination, return 2 properties to use in a query * * @url https://github.com/begriffs/clean_pagination @@ -34,19 +35,20 @@ const isEmpty = (obj) => { * @url http://nodejs.org/api/http.html#http_class_http_serverresponse * * - * @param {http.ClientRequest} req http request to get headers from - * @param {http.ServerResponse} res http response to complete - * @param {int} totalItems total number of items available, can be Infinity - * @param {int} maxRangeSize + * @param {Koa.Context} ctx A Koa Context encapsulates node's request and response objects into a single object + * @param {int} totalItems total number of items available, can be Infinity + * @param {int} maxRangeSize * * @return {Object} * .limit Number of items to return * .skip Zero based position for the first item to return */ -const paginate = function(req, res, totalItems, maxRangeSize) { - /** - * Parse requested range - */ + +// eslint-disable-next-line max-statements +const paginate = function(ctx, totalItems, maxRangeSize) { + /** + * Parse requested range + */ function parseRange(hdr) { var m = hdr && hdr.match(/^(\d+)-(\d*)$/); if (!m) { @@ -58,9 +60,9 @@ const paginate = function(req, res, totalItems, maxRangeSize) { }; } - res.setHeader('Accept-Ranges', 'items'); - res.setHeader('Range-Unit', 'items'); - res.setHeader('Access-Control-Expose-Headers', 'Content-Range, Accept-Ranges, Range-Unit'); + ctx.set('Accept-Ranges', 'items'); + ctx.set('Range-Unit', 'items'); + ctx.set('Access-Control-Expose-Headers', 'Content-Range, Accept-Ranges, Range-Unit'); maxRangeSize = parseInt(maxRangeSize); @@ -69,8 +71,8 @@ const paginate = function(req, res, totalItems, maxRangeSize) { to: (totalItems - 1), }; - if ('items' === req.headers['range-unit']) { - var parsedRange = parseRange(req.headers.range); + if ('items' === ctx.headers['range-unit']) { + var parsedRange = parseRange(ctx.headers.range); if (parsedRange) { range = parsedRange; } @@ -78,12 +80,12 @@ const paginate = function(req, res, totalItems, maxRangeSize) { if ((null !== range.to && range.from > range.to) || (range.from > 0 && range.from >= totalItems)) { if (totalItems > 0 || range.from !== 0) { - res.statusCode = 416; // Requested range unsatisfiable + ctx.statusCode = 416; // Requested range unsatisfiable } - else { - res.statusCode = 204; // No content + else { + ctx.statusCode = 204; // No content } - res.setHeader('Content-Range', `*/${ totalItems}`); + ctx.set('Content-Range', `*/${totalItems}`); return; } @@ -99,7 +101,7 @@ const paginate = function(req, res, totalItems, maxRangeSize) { reportTotal = totalItems; } - else { + else { availableTo = Math.min( range.to, range.from + maxRangeSize - 1 @@ -108,27 +110,27 @@ const paginate = function(req, res, totalItems, maxRangeSize) { reportTotal = '*'; } - res.setHeader('Content-Range', `${range.from }-${ availableTo }/${ reportTotal}`); + ctx.set('Content-Range', `${range.from}-${availableTo}/${reportTotal}`); var availableLimit = availableTo - range.from + 1; if (0 === availableLimit) { - res.statusCode = 204; // no content - res.setHeader('Content-Range', '*/0'); + ctx.statusCode = 204; // no content + ctx.set('Content-Range', '*/0'); return; } if (availableLimit < totalItems) { - res.statusCode = 206; // Partial contents + ctx.statusCode = 206; // Partial contents } - else { - res.statusCode = 200; // OK (all items) + else { + ctx.statusCode = 200; // OK (all items) } // Links function buildLink(rel, itemsFrom, itemsTo) { var to = itemsTo < Infinity ? itemsTo : ''; - return `<${ req.url }>; rel="${ rel }"; items="${ itemsFrom }-${ to }"`; + return `<${ctx.url}>; rel="${rel}"; items="${itemsFrom}-${to}"`; } var requestedLimit = range.to - range.from + 1; @@ -163,7 +165,7 @@ const paginate = function(req, res, totalItems, maxRangeSize) { )); } - res.setHeader('Link', links.join(', ')); + ctx.set('Link', links.join(', ')); // return values named from mongoose methods return { From c0cbb037b91ac133b1b2eab1f29ba5a873bf2869 Mon Sep 17 00:00:00 2001 From: Sefriol Date: Tue, 14 Jul 2020 19:58:45 +0300 Subject: [PATCH 12/22] Koa: Final touches - console to debug - everything is now async - Fix tests - Add remaining attributes to state --- KoaResource.js | 326 ++++++++++++++++++++++++---------------------- package-lock.json | 19 ++- package.json | 4 +- test/testKoa.js | 163 +++++++++++------------ utils.js | 10 +- 5 files changed, 265 insertions(+), 257 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 97004ce..4ba4685 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -22,6 +22,7 @@ const utils = require('./utils'); class Resource { constructor(app, route, modelName, model, options) { this.app = app; + require('koa-qs')(app); this.router = new Router(); this.options = options || {}; if (this.options.convertIds === true) { @@ -95,9 +96,9 @@ class Resource { const errorMW = async(ctx, next) => { try { return await next(); - } + } catch (err) { - ctx.status = 400; + ctx.status = ctx.status || 400; ctx.body = { message: err.message || err, }; @@ -161,7 +162,6 @@ class Resource { routeStack = [...routeStack, ...before]; } routeStack = routeStack.length ? compose(routeStack) : async(ctx, next) => { - console.log(`generated ${position} MW`) return await next(); }; return routeStack; @@ -177,7 +177,6 @@ class Resource { * The next middleware */ static async respond(ctx) { - console.log('test4 respond') if (ctx.headerSent) { debug.respond('Skipping'); return; @@ -230,7 +229,6 @@ class Resource { break; } } - console.log(ctx.state.resource, ctx.status, ctx.body) } /** @@ -285,7 +283,6 @@ class Resource { methodOptions, path, utils.get(options, path, async(ctx, next) => { - console.log(`${type} hook`) return await next(); }) ); @@ -487,13 +484,9 @@ class Resource { }, ]; return { - countDocuments(cb) { - query.model.aggregate(stages).exec((err, items) => { - if (err) { - return cb(err); - } - return cb(null, items.length ? items[0].count : 0); - }); + async countDocuments() { + const items = await query.model.aggregate(stages).exec(); + return items.length ? items[0].count : 0; }, }; } @@ -533,16 +526,17 @@ class Resource { options = Resource.getMethodOptions('index', options); this.methods.push('index'); const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); + // eslint-disable-next-line max-statements const beforeQueryMW = async(ctx, next) => { // Callback - console.log('index: beforeQueryMW'); + debug.index('beforeQueryMiddleWare'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'index'; // Allow before handlers the ability to disable resource CRUD. if (ctx.state.skipResource) { debug.index('Skipping Resource'); - return lastMW(ctx); + return await Resource.respond(ctx); } // Get the find query. @@ -559,87 +553,88 @@ class Resource { } catch (err) { debug.index(err); - ctx.resource = { status: 400, error: err }; + ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); } - // Get the default limit. - const defaults = { limit: 10, skip: 0 }; - let { limit, skip } = ctx.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 && !ctx.headers.range) { - ctx.headers['range-unit'] = 'items'; - ctx.headers.range = `${reqQuery.skip}-${reqQuery.skip + (reqQuery.limit - 1)}`; - } + // Get the default limit. + const defaults = { limit: 10, skip: 0 }; + let { limit, skip } = ctx.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 && !ctx.headers.range) { + ctx.headers['range-unit'] = 'items'; + ctx.headers.range = `${reqQuery.skip}-${reqQuery.skip + (reqQuery.limit - 1)}`; + } - // Get the page range. + // Get the page range. const pageRange = utils.paginate(ctx, 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 (ctx.headers.range) { - reqQuery.limit = pageRange.limit; - reqQuery.skip = pageRange.skip; - } + limit: reqQuery.limit, + skip: reqQuery.skip, + }; + + // Make sure that if there is a range provided in the headers, it takes precedence. + if (ctx.headers.range) { + reqQuery.limit = pageRange.limit; + reqQuery.skip = pageRange.skip; + } - // Next get the items within the index. + // Next get the items within the index. ctx.state.queryExec = ctx.state.query .find(ctx.state.findQuery) - .limit(reqQuery.limit) - .skip(reqQuery.skip) - .select(Resource.getParamQuery(ctx, 'select')) - .sort(Resource.getParamQuery(ctx, 'sort')); + .limit(reqQuery.limit) + .skip(reqQuery.skip) + .select(Resource.getParamQuery(ctx, 'select')) + .sort(Resource.getParamQuery(ctx, 'sort')); - // Only call populate if they provide a populate query. + // Only call populate if they provide a populate query. ctx.state.populate = Resource.getParamQuery(ctx, 'populate'); if (ctx.state.populate) { debug.index(`Populate: ${ctx.state.populate}`); ctx.state.queryExec.populate(ctx.state.populate); - } + } return await next(); }; + const queryMW = async(ctx, next) => { // Callback - console.log('index:afterQueryMW'); + debug.index('queryMiddleware'); try { - const items = await this.indexQuery(ctx.state.queryExec, ctx.state.query.pipeline); + const items = await this.indexQuery(ctx.state.queryExec, ctx.state.query.pipeline).exec(); debug.index(items); ctx.state.item = items; } catch (err) { - debug.index(err); - debug.index(err.name); + debug.index(err); + debug.index(err.name); if (err.name === 'CastError' && ctx.state.populate) { err.message = `Cannot populate "${ctx.state.populate}" as it is not a reference in this resource`; - debug.index(err.message); - } + debug.index(err.message); + } ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); - } + } return await next(); }; - const setMW = async(ctx, next) => { - console.log('index:setMW'); - ctx.state.resource = { status: ctx.statusCode, item: ctx.state.items }; + const afterQueryMW = async(ctx, next) => { + debug.index('afterQueryMiddleWare'); + ctx.state.resource = { status: ctx.status, item: ctx.state.item }; return await next(); }; const middlewares = { beforeQueryMW, queryMW, - afterQueryMW: setMW, + afterQueryMW, lastMW, }; - console.log('index', this.route, middlewares, options) + this._register('index', this.route, middlewares, options); return this; } @@ -653,28 +648,28 @@ class Resource { const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); const beforeQueryMW = async(ctx, next) => { // Callback - console.log('get:beforeQueryMW'); - // Store the internal method for response manipulation. + debug.get('beforeQueryMiddleware'); + // Store the internal method for response manipulation. ctx.state.__rMethod = 'get'; if (ctx.state.skipResource) { - debug.get('Skipping Resource'); - return await lastMW(ctx); - } + debug.get('Skipping Resource'); + return await Resource.respond(ctx); + } ctx.state.modelQuery = (ctx.state.modelQuery || ctx.state.model || this.model).findOne(); ctx.state.search = { '_id': ctx.params[`${this.name}Id`] }; - // Only call populate if they provide a populate query. - const populate = Resource.getParamQuery(ctx, 'populate'); - if (populate) { - debug.get(`Populate: ${populate}`); - ctx.modelQuery.populate(populate); - } - console.log('test1 next') - return await next(); + // Only call populate if they provide a populate query. + const populate = Resource.getParamQuery(ctx, 'populate'); + if (populate) { + debug.get(`Populate: ${populate}`); + ctx.state.modelQuery.populate(populate); + } + return await next(); }; + const queryMW = async(ctx, next) => { // Callback - console.log('get:queryMW'); + debug.get('queryMiddleWare'); try { - ctx.state.item = await ctx.state.modelQuery.where(ctx.state.search).lean().exec(); + ctx.state.item = await ctx.state.modelQuery.where(ctx.state.search).lean().exec(); } catch (err) { ctx.state.resource = { status: 400, error: err }; @@ -683,30 +678,30 @@ class Resource { if (!ctx.state.item) { ctx.state.resource = { status: 404 }; return await lastMW(ctx); - } - return await next(); + } + return await next(); }; const selectMW = async(ctx, next) => { - console.log('get:selectMW'); - // Allow them to only return specified fields. - const select = Resource.getParamQuery(ctx, 'select'); - if (select) { - const newItem = {}; - // Always include the _id. + debug.get('afterMiddleWare (selectMW)'); + // Allow them to only return specified fields. + const select = Resource.getParamQuery(ctx, 'select'); + if (select) { + const newItem = {}; + // Always include the _id. if (ctx.state.item._id) { - newItem._id = ctx.item._id; - } - select.split(' ').map(key => { - key = key.trim(); + newItem._id = ctx.state.item._id; + } + select.split(' ').map(key => { + key = key.trim(); if (Object.prototype.hasOwnProperty.call(ctx.state.item, key)) { - newItem[key] = ctx.item[key]; - } - }); + newItem[key] = ctx.state.item[key]; + } + }); ctx.state.item = newItem; - } + } ctx.state.resource = { status: 200, item: ctx.state.item }; - return await next(); + return await next(); }; const middlewares = { beforeQueryMW, @@ -728,16 +723,18 @@ class Resource { const path = options.path; options = Resource.getMethodOptions('virtual', options); this.methods.push(`virtual/${path}`); + const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); const beforeQueryMW = async(ctx, next) => await next(); + const queryMW = async(ctx, next) => { - console.log('virtual:queryMW'); + debug.virtual('queryMiddleWare'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'virtual'; if (ctx.state.skipResource) { debug.virtual('Skipping Resource'); - return await lastMW(ctx); + return await Resource.respond(ctx); } const query = ctx.state.modelQuery || ctx.state.model; if (!query) { @@ -773,35 +770,37 @@ class Resource { const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); const beforeQueryMW = async(ctx, next) => { + debug.post('beforeQueryMiddleWare'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'post'; if (ctx.state.skipResource) { debug.post('Skipping Resource'); - return await lastMW(ctx); + return await Resource.respond(ctx); } const Model = ctx.state.model || this.model; ctx.state.model = new Model(ctx.request.body); - console.log('postmodel', ctx.state.model); return await next(); }; const queryMW = async(ctx, next) => { + debug.post('queryMiddleWare'); const writeOptions = ctx.state.writeOptions || {}; try { ctx.state.item = await ctx.state.model.save(writeOptions); debug.post(ctx.state.item); } catch (err) { - debug.post(err); + debug.post(err); ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); - } + } return await next(); }; const afterQueryMW = async(ctx, next) => { + debug.post('afterQueryMiddleWare'); ctx.state.resource = { status: 201, item: ctx.state.item }; return await next(); }; @@ -824,12 +823,13 @@ class Resource { this.methods.push('put'); const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); const beforeQueryMW = async(ctx, next) => { + debug.put('beforeQueryMiddleWare'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'put'; if (ctx.state.skipResource) { debug.put('Skipping Resource'); - return await lastMW(ctx); + return await Resource.respond(ctx); } // Remove __v field @@ -839,34 +839,36 @@ class Resource { ctx.state.item = await ctx.state.query.findOne({ _id: ctx.params[`${this.name}Id`] }).exec(); } catch (err) { - debug.put(err); + debug.put(err); ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); - } + } if (!ctx.state.item) { - debug.put(`No ${this.name} found with ${this.name}Id: ${ctx.params[`${this.name}Id`]}`); + debug.put(`No ${this.name} found with ${this.name}Id: ${ctx.params[`${this.name}Id`]}`); ctx.state.resource = { status: 404 }; return await lastMW(ctx); - } + } ctx.state.item.set(update); return await next(); }; const queryMW = async(ctx, next) => { + debug.put('queryMiddleWare'); const writeOptions = ctx.state.writeOptions || {}; try { ctx.state.item = await ctx.state.item.save(writeOptions); debug.put(ctx.state.item); } catch (err) { - debug.put(err); + debug.put(err); ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); - } + } return await next(); }; const afterQueryMW = async(ctx, next) => { + debug.put('afterQueryMiddleWare'); ctx.state.resource = { status: 200, item: ctx.state.item }; return await next(); }; @@ -889,14 +891,15 @@ class Resource { this.methods.push('patch'); const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); const beforeQueryMW = async(ctx, next) => { + debug.patch('beforeQueryMiddleWare'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'patch'; if (ctx.state.skipResource) { debug.patch('Skipping Resource'); - return await lastMW(ctx); + return await Resource.respond(ctx); } - ctx.state.query = ctx.state.modelQuery || ctx.model || this.model; + ctx.state.query = ctx.state.modelQuery || ctx.state.model || this.model; try { ctx.state.item = await ctx.state.query.findOne({ '_id': ctx.params[`${this.name}Id`] }); } @@ -910,64 +913,64 @@ class Resource { return await lastMW(ctx); } - // Ensure patches is an array - const patches = [].concat(ctx.request.body); - let patchFail = null; - try { + // Ensure patches is an array + const patches = [].concat(ctx.request.body); + let patchFail = null; + try { patches.forEach(async(patch) => { - if (patch.op === 'test') { - patchFail = patch; + if (patch.op === 'test') { + patchFail = patch; const success = jsonpatch.applyOperation(ctx.state.item, patch, true); - if (!success || !success.test) { + if (!success || !success.test) { ctx.state.resource = { - status: 412, - name: 'Precondition Failed', - message: 'A json-patch test op has failed. No changes have been applied to the document', + status: 412, + name: 'Precondition Failed', + message: 'A json-patch test op has failed. No changes have been applied to the document', item: ctx.state.item, - patch, + patch, }; return await lastMW(ctx); - } } - }); + } + }); jsonpatch.applyPatch(ctx.state.item, patches, true); - } - catch (err) { - switch (err.name) { - // Check whether JSON PATCH error - case 'TEST_OPERATION_FAILED': + } + catch (err) { + switch (err.name) { + // Check whether JSON PATCH error + case 'TEST_OPERATION_FAILED': ctx.state.resource = { - status: 412, - name: 'Precondition Failed', - message: 'A json-patch test op has failed. No changes have been applied to the document', + status: 412, + name: 'Precondition Failed', + message: 'A json-patch test op has failed. No changes have been applied to the document', item: ctx.state.item, - patch: patchFail, + patch: patchFail, }; return await lastMW(ctx); - 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(), - }]; + 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(), + }]; ctx.state.resource = { - status: 400, + status: 400, item: ctx.state.item, - error: err, + error: err, }; return await lastMW(ctx); - // Something else than JSON PATCH - default: + // Something else than JSON PATCH + default: ctx.state.resource = { status: 400, item: ctx.state.item, error: err }; return await lastMW(ctx); } @@ -976,18 +979,20 @@ class Resource { }; const queryMW = async(ctx, next) => { + debug.patch('queryMiddleWare'); const writeOptions = ctx.state.writeOptions || {}; try { ctx.state.item = await ctx.state.item.save(writeOptions); - } + } catch (err) { ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); - } + } return await next(); }; const afterQueryMW = async(ctx, next) => { + debug.patch('afterQueryMiddleWare'); ctx.state.resource = { status: 200, item: ctx.state.item }; return await next(); }; @@ -1010,12 +1015,13 @@ class Resource { this.methods.push('delete'); const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); const beforeQueryMW = async(ctx, next) => { + debug.delete('beforeQueryMiddleWare'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'delete'; if (ctx.state.skipResource) { debug.delete('Skipping Resource'); - return await lastMW(ctx); + return await Resource.respond(ctx); } ctx.state.query = ctx.state.modelQuery || ctx.state.model || this.model; @@ -1024,37 +1030,39 @@ class Resource { ctx.state.item = await ctx.state.query.findOne({ '_id': ctx.params[`${this.name}Id`] }); } catch (err) { - debug.delete(err); + debug.delete(err); ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); - } + } if (!ctx.state.item) { - debug.delete(`No ${this.name} found with ${this.name}Id: ${ctx.params[`${this.name}Id`]}`); + debug.delete(`No ${this.name} found with ${this.name}Id: ${ctx.params[`${this.name}Id`]}`); ctx.state.resource = { status: 404, error: '' }; return await lastMW(ctx); - } - if (ctx.skipDelete) { + } + if (ctx.state.skipDelete) { ctx.state.resource = { status: 204, item: ctx.state.item, deleted: true }; return await lastMW(ctx); - } + } return await next(); }; const queryMW = async(ctx, next) => { + debug.delete('queryMiddleWare'); const writeOptions = ctx.state.writeOptions || {}; try { ctx.state.item = await ctx.state.item.remove(writeOptions); } catch (err) { - debug.delete(err); + debug.delete(err); ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); - } + } debug.delete(ctx.state.item ); return await next(); }; const afterQueryMW = async(ctx, next) => { + debug.delete('afterQueryMiddleWare'); ctx.state.resource = { status: 204, item: ctx.state.item, deleted: true }; return await next(); }; diff --git a/package-lock.json b/package-lock.json index accdb8c..1217d39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2153,6 +2153,22 @@ } } }, + "koa-qs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/koa-qs/-/koa-qs-3.0.0.tgz", + "integrity": "sha512-05IB5KirwMs3heWW26iTz46HuMAtrlrRMus/aNH1BRDocLyF/099EtCB0MIfQpRuT0TISvaTsWwSy2gctIWiGA==", + "requires": { + "merge-descriptors": "^1.0.1", + "qs": "^6.9.4" + }, + "dependencies": { + "qs": { + "version": "6.9.4", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", + "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==" + } + } + }, "lcov-parse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", @@ -2264,8 +2280,7 @@ "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" }, "methods": { "version": "1.1.2", diff --git a/package.json b/package.json index 6cb639d..d128413 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "main": "Resource.js", "scripts": { "test": "nyc mocha --exit", + "test:express": "nyc mocha ./test/test.js --exit", "test:koa": "nyc mocha ./test/testKoa.js --exit", "coverage": "nyc --reporter=lcov --report-dir=./coverage npm run test", "lint": "eslint Resource.js" @@ -26,9 +27,10 @@ }, "homepage": "https://github.com/travist/resourcejs", "dependencies": { - "@koa/router": "^9.3.1", "debug": "^4.3.2", "fast-json-patch": "^3.1.0", + "@koa/router": "^9.3.1", + "koa-qs": "^3.0.0", "koa": "^2.13.0", "koa-bodyparser": "^4.3.0", "koa-compose": "^4.1.0", diff --git a/test/testKoa.js b/test/testKoa.js index 13f14f1..32b0877 100644 --- a/test/testKoa.js +++ b/test/testKoa.js @@ -91,9 +91,9 @@ function wasInvoked(entity, sequence, method) { describe('Connect to MongoDB', () => { it('Connect to MongoDB', async() => { const connection = await mongoose.connect('mongodb://localhost:27017/test', { - useCreateIndex: true, - useUnifiedTopology: true, - useNewUrlParser: true, + useCreateIndex: true, + useUnifiedTopology: true, + useNewUrlParser: true, }); assert.ok(connection); }); @@ -168,13 +168,13 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const resource1 = Resource(app, '/test', 'resource1', Resource1Model).rest({ - afterDelete(ctx, next) { + async afterDelete(ctx, next) { // Check that the delete item is still being returned via resourcejs. assert.notEqual(ctx.state.resource.item, {}); assert.notEqual(ctx.state.resource.item, []); assert.equal(ctx.state.resource.status, 204); - assert.equal(ctx.statusCode, 200); - // next(); + assert.equal(ctx.status, 404); // In Koa the ctx status is changed in respond + return await next(); }, }); const resource1Swaggerio = require('./snippets/resource1Swaggerio.json'); @@ -216,25 +216,25 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const resource2 = Resource(app, '/test', 'resource2', Resource2Model).rest({ // Register before/after global handlers. - before(ctx, next) { + async before(ctx, next) { // Store the invoked handler and continue. setInvoked('resource2', 'before', ctx); - next(); + return await next(); }, - beforePost(ctx, next) { + async beforePost(ctx, next) { // Store the invoked handler and continue. setInvoked('resource2', 'beforePost', ctx); - next(); + return await next(); }, - after(ctx, next) { + async after(ctx, next) { // Store the invoked handler and continue. setInvoked('resource2', 'after', ctx); - next(); + return await next(); }, - afterPost(ctx, next) { + async afterPost(ctx, next) { // Store the invoked handler and continue. setInvoked('resource2', 'afterPost', ctx); - next(); + return await next(); }, }); const resource2Swaggerio = require('./snippets/resource2Swaggerio.json'); @@ -300,9 +300,9 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const nested1 = Resource(app, '/test/resource1/:resource1Id', 'nested1', Nested1Model).rest({ // Register before global handlers to set the resource1 variable. - before(ctx, next) { + async before(ctx, next) { ctx.request.body.resource1 = ctx.params.resource1Id; - next(); + return await next(); }, }); const nested1Swaggerio = require('./snippets/nested1Swaggerio.json'); @@ -342,18 +342,18 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const nested2 = Resource(app, '/test/resource2/:resource2Id', 'nested2', Nested2Model).rest({ // Register before/after global handlers. - before(ctx, next) { + async before(ctx, next) { ctx.request.body.resource2 = ctx.params.resource2Id; ctx.state.modelQuery = this.model.where('resource2', ctx.params.resource2Id); // Store the invoked handler and continue. setInvoked('nested2', 'before', ctx); - next(); + return await next(); }, - after(ctx, next) { + async after(ctx, next) { // Store the invoked handler and continue. setInvoked('nested2', 'after', ctx); - next(); + return await next(); }, }); const nested2Swaggerio = require('./snippets/nested2Swaggerio.json'); @@ -394,11 +394,11 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const resource3 = Resource(app, '/test', 'resource3', Resource3Model).rest({ - before(ctx, next) { + async before(ctx, next) { // This setting should be passed down to the underlying `save()` command ctx.state.writeOptions = { writeSetting: true }; - next(); + return await next(); }, }); const resource3Swaggerio = require('./snippets/resource3Swaggerio.json'); @@ -427,51 +427,51 @@ describe('Build Resources for following tests', () => { // Create the REST resource and continue. const resource4 = Resource(app, '/test', 'resource4', Resource4Model) .rest({ - beforePatch(ctx, next) { + async beforePatch(ctx, next) { ctx.state.modelQuery = { findOne: async function findOne() { throw new Error('failed'); }, }; - next(); + return await next(); }, }) .virtual({ path: 'undefined_query', - before: function(ctx, next) { + before: async function(ctx, next) { ctx.state.modelQuery = undefined; - return next(); + return await next(); }, }) .virtual({ path: 'defined', - before: function(ctx, next) { + before: async function(ctx, next) { ctx.state.modelQuery = Resource4Model.aggregate([ { $group: { _id: null, titles: { $sum: '$title' } } }, ]); - return next(); + return await next(); }, }) .virtual({ path: 'error', - before: function(ctx, next) { + before: async function(ctx, next) { ctx.state.modelQuery = { exec: async function exec() { throw new Error('Failed'); }, }; - return next(); + return await next(); }, }) .virtual({ path: 'empty', - before: function(ctx, next) { + before: async function(ctx, next) { ctx.state.modelQuery = { exec: async function exec() { return; }, }; - return next(); + return await next(); }, }); const resource4Swaggerio = require('./snippets/resource4Swaggerio.json'); @@ -524,12 +524,10 @@ describe('Test skipResource', () => { const resource = {}; it('/GET empty list', () => request(server) .get('/test/skip') - //.expect('Content-Type', /text\/html/) + .expect('Content-Type', /text\/plain/) .expect(404) .then((res) => { - const response = res.text; - const expected = 'Cannot GET /test/skip'; - assert(response.includes(expected), 'Response not found.'); + assert.equal(res.text, 'Not Found'); })); it('/POST Create new resource', () => request(server) @@ -538,22 +536,18 @@ describe('Test skipResource', () => { title: 'Test1', description: '12345678', }) - .expect('Content-Type', /text\/html/) + .expect('Content-Type', /text\/plain/) .expect(404) .then((res) => { - const response = res.text; - const expected = 'Cannot POST /test/skip'; - assert(response.includes(expected), 'Response not found.'); + assert.equal(res.text, 'Not Found'); })); it('/GET The new resource', () => request(server) .get(`/test/skip/${resource._id}`) - .expect('Content-Type', /text\/html/) + .expect('Content-Type', /text\/plain/) .expect(404) .then((res) => { - const response = res.text; - const expected = `Cannot GET /test/skip/${resource._id}`; - assert(response.includes(expected), 'Response not found.'); + assert.equal(res.text, 'Not Found'); })); it('/PUT Change data on the resource', () => request(server) @@ -561,42 +555,34 @@ describe('Test skipResource', () => { .send({ title: 'Test2', }) - .expect('Content-Type', /text\/html/) + .expect('Content-Type', /text\/plain/) .expect(404) .then((res) => { - const response = res.text; - const expected = `Cannot PUT /test/skip/${resource._id}`; - assert(response.includes(expected), 'Response not found.'); + assert.equal(res.text, 'Not Found'); })); it('/PATCH Change data on the resource', () => request(server) .patch(`/test/skip/${resource._id}`) - .expect('Content-Type', /text\/html/) + .expect('Content-Type', /text\/plain/) .expect(404) .then((res) => { - const response = res.text; - const expected = `Cannot PATCH /test/skip/${resource._id}`; - assert(response.includes(expected), 'Response not found.'); + assert.equal(res.text, 'Not Found'); })); it('/DELETE the resource', () => request(server) .delete(`/test/skip/${resource._id}`) - .expect('Content-Type', /text\/html/) + .expect('Content-Type', /text\/plain/) .expect(404) .then((res) => { - const response = res.text; - const expected = `Cannot DELETE /test/skip/${resource._id}`; - assert(response.includes(expected), 'Response not found.'); + assert.equal(res.text, 'Not Found'); })); it('/VIRTUAL the resource', () => request(server) .get('/test/skip/virtual/resource') - .expect('Content-Type', /text\/html/) + .expect('Content-Type', /text\/plain/) .expect(404) .then((res) => { - const response = res.text; - const expected = 'Cannot GET /test/skip/virtual/resource'; - assert(response.includes(expected), 'Response not found.'); + assert.equal(res.text, 'Not Found'); })); }); @@ -845,12 +831,10 @@ describe('Test single resource CRUD capabilities', () => { it('Cannot /POST to an existing resource', () => request(server) .post(`/test/resource1/${resource._id}`) - .expect('Content-Type', /text\/html/) + .expect('Content-Type', /text\/plain/) .expect(404) .then((res) => { - const response = res.text; - const expected = `Cannot POST /test/resource1/${resource._id}`; - assert(response.includes(expected), 'Response not found.'); + assert.equal(res.text, 'Not Found'); })); it('/DELETE the resource', () => request(server) @@ -1087,6 +1071,7 @@ function testSearch(testPath) { it('Should populate with options', () => request(server) .get(`${testPath}?name=noage&populate[path]=list.data`) + .expect(200) .then((res) => { const response = res.body; @@ -1597,10 +1582,10 @@ describe('Test single resource search capabilities', () => { it('Create an aggregation path', () => { Resource(app, '', 'aggregation', mongoose.model('resource1')).rest({ - beforeIndex(ctx, next) { + async beforeIndex(ctx, next) { ctx.state.modelQuery = mongoose.model('resource1'); ctx.state.modelQuery.pipeline = []; - next(); + return await next(); }, }); }); @@ -2186,12 +2171,10 @@ describe('Test nested resource CRUD capabilities', () => { it('Cannot /POST to an existing nested resource', () => request(server) .post(`/test/resource1/${resource._id}/nested1/${nested._id}`) - .expect('Content-Type', /text\/html/) + .expect('Content-Type', /text\/plain/) .expect(404) .then((res) => { - const response = res.text; - const expected = `Cannot POST /test/resource1/${resource._id}/nested1/${nested._id}`; - assert(response.includes(expected), 'Response not found.'); + assert.equal(res.text, 'Not Found'); })); it('/DELETE the nested resource', () => request(server) @@ -2414,68 +2397,68 @@ describe('Test before hooks', () => { Resource(app, '', 'hook', hookModel).rest({ hooks: { post: { - before(ctx, next) { + async before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); - next(); + return await next(); }, - after(ctx, next) { + async after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); - next(); + return await next(); }, }, get: { - before(ctx, next) { + async before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); - next(); + return await next(); }, - after(ctx, next) { + async after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); - next(); + return await next(); }, }, put: { - before(ctx, next) { + async before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); - next(); + return await next(); }, - after(ctx, next) { + async after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); - next(); + return await next(); }, }, delete: { - before(ctx, next) { + async before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); - next(); + return await next(); }, - after(ctx, next) { + async after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); - next(); + return await next(); }, }, index: { - before(ctx, next) { + async before(ctx, next) { assert.equal(calls.length, 0); calls.push('before'); - next(); + return await next(); }, - after(ctx, next) { + async after(ctx, next) { assert.equal(calls.length, 1); assert.deepEqual(calls, ['before']); calls.push('after'); - next(); + return await next(); }, }, }, diff --git a/utils.js b/utils.js index 3c1ab1f..7c01d5f 100644 --- a/utils.js +++ b/utils.js @@ -80,10 +80,10 @@ const paginate = function(ctx, totalItems, maxRangeSize) { if ((null !== range.to && range.from > range.to) || (range.from > 0 && range.from >= totalItems)) { if (totalItems > 0 || range.from !== 0) { - ctx.statusCode = 416; // Requested range unsatisfiable + ctx.status = 416; // Requested range unsatisfiable } else { - ctx.statusCode = 204; // No content + ctx.status = 204; // No content } ctx.set('Content-Range', `*/${totalItems}`); return; @@ -115,16 +115,16 @@ const paginate = function(ctx, totalItems, maxRangeSize) { var availableLimit = availableTo - range.from + 1; if (0 === availableLimit) { - ctx.statusCode = 204; // no content + ctx.status = 204; // no content ctx.set('Content-Range', '*/0'); return; } if (availableLimit < totalItems) { - ctx.statusCode = 206; // Partial contents + ctx.status = 206; // Partial contents } else { - ctx.statusCode = 200; // OK (all items) + ctx.status = 200; // OK (all items) } // Links From 59583f5f8453b5d28504856cc50c44c2467893d2 Mon Sep 17 00:00:00 2001 From: Sefriol Date: Thu, 16 Jul 2020 18:16:25 +0300 Subject: [PATCH 13/22] Koa: use lastMW instead of respond in skip --- KoaResource.js | 14 +++++++------- test/testKoa.js | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 4ba4685..fa0a3ed 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -536,7 +536,7 @@ class Resource { // Allow before handlers the ability to disable resource CRUD. if (ctx.state.skipResource) { debug.index('Skipping Resource'); - return await Resource.respond(ctx); + return await lastMW(ctx); } // Get the find query. @@ -653,7 +653,7 @@ class Resource { ctx.state.__rMethod = 'get'; if (ctx.state.skipResource) { debug.get('Skipping Resource'); - return await Resource.respond(ctx); + return await lastMW(ctx); } ctx.state.modelQuery = (ctx.state.modelQuery || ctx.state.model || this.model).findOne(); ctx.state.search = { '_id': ctx.params[`${this.name}Id`] }; @@ -734,7 +734,7 @@ class Resource { if (ctx.state.skipResource) { debug.virtual('Skipping Resource'); - return await Resource.respond(ctx); + return await lastMW(ctx); } const query = ctx.state.modelQuery || ctx.state.model; if (!query) { @@ -776,7 +776,7 @@ class Resource { if (ctx.state.skipResource) { debug.post('Skipping Resource'); - return await Resource.respond(ctx); + return await lastMW(ctx); } const Model = ctx.state.model || this.model; @@ -829,7 +829,7 @@ class Resource { if (ctx.state.skipResource) { debug.put('Skipping Resource'); - return await Resource.respond(ctx); + return await lastMW(ctx); } // Remove __v field @@ -897,7 +897,7 @@ class Resource { if (ctx.state.skipResource) { debug.patch('Skipping Resource'); - return await Resource.respond(ctx); + return await lastMW(ctx); } ctx.state.query = ctx.state.modelQuery || ctx.state.model || this.model; try { @@ -1021,7 +1021,7 @@ class Resource { if (ctx.state.skipResource) { debug.delete('Skipping Resource'); - return await Resource.respond(ctx); + return await lastMW(ctx); } ctx.state.query = ctx.state.modelQuery || ctx.state.model || this.model; diff --git a/test/testKoa.js b/test/testKoa.js index 32b0877..b960ba8 100644 --- a/test/testKoa.js +++ b/test/testKoa.js @@ -497,7 +497,6 @@ describe('Build Resources for following tests', () => { const skipResource = Resource(app, '/test', 'skip', SkipModel) .rest({ before: async(ctx, next) => { - console.log(ctx, 'test1.1'); ctx.state.skipResource = true; return await next(); }, From 4d042f6172818aeec39ab2067e73e3611ba6d539 Mon Sep 17 00:00:00 2001 From: Sefriol Date: Thu, 16 Jul 2020 18:16:50 +0300 Subject: [PATCH 14/22] fix(package.json): Modify test script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d128413..5129e26 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A simple Express library to reflect Mongoose models to a REST interface.", "main": "Resource.js", "scripts": { - "test": "nyc mocha --exit", + "test": "npm run test:express && npm run test:koa", "test:express": "nyc mocha ./test/test.js --exit", "test:koa": "nyc mocha ./test/testKoa.js --exit", "coverage": "nyc --reporter=lcov --report-dir=./coverage npm run test", From b9a6a374f33ca5f2002b26528b6eb2750925e721 Mon Sep 17 00:00:00 2001 From: Sefriol Date: Thu, 16 Jul 2020 18:24:48 +0300 Subject: [PATCH 15/22] Koa: Add multidocument POST support - Uses mongodb transactions - Fails if transaction cannot be started --- KoaResource.js | 30 +++++++++++++++++++++++++++--- Resource.js | 28 ++++++++++++++-------------- test/testKoa.js | 25 +++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index fa0a3ed..ce959d9 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -780,7 +780,13 @@ class Resource { } const Model = ctx.state.model || this.model; - ctx.state.model = new Model(ctx.request.body); + if (Array.isArray(ctx.request.body) && ctx.request.body.length) { + ctx.state.many = true; + ctx.state.model = ctx.request.body.map((model) => new Model(model)); + } + else { + ctx.state.model = new Model(ctx.request.body); + } return await next(); }; @@ -788,12 +794,30 @@ class Resource { debug.post('queryMiddleWare'); const writeOptions = ctx.state.writeOptions || {}; try { - ctx.state.item = await ctx.state.model.save(writeOptions); + if (ctx.state.many) { + ctx.state.session = await this.model.startSession(); + await ctx.state.session.startTransaction(); + writeOptions.session = ctx.state.session; + ctx.state.item = await Promise.all(ctx.state.model.map(model => model.save(writeOptions))); + await ctx.state.session.commitTransaction(); + } + else { + ctx.state.item = await ctx.state.model.save(writeOptions); + } debug.post(ctx.state.item); } catch (err) { debug.post(err); - ctx.state.resource = { status: 400, error: err }; + if (ctx.state.many) { + await ctx.state.session.abortTransaction(); + await ctx.state.session.endSession(); + if (err.originalError?.code === 20 && err.originalError?.codeName === 'IllegalOperation') { + err.message = 'Saving multiple documents is not supported by this server'; + ctx.state.resource = { status: 400, error: err }; + } + else ctx.state.resource = { status: 400, error: err }; + } + else ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); } return await next(); diff --git a/Resource.js b/Resource.js index 01b9891..0031b92 100644 --- a/Resource.js +++ b/Resource.js @@ -870,22 +870,22 @@ class Resource { res, item, () => { - const writeOptions = req.writeOptions || {}; - item.save(writeOptions, (err, item) => { - if (err) { - debug.put(err); - return Resource.setResponse(res, { status: 400, error: err }, next); - } + const writeOptions = req.writeOptions || {}; + item.save(writeOptions, (err, item) => { + if (err) { + debug.put(err); + return Resource.setResponse(res, { status: 400, error: err }, next); + } - return options.hooks.put.after.call( - this, - req, - res, - item, - Resource.setResponse.bind(Resource, res, { status: 200, item }, next) - ); + return options.hooks.put.after.call( + this, + req, + res, + item, + Resource.setResponse.bind(Resource, res, { status: 200, item }, next) + ); + }); }); - }); }); }, Resource.respond, options); return this; diff --git a/test/testKoa.js b/test/testKoa.js index b960ba8..e7fc8d6 100644 --- a/test/testKoa.js +++ b/test/testKoa.js @@ -660,6 +660,31 @@ describe('Test single resource CRUD capabilities', () => { assert(resource.hasOwnProperty('_id'), 'Resource ID not found'); })); + it('/POST Reject because empty array', () => request(server) + .post('/test/resource1') + .send([]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + const error = res.body; + assert.equal(error.message, 'resource1 validation failed: title: Path `title` is required.'); + assert(error.hasOwnProperty('errors'), 'errors not found'); + })); + + it('/POST Reject because not replicaSet', () => request(server) + .post('/test/resource1') + .send([{ + title: 'Test1', + description: '12345678', + }]) + .expect('Content-Type', /json/) + .expect(400) + .then((res) => { + const error = res.body; + console.log(error); + assert.equal(error.message, 'Saving multiple documents is not supported by this server'); + })); + it('/GET The new resource', () => request(server) .get(`/test/resource1/${resource._id}`) .expect('Content-Type', /json/) From 775ca88233647b36f9f914d8de15798dd2f05768 Mon Sep 17 00:00:00 2001 From: Sefriol Date: Thu, 16 Jul 2020 22:07:14 +0300 Subject: [PATCH 16/22] Koa: add session and transaction checks --- KoaResource.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index ce959d9..9d20e1c 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -795,8 +795,8 @@ class Resource { const writeOptions = ctx.state.writeOptions || {}; try { if (ctx.state.many) { - ctx.state.session = await this.model.startSession(); - await ctx.state.session.startTransaction(); + if ((ctx.state.session?.constructor?.name === 'ClientSession')) ctx.state.session = await this.model.startSession(); + if (!ctx.state.session.inTransaction()) await ctx.state.session.startTransaction(); writeOptions.session = ctx.state.session; ctx.state.item = await Promise.all(ctx.state.model.map(model => model.save(writeOptions))); await ctx.state.session.commitTransaction(); From ff7a9214c3bbcf964c045acd6e210f950d1e170e Mon Sep 17 00:00:00 2001 From: Sefriol Date: Tue, 21 Jul 2020 00:33:20 +0300 Subject: [PATCH 17/22] Koa: Fix MongoDB error handling in POST --- KoaResource.js | 17 ++++++++++++----- test/testKoa.js | 7 ++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 9d20e1c..19dbe6c 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -795,9 +795,10 @@ class Resource { const writeOptions = ctx.state.writeOptions || {}; try { if (ctx.state.many) { - if ((ctx.state.session?.constructor?.name === 'ClientSession')) ctx.state.session = await this.model.startSession(); + if (utils.get(ctx, 'ctx.state.session.constructor.name') !== 'ClientSession') ctx.state.session = await this.model.startSession(); if (!ctx.state.session.inTransaction()) await ctx.state.session.startTransaction(); writeOptions.session = ctx.state.session; + ctx.state.item = await Promise.all(ctx.state.model.map(model => model.save(writeOptions))); await ctx.state.session.commitTransaction(); } @@ -809,11 +810,17 @@ class Resource { catch (err) { debug.post(err); if (ctx.state.many) { - await ctx.state.session.abortTransaction(); + if (ctx.state.session.inTransaction()) await ctx.state.session.abortTransaction(); await ctx.state.session.endSession(); - if (err.originalError?.code === 20 && err.originalError?.codeName === 'IllegalOperation') { - err.message = 'Saving multiple documents is not supported by this server'; - ctx.state.resource = { status: 400, error: err }; + + if (err instanceof mongodb.MongoError + || err.originalError instanceof mongodb.MongoError + || err.code + || utils.get(err, 'err.originalError.code') ) { + err.errors = [Object.assign({}, err)]; + err.message = 'Error occured while trying to save document into database'; + err.name = 'DatabaseError'; + ctx.state.resource = { status: 500, error: err }; } else ctx.state.resource = { status: 400, error: err }; } diff --git a/test/testKoa.js b/test/testKoa.js index e7fc8d6..2235a62 100644 --- a/test/testKoa.js +++ b/test/testKoa.js @@ -678,11 +678,12 @@ describe('Test single resource CRUD capabilities', () => { description: '12345678', }]) .expect('Content-Type', /json/) - .expect(400) + .expect(500) .then((res) => { const error = res.body; - console.log(error); - assert.equal(error.message, 'Saving multiple documents is not supported by this server'); + assert.equal(error.message, 'Error occured while trying to save document into database'); + assert(error.hasOwnProperty('errors'), 'errors not found'); + assert.equal(error.errors[0].name, 'MongoError'); })); it('/GET The new resource', () => request(server) From b377fde76b6d60a2682032ff02f8679fb48bf0a5 Mon Sep 17 00:00:00 2001 From: Sefriol Date: Thu, 23 Jul 2020 15:02:43 +0300 Subject: [PATCH 18/22] Koa: Change ctx.state variables to more coherient - PUT, PATCH, POST and DELETE use ctx.state.item before hooks * Previously POST used ctx.state.model - INDEX still the same. It's usage of hooks is confusing - GET uses ctx.state.query instead of ctx.state.modelQuery - VIRTUAL now uses beforeQueryMW so that hooks can be used * ctx.state.query stores the query information --- KoaResource.js | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 19dbe6c..37ccf50 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -528,7 +528,7 @@ class Resource { const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); // eslint-disable-next-line max-statements - const beforeQueryMW = async(ctx, next) => { // Callback + const beforeQueryMW = async(ctx, next) => { debug.index('beforeQueryMiddleWare'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'index'; @@ -602,7 +602,7 @@ class Resource { return await next(); }; - const queryMW = async(ctx, next) => { // Callback + const queryMW = async(ctx, next) => { debug.index('queryMiddleware'); try { const items = await this.indexQuery(ctx.state.queryExec, ctx.state.query.pipeline).exec(); @@ -647,7 +647,7 @@ class Resource { this.methods.push('get'); const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); - const beforeQueryMW = async(ctx, next) => { // Callback + const beforeQueryMW = async(ctx, next) => { debug.get('beforeQueryMiddleware'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'get'; @@ -655,21 +655,21 @@ class Resource { debug.get('Skipping Resource'); return await lastMW(ctx); } - ctx.state.modelQuery = (ctx.state.modelQuery || ctx.state.model || this.model).findOne(); + ctx.state.query = (ctx.state.modelQuery || ctx.state.model || this.model).findOne(); ctx.state.search = { '_id': ctx.params[`${this.name}Id`] }; // Only call populate if they provide a populate query. const populate = Resource.getParamQuery(ctx, 'populate'); if (populate) { debug.get(`Populate: ${populate}`); - ctx.state.modelQuery.populate(populate); + ctx.state.query.populate(populate); } return await next(); }; - const queryMW = async(ctx, next) => { // Callback + const queryMW = async(ctx, next) => { debug.get('queryMiddleWare'); try { - ctx.state.item = await ctx.state.modelQuery.where(ctx.state.search).lean().exec(); + ctx.state.item = await ctx.state.query.where(ctx.state.search).lean().exec(); } catch (err) { ctx.state.resource = { status: 400, error: err }; @@ -725,9 +725,7 @@ class Resource { this.methods.push(`virtual/${path}`); const lastMW = compose([this._generateMiddleware.call(this, options, 'after'), Resource.respond]); - const beforeQueryMW = async(ctx, next) => await next(); - - const queryMW = async(ctx, next) => { + const beforeQueryMW = async(ctx, next) => { debug.virtual('queryMiddleWare'); // Store the internal method for response manipulation. ctx.state.__rMethod = 'virtual'; @@ -736,13 +734,17 @@ class Resource { debug.virtual('Skipping Resource'); return await lastMW(ctx); } - const query = ctx.state.modelQuery || ctx.state.model; - if (!query) { - ctx.state.resource = { status: 404 }; - return await next(); + ctx.state.query = ctx.state.modelQuery || ctx.state.model; + if (!ctx.state.query) { + ctx.state.resource = { status: 404 }; } + + return await next(); + }; + + const queryMW = async(ctx, next) => { try { - const item = await query.exec(); + const item = await ctx.state.query.exec(); if (!item) ctx.state.resource = { status: 404 }; else ctx.state.resource = { status: 200, item }; } @@ -782,10 +784,10 @@ class Resource { const Model = ctx.state.model || this.model; if (Array.isArray(ctx.request.body) && ctx.request.body.length) { ctx.state.many = true; - ctx.state.model = ctx.request.body.map((model) => new Model(model)); + ctx.state.item = ctx.request.body.map((model) => new Model(model)); } else { - ctx.state.model = new Model(ctx.request.body); + ctx.state.item = new Model(ctx.request.body); } return await next(); }; @@ -799,11 +801,11 @@ class Resource { if (!ctx.state.session.inTransaction()) await ctx.state.session.startTransaction(); writeOptions.session = ctx.state.session; - ctx.state.item = await Promise.all(ctx.state.model.map(model => model.save(writeOptions))); + ctx.state.item = await Promise.all(ctx.state.item.map(item => item.save(writeOptions))); await ctx.state.session.commitTransaction(); } else { - ctx.state.item = await ctx.state.model.save(writeOptions); + ctx.state.item = await ctx.state.item.save(writeOptions); } debug.post(ctx.state.item); } From c80454f3196636f2f977e3df68768d34aef4b4f4 Mon Sep 17 00:00:00 2001 From: Joakim Date: Thu, 5 Nov 2020 12:54:57 +0200 Subject: [PATCH 19/22] Koa: Fix Post Many session check --- KoaResource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KoaResource.js b/KoaResource.js index 37ccf50..8843f62 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -797,7 +797,7 @@ class Resource { const writeOptions = ctx.state.writeOptions || {}; try { if (ctx.state.many) { - if (utils.get(ctx, 'ctx.state.session.constructor.name') !== 'ClientSession') ctx.state.session = await this.model.startSession(); + if (utils.get(ctx, 'state.session.constructor.name') !== 'ClientSession') ctx.state.session = await this.model.startSession(); if (!ctx.state.session.inTransaction()) await ctx.state.session.startTransaction(); writeOptions.session = ctx.state.session; From c44570fe22e1faaeedad9279aee11c231b022666 Mon Sep 17 00:00:00 2001 From: Joakim Date: Thu, 5 Nov 2020 12:55:30 +0200 Subject: [PATCH 20/22] Koa: Improve Post Many error handling --- KoaResource.js | 31 ++++++++++++++++--------------- test/testKoa.js | 2 +- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index 8843f62..acd986c 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -800,8 +800,20 @@ class Resource { if (utils.get(ctx, 'state.session.constructor.name') !== 'ClientSession') ctx.state.session = await this.model.startSession(); if (!ctx.state.session.inTransaction()) await ctx.state.session.startTransaction(); writeOptions.session = ctx.state.session; - - ctx.state.item = await Promise.all(ctx.state.item.map(item => item.save(writeOptions))); + const errors = [] + ctx.state.item = await Promise.all(ctx.state.item.map(item => item.save(writeOptions))) + .catch((err) => { + errors.push(err) + }) + .finally((result) => { + if (errors.length) { + const err = new Error(`Error${errors.length > 1 ? 's' : ''} occured while trying to save document into database`); + err.name = 'DatabaseError'; + err.errors = errors + throw err + } + else return result + }); await ctx.state.session.commitTransaction(); } else { @@ -811,22 +823,11 @@ class Resource { } catch (err) { debug.post(err); - if (ctx.state.many) { + if (err.name === 'DatabaseError') { if (ctx.state.session.inTransaction()) await ctx.state.session.abortTransaction(); await ctx.state.session.endSession(); - - if (err instanceof mongodb.MongoError - || err.originalError instanceof mongodb.MongoError - || err.code - || utils.get(err, 'err.originalError.code') ) { - err.errors = [Object.assign({}, err)]; - err.message = 'Error occured while trying to save document into database'; - err.name = 'DatabaseError'; - ctx.state.resource = { status: 500, error: err }; - } - else ctx.state.resource = { status: 400, error: err }; } - else ctx.state.resource = { status: 400, error: err }; + ctx.state.resource = { status: 400, error: err }; return await lastMW(ctx); } return await next(); diff --git a/test/testKoa.js b/test/testKoa.js index 2235a62..70e4ebd 100644 --- a/test/testKoa.js +++ b/test/testKoa.js @@ -678,7 +678,7 @@ describe('Test single resource CRUD capabilities', () => { description: '12345678', }]) .expect('Content-Type', /json/) - .expect(500) + .expect(400) .then((res) => { const error = res.body; assert.equal(error.message, 'Error occured while trying to save document into database'); From fec0ed871778b378976e8886f8d46bc6f537aea9 Mon Sep 17 00:00:00 2001 From: Joakim Date: Thu, 19 Aug 2021 14:15:16 +0300 Subject: [PATCH 21/22] Use body for 204 if defined --- KoaResource.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/KoaResource.js b/KoaResource.js index acd986c..15fe5be 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -215,11 +215,11 @@ class Resource { switch (ctx.state.__rMethod) { case 'index': ctx.status = 200; - ctx.body = []; + ctx.body = ctx.body || []; break; default: ctx.status = 200; - ctx.body = {}; + ctx.body = ctx.body || {}; break; } break; @@ -800,19 +800,19 @@ class Resource { if (utils.get(ctx, 'state.session.constructor.name') !== 'ClientSession') ctx.state.session = await this.model.startSession(); if (!ctx.state.session.inTransaction()) await ctx.state.session.startTransaction(); writeOptions.session = ctx.state.session; - const errors = [] + const errors = []; ctx.state.item = await Promise.all(ctx.state.item.map(item => item.save(writeOptions))) .catch((err) => { - errors.push(err) + errors.push(err); }) .finally((result) => { if (errors.length) { const err = new Error(`Error${errors.length > 1 ? 's' : ''} occured while trying to save document into database`); err.name = 'DatabaseError'; - err.errors = errors - throw err + err.errors = errors; + throw err; } - else return result + else return result; }); await ctx.state.session.commitTransaction(); } From e92653feb2c0da34c45718a746d73f170e07e967 Mon Sep 17 00:00:00 2001 From: Joakim Date: Mon, 14 Feb 2022 22:18:20 +0200 Subject: [PATCH 22/22] KoaJS: Add resource to application context for swagger --- KoaResource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/KoaResource.js b/KoaResource.js index 15fe5be..b8279a3 100644 --- a/KoaResource.js +++ b/KoaResource.js @@ -125,7 +125,7 @@ class Resource { } if (!this.app.context.resourcejs[path]) { - this.app.context.resourcejs[path] = {}; + this.app.context.resourcejs[path] = this; } // Add a stack processor so this stack can be executed independently of Express.