bookshelf-virtuals-plugin
Version:
A plugin for Bookshelf that allows getting or setting virtual properties on model instances.
177 lines (153 loc) • 6.25 kB
JavaScript
const _ = require('lodash')
function getVirtual(model, virtualName, ...virtuals) {
if (model.virtuals && typeof model.virtuals === 'object' && model.virtuals[virtualName]) {
return model.virtuals[virtualName].get
? model.virtuals[virtualName].get.apply(model, virtuals)
: model.virtuals[virtualName].apply(model, virtuals)
}
}
function getVirtuals(model, params) {
const attrs = {}
if (model.virtuals != null) {
for (const virtualName in model.virtuals) {
const paramsForVirtual = typeof params === 'object' && params !== null ? params[virtualName] : undefined
attrs[virtualName] = getVirtual(model, virtualName, paramsForVirtual)
}
}
return attrs
}
function setVirtual(value, key) {
const virtual = this.virtuals && this.virtuals[key]
if (virtual) {
if (virtual.set) virtual.set.call(this, value)
return true
}
}
// Virtuals Plugin
// Allows getting/setting virtual (computed) properties on model instances.
// -----
module.exports = (bookshelf) => {
const proto = bookshelf.Model.prototype
const Model = bookshelf.Model.extend({
outputVirtuals: true,
// If virtual properties have been defined they will be created
// as simple getters on the model.
constructor(attributes, options) {
proto.constructor.apply(this, arguments)
const virtuals = this.virtuals
if (virtuals && typeof virtuals === 'object') {
for (const virtualName in virtuals) {
let getter, setter
if (virtuals[virtualName].get) {
getter = virtuals[virtualName].get
setter = virtuals[virtualName].set ? virtuals[virtualName].set : undefined
} else {
getter = virtuals[virtualName]
}
Object.defineProperty(this, virtualName, {
enumerable: true,
get: getter,
set: setter
})
}
}
},
// Passing `{virtuals: true}` or `{virtuals: false}` in the `options`
// controls including virtuals on function-level and overrides the
// model-level setting
toJSON(options) {
let attrs = proto.toJSON.call(this, options)
if (options && options.omitNew && this.isNew()) {
return attrs
}
if (!options || options.virtuals !== false) {
if ((options && options.virtuals === true) || this.outputVirtuals) {
attrs = Object.assign(attrs, getVirtuals(this, options && options.virtualParams))
}
}
return attrs
},
// Allow virtuals to be fetched like normal properties
get(attr) {
if (this.virtuals && typeof this.virtuals === 'object' && this.virtuals[attr]) {
return getVirtual.apply(undefined, [this, attr].concat(Array.from(arguments).slice(1)))
}
return proto.get.apply(this, arguments)
},
// Allow virtuals to be set like normal properties
set(key, value, options) {
if (!key) return this
// Determine whether we're in the middle of a patch operation based on the
// existence of the `patchAttributes` object.
const isPatching = this.patchAttributes != null
// Handle `{key: value}` style arguments.
if (key && typeof key === 'object') {
const nonVirtuals = _.omitBy(key, setVirtual.bind(this))
if (isPatching) {
Object.assign(this.patchAttributes, nonVirtuals)
}
// Set the non-virtual attributes as normal.
return proto.set.call(this, nonVirtuals, options)
}
// Handle `"key", value` style arguments for virtual setter.
if (setVirtual.call(this, value, key)) {
return this
}
// Handle `"key", value` style assignment call to be added to patching
// attributes if set("key", value, ...) called from inside a virtual setter.
if (isPatching) {
this.patchAttributes[key] = value
}
return proto.set.apply(this, arguments)
},
// Override `save` to keep track of state while doing a `patch` operation.
save(key, value, options) {
let attrs = {}
// Handle both `"key", value` and `{key: value}` -style arguments.
if (key == null || typeof key === 'object') {
attrs = key && _.clone(key)
options = _.clone(value) || {}
} else {
attrs[key] = value
options = options ? _.clone(options) : {}
}
// Determine whether to save using update or insert
options.method = this.saveMethod(options)
// Check if we're going to do a patch, in which case deal with virtuals now.
if (options.method === 'update' && options.patch) {
// Extend the model state to collect side effects from the virtual setter
// callback. If `set` is called, this object will be updated in addition
// to the normal `attributes` object.
this.patchAttributes = {}
// Any setter could throw. We need to reject `save` if they do.
try {
// Check if any of the patch attributes are virtuals. If so call their setter. Any setter that calls
// `this.set` will be modifying `this.patchAttributes` instead of `this.attributes`.
for (const attributeName in attrs) {
if (setVirtual.call(this, attrs[attributeName], attributeName)) {
// This was a virtual, so remove it from the attributes to be passed to `Model.save`.
delete attrs[attributeName]
}
}
// Now add any changes that occurred during the update.
Object.assign(attrs, this.patchAttributes)
} catch (e) {
return Promise.reject(e)
} finally {
// Delete the temporary object.
delete this.patchAttributes
}
}
return proto.save.call(this, attrs, options)
}
})
// Lodash methods that we want to implement on the Model.
const modelMethods = ['keys', 'values', 'toPairs', 'invert', 'pick', 'omit']
// Mix in each Lodash method as a proxy to `Model#attributes`.
modelMethods.forEach((method) => {
Model.prototype[method] = function () {
return _[method].apply(_, [Object.assign({}, this.attributes, getVirtuals(this))].concat(Array.from(arguments)))
}
})
bookshelf.Model = Model
}