From 11f6c533b83864ddf93cf2824eea0f50d465da85 Mon Sep 17 00:00:00 2001 From: Jori Lallo Date: Mon, 15 Aug 2016 12:51:26 +0200 Subject: [PATCH] Request time cache, tracking collaborators etc --- frontend/components/Document/Document.js | 4 +- package.json | 2 +- server/api/auth.js | 6 +-- server/api/authentication.js | 11 ++--- server/api/collections.js | 8 ++-- server/api/documents.js | 7 +-- server/api/index.js | 2 + server/api/user.js | 2 +- server/middlewares/cache.js | 24 +++++++++++ ...20160814095336-add-document-createdById.js | 21 +++++++++ ...0814111419-add-document-collaboratorIds.js | 14 ++++++ server/models/Atlas.js | 1 + server/models/Document.js | 25 +++++++++-- server/models/index.js | 4 +- server/presenters.js | 43 ++++++++++++++----- 15 files changed, 141 insertions(+), 33 deletions(-) create mode 100644 server/middlewares/cache.js create mode 100644 server/migrations/20160814095336-add-document-createdById.js create mode 100644 server/migrations/20160814111419-add-document-collaboratorIds.js diff --git a/frontend/components/Document/Document.js b/frontend/components/Document/Document.js index 5eaa33d9c..d50b3fb0c 100644 --- a/frontend/components/Document/Document.js +++ b/frontend/components/Document/Document.js @@ -50,8 +50,8 @@ class Document extends React.Component { return (
diff --git a/package.json b/package.json index 6404b580f..d8bab54c7 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --progress", "build:analyze": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer", "build": "npm run clean && npm run build:webpack", - "start": "cross-env NODE_ENV=development DEBUG=sql ./node_modules/.bin/nodemon --watch server index.js", + "start": "cross-env NODE_ENV=development DEBUG=sql,cache,presenters ./node_modules/.bin/nodemon --watch server index.js", "lint": "eslint frontend", "deploy": "git push heroku master", "heroku-postbuild": "npm run build && npm run sequelize db:migrate", diff --git a/server/api/auth.js b/server/api/auth.js index f2f526c73..2a1fcce8a 100644 --- a/server/api/auth.js +++ b/server/api/auth.js @@ -72,10 +72,10 @@ router.post('auth.slack', async (ctx) => { } ctx.body = { data: { - user: await presentUser(user), - team: await presentTeam(team), + user: await presentUser(ctx, user), + team: await presentTeam(ctx, team), accessToken: user.getJwtToken(), - }}; + } }; }); export default router; diff --git a/server/api/authentication.js b/server/api/authentication.js index ceb0b5105..c9358b7c2 100644 --- a/server/api/authentication.js +++ b/server/api/authentication.js @@ -10,7 +10,7 @@ export default function auth({ require = true } = {}) { const authorizationHeader = ctx.request.get('authorization'); if (authorizationHeader) { const parts = authorizationHeader.split(' '); - if (parts.length == 2) { + if (parts.length === 2) { const scheme = parts[0]; const credentials = parts[1]; @@ -35,7 +35,7 @@ export default function auth({ require = true } = {}) { let payload; try { payload = JWT.decode(token); - } catch(_e) { + } catch (e) { throw httpErrors.Unauthorized('Unable to decode JWT token'); } const user = await User.findOne({ @@ -44,19 +44,20 @@ export default function auth({ require = true } = {}) { try { JWT.verify(token, user.jwtSecret); - } catch(e) { + } catch (e) { throw httpErrors.Unauthorized('Invalid token'); } ctx.state.token = token; ctx.state.user = user; + ctx.cache[user.id] = user; } return next(); }; -}; +} // Export JWT methods as a convenience -export const sign = JWT.sign; +export const sign = JWT.sign; export const verify = JWT.verify; export const decode = JWT.decode; diff --git a/server/api/collections.js b/server/api/collections.js index c69d3a830..b86b2bf21 100644 --- a/server/api/collections.js +++ b/server/api/collections.js @@ -28,7 +28,7 @@ router.post('collections.create', auth(), async (ctx) => { }); ctx.body = { - data: await presentCollection(atlas, true), + data: await presentCollection(ctx, atlas, true), }; }); @@ -47,7 +47,7 @@ router.post('collections.info', auth(), async (ctx) => { if (!atlas) throw httpErrors.NotFound(); ctx.body = { - data: await presentCollection(atlas, true), + data: await presentCollection(ctx, atlas, true), }; }); @@ -68,7 +68,7 @@ router.post('collections.list', auth(), pagination(), async (ctx) => { // Atlases let data = []; await Promise.all(collections.map(async (atlas) => { - return data.push(await presentCollection(atlas, true)); + return data.push(await presentCollection(ctx, atlas, true)); })); data = _orderBy(data, ['updatedAt'], ['desc']); @@ -96,7 +96,7 @@ router.post('collections.updateNavigationTree', auth(), async (ctx) => { await atlas.updateNavigationTree(tree); ctx.body = { - data: await presentCollection(atlas, true), + data: await presentCollection(ctx, atlas, true), }; }); diff --git a/server/api/documents.js b/server/api/documents.js index c8fa8f21d..2efbff904 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -34,11 +34,11 @@ router.post('documents.info', auth({ require: false }), async (ctx) => { } ctx.body = { - data: await presentDocument(document, true), + data: await presentDocument(ctx, document, true), }; } else { ctx.body = { - data: await presentDocument(document), + data: await presentDocument(ctx, document), }; } }); @@ -118,6 +118,7 @@ router.post('documents.create', auth(), async (ctx) => { teamId: user.teamId, userId: user.id, lastModifiedById: user.id, + createdById: user.id, title, text, }); @@ -166,7 +167,7 @@ router.post('documents.update', auth(), async (ctx) => { } ctx.body = { - data: await presentDocument(document, true), + data: await presentDocument(ctx, document, true), }; }); diff --git a/server/api/index.js b/server/api/index.js index fe87dae8e..5d4d6f46d 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -11,6 +11,7 @@ import documents from './documents'; import validation from './validation'; import methodOverride from '../middlewares/methodOverride'; +import cache from '../middlewares/cache'; const api = new Koa(); const router = new Router(); @@ -42,6 +43,7 @@ api.use(async (ctx, next) => { api.use(bodyParser()); api.use(methodOverride()); +api.use(cache()); api.use(validation()); router.use('/', auth.routes()); diff --git a/server/api/user.js b/server/api/user.js index fc19ca6c1..6451ca85d 100644 --- a/server/api/user.js +++ b/server/api/user.js @@ -11,7 +11,7 @@ import { presentUser } from '../presenters'; const router = new Router(); router.post('user.info', auth(), async (ctx) => { - ctx.body = { data: await presentUser(ctx.state.user) }; + ctx.body = { data: await presentUser(ctx, ctx.state.user) }; }); router.post('user.s3Upload', auth(), async (ctx) => { diff --git a/server/middlewares/cache.js b/server/middlewares/cache.js new file mode 100644 index 000000000..20e13cd9e --- /dev/null +++ b/server/middlewares/cache.js @@ -0,0 +1,24 @@ +import debug from 'debug'; + +const debugCache = debug('cache'); + +export default function cache() { + return async function cacheMiddleware(ctx, next) { + ctx.cache = {}; + + ctx.cache.set = async (id, value) => { + ctx.cache[id] = value; + } + + ctx.cache.get = async (id, def) => { + if (ctx.cache[id]) { + debugCache(`hit: ${id}`); + } else { + debugCache(`miss: ${id}`); + ctx.cache.set(id, await def()); + } + return ctx.cache[id]; + }; + return next(); + }; +} diff --git a/server/migrations/20160814095336-add-document-createdById.js b/server/migrations/20160814095336-add-document-createdById.js new file mode 100644 index 000000000..9228addb5 --- /dev/null +++ b/server/migrations/20160814095336-add-document-createdById.js @@ -0,0 +1,21 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.addColumn( + 'documents', + 'createdById', + { + type: 'UUID', + allowNull: true, + references: { + model: 'users', + }, + } + ); + }, + + down: function (queryInterface, Sequelize) { + queryInterface.removeColumn('documents', 'createdById'); + }, +}; diff --git a/server/migrations/20160814111419-add-document-collaboratorIds.js b/server/migrations/20160814111419-add-document-collaboratorIds.js new file mode 100644 index 000000000..b15315fd2 --- /dev/null +++ b/server/migrations/20160814111419-add-document-collaboratorIds.js @@ -0,0 +1,14 @@ +'use strict'; + +module.exports = { + up: function (queryInterface, Sequelize) { + queryInterface.addColumn( + 'documents', + 'collaboratorIds', + { type: Sequelize.ARRAY(Sequelize.UUID) } + ) + }, + down: function (queryInterface, Sequelize) { + queryInterface.removeColumn('documents', 'collaboratorIds'); + }, +}; diff --git a/server/models/Atlas.js b/server/models/Atlas.js index bf717105c..5938804bd 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -29,6 +29,7 @@ const Atlas = sequelize.define('atlas', { teamId: collection.teamId, userId: collection.creatorId, lastModifiedById: collection.creatorId, + createdById: collection.creatorId, title: 'Introduction', text: '# Introduction', }); diff --git a/server/models/Document.js b/server/models/Document.js index 60ce96eda..830ae14ba 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -1,4 +1,5 @@ import slug from 'slug'; +import _ from 'lodash'; import randomstring from 'randomstring'; import { DataTypes, @@ -32,10 +33,20 @@ const createRevision = async (doc) => { }); }; -const documentBeforeSave = (doc) => { +const documentBeforeSave = async (doc) => { doc.html = convertToMarkdown(doc.text); doc.preview = truncateMarkdown(doc.text, 160); + doc.revisionCount = doc.revisionCount + 1; + + // Collaborators + const ids = await Revision.findAll({ + attributes: [[DataTypes.literal('DISTINCT "userId"'), 'userId']], + }).map(rev => rev.userId); + // We'll add the current user as revision hasn't been generated yet + ids.push(doc.lastModifiedById); + doc.collaboratorIds = _.uniq(ids); + return doc; }; @@ -50,13 +61,21 @@ const Document = sequelize.define('document', { revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 }, parentDocumentId: DataTypes.UUID, - lastModifiedById: { - type: 'UUID', + createdById: { + type: DataTypes.UUID, allowNull: false, references: { model: 'users', }, }, + lastModifiedById: { + type: DataTypes.UUID, + allowNull: false, + references: { + model: 'users', + }, + }, + collaboratorIds: DataTypes.ARRAY(DataTypes.UUID), }, { paranoid: true, hooks: { diff --git a/server/models/index.js b/server/models/index.js index b2dc15375..9a238c61c 100644 --- a/server/models/index.js +++ b/server/models/index.js @@ -2,10 +2,12 @@ import User from './User'; import Team from './Team'; import Atlas from './Atlas'; import Document from './Document'; +import Revision from './Revision'; export { User, Team, Atlas, Document, -}; \ No newline at end of file + Revision, +}; diff --git a/server/presenters.js b/server/presenters.js index da98ea6f2..c5aa397d0 100644 --- a/server/presenters.js +++ b/server/presenters.js @@ -1,7 +1,10 @@ +import Sequelize from 'sequelize'; import _orderBy from 'lodash.orderby'; -import { Document, Atlas } from './models'; +import { Document, Atlas, User, Revision } from './models'; + +export function presentUser(ctx, user) { + ctx.cache.set(user.id, user); -export function presentUser(user) { return new Promise(async (resolve, _reject) => { const data = { id: user.id, @@ -13,7 +16,9 @@ export function presentUser(user) { }); } -export function presentTeam(team) { +export function presentTeam(ctx, team) { + ctx.cache.set(team.id, team); + return new Promise(async (resolve, _reject) => { resolve({ id: team.id, @@ -22,7 +27,9 @@ export function presentTeam(team) { }); } -export async function presentDocument(document, includeCollection = false) { +export async function presentDocument(ctx, document, includeCollection = false) { + ctx.cache.set(document.id, document); + const data = { id: document.id, url: document.buildUrl(), @@ -32,25 +39,41 @@ export async function presentDocument(document, includeCollection = false) { html: document.html, preview: document.preview, createdAt: document.createdAt, + createdBy: undefined, updatedAt: document.updatedAt, - collection: document.atlasId, + updatedBy: undefined, team: document.teamId, + collaborators: [], }; if (includeCollection) { const collection = await Atlas.findOne({ where: { id: document.atlasId, } }); - data.collection = await presentCollection(collection, false); + data.collection = await ctx.cache.get( + collection.id, + async () => await presentCollection(ctx, collection, false) + ); } - const user = await document.getUser(); - data.user = await presentUser(user); + const createdBy = await ctx.cache.get( + document.createdById, + async () => await User.findById(document.createdById) + ); + data.createdBy = await presentUser(ctx, createdBy); + + const updatedBy = await ctx.cache.get( + document.createdById, + async () => await User.findById(document.updatedById) + ); + data.createdBy = await presentUser(ctx, updatedBy); return data; } -export function presentCollection(collection, includeRecentDocuments=false) { +export function presentCollection(ctx, collection, includeRecentDocuments=false) { + ctx.cache.set(collection.id, collection); + return new Promise(async (resolve, _reject) => { const data = { id: collection.id, @@ -74,7 +97,7 @@ export function presentCollection(collection, includeRecentDocuments=false) { const recentDocuments = []; await Promise.all(documents.map(async (document) => { - recentDocuments.push(await presentDocument(document, true)); + recentDocuments.push(await presentDocument(ctx, document, true)); })); data.recentDocuments = _orderBy(recentDocuments, ['updatedAt'], ['desc']); }