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']);
}