Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Endpoint/recover #247

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions policies/audit-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,65 @@ module.exports = {
logDelete: internals.logDelete
}

/**
* Policy to log recover actions.
* @param model
* @param logger
* @returns {logRecoverForModel}
*/
internals.logRecover = function(mongoose, model, logger) {
const logRecoverForModel = async function logRecoverForModel(request, h) {
const Log = logger.bind('logRecover')
try {
const AuditLog = mongoose.model('auditLog')

const ipAddress = internals.getIP(request)
const userId = _.get(request.auth.credentials, config.userIdKey)
let documents = request.params._id || request.payload
if (_.isArray(documents) && documents[0]._id) {
documents = documents.map(function(doc) {
return doc._id
})
} else if (!_.isArray(documents)) {
documents = [documents]
}

await AuditLog.create({
method: 'POST',
action: 'Recover',
endpoint: request.path,
user: userId || null,
collectionName: model.collectionName,
childCollectionName: null,
associationType: null,
documents: documents || null,
payload: _.isEmpty(request.payload) ? null : request.payload,
params: _.isEmpty(request.params) ? null : request.params,
result: request.response.source || null,
isError: _.isError(request.response),
statusCode:
request.response.statusCode || request.response.output.statusCode,
responseMessage: request.response.output
? request.response.output.payload.message
: null,
ipAddress
})
return h.continue
} catch (err) {
Log.error(err)
return h.continue
}
}

logRecoverForModel.applyPoint = 'onPreResponse'
return logRecoverForModel
}
internals.logRecover.applyPoint = 'onPreResponse'

module.exports = {
logRecover: internals.logRecover
}

/**
* Policy to log add actions.
* @param model
Expand Down
49 changes: 48 additions & 1 deletion utilities/handler-helper-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,16 @@ module.exports = function() {
* @returns {Function} A handler function
*/
generateUpdateHandler: generateUpdateHandler,


/**
* Handles incoming RECOVER requests to /RESOURCE/{_id}/recover
* @param model: A mongoose model.
* @param options: Options object.
* @param logger: A logging object.
* @returns {Function} A handler function
*/
generateRecoverHandler: generateRecoverHandler,

/**
* Handles incoming PUT requests to /OWNER_RESOURCE/{ownerId}/CHILD_RESOURCE/{childId}
* @param ownerModel: A mongoose model.
Expand Down Expand Up @@ -248,6 +257,44 @@ function generateUpdateHandler(model, options, logger) {
}
}

/**
* Handles incoming Recover requests to /RESOURCE/{_id}/recover or /RESOURCE/recover
* @param model: A mongoose model.
* @param options: Options object.
* @param logger: A logging object.
* @returns {Function} A handler function
*/
function generateRecoverHandler(model, options, logger) {
const Log = logger.bind()
options = options || {}

return async function(request, h) {
try {
Log.log(
'params(%s), query(%s), payload(%s)',
JSON.stringify(request.params),
JSON.stringify(request.query),
JSON.stringify(request.payload)
)

if (request.params._id) {
await handlerHelper.recoverOneHandler(
model,
request.params._id,
request,
Log
)
} else {
await handlerHelper.recoverManyHandler(model, request, Log)
}

return h.response().code(204)
} catch (err) {
handleError(err, Log)
}
}
}

/**
* Handles incoming DELETE requests to /RESOURCE/{_id} or /RESOURCE
* @param model: A mongoose model.
Expand Down
256 changes: 256 additions & 0 deletions utilities/handler-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ module.exports = {
update: _update,

updateHandler: _updateHandler,

recoverOne: _recoverOne,

recoverOneHandler: _recoverOneHandler,

recoverMany: _recoverMany,

recoverManyHandler: _recoverManyHandler,

deleteOne: _deleteOne,

Expand Down Expand Up @@ -745,6 +753,254 @@ async function _updateHandler(model, _id, request, Log) {
}
}

/**
* Recovers a model document.
* @param {...any} args
* **Positional:**
* - function recoverOne(model, _id, Log)
*
* **Named:**
* - function deleteOne({
prudradeep marked this conversation as resolved.
Show resolved Hide resolved
* model,
* _id,
* Log = RestHapi.getLogger('deleteOne'),
prudradeep marked this conversation as resolved.
Show resolved Hide resolved
* restCall = false,
* credentials
* })
*
* **Params:**
* - model {object | string}: A mongoose model.
* - _id: The document id.
* - Log: A logging object.
* - restCall: If 'true', then will call DELETE /model/{_id}
prudradeep marked this conversation as resolved.
Show resolved Hide resolved
* - credentials: Credentials for accessing the endpoint.
*
* @returns {object} A promise for the resulting model document.
*/
function _recoverOne(...args) {
if (args.length > 1) {
return _recoverOneV1(...args)
} else {
return _recoverOneV2(...args)
}
}

