diff --git a/app/scenes/Search/Search.js b/app/scenes/Search/Search.js index b669ee2a..377dbc04 100644 --- a/app/scenes/Search/Search.js +++ b/app/scenes/Search/Search.js @@ -5,7 +5,7 @@ import keydown from 'react-keydown'; import Waypoint from 'react-waypoint'; import { observable, action } from 'mobx'; import { observer, inject } from 'mobx-react'; -import { SearchResult } from 'types'; +import type { SearchResult } from 'types'; import _ from 'lodash'; import DocumentsStore, { DEFAULT_PAGINATION_LIMIT, diff --git a/server/api/documents.test.js b/server/api/documents.test.js index ed7d38b0..984a4e0a 100644 --- a/server/api/documents.test.js +++ b/server/api/documents.test.js @@ -3,7 +3,7 @@ import TestServer from 'fetch-test-server'; import app from '..'; import { Document, View, Star, Revision } from '../models'; import { flushdb, seed } from '../test/support'; -import { buildShare, buildUser } from '../test/factories'; +import { buildShare, buildUser, buildDocument } from '../test/factories'; const server = new TestServer(app.callback()); @@ -231,6 +231,68 @@ describe('#documents.search', async () => { expect(body.data[0].document.text).toEqual('# Much guidance'); }); + it('should return results in ranked order', async () => { + const { user } = await seed(); + const firstResult = await buildDocument({ + title: 'search term', + text: 'random text', + userId: user.id, + teamId: user.teamId, + }); + const secondResult = await buildDocument({ + title: 'random text', + text: 'search term', + userId: user.id, + teamId: user.teamId, + }); + + const res = await server.post('/api/documents.search', { + body: { token: user.getJwtToken(), query: 'search term' }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(2); + expect(body.data[0].document.id).toEqual(firstResult.id); + expect(body.data[1].document.id).toEqual(secondResult.id); + }); + + it('should return draft documents created by user', async () => { + const { user } = await seed(); + await buildDocument({ + title: 'search term', + text: 'search term', + publishedAt: null, + userId: user.id, + teamId: user.teamId, + }); + const res = await server.post('/api/documents.search', { + body: { token: user.getJwtToken(), query: 'search term' }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(1); + expect(body.data[0].document.text).toEqual('search term'); + }); + + it('should not return draft documents created by other users', async () => { + const { user } = await seed(); + await buildDocument({ + title: 'search term', + text: 'search term', + publishedAt: null, + teamId: user.teamId, + }); + const res = await server.post('/api/documents.search', { + body: { token: user.getJwtToken(), query: 'search term' }, + }); + const body = await res.json(); + + expect(res.status).toEqual(200); + expect(body.data.length).toEqual(0); + }); + it('should require authentication', async () => { const res = await server.post('/api/documents.search'); const body = await res.json(); diff --git a/server/models/Document.js b/server/models/Document.js index a5abcac0..05fb6675 100644 --- a/server/models/Document.js +++ b/server/models/Document.js @@ -7,7 +7,7 @@ import Plain from 'slate-plain-serializer'; import Sequelize from 'sequelize'; import isUUID from 'validator/lib/isUUID'; -import { Collection } from '../models'; +import { Collection, User } from '../models'; import { DataTypes, sequelize } from '../sequelize'; import events from '../events'; import parseTitle from '../../shared/utils/parseTitle'; @@ -211,8 +211,11 @@ Document.searchForUser = async ( FROM documents WHERE "searchVector" @@ plainto_tsquery('english', :query) AND "teamId" = '${user.teamId}'::uuid AND - "deletedAt" IS NULL - ORDER BY "searchRanking", "updatedAt" DESC + "deletedAt" IS NULL AND + ("publishedAt" IS NOT NULL OR "createdById" = '${user.id}') + ORDER BY + "searchRanking" DESC, + "updatedAt" DESC LIMIT :limit OFFSET :offset; `; @@ -227,12 +230,15 @@ Document.searchForUser = async ( }); // Second query to get associated document data - const withViewsScope = { method: ['withViews', user.id] }; - const documents = await Document.scope( - 'defaultScope', - withViewsScope - ).findAll({ + const documents = await Document.scope({ + method: ['withViews', user.id], + }).findAll({ where: { id: map(results, 'id') }, + include: [ + { model: Collection, as: 'collection' }, + { model: User, as: 'createdBy', paranoid: false }, + { model: User, as: 'updatedBy', paranoid: false }, + ], }); return map(results, result => ({ diff --git a/server/test/factories.js b/server/test/factories.js index 65b8b828..a93b4d69 100644 --- a/server/test/factories.js +++ b/server/test/factories.js @@ -1,5 +1,5 @@ // @flow -import { Share, Team, User } from '../models'; +import { Share, Team, User, Document, Collection } from '../models'; import uuid from 'uuid'; let count = 0; @@ -45,3 +45,53 @@ export async function buildUser(overrides: Object = {}) { ...overrides, }); } + +export async function buildCollection(overrides: Object = {}) { + count++; + + if (!overrides.teamId) { + const team = await buildTeam(); + overrides.teamId = team.id; + } + + if (!overrides.userId) { + const user = await buildUser(); + overrides.userId = user.id; + } + + return Collection.create({ + name: 'Test Collection', + description: 'Test collection description', + creatorId: overrides.userId, + type: 'atlas', + ...overrides, + }); +} + +export async function buildDocument(overrides: Object = {}) { + count++; + + if (!overrides.teamId) { + const team = await buildTeam(); + overrides.teamId = team.id; + } + + if (!overrides.userId) { + const user = await buildUser(); + overrides.userId = user.id; + } + + if (!overrides.atlasId) { + const collection = await buildCollection(overrides); + overrides.atlasId = collection.id; + } + + return Document.create({ + title: `Document ${count}`, + text: 'This is the text in an example document', + publishedAt: new Date(), + lastModifiedById: overrides.userId, + createdById: overrides.userId, + ...overrides, + }); +}