rest-hapi
Version:
A RESTful API generator for hapi
1,961 lines (1,817 loc) • 71.6 kB
JavaScript
'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 =