UNPKG

schwifty

Version:

A hapi plugin integrating Objection ORM

248 lines (181 loc) 7.23 kB
'use strict'; const Objection = require('objection'); const Helpers = require('./helpers'); const internals = {}; module.exports = class SchwiftyModel extends Objection.Model { static createValidator() { return new internals.Validator(); } // Used by objection for bindKnex() caching static uniqueTag(...args) { if (!this.hasOwnProperty('$$schwiftyUniqueId')) { Helpers.setNonEnumerableProperty( this, '$$schwiftyUniqueId', ++SchwiftyModel.$$schwiftyUniqueCounter ); } // Typically "{table-name}_{model-name}" let uniqueTag = super.uniqueTag(...args); if (Helpers.getSandbox(this)) { // We need to provide an id because it's possible that a sandboxed // model might have the same model name and table name (i.e. same unique tag) // as another model on the server. That works great in schwifty/hapi, but // does not work nicely with objection's bindKnex() cache. We additionally // copy the same $$schwiftyUniqueId to the bound model (within bindKnex() and // bindTransaction()) so that bound models remain indistiguishable from unbound models. uniqueTag += `_id:${this.$$schwiftyUniqueId}`; } return uniqueTag; } static bindKnex(...args) { const BoundModel = super.bindKnex(...args); return internals.copyProperties(this, BoundModel); } static bindTransaction(...args) { const BoundModel = super.bindTransaction(...args); return internals.copyProperties(this, BoundModel); } // Caches schema, with and without optional keys // Will create $$joiSchema and $$joiSchemaPatch properties static getJoiSchema(patch) { if (!this.hasOwnProperty('$$joiSchema')) { Helpers.setNonEnumerableProperty( this, '$$joiSchema', this.joiSchema ); } const schema = this.$$joiSchema; if (patch) { if (!this.hasOwnProperty('$$joiSchemaPatch')) { Helpers.setNonEnumerableProperty( this, '$$joiSchemaPatch', internals.patchSchema(schema) ); } return this.$$joiSchemaPatch; } return schema; } static field(name) { const fullSchema = this.getJoiSchema().extract(name); return fullSchema .optional() .prefs({ noDefaults: true }) .alter({ full: () => fullSchema, patch: (schema) => schema }); } // Applies default jsonAttributes based upon joiSchema, // otherwise fallsback to however jsonAttributes has been set static get jsonAttributes() { // Once it's set, never recompute from joiSchema if (this.hasOwnProperty('$$schwiftyJsonAttributes')) { return this.$$schwiftyJsonAttributes; } const joiSchema = this.getJoiSchema(); if (!joiSchema) { return null; } const schemaKeyDescs = joiSchema.describe().keys || {}; // Will set the memo, see the jsonAttributes setter this.jsonAttributes = Object.keys(schemaKeyDescs).filter((field) => { const type = schemaKeyDescs[field].type; // These are the joi types we want to be parsed/serialized as json return (type === 'array') || (type === 'object'); }); // Yes, this will re-enter the getter, but it's // guaranteed not to loop because the memo is set return this.jsonAttributes; } // This is a necessity because jsonAttributes must // remain settable for objection's base Model class. // Behold. static set jsonAttributes(value) { Helpers.setNonEnumerableProperty( this, '$$schwiftyJsonAttributes', value ); } }; Helpers.setNonEnumerableProperty(module.exports, '$$schwiftyUniqueCounter', 0); internals.Validator = class SchwiftyValidator extends Objection.Validator { beforeValidate(args) { const json = args.json; const model = args.model; const options = args.options; const ctx = args.ctx; ctx.joiSchema = model.constructor.getJoiSchema(options.patch); if (model.$beforeValidate !== Objection.Model.prototype.$beforeValidate) { ctx.joiSchema = model.$beforeValidate(ctx.joiSchema, json, options); } } validate(args) { const json = args.json; const model = args.model; const ctx = args.ctx; if (!ctx.joiSchema) { return json; } const validation = ctx.joiSchema.validate(json); if (validation.error) { throw internals.parseJoiValidationError(validation, model.constructor); } return validation.value; } }; internals.patchSchema = (schema) => { if (!schema) { return; } const keys = Object.keys(schema.describe().keys || {}); // Make all keys optional, do not enforce defaults if (keys.length) { schema = schema.fork(keys, (s) => s.optional()); } return schema.prefs({ noDefaults: true }); }; // Converts a Joi error object to the format the Object.ValidationError constructor expects as input // https://github.com/Vincit/objection.js/blob/aa3f1a0bb830211e478aa6a664561155c98850f4/lib/model/ValidationError.js#L10 internals.parseJoiValidationError = (validation, Model) => { const errors = validation.error.details; const validationInfo = { data: {}, type: 'ModelValidation' }; // We don't set a message, as Objection will build an error message from the message properties of // values within the data property of the ValidationError constructor's input for (let i = 0; i < errors.length; ++i) { const error = errors[i]; validationInfo.data[error.path] = validationInfo.data[error.path] || []; validationInfo.data[error.path].push({ // Format matches data property documented here: http://vincit.github.io/objection.js/#validationerror message: error.message, keyword: error.type, params: error.context }); } // inherited standard method on Objection models (http://vincit.github.io/objection.js/#createvalidationerror) // just handles creating a standard ValidationError return Model.createValidationError(validationInfo); }; internals.copyProperties = (SourceModel, TargetModel) => { if (!TargetModel.hasOwnProperty('$$schwiftyBound')) { internals.hoistModelProperties.forEach((property) => { Helpers.copyDescriptor(property, SourceModel, TargetModel); }); Helpers.setNonEnumerableProperty(TargetModel, '$$schwiftyBound', true); } return TargetModel; }; internals.hoistModelProperties = [ Helpers.symbols.sandbox, '$$schwiftyUniqueId', '$$joiSchema', '$$joiSchemaPatch', '$$schwiftyJsonAttributes' ];