diff --git a/server/api/documents.js b/server/api/documents.js index b21b7da8c..5e098ca8b 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -4,10 +4,16 @@ import httpErrors from 'http-errors'; import auth from './middlewares/authentication'; import pagination from './middlewares/pagination'; -import { presentDocument } from '../presenters'; -import { Document, Collection, Star, View } from '../models'; +import { presentDocument, presentRevision } from '../presenters'; +import { Document, Collection, Star, View, Revision } from '../models'; + +const authDocumentForUser = (ctx, document) => { + const user = ctx.state.user; + if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound(); +}; const router = new Router(); + router.post('documents.list', auth(), pagination(), async ctx => { let { sort = 'updatedAt', direction } = ctx.body; if (direction !== 'ASC') direction = 'DESC'; @@ -101,23 +107,38 @@ router.post('documents.info', auth(), async ctx => { ctx.assertPresent(id, 'id is required'); const document = await Document.findById(id); - if (!document) throw httpErrors.NotFound(); - - // Don't expose private documents outside the team - if (document.private) { - if (!ctx.state.user) throw httpErrors.NotFound(); - - const user = await ctx.state.user; - if (document.teamId !== user.teamId) { - throw httpErrors.NotFound(); - } - } + authDocumentForUser(ctx, document); ctx.body = { data: await presentDocument(ctx, document), }; }); +router.post('documents.revisions', auth(), pagination(), async ctx => { + let { id, sort = 'updatedAt', direction } = ctx.body; + if (direction !== 'ASC') direction = 'DESC'; + ctx.assertPresent(id, 'id is required'); + const document = await Document.findById(id); + + authDocumentForUser(ctx, document); + + const revisions = await Revision.findAll({ + where: { documentId: id }, + order: [[sort, direction]], + offset: ctx.state.pagination.offset, + limit: ctx.state.pagination.limit, + }); + + const data = await Promise.all( + revisions.map(revision => presentRevision(ctx, revision)) + ); + + ctx.body = { + pagination: ctx.state.pagination, + data, + }; +}); + router.post('documents.search', auth(), async ctx => { const { query } = ctx.body; ctx.assertPresent(query, 'query is required'); @@ -142,8 +163,7 @@ router.post('documents.star', auth(), async ctx => { const user = await ctx.state.user; const document = await Document.findById(id); - if (!document || document.teamId !== user.teamId) - throw httpErrors.BadRequest(); + authDocumentForUser(ctx, document); await Star.findOrCreate({ where: { documentId: document.id, userId: user.id }, @@ -156,8 +176,7 @@ router.post('documents.unstar', auth(), async ctx => { const user = await ctx.state.user; const document = await Document.findById(id); - if (!document || document.teamId !== user.teamId) - throw httpErrors.BadRequest(); + authDocumentForUser(ctx, document); await Star.destroy({ where: { documentId: document.id, userId: user.id }, @@ -228,7 +247,7 @@ router.post('documents.update', auth(), async ctx => { const document = await Document.findById(id); const collection = document.collection; - if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound(); + authDocumentForUser(ctx, document); // Update document if (title) document.title = title; @@ -254,15 +273,14 @@ router.post('documents.move', auth(), async ctx => { ctx.assertUuid(parentDocument, 'parentDocument must be an uuid'); if (index) ctx.assertPositiveInteger(index, 'index must be an integer (>=0)'); - const user = ctx.state.user; const document = await Document.findById(id); const collection = await Collection.findById(document.atlasId); + authDocumentForUser(ctx, document); + if (collection.type !== 'atlas') throw httpErrors.BadRequest("This document can't be moved"); - if (!document || document.teamId !== user.teamId) throw httpErrors.NotFound(); - // Set parent document if (parentDocument) { const parent = await Document.findById(parentDocument); @@ -292,12 +310,10 @@ router.post('documents.delete', auth(), async ctx => { const { id } = ctx.body; ctx.assertPresent(id, 'id is required'); - const user = ctx.state.user; const document = await Document.findById(id); const collection = await Collection.findById(document.atlasId); - if (!document || document.teamId !== user.teamId) - throw httpErrors.BadRequest(); + authDocumentForUser(ctx, document); if (collection.type === 'atlas') { // Don't allow deletion of root docs diff --git a/server/api/documents.test.js b/server/api/documents.test.js index 8f1b7c580..7d2bc37df 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -43,6 +43,24 @@ describe('#documents.list', async () => { }); }); +describe('#documents.revision', async () => { + it("should return document's revisions", async () => { + const { user, document } = await seed(); + const res = await server.post('/api/documents.revisions', { + body: { + token: user.getJwtToken(), + id: document.id, + }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(1); + expect(body.data[0].id).not.toEqual(document.id); + expect(body.data[0].title).toEqual(document.title); + }); +}); + describe('#documents.search', async () => { it('should return results', async () => { const { user } = await seed(); diff --git a/server/presenters/index.js b/server/presenters/index.js index 22e34f254..8eab4228e 100644 --- a/server/presenters/index.js +++ b/server/presenters/index.js @@ -2,6 +2,7 @@ import presentUser from './user'; import presentView from './view'; import presentDocument from './document'; +import presentRevision from './revision'; import presentCollection from './collection'; import presentApiKey from './apiKey'; import presentTeam from './team'; @@ -10,6 +11,7 @@ export { presentUser, presentView, presentDocument, + presentRevision, presentCollection, presentApiKey, presentTeam, diff --git a/server/presenters/revision.js b/server/presenters/revision.js new file mode 100644 index 000000000..ef1377674 --- /dev/null +++ b/server/presenters/revision.js @@ -0,0 +1,15 @@ +// @flow +import _ from 'lodash'; +import { Revision } from '../models'; + +function present(ctx: Object, revision: Revision) { + return { + id: revision.id, + title: revision.title, + text: revision.text, + createdAt: revision.createdAt, + updatedAt: revision.updatedAt, + }; +} + +export default present;