server/models/abstract.js

const _ = require('lodash');
const db = require('../db');
const inflection = require('inflection');

const proto = db.bookshelf.Model.prototype;

/**
 * Abstract database model.
 *
 * @class
 * @extends bookshelf.Model
 */
const Abstract = db.bookshelf.Model.extend({

  /**
   * Parses data from the specified source into this record's columns.
   *
   *     // The following code:
   *     record.parseFrom(data, [ 'name', 'siteUrl' ]);
   *
   *     // Is equivalent to:
   *     if (data.hasOwnProperty('name')) {
   *       record.set('name', data.name);
   *     }
   *     if (data.hasOwnProperty('siteUrl')) {
   *       record.set('site_url', data.siteUrl);
   *     }
   *
   *     // The following code:
   *     record.parseFrom(data, [ 'street', 'zipCode' ], { columnPrefix: 'address_', sourcePrefix: 'address.' });
   *
   *     // Is equivalent to:
   *     if (data.hasOwnProperty('address') && data.address.hasOwnProperty('street')) {
   *       record.set('address_street', data.address.street);
   *     }
   *     if (data.hasOwnProperty('address') && data.address.hasOwnProperty('zipCode')) {
   *       record.set('address_zip_code', data.address.zipCode);
   *     }
   *
   * @method
   * @memberof Abstract
   * @instance
   * @param {object} source - Source object (typically the parsed JSON request body).
   * @param {string[]} properties - Camel-cased properties of the source object to parse.
   *   The column names will correspond to the underscored names of the properties (e.g. `zipCode` => `zip_code`).
   * @param {object} [options] - Deserialization options.
   * @param {string} [options.columnPrefix] - Prefix to prepend to column names.
   *   For example, if the prefix is `address_` and one of the properties to parse is `zipCode`, the column
   *   in which the value is stored will be `address_zip_code`.
   * @param {string} [options.sourcePrefix] - Prefix to prepend to property names before extracting them from the
   *   source object. For example, if the prefix is `address.` and one of the properties to parse is `zipCode`, the
   *   `zipCode` property of the source's object `address` sub-object will be extracted.
   * @returns {Model} This record.
   */
  parseFrom: function(source, properties, options = {}) {

    const columnPrefix = options.columnPrefix || '';
    const sourcePrefix = options.sourcePrefix || '';

    for (let property of properties) {

      // Determine the complete source property name, e.g. `zipCode` or `address.zipCode` (with the "sourcePrefix" option).
      const sourceProperty = `${sourcePrefix}${property}`;

      // Only parse the property if it's in the source object.
      if (_.has(source, sourceProperty)) {
        // Determine the column name by underscoring the property name and prepending the column prefix (if any).
        const column = `${columnPrefix}${inflection.underscore(property)}`;
        this.set(column, _.get(source, sourceProperty));
      }
    }

    return this;
  },

  /**
   * Serializes columns of this record into the specified target object.
   *
   *     // The following code:
   *     record.serializeTo(target, [ 'name', 'site_url' ]);
   *
   *     // Is equivalent to:
   *     if (record.has('name')) {
   *       target.name = record.get('name');
   *     }
   *     if (record.has('site_url')) {
   *       target.siteUrl = record.get('site_url');
   *     }
   *
   *     // The following code:
   *     record.serializeTo(target, [ 'street', 'zip_code' ], { columnPrefix: 'address_', targetPrefix: 'address.' });
   *
   *     // Is equivalent to:
   *     if (!target.hasOwnProperty('address')) {
   *       target.address = {};
   *     }
   *     if (record.has('address_street')) {
   *       target.address.street = record.get('address_street');
   *     }
   *     if (record.has('address_zip_code')) {
   *       target.address.zipCode = record.get('address_zip_code');
   *     }
   *
   * @method
   * @memberOf Abstract
   * @instance
   * @param {object} target - Target object to attach serialized properties to.
   * @param {string[]} properties - Underscored column names of the record to serialize.
   *   The target property names will correspond to the camelized names of the columns (e.g. `zip_code` => `zipCode`).
   * @param {object} [options] - Serialization options.
   * @param {string} [options.columnPrefix] - Prefix to prepend to the column names to serialize. For example, if the
   *   prefix is `address` and one of the columns to serialize is `zip_code`, the column from which the value is
   *   extracted will be `address_zip_code`.
   * @param {string} [options.targetPrefix] - Prefix to prepend to the property names of the target object. For
   *   example, if the target prefix is `address.` and one of the columns to serialize is `zip_code`, the `zipCode`
   *   property of the target object's `address` sub-object will be set to the value of the column.
   * @returns {object} The target object.
   */
  serializeTo: function(target, properties, options = {}) {

    const columnPrefix = options.columnPrefix || '';
    const targetPrefix = options.targetPrefix || '';

    for (let property of properties) {
      const column = `${columnPrefix}${property}`;
      if (this.has(column)) {
        const columnWithoutPrefix = column.slice(columnPrefix.length);
        const targetProperty = `${targetPrefix}${inflection.camelize(columnWithoutPrefix, true)}`;
        _.set(target, targetProperty, this.get(column));
      }
    }

    return target;
  }
});

module.exports = Abstract;