Request time cache, tracking collaborators etc

This commit is contained in:
Jori Lallo
2016-08-15 12:51:26 +02:00
parent 94e39d74bf
commit 11f6c533b8
15 changed files with 141 additions and 33 deletions

View File

@ -50,8 +50,8 @@ class Document extends React.Component {
return (
<div className={ styles.container }>
<PublishingInfo
name={ this.props.document.user.name }
avatarUrl={ this.props.document.user.avatarUrl }
name={ this.props.document.createdBy.name }
avatarUrl={ this.props.document.createdBy.avatarUrl }
createdAt={ this.props.document.createdAt }
updatedAt={ this.props.document.updatedAt }
/>

View File

@ -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",

View File

@ -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;

View File

@ -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;

View File

@ -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),
};
});

View File

@ -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),
};
});

View File

@ -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());

View File

@ -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) => {

View File

@ -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();
};
}

View File

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

View File

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

View File

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

View File

@ -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: {

View File

@ -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,
};
Revision,
};

View File

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