Request time cache, tracking collaborators etc
This commit is contained in:
@ -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 }
|
||||
/>
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -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());
|
||||
|
@ -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) => {
|
||||
|
24
server/middlewares/cache.js
Normal file
24
server/middlewares/cache.js
Normal 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();
|
||||
};
|
||||
}
|
21
server/migrations/20160814095336-add-document-createdById.js
Normal file
21
server/migrations/20160814095336-add-document-createdById.js
Normal 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');
|
||||
},
|
||||
};
|
@ -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');
|
||||
},
|
||||
};
|
@ -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',
|
||||
});
|
||||
|
@ -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: {
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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']);
|
||||
}
|
||||
|
Reference in New Issue
Block a user