diff --git a/app.js b/app.js index 2989ae00a..b65126aa0 100644 --- a/app.js +++ b/app.js @@ -1,14 +1,11 @@ const express = require('express'); -const bodyParser = require('body-parser'); const morgan = require('morgan'); const path = require('path'); const merge = require('lodash/merge'); const helmet = require('helmet'); const compression = require('compression'); -const cookieParser = require('cookie-parser'); const {HELMET_CONFIGURATION} = require('./config'); const {MOUNT_PATH} = require('./url'); -const {applyLocals} = require('./services/locals'); const routes = require('./routes'); const debug = require('debug')('talk:app'); @@ -36,12 +33,6 @@ app.use(helmet(merge(HELMET_CONFIGURATION, { // Compress the responses if appropriate. app.use(compression()); -// Parse the cookies on the request. -app.use(cookieParser()); - -// Parse the body json if it's there. -app.use(bodyParser.json()); - //============================================================================== // VIEW CONFIGURATION //============================================================================== @@ -53,9 +44,6 @@ app.set('view engine', 'ejs'); // ROUTES //============================================================================== -// Add the locals to the app renderer. -applyLocals(app.locals); - debug(`mounting routes on the ${MOUNT_PATH} path`); // Actually apply the routes. diff --git a/bin/cli-serve b/bin/cli-serve index 4324cac92..723558651 100755 --- a/bin/cli-serve +++ b/bin/cli-serve @@ -1,6 +1,7 @@ #!/usr/bin/env node const program = require('./commander'); +const util = require('./util'); const serve = require('../serve'); //============================================================================== @@ -13,5 +14,8 @@ program .parse(process.argv); // Start serving. -serve({jobs: program.jobs, websockets: program.websockets}); +serve({jobs: program.jobs, websockets: program.websockets}).catch((err) => { + console.error(err); + util.shutdown(1); +}); diff --git a/config.js b/config.js index 3e167b294..b5b55a5a5 100644 --- a/config.js +++ b/config.js @@ -20,6 +20,10 @@ const CONFIG = { // WEBPACK indicates when webpack is currently building. WEBPACK: process.env.WEBPACK === 'TRUE', + // When TRUE, it ensures that database indexes created in core will not add + // indexes. + CREATE_MONGO_INDEXES: process.env.DISABLE_CREATE_MONGO_INDEXES !== 'TRUE', + //------------------------------------------------------------------------------ // JWT based configuration //------------------------------------------------------------------------------ diff --git a/graph/connectors.js b/graph/connectors.js index ecaa79550..cc7e45d21 100644 --- a/graph/connectors.js +++ b/graph/connectors.js @@ -5,7 +5,6 @@ const errors = require('../errors'); const Action = require('../models/action'); const Asset = require('../models/asset'); const Comment = require('../models/comment'); -const Setting = require('../models/setting'); const User = require('../models/user'); // Services. @@ -19,7 +18,6 @@ const Jwt = require('../services/jwt'); const Karma = require('../services/karma'); const Kue = require('../services/kue'); const Limit = require('../services/limit'); -const Locals = require('../services/locals'); const Mailer = require('../services/mailer'); const Metadata = require('../services/metadata'); const Migration = require('../services/migration'); @@ -45,7 +43,6 @@ const connectors = { Action, Asset, Comment, - Setting, User, }, services: { @@ -59,7 +56,6 @@ const connectors = { Karma, Kue, Limit, - Locals, Mailer, Metadata, Migration, diff --git a/middleware/staticTemplate.js b/middleware/staticTemplate.js new file mode 100644 index 000000000..823ab6624 --- /dev/null +++ b/middleware/staticTemplate.js @@ -0,0 +1,45 @@ +const { + BASE_URL, + BASE_PATH, + MOUNT_PATH, + STATIC_URL, +} = require('../url'); + +const { + RECAPTCHA_PUBLIC, + WEBSOCKET_LIVE_URI, +} = require('../config'); + +// TEMPLATE_LOCALS stores the static data that is provided as a `text/json` on +// to the client from the template. +const TEMPLATE_LOCALS = { + BASE_URL, + BASE_PATH, + MOUNT_PATH, + STATIC_URL, + data: { + TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC, + LIVE_URI: WEBSOCKET_LIVE_URI, + STATIC_URL, + }, +}; + +// attachLocals will attach the locals to the response only. +const attachLocals = (locals) => { + for (const key in TEMPLATE_LOCALS) { + const value = TEMPLATE_LOCALS[key]; + + locals[key] = value; + } +}; + +module.exports = (req, res, next) => { + + // Always attach the locals. + attachLocals(res.locals); + + // Forward the request. + next(); +}; + +module.exports.attachLocals = attachLocals; diff --git a/models/comment.js b/models/comment.js index 73aa22d0b..bc4cecd5d 100644 --- a/models/comment.js +++ b/models/comment.js @@ -108,10 +108,60 @@ CommentSchema.index({ background: false }); +CommentSchema.index({ + 'status': 1, + 'created_at': 1, +}, { + background: true, +}); + +CommentSchema.index({ + 'status': 1, + 'created_at': 1, + 'asset_id': 1, +}, { + background: true, +}); + // Add an index that is optimized for sorting based on the action count data. CommentSchema.index({ 'created_at': 1, - 'action_counts': 1, + 'action_counts.flag': 1, +}, { + background: true, +}); + +CommentSchema.index({ + 'created_at': 1, + 'action_counts.flag': 1, + 'status': 1, +}, { + background: true, +}); + +// Add an index that is optimized for finding flagged comments. +CommentSchema.index({ + 'asset_id': 1, + 'created_at': 1, + 'action_counts.flag': 1, +}, { + background: true, +}); + +// Add an index for the reply sort. +CommentSchema.index({ + 'asset_id': 1, + 'created_at': -1, + 'reply_count': -1, +}, { + background: true, +}); + +// Optimize for tag searches/counts. +CommentSchema.index({ + 'asset_id': 1, + 'tags.tag.name': 1, + 'status': 1, }, { background: true, }); diff --git a/plugin-api/beta/server/getReactionConfig.js b/plugin-api/beta/server/getReactionConfig.js index 2fffb4d1c..f4302050c 100644 --- a/plugin-api/beta/server/getReactionConfig.js +++ b/plugin-api/beta/server/getReactionConfig.js @@ -1,10 +1,24 @@ const {SEARCH_OTHER_USERS} = require('../../../perms/constants'); const errors = require('../../../errors'); const pluralize = require('pluralize'); +const CommentModel = require('../../../models/comment'); +const sc = require('snake-case'); +const {CREATE_MONGO_INDEXES} = require('../../../config'); function getReactionConfig(reaction) { reaction = reaction.toLowerCase(); + if (CREATE_MONGO_INDEXES) { + + // Create the index on the comment model based on the reaction config. + CommentModel.collection.createIndex({ + created_at: 1, + [`action_counts.${sc(reaction)}`]: 1 + }, { + background: true, + }); + } + const reactionPlural = pluralize(reaction); const Reaction = reaction.charAt(0).toUpperCase() + reaction.slice(1); const REACTION = reaction.toUpperCase(); diff --git a/routes/admin/index.js b/routes/admin/index.js index b3872d626..d6cb481de 100644 --- a/routes/admin/index.js +++ b/routes/admin/index.js @@ -1,6 +1,5 @@ const express = require('express'); const router = express.Router(); -const {data} = require('../static'); // Get /email-confirmation expects a signed JWT in the hash router.get('/confirm-email', (req, res) => { @@ -17,7 +16,7 @@ router.get('/password-reset', (req, res) => { }); router.get('*', (req, res) => { - res.render('admin', {data}); + res.render('admin'); }); module.exports = router; diff --git a/routes/embed/index.js b/routes/embed/index.js index c360c1f76..852d9afa7 100644 --- a/routes/embed/index.js +++ b/routes/embed/index.js @@ -1,13 +1,12 @@ const express = require('express'); const router = express.Router(); const SettingsService = require('../../services/settings'); -const {data} = require('../static'); router.use('/:embed', async (req, res, next) => { switch (req.params.embed) { case 'stream': { - const {customCssUrl} = await SettingsService.retrieve(); - return res.render('embed/stream', {customCssUrl, data}); + const {customCssUrl} = await SettingsService.retrieve('customCssUrl'); + return res.render('embed/stream', {customCssUrl}); } } diff --git a/routes/index.js b/routes/index.js index 23e8d8a47..0ee012a79 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,17 +1,20 @@ -const express = require('express'); -const path = require('path'); -const plugins = require('../services/plugins'); -const debug = require('debug')('talk:routes'); -const authentication = require('../middleware/authentication'); -const {passport} = require('../services/passport'); -const pubsub = require('../middleware/pubsub'); -const i18n = require('../services/i18n'); -const enabled = require('debug').enabled; -const errors = require('../errors'); -const {createGraphOptions} = require('../graph'); const accepts = require('accepts'); const apollo = require('graphql-server-express'); +const authentication = require('../middleware/authentication'); +const bodyParser = require('body-parser'); +const cookieParser = require('cookie-parser'); +const debug = require('debug')('talk:routes'); +const enabled = require('debug').enabled; +const errors = require('../errors'); +const express = require('express'); +const i18n = require('../services/i18n'); +const path = require('path'); +const plugins = require('../services/plugins'); +const pubsub = require('../middleware/pubsub'); const {DISABLE_STATIC_SERVER} = require('../config'); +const {createGraphOptions} = require('../graph'); +const {passport} = require('../services/passport'); +const staticTemplate = require('../middleware/staticTemplate'); const router = express.Router(); @@ -61,10 +64,23 @@ if (!DISABLE_STATIC_SERVER) { router.get('/embed.js.map', serveFile('../dist/embed.js.map')); } +//============================================================================== +// STATIC ROUTES +//============================================================================== + +router.use('/admin', staticTemplate, require('./admin')); +router.use('/embed', staticTemplate, require('./embed')); + //============================================================================== // PASSPORT MIDDLEWARE //============================================================================== +// Parse the cookies on the request. +router.use(cookieParser()); + +// Parse the body json if it's there. +router.use(bodyParser.json()); + const passportDebug = require('debug')('talk:passport'); // Install the passport plugins. @@ -112,13 +128,11 @@ if (process.env.NODE_ENV !== 'production') { //============================================================================== router.use('/api/v1', require('./api')); -router.use('/admin', require('./admin')); -router.use('/embed', require('./embed')); +// Development routes. if (process.env.NODE_ENV !== 'production') { - router.use('/assets', require('./assets')); - - router.get('/', (req, res) => { + router.use('/assets', staticTemplate, require('./assets')); + router.get('/', staticTemplate, (req, res) => { return res.render('article', { title: 'Coral Talk', asset_url: '', diff --git a/routes/static.js b/routes/static.js deleted file mode 100644 index 12bf01282..000000000 --- a/routes/static.js +++ /dev/null @@ -1,13 +0,0 @@ -const { - RECAPTCHA_PUBLIC, - WEBSOCKET_LIVE_URI, -} = require('../config'); -const { - STATIC_URL, -} = require('../url'); - -module.exports.data = { - TALK_RECAPTCHA_PUBLIC: RECAPTCHA_PUBLIC, - LIVE_URI: WEBSOCKET_LIVE_URI, - STATIC_URL, -}; diff --git a/serve.js b/serve.js index e97095d52..b98a49085 100644 --- a/serve.js +++ b/serve.js @@ -75,9 +75,6 @@ function normalizePort(val) { async function onListening() { - // Start the cache instance. - await cache.init(); - let addr = server.address(); let bind = typeof addr === 'string' ? `pipe ${addr}` @@ -88,7 +85,10 @@ async function onListening() { /** * Start the app. */ -async function serve({jobs = true, websockets = true} = {}) { +async function serve({jobs = false, websockets = false} = {}) { + + // Start the cache instance. + await cache.init(); try { diff --git a/services/cache.js b/services/cache.js index 77db0ce1f..155992fe0 100644 --- a/services/cache.js +++ b/services/cache.js @@ -21,7 +21,7 @@ const keyfunc = (key) => { * This wraps a complicated function with a cache, in the event that the item is * not inside the cache, it will perform the work to get it and then set it * followed by returning the value. - * @param {Mixed} key Either an array of items or string represening this + * @param {Mixed} key Either an array of items or string representing this * work * @param {Integer} expiry Time in seconds for the cache entry to live for * @param {Function} work A function that returns a promise that can be @@ -30,7 +30,7 @@ const keyfunc = (key) => { */ cache.wrap = async (key, expiry, work, kf = keyfunc) => { let value = await cache.get(key, kf); - if (value !== null) { + if (typeof value !== 'undefined' && value !== null) { debug('wrap: hit', kf(key)); return value; } @@ -187,11 +187,13 @@ cache.wrapMany = async (keys, expiry, work, kf = keyfunc) => { * @return {Promise} */ cache.get = async (key, kf = keyfunc) => cache.client.get(kf(key)).then((reply) => { - if (reply !== null) { + if (typeof reply !== 'undefined' && reply !== null) { // Parse the stored cache value from JSON. return JSON.parse(reply); } + + return null; }); /** @@ -207,7 +209,7 @@ cache.getMany = async (keys, kf = keyfunc) => cache.client.mget(keys.map(kf)).th for (let i = 0; i < replies.length; i++) { let value = null; - if (replies[i] != null) { + if (typeof replies[i] !== 'undefined' && replies[i] !== null) { // Parse the stored cache value from JSON. value = JSON.parse(replies[i]); diff --git a/services/hcache.js b/services/hcache.js new file mode 100644 index 000000000..72e52d9eb --- /dev/null +++ b/services/hcache.js @@ -0,0 +1,61 @@ +const cache = require('./cache'); +const debug = require('debug')('talk:services:hcache'); + +const kf = (key) => `hcache:${key}`; + +const hcache = module.exports = {}; + +hcache.get = async (key, field = '__default__') => { + + // Get the current value from redis. + const reply = await cache.client.hget(kf(key), field); + + if (typeof reply !== 'undefined' && reply !== null) { + return JSON.parse(reply); + } + + return null; +}; + +hcache.set = async (key, field = '__default__', value, expiry = 60) => { + + // Serialize the value as JSON. + let reply = JSON.stringify(value); + + return cache.client + .pipeline() + .hset(kf(key), field, reply) + .expire(kf(key), expiry) + .exec(); +}; + +hcache.del = async (key, field = null) => { + if (field === null) { + return cache.client.del(kf(key)); + } + + return cache.client.hdel(kf(key), field); +}; + +hcache.wrap = async (key, field, expiry, work) => { + let value = await hcache.get(key, field); + if (value !== null) { + debug('wrap: hit', kf(key)); + return value; + } + + debug('wrap: miss', kf(key)); + + value = await work(); + + process.nextTick(async () => { + try { + await hcache.set(key, field, value, expiry); + debug('wrap: set complete'); + } catch (err) { + console.error(err); + } + }); + + return value; +}; diff --git a/services/locals.js b/services/locals.js deleted file mode 100644 index 9a6a6bec8..000000000 --- a/services/locals.js +++ /dev/null @@ -1,20 +0,0 @@ -const { - BASE_URL, - BASE_PATH, - MOUNT_PATH, - STATIC_URL, -} = require('../url'); - -const applyLocals = (locals) => { - - // Apply the BASE_PATH, BASE_URL, and MOUNT_PATH on the app.locals, which will - // make them available on the templates and the routers. - locals.BASE_URL = BASE_URL; - locals.BASE_PATH = BASE_PATH; - locals.MOUNT_PATH = MOUNT_PATH; - locals.STATIC_URL = STATIC_URL; -}; - -module.exports = { - applyLocals, -}; diff --git a/services/mailer.js b/services/mailer.js index f04a6dc92..a4f694b30 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -4,7 +4,7 @@ const kue = require('./kue'); const path = require('path'); const fs = require('fs'); const _ = require('lodash'); -const {applyLocals} = require('./locals'); +const {attachLocals} = require('../middleware/staticTemplate'); const i18n = require('./i18n'); @@ -97,7 +97,7 @@ const mailer = module.exports = { // Prefix the subject with `[Talk]`. subject = `[Talk] ${subject}`; - applyLocals(locals); + attachLocals(locals); // Attach the templating function. locals['t'] = i18n.t; diff --git a/services/mongoose.js b/services/mongoose.js index fb23a5fe1..747df2d66 100644 --- a/services/mongoose.js +++ b/services/mongoose.js @@ -5,7 +5,8 @@ const queryDebugger = require('debug')('talk:db:query'); const { MONGO_URL, - WEBPACK + WEBPACK, + CREATE_MONGO_INDEXES, } = require('../config'); // Loading the formatter from Mongoose: @@ -56,6 +57,9 @@ if (WEBPACK) { mongoose .connect(MONGO_URL, { useMongoClient: true, + config: { + autoIndex: CREATE_MONGO_INDEXES, + }, }) .then(() => { debug('connection established'); @@ -69,7 +73,7 @@ if (WEBPACK) { module.exports = mongoose; // Here we include all the models that mongoose is used for, this ensures that -// when we import mongoose that we also start up all the indexing opreations +// when we import mongoose that we also start up all the indexing operations // here. require('../models/action'); require('../models/asset'); diff --git a/services/settings.js b/services/settings.js index 310884002..90ff6bbd6 100644 --- a/services/settings.js +++ b/services/settings.js @@ -1,4 +1,5 @@ const SettingModel = require('../models/setting'); +const hcache = require('./hcache'); const errors = require('../errors'); const {dotize} = require('./utils'); @@ -7,6 +8,20 @@ const {dotize} = require('./utils'); */ const selector = {id: '1'}; +const retrieve = async (fields) => { + let settings; + if (fields) { + settings = await SettingModel.findOne(selector).select(fields); + } else { + settings = await SettingModel.findOne(selector); + } + if (!settings) { + throw errors.ErrSettingsNotInit; + } + + return settings; +}; + /** * The Setting Service object exposing the Setting model. */ @@ -16,16 +31,16 @@ module.exports = class SettingsService { * Gets the entire settings record and sends it back * @return {Promise} settings the whole settings record */ - static retrieve() { - return SettingModel - .findOne(selector) - .then((settings) => { - if (!settings) { - return Promise.reject(errors.ErrSettingsNotInit); - } + static async retrieve(fields) { + if (process.env.NODE_ENV === 'production') { - return settings; - }); + // When in production, wrap the settings retrieval with a cache. + const settings = await hcache.wrap('settings', fields, 60, () => retrieve(fields)); + + return new SettingModel(settings); + } + + return retrieve(fields); } /** @@ -33,14 +48,20 @@ module.exports = class SettingsService { * @param {object} setting a hash of whatever settings you want to update * @return {Promise} settings Promise that resolves to the entire (updated) settings object. */ - static update(settings) { - return SettingModel.findOneAndUpdate(selector, { + static async update(settings) { + const updatedSettings = await SettingModel.findOneAndUpdate(selector, { $set: dotize(settings) }, { upsert: true, new: true, setDefaultsOnInsert: true }); + + if (process.env.NODE_ENV === 'production') { + await hcache.del('settings'); + } + + return updatedSettings; } /** diff --git a/views/embed/stream.ejs b/views/embed/stream.ejs index aeea82bc5..3146481e3 100644 --- a/views/embed/stream.ejs +++ b/views/embed/stream.ejs @@ -6,16 +6,15 @@ - <% if (locals.customCssUrl) { %> - - <% } %> - <% if (data != null) { %> - - <% } %> + <%_ if (locals.customCssUrl) { _%> + + <%_ } _%> + <%_ if (data != null) { _%> + + <%_ } _%> -