Skip to content

Commit

Permalink
feature: search quotes (lukePeavey#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
Mitra Mejia authored Aug 31, 2020
1 parent fdaa052 commit 535c11c
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 13 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ data/*

# Environment variables
.env
.envrc
.env.local
.env.development.local
.env.test.local
Expand All @@ -27,3 +28,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock

# jetbrains
.idea/
46 changes: 35 additions & 11 deletions __tests__/integration.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require('dotenv').config()
const request = require('supertest')
const mongoose = require('mongoose')
const _ = require('lodash')
const range = require('lodash/range')
const app = require('../src/app')
const Quotes = require('../src/models/Quotes')
Expand Down Expand Up @@ -76,30 +77,53 @@ describe('GET /quotes', () => {
})
})

it('with params of "limit=2&skip=1&tags=life,love" should respond successfully', async () => {
const response = await request(app).get(
'/quotes?limit=2&skip=1&tags=life,love'
)
const { status, type, body } = response
it(`With "limit=2&skip=0&tags=inspirational" should respond
successfully`, async () => {
const URL = '/quotes?limit=2&skip=0&tags=inspirational'
const { status, type, body } = await request(app).get(URL)

expect(status).toBe(200)
expect(type).toBe('application/json')
expect(body).toEqual({
expect(body).toEqual(expect.objectContaining({
count: expect.any(Number),
totalCount: expect.any(Number),
lastItemIndex: expect.any(Number),
results: expect.any(Array),
})
}))
expect(body.results[0]).toEqual({
_id: expect.any(String),
author: expect.any(String),
content: expect.any(String),
tags: expect.any(Array),
length: expect.any(Number),
})
expect(body.results[0].tags).toContain('love')
expect(body.results[0].tags).toContain('life')
expect(body.results[0].tags).toContain('inspirational')
})
})

describe('GET /search/quotes', () => {
it(`Response is OK`, async () => {
const query = 'a divided house'
const URL = `/search/quotes?query=${encodeURI(query)}`
const { status, type, body } = await request(app).get(URL)

// Response matches schema
expect(status).toBe(200)
expect(type).toBe('application/json')
expect(body).toEqual(
expect.objectContaining({
count: expect.any(Number),
totalCount: expect.any(Number),
results: expect.any(Array),
})
)
})

// Should either respond with an error or empty results...
it.todo('When called without a `query`...')

it.todo('Returns the correct quote(s) when searching by content')

it.todo('Returns correct quote(s) when searching by author name')
})

describe('GET /authors', () => {
Expand All @@ -121,7 +145,7 @@ describe('GET /quotes/:id', () => {
author: quote.author,
content: quote.content,
tags: expect.any(Array),
length: expect.any(Number)
length: expect.any(Number),
})
})

Expand Down
3 changes: 2 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,5 +182,6 @@ module.exports = {
// watchPathIgnorePatterns: [],

// Whether to use watchman for file crawling
// watchman: true,
// TODO: this should be set to true (default)
watchman: false,
};
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
},
"dependencies": {
"cors": "^2.8.5",
"chalk": "^4.0.0",
"cli-table3": "^0.5.1",
"dotenv": "^8.2.0",
"express": "4.17.1",
"http-errors": "^1.7.2",
Expand All @@ -49,7 +51,6 @@
"devDependencies": {
"@types/jest": "^25.1.4",
"babel-eslint": "^10.1.0",
"cli-table3": "^0.5.1",
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-config-prettier": "^6.10.1",
Expand Down
62 changes: 62 additions & 0 deletions src/controllers/search/searchQuotes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const clamp = require('lodash/clamp')
const omit = require('lodash/omit')
const createError = require('http-errors')
const Quotes = require('../../models/Quotes')

/**
* Search for quotes by content and author
*
* @param {Object} params
* @param {Object} params.query The search query
* @param {number} [params.limit = 20] The maximum number of results to include
* in a single response.
* @param {number} [params.skip = 0] The offset for pagination.
*/
module.exports = async function searchQuotes(req, res, next) {
try {
const { query = '' } = req.query
let { limit = 20, skip = 0 } = req.query

// Use a $text search query to search `content` and `author` fields
// @see https://docs.mongodb.com/manual/reference/operator/query/text
const filter = {
$text: { $search: query },
}

// Add a `score` field that will be used to sort results by text score.
// @see https://docs.mongodb.com/manual/reference/operator/projection/meta
const projection = {
score: { $meta: 'textScore' },
}

// Sorting and pagination params
limit = clamp(parseInt(limit), 0, 50) || 20
skip = parseInt(skip) || 0

const [results, totalCount] = await Promise.all([
Quotes.find(filter, projection)
.sort({ score: { $meta: 'textScore' } })
.skip(skip)
.limit(limit)
.select('-__v -authorId'),

Quotes.countDocuments(filter),
])

// `lastItemIndex` is the offset of the last result returned by this
// request. When paginating through results, this would be used as the
// `skip` parameter when requesting the next page of results. It will be
// set to `null` if there are no additional results.
const lastItemIndex = skip + results.length

// Return a paginated list of quotes to client
res.status(200).json({
count: results.length,
totalCount,
lastItemIndex: lastItemIndex >= totalCount ? null : lastItemIndex,
results: results.map(doc => omit(doc.toJSON(), 'score')),
})
} catch (error) {
return next(error)
}
}
6 changes: 6 additions & 0 deletions src/routes.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { Router } = require('express')
const listQuotes = require('./controllers/quotes/listQuotes')
const getQuoteById = require('./controllers/quotes/getQuoteById')
const searchQuotes = require('./controllers/search/searchQuotes')
const randomQuote = require('./controllers/quotes/randomQuote')
const listAuthors = require('./controllers/authors/listAuthors')
const getAuthorById = require('./controllers/authors/getAuthorById')
Expand All @@ -26,4 +27,9 @@ router.get('/authors/:id', getAuthorById)
**-----------------------------------------------*/
router.get('/tags', listTags)

/**------------------------------------------------
** Search
**-----------------------------------------------*/
router.get('/search/quotes', searchQuotes)

module.exports = router

0 comments on commit 535c11c

Please sign in to comment.