lib/routes/content.js
'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;