src/Req.js
import { cloneDeep, isPlainObject } from 'lodash'
import assignDeep from 'assign-deep'
import defaultsDeep from '@nodeutils/defaults-deep'
import { deleteUndefineds } from './utils'
/**
* A wrapper for the request object with helper methods and access to Autonym model data.
*/
export default class Req {
/**
* @param {http.IncomingMessage} raw The raw IncomingMessage object.
* @param {Model} model The Autonym model instance.
* @param {Meta} meta The meta object aggregated by policies during the request.
* @example
* const req = new AutonymReq(request, Post, meta)
*/
constructor(raw, model, meta) {
raw.autonym = this
raw.autonymMeta = meta
this._raw = raw
this._model = model
this._data = this.hasBody() ? cloneDeep(raw.body) : null
this._originalData = null
this._isValidated = false
}
/**
* Gets the raw request.
* @returns {http.IncomingMessage} The raw request.
*/
getRaw() {
return this._raw
}
/**
* Gets the request payload.
* @returns {Record} The data.
* @throws {ReferenceError} If the request does not have a body.
*/
getData() {
if (!this.hasBody()) {
throw new ReferenceError('Cannot get request data from a request without a body.')
}
return this._data
}
/**
* Merges the request data with the given data, without modifying the original request.
* @param {Record} data The new properties to set.
* @param {boolean} [replace] If true, replaces the data on the response instead of merging it.
* @returns {void}
* @throws {ReferenceError} If the request does not have a body.
* @example
* console.log(req.getData()) // { title: 'Hello World' }
* req.setData({ name: 'Test' })
* console.log(req.getData()) // { name: 'Test', title: 'Hello World' }
* @example
* console.log(req.getData()) // { title: 'Hello World' }
* req.setData({ name: 'Test' }, true)
* console.log(req.getData()) // { name: 'Test' }
*/
setData(data, replace = false) {
if (!isPlainObject(data)) {
throw new TypeError('The data must be a plain object.')
}
if (!this.hasBody()) {
throw new ReferenceError('Cannot set request data on a request without a body.')
}
if (replace) {
this._data = data
} else {
assignDeep(this._data, data)
}
deleteUndefineds(this._data)
}
/**
* Gets the data that was originally on the request body.
* @returns {Record} The data.
* @throws {ReferenceError} If the request does not have a body.
*/
getRequestData() {
if (!this.hasBody()) {
throw new ReferenceError('Cannot get request data from a request without a body.')
}
return this.getRaw().body
}
/**
* For update queries, gets the data of the original record to update. For create queries, gets an empty object.
* @returns {Promise.<Record, AutonymError>} The original record data.
* @throws {ReferenceError} If the request is not a create or update request.
* @example
* console.log(req.getData()) // { title: 'Test' }
* const originalData = await req.getOriginalData()
* console.log(originalData) // { title: 'Hello World', body: 'This is my first post.' }
*/
async getOriginalData() {
if (!this.isWriting()) {
throw new ReferenceError('Cannot get original data on a request without a body.')
}
if (!this._originalData) {
this._originalData = this.isCreating() ? {} : this.getModel().findOne(this.getId())
}
return this._originalData
}
/**
* Gets the result of merging the original data (see `#getOriginalData`) with the request data.
* @returns {Promise.<Record, AutonymError>} The merged data.
* @example
* console.log(req.getData()) // { title: 'Test' }
* const originalData = await req.getCompleteData()
* console.log(originalData) // { title: 'Test', body: 'This is my first post.' }
*/
async getCompleteData() {
const originalData = await this.getOriginalData()
return defaultsDeep(this.getData(), originalData)
}
/**
* Gets the model instance.
* @returns {Model} The model.
*/
getModel() {
return this._model
}
/**
* Gets the request query.
* @returns {object} The query.
*/
getQuery() {
return this.getRaw().query
}
/**
* Gets the requested record id.
* @returns {string} The record id.
* @throws {ReferenceError} If it is a create or find request.
*/
getId() {
if (!this.hasId()) {
throw new ReferenceError('Cannot get id of request that creates or finds multiple.')
}
return this.getRaw().params.id
}
/**
* Gets the given header.
* @param {string} header The header to find.
* @returns {string|undefined} The header value.
*/
getHeader(header) {
return this.getRaw().get(header)
}
/**
* Whether this step is occurring with safe data, i.e. the data has been validated, filtered, and populated with
* defaults.
* @returns {boolean} True if it has passed the preSchema and validateAgainstSchema steps.
*/
isValidated() {
return this._isValidated
}
/**
* Whether this is a create request.
* @returns {boolean} True if it is a create request.
*/
isCreating() {
return this.getRaw().method === 'POST'
}
/**
* Whether this is a find request.
* @returns {boolean} True if it is a find request.
*/
isFinding() {
return this.getRaw().method === 'GET' && !this.getRaw().params.id
}
/**
* Whether this is a findOne request.
* @returns {boolean} True if it is a findOne request.
*/
isFindingOne() {
return this.getRaw().method === 'GET' && this.getRaw().params.id
}
/**
* Whether this is a findOneAndUpdate request.
* @returns {boolean} True if it is a findOneAndUpdate request.
*/
isFindingOneAndUpdating() {
return this.getRaw().method === 'PATCH' || this.getRaw().method === 'PUT'
}
/**
* Whether this is a findOneAndDelete request.
* @returns {boolean} True if it is a findOneAndDelete request.
*/
isFindingOneAndDeleting() {
return this.getRaw().method === 'DELETE'
}
/**
* Whether this is a readonly request to fetch data.
* @returns {boolean} True if it is a find or findOne request.
*/
isGetting() {
return this.isFinding() || this.isFindingOne()
}
/**
* Whether this is a request that will return response data.
* @returns {boolean} True if it is a create, find, findOne, or findOneAndUpdate request.
*/
isReading() {
return !this.isFindingOneAndDeleting()
}
/**
* Whether this request has a body.
* @returns {boolean} True if it is a create or findOneAndUpdate request.
*/
hasBody() {
return this.isCreating() || this.isFindingOneAndUpdating()
}
/**
* Whether this is a request that will modify the data store.
* @returns {boolean} True if it is a create, findOneAndUpdate, or findOneAndDelete request.
*/
isWriting() {
return this.hasBody() || this.isFindingOneAndDeleting()
}
/**
* Whether this is a request for a particular record.
* @returns {boolean} True if it is a findOne, findOneAndUpdate, or findOneAndDelete request.
*/
hasId() {
return this.isFindingOne() || this.isFindingOneAndUpdating() || this.isFindingOneAndDeleting()
}
}