punchcard-cms/punchcard

View on GitHub
lib/utils.js

Summary

Maintainability
C
1 day
Test Coverage
'use strict';

const util = require('util');
const moment = require('moment-timezone');
const content = require('./content/utils');
const routes = require('./routes/utils');
const _ = require('lodash');
const Promise = require('bluebird');

moment.tz.setDefault('UTC');

/*
 * Filters an array of objects based on the value of a key in the objects
 *
 * @param {string} key - The key in the object to check
 * @param {string} value - The value to check against
 * @param {array} arr - An array of {Object}s
 *
 * @returns {object|boolean} - Will return the first filtered object, or `false` if no objects match
 */
const singleItem = (key, value, arr) => {
  const filtered = arr.filter(type => {
    if (type[key] === value) {
      return true;
    }

    return false;
  });

  if (filtered.length === 0) {
    return false;
  }

  return filtered[0];
};

/*
 * Logs out full object
 *
 * @param {object|string} object - The object to be logged
 */
/* istanbul ignore next */
const log = (object) => {
  // Mean to console.log out, so disabling
  console.log(util.inspect(object, false, null)); // eslint-disable-line no-console
};

/*
 * Formats string date components in to ISO date
 *
 * @param {string} date - The date to be transformed
 * @param {string} time - The time to be transformed
 * @param {string} zone - The timezone the date and time are in (e.g. America/New_York)
 *
 * @returns {string} - An ISO formatted date in GMT
 */
const isoTime = (date, time, zone) => {
  if (date === '' || date === undefined || time === '' || time === undefined || zone === '' || zone === undefined) {
    return null;
  }

  const converted = moment.tz(`${date} ${time}`, zone);

  return converted.toISOString();
};

/*
 * @typedef FormattedDate
 * @type object
 *
 * @property {string} date - The date, formatted YYYY-MM-DD (e.g. 2016-05-25)
 * @property {string} time - The time, formatted HH:mm (e.g. 13:01)
 * @property {string} zone - The time zone, formatted Z (e.g. America/New_York)
 */

/*
 * Formats ISO date in to requisite components
 *
 * @param {string} date - ISO UTC formatted date
 * @param {string} zone - Timezone to retrieve date in
 *
 * @returns {FormattedDate} - The individually formatted date components
 */
const inputTime = (date, zone) => {
  let tz = zone;

  if (date === '' || date === undefined || date === null) {
    return null;
  }

  if (tz === undefined || tz === null) {
    tz = 'UTC';
  }

  // America/New_York is hard coded throughout the system right now, need to format correctly
  return {
    date: moment(date).tz('America/New_York').format('YYYY-MM-DD'),
    time: moment(date).tz('America/New_York').format('HH:mm'),
    zone: 'America/New_York',
  };
};

const time = {
  iso: isoTime,
  input: inputTime,
};


/*
 * Generate Config Object for Only
 */
const config = values => {
  const result = {};

  Object.keys(values).map(value => {
    const split = value.split('--');
    const plugin = split[0];
    const input = split[1];

    // Repeat
    // const index = split[2];

    if (!result.hasOwnProperty(plugin)) {
      result[plugin] = {};
    }

    result[plugin][input] = {
      value: values[value],
    };
  });

  return result;
};

/*
 * @typedef RequestBody
 * @type object
 *
 * @property {string | array} input - inputName--inputType : value
 */

/*
 * @typedef FormattedBody
 * @type object
 *
 * @property {string} FormattedBody.name - Name of the input
 * @property {object | array} FormattedBody.name.type - Type of the input
 * @property {string} FormattedBody.name.type.value - Value of the input
 */

/*
 * Formats raw data from form to object
 *
 * @param {RequestBody} body - inputName--inputType: value
 *
 *
 * @returns {object} - formatted object of data
 */
const format = body => {
  const data = {};
  const single = Object.keys(body).filter(input => {
    if ((input === 'sunset-date') || (input === 'sunset-time') || (input === 'sunrise-date') || (input === 'sunrise-time')) {
      data[input] = { 'value': body[input] };

      return false;
    }
    if (input.split('--').length < 3) {
      return true;
    }

    return false;
  });
  const multiple = Object.keys(body).filter(input => {
    if (input.split('--').length >= 3) {
      return true;
    }

    return false;
  });

  single.forEach(input => {
    const inputs = input.split('--');
    const inputName = inputs[0];
    const inputType = inputs[1];
    if (!data.hasOwnProperty(inputName)) {
      // Creates an object for single instance
      data[inputName] = {};
    }
    data[inputName][inputType] = { 'value': body[input] };
  });

  multiple.forEach(input => {
    const inputs = input.split('--');
    const inputName = inputs[0];
    const inputType = inputs[1];
    const index = inputs[2];
    if (!data.hasOwnProperty(inputName)) {
      // Creates an array for multiple instances
      data[inputName] = [];
    }
    if (!data[inputName][index]) {
      // Create an object for instance
      data[inputName][index] = {};
    }

    // Sets value of input
    data[inputName][index][inputType] = { 'value': body[input] };
  });

  // Deletes the empty instances
  Object.keys(data).forEach(key => {
    if (Array.isArray(data[key])) {
      data[key] = data[key].filter(input => {
        return Object.keys(input).filter(type => {
          if (input[type].value !== null && input[type].value !== '' && input[type].value !== []) {
            return true;
          }

          return false;
        }).length > 0;
      });

      // Delete empty arrays
      if (data[key].length === 0) {
        delete data[key];
      }
    }
  });

  return data;
};

