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) { _%>
+
+ <%_ } _%>