punchcard-cms/punchcard

View on GitHub
lib/routes/content.js

Summary

Maintainability
F
4 days
Test Coverage
'use strict';

/*
 * @fileoverview Content system routing
 *
 */
const config = require('config');
const types = require('punchcard-content-types');
const uuid = require('uuid');
const _ = require('lodash');
const path = require('path');
const isUUID = require('validator/lib/isUUID');

const utils = require('../utils');
const cutils = require('../content/utils');
const database = require('../database');
const workflows = require('../workflows');
const schedule = require('../schedule');
const storage = require('../storage');

/*
 * Content Route Resolution
 *
 * @param {object} application - Express Application
 * @returns {object} - Configured Express Application
 */
const routes = application => {
  return new Promise(resolve => {
    const app = application;
    const references = app.get('references');

    /*
     * @name Content Home Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
    */
    app.get(`/${config.content.base}`, (req, res) => {
      res.render('content/home', {
        content: {
          home: config.content.home,
          base: config.content.base,
          types: req.content.types,
        },
      });
    });

    /*
     * @name Individual Content Type Landing Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type`, (req, res) => {
      return database
        .distinct(database.raw('ON (id) id'))
        .select('*')
        .from(`content-type--${req.content.type.id}`)
        .orderBy('id', 'DESC')
        .orderBy('revision', 'DESC').then(rws => {
          // add itentifier to each row
          const rows = utils.routes.identifier(rws, req.content.type);

          res.render('content/landing', {
            title: config.content.base,
            content: rows,
            type: req.content.type,
            config: config.content,
          });
        });
    });

    /*
     * @name Individual Content Type Add Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/${config.content.actions.add}`, (req, res, next) => {
      const errors = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].errors`, {});
      const files = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].files`, {});
      let values = _.get(req.session, `form.content.add[${req.params.type.toLowerCase()}].content`, {});

      _.unset(req.session, 'form.content.add.errors');
      _.unset(req.session, 'form.content.add.content');

      const steps = _.cloneDeep(req.content.workflow).steps.reverse();
      const step = steps[steps.length - 1];

      // inject files into values
      values = cutils.filehold(files, values, req.content.type.attributes);

      return utils.fill(req.content.types, req.content.type, references, database).then(ct => {
        return types.only(req.params.type.toLowerCase(), values, [ct], config).then(merged => {
          return types.form(merged, errors, config).then(form => {
            res.render('content/add', {
              form,
              action: `/${config.content.base}/${req.params.type.toLowerCase()}/${config.content.actions.save}`,
              type: ct,
              config: config.content,
              step,
            });
          });
        }).catch(e => {
          next(e);
        });
      });
    });

    /*
     * @name Individual Piece of Content
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/:id`, (req, res, next) => {
      return database
        .select('*')
        .from(`content-type--${req.content.type.id}`)
        .where('id', req.params.id)
        .orderBy('revision', 'DESC').then(rws => {
          if (rws.length < 1) {
            const err = {
              message: config.content.messages.missing.id.replace('%type', req.params.type).replace('%id', req.params.id),
              safe: `/${config.content.base}/${req.params.type}`,
              status: 404,
            };

            return next(err);
          }

          // add itentifier to each row
          const rows = utils.routes.identifier(rws, req.content.type);

          res.render('content/content', {
            title: config.content.messages.content.title.replace('%id', req.params.id),
            content: rows,
            type: req.content.type,
            workflow: _.cloneDeep(req.content.workflow).steps.reverse(),
            config: config.content,
          });

          return true;
        });
    });

    /*
     * @name Specific revision of an Individual Piece of Content
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/:id/:revision`, (req, res, next) => {
      return database
        .select('*')
        .from(`content-type--${req.content.type.id}`)
        .where('revision', req.params.revision)
        .orderBy('revision', 'DESC').then(rws => {
          if (rws.length < 1) {
            const err = {
              message: config.content.messages.missing.revision.replace('%revision', req.params.revision).replace('%type', req.params.type).replace('%id', req.params.id),
              safe: `/${config.content.base}/${req.params.type}`,
              status: 404,
            };

            return next(err);
          }

          // add itentifier to each row
          const rows = utils.routes.identifier(rws, req.content.type);

          res.render('content/content', {
            title: config.content.messages.revisions.title.replace('%revision', req.params.revision).replace('%id', req.params.id),
            content: rows,
            type: req.content.type,
            config: config.content,
          });

          return true;
        });
    });

    /*
     * @name Individual Content Type Edit Page
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.get(`/${config.content.base}/:type/:id/:revision/${config.content.actions.edit}`, (req, res, next) => {
      const errors = _.get(req.session, `form.content.edit[${req.params.type.toLowerCase()}].errors`, {});
      const id = _.get(req.session, `form.content.edit[${req.params.type.toLowerCase()}].id`, _.get(req, 'params.id'));
      const revision = _.get(req.session, `form.content.edit[${req.params.type.toLowerCase()}].revision`, _.get(req, 'params.revision'));
      const values = _.get(req.session, `form.content.edit[${req.params.type.toLowerCase()}].content`, {});
      let data = {};

      const steps = _.cloneDeep(req.content.workflow).steps.reverse();

      // new revision, re-start workflow process
      const step = steps[steps.length - 1];

      _.unset(req.session, 'form.content.edit');

      // get all file inputs from the content type config
      const fileinputs = cutils.fileinputs(req.content.type.attributes);

      // something went wrong on save:
      if (Object.keys(values).length > 0) {
        // add the previous session data back in
        _.set(req.session, 'form.content.edit', {
          [req.content.type.id]: {
            id,
            revision,
          },
        });

        // Add absolute path to files to values
        if (Array.isArray(fileinputs) && fileinputs.length > 0) {
          fileinputs.forEach(filer => {
            if (values[filer.attr] && values[filer.attr].hasOwnProperty('original') && values[filer.attr].original !== '') {
              values[filer.attr].absolute = path.join(config.storage.public, values[filer.attr].relative);
            }
          });
        }

        return utils.fill(req.content.types, req.content.type, references, database).then(ct => {
          return types.only(req.params.type.toLowerCase(), values, [ct], config).then(merged => {
            return types.form(merged, errors, config).then(form => {
              res.render('content/add', {
                form,
                action: `/${config.content.base}/${req.params.type.toLowerCase()}/${config.content.actions.save}`,
                type: ct,
                step,
                config: config.content,
                storage: config.storage,
              });
            });
          }).catch(e => {
            next(e);
          });
        });
      }

      // eslint mad if no return, then mad at this else if it is there
      else { // eslint-disable-line no-else-return
        return utils.fill(req.content.types, req.content.type, references, database).then(ct => {
          // Search for the revision
          return database(`content-type--${ct.id}`).where({
            id: req.params.id,
            revision: req.params.revision,
          }).then(rows => {
            if (rows.length < 1) {
              const err = {
                message: config.content.messages.missing.revision.replace('%type', req.params.type).replace('%id', req.params.id).replace('%revision', req.params.revision),
                safe: `/${config.content.base}/${req.params.type}`,
                status: 404,
              };

              return next(err);
            }
            data = rows[0].value;

            // add session data for this content
            _.set(req.session, 'form.content.edit', {
              [req.params.type.toLowerCase()]: {
                id: rows[0].id,
                revision: rows[0].revision,
              },
            });

            if (Array.isArray(fileinputs) && fileinputs.length > 0) {
              // Add file paths to data
              data = cutils.filepaths(fileinputs, data);
            }

            return types.only(ct.id, data, [ct], config);
          }).then(only => {
            return types.form(only, null, config);
          }).then(form => {
            res.render('content/add', {
              form,
              action: `/${config.content.base}/${req.params.type.toLowerCase()}/${config.content.actions.save}`,
              type: ct,
              data,
              step,
              config: config.content,
            });
          }).catch(e => {
            next(e);
          });
        });
      }
    });

    /*
     * @name Save - Post to Content Type
     * Save content type to db
     *
     * @param {object} req - HTTP Request
     * @param {object} res - HTTP Response
     * @param {object} next - Express callback
    */
    app.post(`/${config.content.base}/:type/${config.content.actions.save}`, (req, res, next) => {
      const referrer = _.get(req.session, 'referrer') || req.get('Referrer');
      let audits;
      let publishable = false;
      let check = 'publish';
      let files = [];
      let source = {
        type: 'add',
        id: '',
        revision: Math.floor(1000000 + Math.random() * 9000000),
      };

      if (req.body.submit === config.content.actions.new) {
        check = 'save';
      }

      // determine data source from referrer
      if (_.includes(referrer, '/edit')) {
        source = {
          type: 'edit',
          id: _.get(req.session, `form.content.edit[${req.params.type.toLowerCase()}].id`, ''),
          revision: _.get(req.session, `form.content.edit[${req.params.type.toLowerCase()}].revision`, ''),
        };
      }
      else if (!_.includes(referrer, '/add')) {
        // if neither edit or add, something nefarious is afoot
        const err = {
          message: 'You may only save from an edit or add form. For now...',
          safe: `/${config.applications.base}/${req.content.type.id}`,
          status: 500,
        };

        return next(err);
      }

      // Validation
      const validated = types.form.validate(req.body, req.content.type, check);

      if (validated === true) {
        // Publishable
        if (check === 'publish') {
          publishable = true;
        }
        else if (types.form.validate(req.body, req.content.type, 'publish') === true) {
          publishable = true;
        }

        // determine piece-of-content's id
        let id;
        if (source.type === 'edit' && isUUID(source.id)) {
          id = source.id;
        }
        else {
          id = uuid.v4();
        }

        // Sunrise/Sunset
        const sunrise = utils.time.iso(req.body['sunrise-date'], req.body['sunrise-time'], 'America/New_York');
        const sunset = utils.time.iso(req.body['sunset-date'], req.body['sunset-time'], 'America/New_York');

        // language
        let language = 'us-en';
        if (req.body.language) {
          language = req.body.language;
        }

        // approval step returns to first step after saving a new revision
        const steps = _.cloneDeep(req.content.workflow).steps.reverse();
        const approval = steps.length - 1;
        const request = _.clone(req, true);
        const rev = {
          approval,
        };
        const self = req.content.workflow.steps[approval].hasOwnProperty('self') && req.content.workflow.steps[approval].self === true;

        if (self) {
          rev.approval++;
        }

        if (check === 'publish') {
          if (self) {
            request.body['comment-textarea'] = 'Self published';
            request.body['action--select'] = 'self-published';
          }
          else {
            request.body['comment-textarea'] = 'content submitted';
            request.body['action--select'] = 'submit';
          }
        }

        audits = workflows.audits(rev, req.content.workflow, request);

        const data = utils.format(req.body);

        // if files were uploaded
        if (req.hasOwnProperty('files') && typeof req.files === 'object' && Object.keys(req.files).length > 0) {
          files = Object.keys(req.files).map(file => {
            return req.files[file];
          });
        }

        const insert = {
          id,
          language,
          sunrise,
          sunset,
          publishable,
          author: req.user.id,
          value: data,
        };

        return storage.put(files).then(results => {
          // if any files were uploaded
          if (Object.keys(results).length > 0) {
            insert.value = _.merge({}, data, utils.format(results));
          }

          return database(`content-type--${req.content.type.id}`).where({
            id,
            revision: source.revision,
          });
        }).then(results => {
          const content = results[0];

          // get all file inputs from the content type config
          insert.value = cutils.filecompare(files, insert.value, content, req.content.type.attributes);

          return database(`content-type--${req.params.type.toLowerCase()}`).insert(_.merge({}, insert, audits)).returning('*');
        }).then(revision => {
          const latest = utils.routes.identifier(revision, req.content.type)[0];

          if (latest.publishable && latest.approval === 1) {
            return schedule(latest, req.content.type, req.app.get('applications-apps')).then(() => {
              return latest;
            });
          }

          return latest;
        }).then((latest) => {
          _.set(req.session, 'form.content.recent', {
            [req.params.type.toLowerCase()]: {
              id: latest.id,
              revision: latest.revision,
            },
          });

          // redirect to piece-of-content
          res.redirect(`/${config.content.base}/${req.content.type.id}/${latest.id}`);
        }).catch(e => {
          const err = {
            message: 'Something went wrong during save',
            error: e.stack,
            safe: `/${config.applications.base}/${req.content.type.id}`,
            status: 500,
          };

          return next(err);
        });
      }

      // eslint mad if no return, then mad at this else if it is there
      else { // eslint-disable-line no-else-return
        _.set(req.session, `form.content.${source.type}`, {
          [req.params.type.toLowerCase()]: {
            errors: validated,
            content: utils.format(req.body),
            files: req.files,
          },
        });
        res.redirect(referrer || req.get('Referrer'));
      }

      return true;
    });

    resolve(app);
  });
};

module.exports = routes;