UNPKG

rest-hapi

Version:
1,961 lines (1,817 loc) 71.6 kB
'use strict' const Boom = require('@hapi/boom') const QueryHelper = require('./query-helper') const JoiMongooseHelper = require('./joi-mongoose-helper') const config = require('../config') const _ = require('lodash') const { truncatedStringify } = require('./log-util') // TODO: add a "clean" method that clears out all soft-deleted docs // TODO: add an optional TTL config setting that determines how long soft-deleted docs remain in the system // TODO: possibly remove "MANY_ONE" association and make it implied // TODO: possibly remove "ONE_ONE" association and make it implied module.exports = { list: _list, listHandler: _listHandler, find: _find, findHandler: _findHandler, create: _create, createHandler: _createHandler, update: _update, updateHandler: _updateHandler, deleteOne: _deleteOne, deleteOneHandler: _deleteOneHandler, deleteMany: _deleteMany, deleteManyHandler: _deleteManyHandler, addOne: _addOne, addOneHandler: _addOneHandler, removeOne: _removeOne, removeOneHandler: _removeOneHandler, addMany: _addMany, addManyHandler: _addManyHandler, removeMany: _removeMany, removeManyHandler: _removeManyHandler, getAll: _getAll, getAllHandler: _getAllHandler } /** * Finds a list of model documents. * @param {...any} args * **Positional:** * - function list(model, query, Log) * * **Named:** * - function list({ * model, * query, * Log = RestHapi.getLogger('list'), * restCall = false, * credentials * }) * * **Params:** * - model {object | string}: A mongoose model. * - query: rest-hapi query parameters to be converted to a mongoose query. * - Log: A logging object. * - restCall: If 'true', then will call GET /model * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model documents or the count of the query results. */ function _list(...args) { if (args.length > 1) { return _listV1(...args) } else { return _listV2(...args) } } function _listV1(model, query, Log) { model = getModel(model) const request = { query: query } return _listHandler(model, request, Log) } async function _listV2({ model, query, Log, restCall = false, credentials }) { model = getModel(model) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('list') if (restCall) { assertServer() credentials = defaultCreds(credentials) const request = { method: 'Get', url: `/${model.routeOptions.alias || model.modelName}`, query, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _listV1(model, query, Log) } } /** * Finds a list of model documents. * @param model {object | string}: A mongoose model. * @param request: The Hapi request object, or a container for the wrapper query. * @param Log: A logging object. * @returns {object} A promise for the resulting model documents or the count of the query results. * @private */ async function _listHandler(model, request, Log) { try { let query = Object.assign({}, request.query) try { if ( model.routeOptions && model.routeOptions.list && model.routeOptions.list.pre ) { query = await model.routeOptions.list.pre(query, request, Log) } } catch (err) { handleError(err, 'There was a preprocessing error.', Boom.badRequest, Log) } let mongooseQuery = {} let flatten = false if (query.$flatten) { flatten = true } delete query.$flatten const { $embed } = query if (query.$count) { mongooseQuery = model.countDocuments() mongooseQuery = QueryHelper.createMongooseQuery( model, query, mongooseQuery, Log ).lean() const result = await mongooseQuery.exec() if (config.truncateLogs) { Log.info( 'Result: %s', truncatedStringify(result, config.truncateStringLength) ) } else { Log.info('Result: %s', JSON.stringify(result, null, 2)) } return result } mongooseQuery = model.find() mongooseQuery = QueryHelper.createMongooseQuery( model, query, mongooseQuery, Log ).lean() const filter = mongooseQuery.getFilter() const count = await model.countDocuments(filter) mongooseQuery = QueryHelper.paginate(query, mongooseQuery, Log) let result = await mongooseQuery.exec() try { if ( model.routeOptions && model.routeOptions.list && model.routeOptions.list.post ) { result = await model.routeOptions.list.post(request, result, Log) } } catch (err) { handleError( err, 'There was a postprocessing error.', Boom.badRequest, Log ) } result = result.map(data => { const result = data if (model.routeOptions) { const associations = model.routeOptions.associations for (const associationKey in associations) { const association = associations[associationKey] if (association.type === 'ONE_MANY' && data[associationKey]) { // EXPL: we have to manually populate the return value for virtual (e.g. ONE_MANY) associations if (data[associationKey].toJSON) { // TODO: look into .toJSON and see why it appears sometimes and not other times result[associationKey] = data[associationKey].toJSON() } else { result[associationKey] = data[associationKey] } } if (config.enableSoftDelete && config.filterDeletedEmbeds) { // EXPL: remove soft deleted documents from populated properties filterDeletedEmbeds(result, {}, '', 0, Log) } if (flatten && $embed) { flattenEmbeds(result, associations, $embed) } } } if (config.logListResult) { if (config.truncateLogs) { Log.info( 'Result: %s', truncatedStringify(result, config.truncateStringLength) ) } else { Log.info('Result: %s', JSON.stringify(result, null, 2)) } } return result }) const pages = { current: query.$page || 1, prev: 0, hasPrev: false, next: 0, hasNext: false, total: 0 } const items = { limit: query.$limit, begin: (query.$page || 1) * query.$limit - query.$limit + 1, end: (query.$page || 1) * query.$limit, total: count } pages.total = Math.ceil(count / query.$limit) pages.next = pages.current + 1 pages.hasNext = pages.next <= pages.total pages.prev = pages.current - 1 pages.hasPrev = pages.prev !== 0 if (items.begin > items.total) { items.begin = items.total } if (items.end > items.total) { items.end = items.total } return { docs: result, pages: pages, items: items } } catch (err) { handleError(err, null, null, Log) } } /** * Finds a model document. * @param {...any} args * **Positional:** * - function find(model, _id, query, Log) * * **Named:** * - function find({ * model, * _id, * query, * Log = RestHapi.getLogger('find'), * restCall = false, * credentials * }) * * **Params:** * - model {object | string}: A mongoose model. * - _id: The document id. * - query: rest-hapi query parameters to be converted to a mongoose query. * - Log: A logging object. * - restCall: If 'true', then will call GET /model/{_id} * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model document. */ function _find(...args) { if (args.length > 1) { return _findV1(...args) } else { return _findV2(...args) } } function _findV1(model, _id, query, Log) { model = getModel(model) const request = { params: { _id: _id }, query: query } return _findHandler(model, _id, request, Log) } async function _findV2({ model, _id, query, Log, restCall = false, credentials }) { model = getModel(model) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('find') if (restCall) { assertServer() credentials = defaultCreds(credentials) const request = { method: 'Get', url: `/${model.routeOptions.alias || model.modelName}/${_id}`, params: { _id }, query, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _findV1(model, _id, query, Log) } } /** * Finds a model document. * @param model {object | string}: A mongoose model. * @param _id: The document id. * @param request: The Hapi request object, or a container for the wrapper query. * @param Log: A logging object. * @returns {object} A promise for the resulting model document. * @private */ async function _findHandler(model, _id, request, Log) { try { let query = Object.assign({}, request.query) try { if ( model.routeOptions && model.routeOptions.find && model.routeOptions.find.pre ) { query = await model.routeOptions.find.pre(_id, query, request, Log) } } catch (err) { handleError(err, 'There was a preprocessing error.', Boom.badRequest, Log) } let flatten = false if (query.$flatten) { flatten = true } delete query.$flatten const { $embed } = query let mongooseQuery = model.findOne({ _id: _id }) mongooseQuery = QueryHelper.createMongooseQuery( model, query, mongooseQuery, Log ).lean() const result = await mongooseQuery.exec() if (result) { let data = result try { if ( model.routeOptions && model.routeOptions.find && model.routeOptions.find.post ) { data = await model.routeOptions.find.post(request, result, Log) } } catch (err) { handleError( err, 'There was a postprocessing error.', Boom.badRequest, Log ) } if (model.routeOptions) { const associations = model.routeOptions.associations for (const associationKey in associations) { const association = associations[associationKey] if (association.type === 'ONE_MANY' && data[associationKey]) { // EXPL: we have to manually populate the return value for virtual (e.g. ONE_MANY) associations result[associationKey] = data[associationKey] } } if (config.enableSoftDelete && config.filterDeletedEmbeds) { // EXPL: remove soft deleted documents from populated properties filterDeletedEmbeds(result, {}, '', 0, Log) } if (flatten && $embed) { flattenEmbeds(result, associations, $embed) } } if (config.truncateLogs) { Log.info( 'Result: %s', truncatedStringify(result, config.truncateStringLength) ) } else { Log.info('Result: %s', JSON.stringify(result, null, 2)) } return result } else { throw Boom.notFound('No resource was found with that id.') } } catch (err) { handleError(err, null, null, Log) } } /** * Creates one or more model documents. * @param {...any} args * **Positional:** * - function create(model, payload, Log) * * **Named:** * - function create({ * model, * payload, * Log = RestHapi.getLogger('create'), * restCall = false, * credentials * }) * * **Params:** * - model {object | string}: A mongoose model. * - payload: Data used to create the model document/s. * - Log: A logging object. * - restCall: If 'true', then will call POST /model * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model document. */ function _create(...args) { if (args.length > 1) { return _createV1(...args) } else { return _createV2(...args) } } function _createV1(model, payload, Log) { model = getModel(model) const request = { payload: payload } return _createHandler(model, request, Log) } async function _createV2({ model, payload, Log, restCall = false, credentials }) { model = getModel(model) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('create') if (restCall) { assertServer() credentials = defaultCreds(credentials) const request = { method: 'Post', url: `/${model.routeOptions.alias || model.modelName}`, payload, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _createV1(model, payload, Log) } } // TODO: make sure errors are catching in correct order /** * Creates one or more model documents. * @param model {object | string}: A mongoose model. * @param request: The Hapi request object, or a container for the wrapper payload. * @param Log: A logging object. * @returns {object} A promise for the resulting model document/s. * @private */ async function _createHandler(model, request, Log) { let payload = null try { // EXPL: make a copy of the payload so that request.payload remains unchanged let isArray = true if (!_.isArray(request.payload)) { payload = [Object.assign({}, request.payload)] isArray = false } else { payload = request.payload.map(item => { return _.isObject(item) ? _.assignIn({}, item) : item }) } try { if ( model.routeOptions && model.routeOptions.create && model.routeOptions.create.pre ) { for (const document of payload) { await model.routeOptions.create.pre(document, request, Log) } } } catch (err) { handleError( err, 'There was a preprocessing error creating the resource.', Boom.badRequest, Log ) } if (config.enableCreatedAt) { for (const document of payload) { document.createdAt = new Date() } } let data try { data = await model.create(payload) } catch (err) { Log.error(err) if (err.code === 11000) { throw Boom.conflict('There was a duplicate key error.') } else { throw Boom.badImplementation( 'There was an error creating the resource.' ) } } // EXPL: rather than returning the raw "create" data, we filter the data through a separate query const attributes = QueryHelper.createAttributesFilter({}, model, Log) data = data.map(item => { return item._id }) const result = await model .find() .where({ _id: { $in: data } }) .select(attributes) .lean() .exec() try { if ( model.routeOptions && model.routeOptions.create && model.routeOptions.create.post ) { for (const document of result) { await model.routeOptions.create.post(document, request, result, Log) } } } catch (err) { handleError( err, 'There was a postprocessing error creating the resource.', Boom.badRequest, Log ) } if (isArray) { return result } else { return result[0] } } catch (err) { handleError(err, null, null, Log) } } /** * Updates a model document. * @param {...any} args * **Positional:** * - function update(model, _id, payload, Log) * * **Named:** * - function update({ * model, * _id, * payload, * Log = RestHapi.getLogger('update'), * restCall = false, * credentials * }) * * **Params:** * - model {object | string}: A mongoose model. * - _id: The document id. * - payload: Data used to update the model document. * - Log: A logging object. * - restCall: If 'true', then will call PUT /model/{_id} * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model document. */ function _update(...args) { if (args.length > 1) { return _updateV1(...args) } else { return _updateV2(...args) } } function _updateV1(model, _id, payload, Log) { model = getModel(model) const request = { params: { _id: _id }, payload: payload } return _updateHandler(model, _id, request, Log) } async function _updateV2({ model, _id, payload, Log, restCall = false, credentials }) { model = getModel(model) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('update') if (restCall) { assertServer() credentials = defaultCreds(credentials) const request = { method: 'Put', url: `/${model.routeOptions.alias || model.modelName}/${_id}`, params: { _id }, payload, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _updateV1(model, _id, payload, Log) } } /** * Updates a model document. * @param model {object | string}: A mongoose model. * @param _id: The document id. * @param request: The Hapi request object, or a container for the wrapper payload. * @param Log: A logging object. * @returns {object} A promise for the resulting model document. * @private */ async function _updateHandler(model, _id, request, Log) { let payload = Object.assign({}, request.payload) try { try { if ( model.routeOptions && model.routeOptions.update && model.routeOptions.update.pre ) { payload = await model.routeOptions.update.pre( _id, payload, request, Log ) } } catch (err) { handleError( err, 'There was a preprocessing error updating the resource.', Boom.badRequest, Log ) } if (config.enableUpdatedAt) { payload.updatedAt = new Date() } let result try { result = await model.findByIdAndUpdate(_id, payload, { runValidators: config.enableMongooseRunValidators }) } catch (err) { Log.error(err) if (err.code === 11000) { throw Boom.conflict('There was a duplicate key error.') } else { throw Boom.badImplementation( 'There was an error updating the resource.' ) } } if (result) { const attributes = QueryHelper.createAttributesFilter({}, model, Log) result = await model.findOne({ _id: result._id }, attributes).lean() try { if ( model.routeOptions && model.routeOptions.update && model.routeOptions.update.post ) { result = await model.routeOptions.update.post(request, result, Log) } } catch (err) { handleError( err, 'There was a postprocessing error updating the resource.', Boom.badRequest, Log ) } return result } else { throw Boom.notFound('No resource was found with that id.') } } catch (err) { handleError(err, null, null, Log) } } /** * Deletes a model document. * @param {...any} args * **Positional:** * - function deleteOne(model, _id, hardDelete = false, Log) * * **Named:** * - function deleteOne({ * model, * _id, * hardDelete = false, * Log = RestHapi.getLogger('deleteOne'), * restCall = false, * credentials * }) * * **Params:** * - model {object | string}: A mongoose model. * - _id: The document id. * - hardDelete: Flag used to determine a soft or hard delete. * - Log: A logging object. * - restCall: If 'true', then will call DELETE /model/{_id} * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model document. */ function _deleteOne(...args) { if (args.length > 1) { return _deleteOneV1(...args) } else { return _deleteOneV2(...args) } } function _deleteOneV1(model, _id, hardDelete, Log) { model = getModel(model) const request = { params: { _id: _id } } return _deleteOneHandler(model, _id, hardDelete, request, Log) } async function _deleteOneV2({ model, _id, hardDelete = false, Log, restCall = false, credentials }) { model = getModel(model) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('deleteOne') if (restCall) { assertServer() credentials = defaultCreds(credentials) const request = { method: 'Delete', url: `/${model.routeOptions.alias || model.modelName}/${_id}`, params: { _id }, payload: { hardDelete }, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _deleteOneV1(model, _id, hardDelete, Log) } } /** * Deletes a model document * @param model {object | string}: A mongoose model. * @param _id: The document id. * @param hardDelete: Flag used to determine a soft or hard delete. * @param request: The Hapi request object. * @param Log: A logging object. * @returns {object} A promise returning true if the delete succeeds. * @private */ // TODO: only update "deleteAt" the first time a document is deleted async function _deleteOneHandler(model, _id, hardDelete, request, Log) { try { try { if ( model.routeOptions && model.routeOptions.delete && model.routeOptions.delete.pre ) { await model.routeOptions.delete.pre(_id, hardDelete, request, Log) } } catch (err) { handleError( err, 'There was a preprocessing error deleting the resource.', Boom.badRequest, Log ) } let deleted try { if (config.enableSoftDelete && !hardDelete) { const payload = { isDeleted: true } if (config.enableDeletedAt) { payload.deletedAt = new Date() } if (config.enableDeletedBy && config.enableSoftDelete) { const deletedBy = request.payload.deletedBy || request.payload[0].deletedBy if (deletedBy) { payload.deletedBy = deletedBy } } deleted = await model.findByIdAndUpdate(_id, payload, { new: true, runValidators: config.enableMongooseRunValidators }) } else { deleted = await model.findByIdAndRemove(_id) } } catch (err) { handleError( err, 'There was an error deleting the resource.', Boom.badImplementation, Log ) } // TODO: clean up associations/set rules for ON DELETE CASCADE/etc. if (deleted) { // TODO: add eventLogs try { if ( model.routeOptions && model.routeOptions.delete && model.routeOptions.delete.post ) { await model.routeOptions.delete.post( hardDelete, deleted, request, Log ) } } catch (err) { handleError( err, 'There was a postprocessing error deleting the resource.', Boom.badRequest, Log ) } return true } else { throw Boom.notFound('No resource was found with that id.') } } catch (err) { handleError(err, null, null, Log) } } /** * Deletes multiple documents. * @param {...any} args * **Positional:** * - function deleteMany(model, payload, Log) * * **Named:** * - function deleteMany({ * model, * payload, * Log = RestHapi.getLogger('delete'), * restCall = false, * credentials * }) * * **Params:** * - model {object | string}: A mongoose model. * - payload: Either an array of ids or an array of objects containing an id and a "hardDelete" flag. * - Log: A logging object. * - restCall: If 'true', then will call DELETE /model * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model document. */ function _deleteMany(...args) { if (args.length > 1) { return _deleteManyV1(...args) } else { return _deleteManyV2(...args) } } function _deleteManyV1(model, payload, Log) { model = getModel(model) const request = { payload: payload } return _deleteManyHandler(model, request, Log) } async function _deleteManyV2({ model, payload, Log, restCall = false, credentials }) { model = getModel(model) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('deleteMany') if (restCall) { assertServer() credentials = defaultCreds(credentials) const request = { method: 'Delete', url: `/${model.routeOptions.alias || model.modelName}`, payload, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _deleteManyV1(model, payload, Log) } } /** * Deletes multiple documents. * @param model {object | string}: A mongoose model. * @param request: The Hapi request object, or a container for the wrapper payload. * @param Log: A logging object. * @returns {object} A promise returning true if the delete succeeds. * @private */ // TODO: prevent Promise.all from catching first error and returning early. Catch individual errors and return a list // TODO(cont) of ids that failed async function _deleteManyHandler(model, request, Log) { try { // EXPL: make a copy of the payload so that request.payload remains unchanged const payload = request.payload.map(item => { return _.isObject(item) ? _.assignIn({}, item) : item }) const promises = [] for (const arg of payload) { if (JoiMongooseHelper.isObjectId(arg)) { promises.push(_deleteOneHandler(model, arg, false, request, Log)) } else { promises.push( _deleteOneHandler(model, arg._id, arg.hardDelete, request, Log) ) } } await Promise.all(promises) return true } catch (err) { handleError(err, null, null, Log) } } /** * Adds an association to a document * @param {...any} args * **Positional:** * - function addOne( * ownerModel, * ownerId, * childModel, * childId, * associationName, * payload, * Log * ) * * **Named:** * - function addOne({ * ownerModel, * ownerId, * childModel, * childId, * associationName, * payload = {}, * Log = RestHapi.getLogger('addOne'), * restCall = false, * credentials * }) * * **Params:** * - ownerModel {object | string}: The model that is being added to. * - ownerId: The id of the owner document. * - childModel {object | string}: The model that is being added. * - childId: The id of the child document. * - associationName: The name of the association from the ownerModel's perspective. * - payload: An object containing an extra linking-model fields. * - Log: A logging object * - restCall: If 'true', then will call PUT /ownerModel/{ownerId}/childModel/{childId} * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model document. */ function _addOne(...args) { if (args.length > 1) { return _addOneV1(...args) } else { return _addOneV2(...args) } } function _addOneV1( ownerModel, ownerId, childModel, childId, associationName, payload, Log ) { ownerModel = getModel(ownerModel) childModel = getModel(childModel) const request = { params: { ownerId: ownerId, childId: childId }, payload: payload } return _addOneHandler( ownerModel, ownerId, childModel, childId, associationName, request, Log ) } async function _addOneV2({ ownerModel, ownerId, childModel, childId, associationName, payload = {}, Log, restCall = false, credentials }) { ownerModel = getModel(ownerModel) childModel = getModel(childModel) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('addOne') if (restCall) { assertServer() credentials = defaultCreds(credentials) const association = ownerModel.routeOptions.associations[associationName] const ownerAlias = ownerModel.routeOptions.alias || ownerModel.modelName const childAlias = association.alias || association.include.model.modelName const request = { method: 'Put', url: `/${ownerAlias}/${ownerId}/${childAlias}/${childId}`, payload, params: { ownerId, childId }, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _addOneV1( ownerModel, ownerId, childModel, childId, associationName, payload, Log ) } } /** * Adds an association to a document * @param ownerModel {object | string}: The model that is being added to. * @param ownerId: The id of the owner document. * @param childModel {object | string}: The model that is being added. * @param childId: The id of the child document. * @param associationName: The name of the association from the ownerModel's perspective. * @param request: The Hapi request object, or a container for the wrapper payload. * @param Log: A logging object * @returns {object} A promise returning true if the add succeeds. * @private */ async function _addOneHandler( ownerModel, ownerId, childModel, childId, associationName, request, Log ) { try { const ownerObject = await ownerModel .findOne({ _id: ownerId }) .select(associationName) let payload = Object.assign({}, request.payload) if (ownerObject) { if (!payload) { payload = {} } payload.childId = childId payload = [payload] try { if ( ownerModel.routeOptions && ownerModel.routeOptions.add && ownerModel.routeOptions.add[associationName] && ownerModel.routeOptions.add[associationName].pre ) { payload = await ownerModel.routeOptions.add[associationName].pre( payload, request, Log ) } } catch (err) { handleError( err, 'There was a preprocessing error while setting the association.', Boom.badRequest, Log ) } try { await _setAssociation( ownerModel, ownerObject, childModel, childId, associationName, payload, Log ) } catch (err) { handleError( err, 'There was a database error while setting the association.', Boom.badImplementation, Log ) } try { if ( ownerModel.routeOptions && ownerModel.routeOptions.add && ownerModel.routeOptions.add[associationName] && ownerModel.routeOptions.add[associationName].post ) { await ownerModel.routeOptions.add[associationName].post( payload, request, Log ) } } catch (err) { handleError( err, 'There was a postprocessing error after setting the association.', Boom.badRequest, Log ) } return true } else { throw Boom.notFound('No resource was found with that id.') } } catch (err) { handleError(err, null, null, Log) } } /** * Removes an association to a document * @param {...any} args * **Positional:** * - function removeOne( * ownerModel, * ownerId, * childModel, * childId, * associationName, * payload, * Log * ) * * **Named:** * - function removeOne({ * ownerModel, * ownerId, * childModel, * childId, * associationName, * payload = {}, * Log = RestHapi.getLogger('removeOne'), * restCall = false, * credentials * }) * * **Params:** * - ownerModel {object | string}: The model that is being removed from. * - ownerId: The id of the owner document. * - childModel {object | string}: The model that is being removed. * - childId: The id of the child document. * - associationName: The name of the association from the ownerModel's perspective. * - payload: An object containing an extra linking-model fields. * - Log: A logging object * - restCall: If 'true', then will call DELETE /ownerModel/{ownerId}/childModel/{childId} * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model document. */ function _removeOne(...args) { if (args.length > 1) { return _removeOneV1(...args) } else { return _removeOneV2(...args) } } function _removeOneV1( ownerModel, ownerId, childModel, childId, associationName, payload, Log ) { ownerModel = getModel(ownerModel) childModel = getModel(childModel) const request = { params: { ownerId: ownerId, childId: childId }, payload: payload } return _removeOneHandler( ownerModel, ownerId, childModel, childId, associationName, request, Log ) } async function _removeOneV2({ ownerModel, ownerId, childModel, childId, associationName, payload = {}, Log, restCall = false, credentials }) { ownerModel = getModel(ownerModel) childModel = getModel(childModel) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('removeOne') if (restCall) { assertServer() credentials = defaultCreds(credentials) const association = ownerModel.routeOptions.associations[associationName] const ownerAlias = ownerModel.routeOptions.alias || ownerModel.modelName const childAlias = association.alias || association.include.model.modelName const request = { method: 'Delete', url: `/${ownerAlias}/${ownerId}/${childAlias}/${childId}`, payload, params: { ownerId, childId }, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _removeOneV1( ownerModel, ownerId, childModel, childId, associationName, payload, Log ) } } /** * Removes an association to a document * @param ownerModel {object | string}: The model that is being removed from. * @param ownerId: The id of the owner document. * @param childModel {object | string}: The model that is being removed. * @param childId: The id of the child document. * @param associationName: The name of the association from the ownerModel's perspective. * @param request: The Hapi request object. * @param Log: A logging object * @returns {object} A promise returning true if the remove succeeds. * @private */ async function _removeOneHandler( ownerModel, ownerId, childModel, childId, associationName, request, Log ) { try { const ownerObject = await ownerModel .findOne({ _id: ownerId }) .select(associationName) if (ownerObject) { try { if ( ownerModel.routeOptions && ownerModel.routeOptions.remove && ownerModel.routeOptions.remove[associationName] && ownerModel.routeOptions.remove[associationName].pre ) { await ownerModel.routeOptions.remove[associationName].pre( {}, request, Log ) } } catch (err) { handleError( err, 'There was a preprocessing error while removing the association.', Boom.badRequest, Log ) } try { await _removeAssociation( ownerModel, ownerObject, childModel, childId, associationName, Log ) } catch (err) { handleError( err, 'There was a database error while removing the association.', Boom.badImplementation, Log ) } try { if ( ownerModel.routeOptions && ownerModel.routeOptions.remove && ownerModel.routeOptions.remove[associationName] && ownerModel.routeOptions.remove[associationName].post ) { await ownerModel.routeOptions.remove[associationName].post( {}, request, Log ) } } catch (err) { handleError( err, 'There was a postprocessing error after removing the association.', Boom.badRequest, Log ) } return true } else { throw Boom.notFound('No resource was found with that id.') } } catch (err) { handleError(err, null, null, Log) } } /** * Adds multiple associations to a document. * @param {...any} args * **Positional:** * - function addMany( * ownerModel, * ownerId, * childModel, * associationName, * payload, * Log * ) * * **Named:** * - function addMany({ * ownerModel, * ownerId, * childModel, * associationName, * payload = {}, * Log = RestHapi.getLogger('addMany'), * restCall = false, * credentials * }) * * **Params:** * - ownerModel {object | string}: The model that is being added to. * - ownerId: The id of the owner document. * - childModel {object | string}: The model that is being added. * - associationName: The name of the association from the ownerModel's perspective. * - payload: Either a list of id's or a list of id's along with extra linking-model fields. * - Log: A logging object * - restCall: If 'true', then will call POST /ownerModel/{ownerId}/childModel * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model document. */ function _addMany(...args) { if (args.length > 1) { return _addManyV1(...args) } else { return _addManyV2(...args) } } function _addManyV1( ownerModel, ownerId, childModel, associationName, payload, Log ) { ownerModel = getModel(ownerModel) childModel = getModel(childModel) const request = { params: { ownerId: ownerId }, payload: payload } return _addManyHandler( ownerModel, ownerId, childModel, associationName, request, Log ) } async function _addManyV2({ ownerModel, ownerId, childModel, associationName, payload = {}, Log, restCall = false, credentials }) { ownerModel = getModel(ownerModel) childModel = getModel(childModel) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('addMany') if (restCall) { assertServer() credentials = defaultCreds(credentials) const association = ownerModel.routeOptions.associations[associationName] const ownerAlias = ownerModel.routeOptions.alias || ownerModel.modelName const childAlias = association.alias || association.include.model.modelName const request = { method: 'Post', url: `/${ownerAlias}/${ownerId}/${childAlias}`, payload, params: { ownerId }, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _addManyV1( ownerModel, ownerId, childModel, associationName, payload, Log ) } } /** * Adds multiple associations to a document. * @param ownerModel {object | string}: The model that is being added to. * @param ownerId: The id of the owner document. * @param childModel {object | string}: The model that is being added. * @param associationName: The name of the association from the ownerModel's perspective. * @param request: The Hapi request object, or a container for the wrapper payload. * @param Log: A logging object * @returns {object} A promise returning true if the add succeeds. * @private */ async function _addManyHandler( ownerModel, ownerId, childModel, associationName, request, Log ) { try { // EXPL: make a copy of the payload so that request.payload remains unchanged let payload = request.payload.map(item => { return _.isObject(item) ? _.cloneDeep(item) : item }) if (_.isEmpty(request.payload)) { throw Boom.badRequest('Payload is empty.') } const ownerObject = await ownerModel .findOne({ _id: ownerId }) .select(associationName) if (ownerObject) { try { if ( ownerModel.routeOptions && ownerModel.routeOptions.add && ownerModel.routeOptions.add[associationName] && ownerModel.routeOptions.add[associationName].pre ) { payload = await ownerModel.routeOptions.add[associationName].pre( payload, request, Log ) } } catch (err) { handleError( err, 'There was a preprocessing error while setting the association.', Boom.badRequest, Log ) } let childIds = [] // EXPL: the payload is an array of Ids if ( typeof payload[0] === 'string' || payload[0] instanceof String || payload[0]._bsontype === 'ObjectID' ) { childIds = payload } else { // EXPL: the payload contains extra fields childIds = payload.map(object => { return object.childId }) } for (const childId of childIds) { try { await _setAssociation( ownerModel, ownerObject, childModel, childId, associationName, payload, Log ) } catch (err) { handleError( err, 'There was an internal error while setting the associations.', Boom.badImplementation, Log ) } } try { if ( ownerModel.routeOptions && ownerModel.routeOptions.add && ownerModel.routeOptions.add[associationName] && ownerModel.routeOptions.add[associationName].post ) { await ownerModel.routeOptions.add[associationName].post( payload, request, Log ) } } catch (err) { handleError( err, 'There was a postprocessing error after setting the associations.', Boom.badRequest, Log ) } return true } else { throw Boom.notFound('No owner resource was found with that id.') } } catch (err) { handleError(err, null, null, Log) } } /** * Removes multiple associations from a document * @param {...any} args * **Positional:** * - function removeMany( * ownerModel, * ownerId, * childModel, * associationName, * payload, * Log * ) * * **Named:** * - function removeMany({ * ownerModel, * ownerId, * childModel, * associationName, * payload = {}, * Log = RestHapi.getLogger('removeMany'), * restCall = false, * credentials * }) * * **Params:** * - ownerModel {object | string}: The model that is being added from. * - ownerId: The id of the owner document. * - childModel {object | string}: The model that is being removed. * - associationName: The name of the association from the ownerModel's perspective. * - payload: Either a list of id's or a list of id's along with extra linking-model fields. * - Log: A logging object * - restCall: If 'true', then will call DELETE /ownerModel/{ownerId}/childModel * - credentials: Credentials for accessing the endpoint. * * @returns {object} A promise for the resulting model document. */ function _removeMany(...args) { if (args.length > 1) { return _removeManyV1(...args) } else { return _removeManyV2(...args) } } function _removeManyV1( ownerModel, ownerId, childModel, associationName, payload, Log ) { ownerModel = getModel(ownerModel) childModel = getModel(childModel) const request = { params: { ownerId: ownerId }, payload: payload } return _removeManyHandler( ownerModel, ownerId, childModel, associationName, request, Log ) } async function _removeManyV2({ ownerModel, ownerId, childModel, associationName, payload = {}, Log, restCall = false, credentials }) { ownerModel = getModel(ownerModel) childModel = getModel(childModel) const RestHapi = require('../rest-hapi') Log = Log || RestHapi.getLogger('removeMany') if (restCall) { assertServer() credentials = defaultCreds(credentials) const association = ownerModel.routeOptions.associations[associationName] const ownerAlias = ownerModel.routeOptions.alias || ownerModel.modelName const childAlias = association.alias || association.include.model.modelName const request = { method: 'Delete', url: `/${ownerAlias}/${ownerId}/${childAlias}`, payload, params: { ownerId }, credentials, headers: { authorization: 'Bearer' } } const injectOptions = RestHapi.testHelper.mockInjection(request) const { result } = await RestHapi.server.inject(injectOptions) return result } else { return _removeManyV1( ownerModel, ownerId, childModel, associationName, payload, Log ) } } /** * Removes multiple associations from a document * @param ownerModel {object | string}: The model that is being removed from. * @param ownerId: The id of the owner document. * @param childModel {object | string}: The model that is being removed. * @param associationName: The name of the association from the ownerModel's perspective. * @param request: The Hapi request object, or a container for the wrapper payload. * @param Log: A logging object * @returns {object} A promise returning true if the remove succeeds. * @private */ async function _removeManyHandler( ownerModel, ownerId, childModel, associationName, request, Log ) { try { // EXPL: make a copy of the payload so that request.payload remains unchanged let payload = request.payload.map(item => { return _.isObject(item) ? _.cloneDeep(item) : item }) if (_.isEmpty(request.payload)) { throw Boom.badRequest('Payload is empty.') } const ownerObject = await ownerModel .findOne({ _id: ownerId }) .select(associationName) if (ownerObject) { try { if ( ownerModel.routeOptions && ownerModel.routeOptions.remove && ownerModel.routeOptions.remove[associationName] && ownerModel.routeOptions.remove[associationName].pre ) { payload = await ownerModel.routeOptions.remove[associationName].pre( payload, request, Log ) } } catch (err) { handleError( err, 'There was a preprocessing error while removing the associations.', Boom.badRequest, Log ) } for (const childId of payload) { try { await _removeAssociation( ownerModel, ownerObject, childModel, childId, associationName, Log ) } catch (err) { handleError( err, 'There was an internal error while removing the associations.', Boom.badImplementation, Log ) } } try { if ( ownerModel.routeOptions && ownerModel.routeOptions.remove && ownerModel.routeOptions.remove[associationName] && ownerModel.routeOptions.remove[associationName].post ) { await ownerModel.routeOptions.remove[associationName].post( payload, request, Log ) } } catch (err) { handleError( err, 'There was a postprocessing error after removing the associations.', Boom.badRequest, Log ) } return true } else { throw Boom.notFound('No owner resource was found with that id.') } } catch (err) { handleError(err, null, null, Log) } } /** * Get all of the associations for a document * @param {...any} args * **Positional:** * - function getAll( * ownerModel, * ownerId, * childModel, * associationName, * query, * Log * ) * * **Named:** * - function getAll({ * ownerModel, * ownerId, * childModel, * associationName, * query, * Log =