function _recoverOneV1(model, _id, Log) {
model = getModel(model)
const request = { params: { _id: _id } }
return _recoverOneHandler(model, _id, request, Log)
}

async function _recoverOneV2({
model,
_id,
Log,
restCall = false,
credentials
}) {
model = getModel(model)
const RestHapi = require('../rest-hapi')
Log = Log || RestHapi.getLogger('recoverOne')

if (restCall) {
assertServer()
credentials = defaultCreds(credentials)

const request = {
method: 'Post',
url: `/${model.routeOptions.alias || model.modelName}/${_id}/recover`,
params: { _id },
credentials,
headers: { authorization: 'Bearer' }
}

const injectOptions = RestHapi.testHelper.mockInjection(request)
const { result } = await RestHapi.server.inject(injectOptions)
return result
} else {
return _recoverOneV1(model, _id, Log)
}
}

/**
* Recovers a model document
* @param model {object | string}: A mongoose model.
* @param _id: The document id.
* @param request: The Hapi request object.
* @param Log: A logging object.
* @returns {object} A promise returning true if the recover succeeds.
* @private
*/
async function _recoverOneHandler(model, _id, request, Log) {
try {
try {
if (
model.routeOptions &&
model.routeOptions.recover &&
model.routeOptions.recover.pre
) {
await model.routeOptions.recover.pre(_id, request, Log)
}
} catch (err) {
handleError(
err,
'There was a preprocessing error recovering the resource.',
Boom.badRequest,
Log
)
}
let recovered

try {
const payload = { $set: {isDeleted: false}, $unset: {deletedAt: ""} }
prudradeep marked this conversation as resolved.
Show resolved Hide resolved
recovered = await model.findByIdAndUpdate(_id, payload, {
new: true,
runValidators: config.enableMongooseRunValidators
})
} catch (err) {
handleError(
err,
'There was an error recovering the resource.',
Boom.badImplementation,
Log
)
}
// TODO: clean up associations/set rules for ON DELETE CASCADE/etc.
if (recovered) {
// TODO: add eventLogs

try {
if (
model.routeOptions &&
model.routeOptions.recover &&
model.routeOptions.recover.post
) {
await model.routeOptions.recover.post(
recovered,
request,
Log
)
}
} catch (err) {
handleError(
err,
'There was a postprocessing error recovering 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)
}
}

/**
* Recovers multiple documents.
* @param {...any} args
* **Positional:**
* - function recoverMany(model, payload, Log)
*
* **Named:**
* - function recoverMany({
* 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.
* - Log: A logging object.
* - restCall: If 'true', then will call POST /model
prudradeep marked this conversation as resolved.
Show resolved Hide resolved
* - credentials: Credentials for accessing the endpoint.
*
* @returns {object} A promise for the resulting model document.
*/
function _recoverMany(...args) {
if (args.length > 1) {
return _recoverManyV1(...args)
} else {
return _recoverManyV2(...args)
}
}

function _recoverManyV1(model, payload, Log) {
model = getModel(model)
const request = { payload: payload }
return _recoverManyHandler(model, request, Log)
}

async function _recoverManyV2({
model,
payload,
Log,
restCall = false,
credentials
}) {
model = getModel(model)
const RestHapi = require('../rest-hapi')
Log = Log || RestHapi.getLogger('recoverOne')

if (restCall) {
assertServer()
credentials = defaultCreds(credentials)

const request = {
method: 'Post',
url: `/${model.routeOptions.alias || model.modelName}/recover`,
payload,
credentials,
headers: { authorization: 'Bearer' }
}

const injectOptions = RestHapi.testHelper.mockInjection(request)
const { result } = await RestHapi.server.inject(injectOptions)
return result
} else {
return _recoverManyV1(model, payload, Log)
}
}

/**
* Recovers 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 recover 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 _recoverManyHandler(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(_recoverOneHandler(model, arg, request, Log))
} else {
promises.push(
_recoverOneHandler(model, arg._id, request, Log)
)
}
}

await Promise.all(promises)
return true
} catch (err) {
handleError(err, null, null, Log)
}
}

/**
* Deletes a model document.
* @param {...any} args
Expand Down
Loading