Merge master

This commit is contained in:
Tom Moor
2017-07-08 22:30:20 -07:00
58 changed files with 828 additions and 524 deletions

View File

@@ -5,8 +5,7 @@
"<rootDir>/server"
],
"setupFiles": [
"<rootDir>/__mocks__/console.js",
"./server/test/helper.js"
"<rootDir>/__mocks__/console.js"
],
"testEnvironment": "node"
}
}

View File

@@ -24,7 +24,7 @@ router.post('collections.create', auth(), async ctx => {
});
ctx.body = {
data: await presentCollection(ctx, atlas, true),
data: await presentCollection(ctx, atlas),
};
});
@@ -33,7 +33,7 @@ router.post('collections.info', auth(), async ctx => {
ctx.assertPresent(id, 'id is required');
const user = ctx.state.user;
const atlas = await Collection.findOne({
const atlas = await Collection.scope('withRecentDocuments').findOne({
where: {
id,
teamId: user.teamId,
@@ -43,7 +43,7 @@ router.post('collections.info', auth(), async ctx => {
if (!atlas) throw httpErrors.NotFound();
ctx.body = {
data: await presentCollection(ctx, atlas, true),
data: await presentCollection(ctx, atlas),
};
});
@@ -58,16 +58,10 @@ router.post('collections.list', auth(), pagination(), async ctx => {
limit: ctx.state.pagination.limit,
});
// Collectiones
let data = [];
await Promise.all(
collections.map(async atlas => {
return data.push(await presentCollection(ctx, atlas, true));
})
const data = await Promise.all(
collections.map(async atlas => await presentCollection(ctx, atlas))
);
data = _.orderBy(data, ['updatedAt'], ['desc']);
ctx.body = {
pagination: ctx.state.pagination,
data,

View File

@@ -8,7 +8,6 @@ import { presentDocument } from '../presenters';
import { Document, Collection, Star, View } from '../models';
const router = new Router();
router.post('documents.list', auth(), pagination(), async ctx => {
let { sort = 'updatedAt', direction } = ctx.body;
if (direction !== 'ASC') direction = 'DESC';
@@ -19,9 +18,12 @@ router.post('documents.list', auth(), pagination(), async ctx => {
order: [[sort, direction]],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
include: [{ model: Star, as: 'starred', where: { userId: user.id } }],
});
let data = await Promise.all(documents.map(doc => presentDocument(ctx, doc)));
const data = await Promise.all(
documents.map(document => presentDocument(ctx, document))
);
ctx.body = {
pagination: ctx.state.pagination,
@@ -42,7 +44,7 @@ router.post('documents.viewed', auth(), pagination(), async ctx => {
limit: ctx.state.pagination.limit,
});
let data = await Promise.all(
const data = await Promise.all(
views.map(view => presentDocument(ctx, view.document))
);
@@ -60,12 +62,17 @@ router.post('documents.starred', auth(), pagination(), async ctx => {
const views = await Star.findAll({
where: { userId: user.id },
order: [[sort, direction]],
include: [{ model: Document }],
include: [
{
model: Document,
include: [{ model: Star, as: 'starred', where: { userId: user.id } }],
},
],
offset: ctx.state.pagination.offset,
limit: ctx.state.pagination.limit,
});
let data = await Promise.all(
const data = await Promise.all(
views.map(view => presentDocument(ctx, view.document))
);
@@ -94,8 +101,7 @@ router.post('documents.info', auth(), async ctx => {
ctx.body = {
data: await presentDocument(ctx, document, {
includeCollection: document.private,
includeCollaborators: true,
includeViews: true,
}),
};
});
@@ -108,16 +114,8 @@ router.post('documents.search', auth(), async ctx => {
const documents = await Document.searchForUser(user, query);
const data = [];
await Promise.all(
documents.map(async document => {
data.push(
await presentDocument(ctx, document, {
includeCollection: true,
includeCollaborators: true,
})
);
})
const data = await Promise.all(
documents.map(async document => await presentDocument(ctx, document))
);
ctx.body = {
@@ -200,11 +198,7 @@ router.post('documents.create', auth(), async ctx => {
}
ctx.body = {
data: await presentDocument(ctx, newDocument, {
includeCollection: true,
includeCollaborators: true,
collection: ownerCollection,
}),
data: await presentDocument(ctx, newDocument),
};
});
@@ -230,11 +224,7 @@ router.post('documents.update', auth(), async ctx => {
}
ctx.body = {
data: await presentDocument(ctx, document, {
includeCollection: true,
includeCollaborators: true,
collection: collection,
}),
data: await presentDocument(ctx, document),
};
});
@@ -273,11 +263,7 @@ router.post('documents.move', auth(), async ctx => {
}
ctx.body = {
data: await presentDocument(ctx, document, {
includeCollection: true,
includeCollaborators: true,
collection: collection,
}),
data: await presentDocument(ctx, document),
};
});

View File

@@ -11,4 +11,4 @@
"use_env_variable": "DATABASE_URL",
"dialect": "postgres"
}
}
}

View File

@@ -1,9 +1,10 @@
// @flow
import debug from 'debug';
const debugCache = debug('cache');
export default function cache() {
return async function cacheMiddleware(ctx, next) {
return async function cacheMiddleware(ctx: Object, next: Function) {
ctx.cache = {};
ctx.cache.set = async (id, value) => {

View File

@@ -1,14 +1,16 @@
module.exports = {
up: (queryInterface, Sequelize) => {
queryInterface.renameTable('atlases', 'collections');
queryInterface.addColumn('collections', 'documentStructure', {
type: Sequelize.JSONB,
allowNull: true,
queryInterface.renameTable('atlases', 'collections').then(() => {
queryInterface.addColumn('collections', 'documentStructure', {
type: Sequelize.JSONB,
allowNull: true,
});
});
},
down: (queryInterface, _Sequelize) => {
queryInterface.renameTable('collections', 'atlases');
queryInterface.removeColumn('atlases', 'documentStructure');
queryInterface.renameTable('collections', 'atlases').then(() => {
queryInterface.removeColumn('atlases', 'documentStructure');
});
},
};

View File

@@ -1,40 +1,44 @@
module.exports = {
up: function(queryInterface, Sequelize) {
queryInterface.createTable('views', {
id: {
type: Sequelize.UUID,
allowNull: false,
primaryKey: true,
},
documentId: {
type: Sequelize.UUID,
allowNull: false,
},
userId: {
type: Sequelize.UUID,
allowNull: false,
},
count: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 1,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
});
queryInterface.addIndex('views', ['documentId', 'userId'], {
indicesType: 'UNIQUE',
});
queryInterface
.createTable('views', {
id: {
type: Sequelize.UUID,
allowNull: false,
primaryKey: true,
},
documentId: {
type: Sequelize.UUID,
allowNull: false,
},
userId: {
type: Sequelize.UUID,
allowNull: false,
},
count: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 1,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
})
.then(() => {
queryInterface.addIndex('views', ['documentId', 'userId'], {
indicesType: 'UNIQUE',
});
});
},
down: function(queryInterface, Sequelize) {
queryInterface.removeIndex('views', ['documentId', 'userId']);
queryInterface.dropTable('views');
queryInterface.removeIndex('views', ['documentId', 'userId']).then(() => {
queryInterface.dropTable('views');
});
},
};

View File

@@ -1,35 +1,39 @@
module.exports = {
up: function(queryInterface, Sequelize) {
queryInterface.createTable('stars', {
id: {
type: Sequelize.UUID,
allowNull: false,
primaryKey: true,
},
documentId: {
type: Sequelize.UUID,
allowNull: false,
},
userId: {
type: Sequelize.UUID,
allowNull: false,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
});
queryInterface.addIndex('stars', ['documentId', 'userId'], {
indicesType: 'UNIQUE',
});
queryInterface
.createTable('stars', {
id: {
type: Sequelize.UUID,
allowNull: false,
primaryKey: true,
},
documentId: {
type: Sequelize.UUID,
allowNull: false,
},
userId: {
type: Sequelize.UUID,
allowNull: false,
},
createdAt: {
type: Sequelize.DATE,
allowNull: false,
},
updatedAt: {
type: Sequelize.DATE,
allowNull: false,
},
})
.then(() => {
queryInterface.addIndex('stars', ['documentId', 'userId'], {
indicesType: 'UNIQUE',
});
});
},
down: function(queryInterface, Sequelize) {
queryInterface.removeIndex('stars', ['documentId', 'userId']);
queryInterface.dropTable('stars');
queryInterface.removeIndex('stars', ['documentId', 'userId']).then(() => {
queryInterface.dropTable('stars');
});
},
};

View File

@@ -60,6 +60,16 @@ const Collection = sequelize.define(
as: 'documents',
foreignKey: 'atlasId',
});
Collection.addScope('withRecentDocuments', {
include: [
{
as: 'documents',
limit: 10,
model: models.Document,
order: [['updatedAt', 'DESC']],
},
],
});
},
},
instanceMethods: {

View File

@@ -100,7 +100,7 @@ const Document = sequelize.define(
instanceMethods: {
getUrl() {
const slugifiedTitle = slugify(this.title);
return `/d/${slugifiedTitle}-${this.urlId}`;
return `/doc/${slugifiedTitle}-${this.urlId}`;
},
toJSON() {
// Warning: only use for new documents as order of children is
@@ -115,7 +115,32 @@ const Document = sequelize.define(
},
classMethods: {
associate: models => {
Document.belongsTo(models.User);
Document.belongsTo(models.Collection, {
as: 'collection',
foreignKey: 'atlasId',
});
Document.belongsTo(models.User, {
as: 'createdBy',
foreignKey: 'createdById',
});
Document.belongsTo(models.User, {
as: 'updatedBy',
foreignKey: 'lastModifiedById',
});
Document.hasMany(models.Star, {
as: 'starred',
});
Document.addScope(
'defaultScope',
{
include: [
{ model: models.Collection, as: 'collection' },
{ model: models.User, as: 'createdBy' },
{ model: models.User, as: 'updatedBy' },
],
},
{ override: true }
);
},
findById: async id => {
if (isUUID(id)) {

View File

@@ -1,8 +1,9 @@
// @flow
import _ from 'lodash';
import { Document } from '../models';
import { Collection } from '../models';
import presentDocument from './document';
async function present(ctx, collection, includeRecentDocuments = false) {
async function present(ctx: Object, collection: Collection) {
ctx.cache.set(collection.id, collection);
const data = {
@@ -13,31 +14,21 @@ async function present(ctx, collection, includeRecentDocuments = false) {
type: collection.type,
createdAt: collection.createdAt,
updatedAt: collection.updatedAt,
recentDocuments: undefined,
documents: undefined,
};
if (collection.type === 'atlas')
if (collection.type === 'atlas') {
data.documents = await collection.getDocumentsStructure();
}
if (includeRecentDocuments) {
const documents = await Document.findAll({
where: {
atlasId: collection.id,
},
limit: 10,
order: [['updatedAt', 'DESC']],
});
const recentDocuments = [];
await Promise.all(
documents.map(async document => {
recentDocuments.push(
await presentDocument(ctx, document, {
includeCollaborators: true,
})
);
})
if (collection.documents) {
data.recentDocuments = await Promise.all(
collection.documents.map(
async document =>
await presentDocument(ctx, document, { includeCollaborators: true })
)
);
data.recentDocuments = _.orderBy(recentDocuments, ['updatedAt'], ['desc']);
}
return data;

View File

@@ -1,26 +1,22 @@
// @flow
import { Collection, Star, User, View, Document } from '../models';
import _ from 'lodash';
import { User, Document, View } from '../models';
import presentUser from './user';
import presentCollection from './collection';
import _ from 'lodash';
type Options = {
includeCollection?: boolean,
includeCollaborators?: boolean,
includeViews?: boolean,
};
async function present(ctx: Object, document: Document, options: Options) {
async function present(ctx: Object, document: Document, options: ?Options) {
options = {
includeCollection: true,
includeCollaborators: true,
includeViews: true,
includeViews: false,
...options,
};
ctx.cache.set(document.id, document);
const userId = ctx.state.user.id;
let data = {
const data = {
id: document.id,
url: document.getUrl(),
private: document.private,
@@ -29,36 +25,23 @@ async function present(ctx: Object, document: Document, options: Options) {
html: document.html,
preview: document.preview,
createdAt: document.createdAt,
createdBy: undefined,
starred: false,
createdBy: presentUser(ctx, document.createdBy),
updatedAt: document.updatedAt,
updatedBy: undefined,
updatedBy: presentUser(ctx, document.updatedBy),
team: document.teamId,
collaborators: [],
starred: !!document.starred,
collection: undefined,
views: undefined,
};
data.starred = !!await Star.findOne({
where: { documentId: document.id, userId },
});
if (options.includeViews) {
// $FlowIssue not found in object literal?
data.views = await View.sum('count', {
where: { documentId: document.id },
});
if (document.private) {
data.collection = await presentCollection(ctx, document.collection);
}
if (options.includeCollection) {
// $FlowIssue not found in object literal?
data.collection = await ctx.cache.get(document.atlasId, async () => {
const collection =
options.collection ||
(await Collection.findOne({
where: {
id: document.atlasId,
},
}));
return presentCollection(ctx, collection);
if (options.includeViews) {
data.views = await View.sum('count', {
where: { documentId: document.id },
});
}
@@ -66,27 +49,13 @@ async function present(ctx: Object, document: Document, options: Options) {
// This could be further optimized by using ctx.cache
data['collaborators'] = await User.findAll({
where: {
id: {
$in: _.takeRight(document.collaboratorIds, 10) || [],
},
id: { $in: _.takeRight(document.collaboratorIds, 10) || [] },
},
}).map(user => presentUser(ctx, user));
// $FlowIssue not found in object literal?
data.collaboratorCount = document.collaboratorIds.length;
}
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.lastModifiedById,
async () => await User.findById(document.lastModifiedById)
);
data.updatedBy = await presentUser(ctx, updatedBy);
return data;
}

View File

@@ -1,4 +1,7 @@
function present(ctx, team) {
// @flow
import { Team } from '../models';
function present(ctx: Object, team: Team) {
ctx.cache.set(team.id, team);
return {

View File

@@ -1,28 +1,33 @@
<!doctype html>
<html>
<head>
<title>Atlas</title>
<link href="/static/styles.css" rel="stylesheet"></head>
<style>
body, html {
margin: 0;
padding: 0;
}
body {
display: flex;
width: 100%;
height: 100%;
}
<head>
<title>Atlas</title>
<link href="/static/styles.css" rel="stylesheet">
</head>
<style>
body,
html {
margin: 0;
padding: 0;
}
#root {
flex: 1;
height: 100%;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="/static/bundle.js"></script>
</body>
</html>
body {
display: flex;
width: 100%;
height: 100%;
}
#root {
flex: 1;
height: 100%;
}
</style>
</head>
<body>
<div id="root"></div>
<script src="/static/bundle.js"></script>
</body>
</html>

View File

@@ -1,16 +1,30 @@
<!doctype html>
<html>
<head>
<title>Atlas</title>
<style>
body,
html {
display: flex;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
</body>
</html>
<head>
<title>Atlas</title>
<style>
body,
html {
margin: 0;
padding: 0;
}
body {
display: flex;
width: 100%;
height: 100%;
}
#root {
flex: 1;
height: 100%;
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -1,4 +1,5 @@
require('localenv');
// @flow
require('../../init');
// test environment variables
process.env.DATABASE_URL = process.env.DATABASE_URL_TEST;