/*
 * @typedef Reference
 * @type object
 *
 * @property {type} Reference.id - Id of the content type
 * @property {attr} Reference.attr - Attribute Index
 * @property {input} Reference.input - Input of the reference
 * @property {length} Reference.length - Number of instances
 * @property {ct.id} Reference.ct.id - Id of the referenced content type
 * @property {ct.index} Reference.ct.index - Position of the reference content type
 */

/*
 * Inititalizes reference
 *
 * @param {object} types - content types object
 *
 *
 * * @returns {object} - content type and reference data
 */
const references = (types) => {
  const cts = _.cloneDeep(types);
  const refs = [];
  const position = {};

  // Saves position of each content type
  cts.forEach((type, index) => {
    if (!position.hasOwnProperty(type.id)) {
      position[type.id] = index;
    }
  });

  // Iterates through each content type and attributes
  cts.forEach(ct => {
    ct.attributes.forEach((attr, index) => {
      let inputs;
      let length;

      // Get the number of instances if repeatable
      if (Array.isArray(attr.inputs)) {
        inputs = attr.inputs[0];
        length = attr.inputs.length;
      }
      else {
        inputs = attr.inputs;
      }

      // Iterate through each inputs
      Object.keys(inputs).forEach(input => {
        // Checks if input is a reference
        if (inputs[input].hasOwnProperty('reference') && inputs[input].reference === true) {
          // Throw error if contentType is not defined
          if (!_.get(inputs[input], 'settings.contentType')) {
            throw new Error('Reference must have a content type');
          }

          if (!position.hasOwnProperty(inputs[input].settings.contentType)) {
            throw new Error(`Content Type ${inputs[input].settings.contentType} is not valid`);
          }

          if (_.get(inputs[input], 'settings.view') === 'radio') {
            inputs[input].type = 'radio';
          }

          // Defines and push reference object
          refs.push({
            type: ct.id,
            attr: index,
            input,
            length,
            ct: inputs[input].settings.contentType,
          });
        }
      });
    });
  });

  return {
    cts,
    references: refs,
  };
};


/*
 * Fills in content type data from database
 *
 * @param {object} types - content types object
 * @param {object} type - content type
 * @param {array} refs - reference data
 * @param {object} database - database object
 *
 *
 * @returns {object} - content type object with reference data
 */
const fill = (types, type, refs, database) => {
  const ct = _.cloneDeep(type);

  if (!ct) {
    return new Promise(resolve => {
      resolve(ct);
    });
  }
  const ref = refs.filter(reference => {
    return ct.id === reference.type;
  });

  return Promise.map(ref, input => {
    // subquery for select
    const where = database
          .max('revision')
          .from(`content-type--${input.ct}`)
          .groupBy('id');

    // Get the records from content-type
    return database
      .select('id', 'revision', 'value')
      .from(`content-type--${input.ct}`)
      .whereIn('revision', where).then(rws => {
        // stores referred type
        const refCT = types.find((val) => {
          return val.id === input.ct;
        });
        const rows = routes.identifier(rws, refCT);
        const options = rows.map(row => {
          return {
            value: row.id,
            label: row.identifier,
          };
        });

        // If repeatables set options for each instance
        if (input.length) {
          for (let i = 0; i < input.length; i++) {
            const inp = ct.attributes[input.attr].inputs[i][input.input];

            if (inp.type === 'select') {
              options.unshift({
                value: '',
                label: inp.settings.placeholder,
              });
            }
            inp.options = options;
          }
        }
        else {
          const inp = ct.attributes[input.attr].inputs[input.input];

          if (inp.type === 'select') {
            options.unshift({
              value: '',
              label: inp.settings.placeholder,
            });
          }
          inp.options = options;
        }
      });
  }).then(() => {
    return ct;
  });
};

module.exports = {
  content,
  routes,
  singleItem,
  log,
  time,
  config,
  format,
  fill,
  references,
};