-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #43 from andela/ft-search-functionality-166816116
#166816116 User can search and filter articles
- Loading branch information
Showing
13 changed files
with
632 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,5 +8,7 @@ | |
} | ||
} | ||
], | ||
] | ||
], | ||
"sourceMaps": true, | ||
"retainLines": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* eslint-disable no-nested-ternary */ | ||
import searchBy from '../services/search.service'; | ||
import Helper from '../services/helper'; | ||
|
||
export default { | ||
/** | ||
* @method searchFilter | ||
* Handles the logic for filtering and searching based on user input | ||
* @param {object} request | ||
* @param {object} response | ||
* @param {function} next | ||
* @returns {object|function} API response object | ||
*/ | ||
|
||
async searchFilter(request, response, next) { | ||
try { | ||
const { tag, author, title } = request.query; | ||
let searchResult; | ||
switch (Object.keys(request.query).length > 1) { | ||
case true: | ||
searchResult = await searchBy('multiple', request.query); | ||
break; | ||
default: | ||
searchResult = author | ||
? await searchBy('author', author) | ||
: tag | ||
? await searchBy('tag', tag) | ||
: title | ||
? await searchBy('title', title) | ||
: undefined; | ||
} | ||
if (!searchResult) { | ||
return Helper.failResponse(response, 400, { | ||
message: `Invalid filter provided. You can only filter by 'tag', 'title' or 'author'` | ||
}); | ||
} | ||
return Helper.successResponse(response, 200, searchResult); | ||
} catch (error) { | ||
next(error); | ||
} | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import Helper from '../services/helper'; | ||
|
||
export default { | ||
/** | ||
* @method searchFilterCheck | ||
* Validates incoming request parameters on searching and filtering. | ||
* @param {object} request | ||
* @param {object} response | ||
* @param {function} next | ||
* @returns {object|function} API response object | ||
*/ | ||
|
||
async searchFilterCheck(request, response, next) { | ||
const filterParams = Object.keys(request.query); | ||
if (filterParams.length < 1) { | ||
return Helper.failResponse(response, 400, { | ||
message: `Filter parameters cannot be empty` | ||
}); | ||
} | ||
const errors = []; | ||
const { tag, author, title } = request.query; | ||
|
||
if (filterParams.includes('tag')) { | ||
if (!tag) { | ||
errors.push('Tag cannot be empty'); | ||
} | ||
request.query.tag = tag.toLowerCase().trim(); | ||
} | ||
|
||
if (filterParams.includes('author')) { | ||
if (!author) { | ||
errors.push('Author cannot be empty'); | ||
} | ||
request.query.author = author.toLowerCase().trim(); | ||
} | ||
|
||
if (filterParams.includes('title')) { | ||
if (!title) { | ||
errors.push('Title cannot be empty'); | ||
} | ||
request.query.title = title.toLowerCase().trim(); | ||
} | ||
|
||
if (errors.length > 0) { | ||
return Helper.failResponse(response, 400, { message: errors[0] }); | ||
} | ||
next(); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import express from 'express'; | ||
import searchController from '../../controllers/search.controller'; | ||
import filter from '../../middlewares/search.middleware'; | ||
|
||
const { searchFilterCheck } = filter; | ||
const { searchFilter } = searchController; | ||
const router = express.Router(); | ||
router.get('/', searchFilterCheck, searchFilter); | ||
export default router; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
/* eslint-disable default-case */ | ||
import Sequelize from 'sequelize'; | ||
import model from '../db/models'; | ||
|
||
const { Op } = Sequelize; | ||
const { Article, User, Tag } = model; | ||
|
||
/** | ||
* @method setAuthorQuery | ||
* Sets and applies filter query to the User model association | ||
* @param {string} value filter/search value | ||
* @returns {object} User model association | ||
*/ | ||
|
||
const setAuthorQuery = async value => { | ||
return { | ||
model: User, | ||
as: 'author', | ||
where: { | ||
[Op.or]: [ | ||
{ firstName: { [Op.iLike]: `%${value}%` } }, | ||
{ lastName: { [Op.iLike]: `%${value}%` } }, | ||
{ userName: { [Op.iLike]: `%${value}%` } } | ||
] | ||
} | ||
}; | ||
}; | ||
|
||
/** | ||
* @method setTagQuery | ||
* Sets and applies filter query to the Tag model association | ||
* @param {string} value filter/search value | ||
* @returns {object} Tag model association | ||
*/ | ||
|
||
const setTagQuery = async value => { | ||
return { | ||
model: Tag, | ||
as: 'Tags', | ||
where: { | ||
name: { [Op.iLike]: `%${value}%` } | ||
} | ||
}; | ||
}; | ||
|
||
/** | ||
* @method setMessage | ||
* sets response message based on search result | ||
* @param {array} searchResult | ||
* @returns {string} result message | ||
*/ | ||
|
||
const setMessage = async searchResult => { | ||
let message = `No Article match found`; | ||
if (searchResult.length > 0) { | ||
message = `${searchResult.length} Article match(es) found`; | ||
} | ||
return message; | ||
}; | ||
|
||
/** | ||
* @method getSearchResult | ||
* Handles the logic for querying the database based on search input/filter provided | ||
* @param {boolean} multiple if number of search entry is more than one | ||
* @param {object|array} queryParams object of search entry query or array of search entry query objects | ||
* @param {boolean} hasTitle if title filter is applied to search entry | ||
* @returns {array} array of instance(s) of article(s) found based on filter applied | ||
*/ | ||
|
||
const getSearchResult = async (multiple, queryParams, hasTitle) => { | ||
let include; | ||
|
||
const titleValue = | ||
Array.isArray(queryParams) && hasTitle | ||
? queryParams.shift().title | ||
: queryParams.title; | ||
|
||
if (!multiple && hasTitle) include = undefined; | ||
if (!multiple && !hasTitle) include = [queryParams]; | ||
if (multiple) include = queryParams; | ||
|
||
const searchResult = await Article.findAll({ | ||
where: !hasTitle | ||
? { isPublished: true } | ||
: { | ||
title: { | ||
[Op.iLike]: `%${titleValue}%` | ||
}, | ||
isPublished: true | ||
}, | ||
include | ||
}); | ||
|
||
return searchResult; | ||
}; | ||
|
||
/** | ||
* @method formatResponse | ||
* Handles the logic for formatting search results | ||
* @param {array} searchResult | ||
* @returns {object} formatted response object | ||
*/ | ||
|
||
const formatResponse = async searchResult => { | ||
const mappedResult = searchResult.map(result => { | ||
const author = result.author | ||
? { | ||
firstName: result.author.firstName, | ||
lastName: result.author.lastName, | ||
userName: result.author.userName, | ||
email: result.author.email, | ||
image: result.author.image | ||
} | ||
: undefined; | ||
const tags = result.Tags | ||
? { | ||
name: result.Tags.map(tag => tag.name) | ||
} | ||
: undefined; | ||
|
||
const response = { | ||
id: result.id, | ||
title: result.title, | ||
slug: result.slug, | ||
description: result.description, | ||
body: result.body, | ||
image: result.image, | ||
viewsCount: result.viewsCount, | ||
readTime: result.readTime, | ||
averageRating: result.averageRating, | ||
sumOfRating: result.sumOfRating, | ||
publishedAt: result.publishedAt, | ||
author, | ||
tags | ||
}; | ||
return response; | ||
}); | ||
return mappedResult; | ||
}; | ||
|
||
/** | ||
* @method searchBy | ||
* Handles the logic for filtering and searching based on parameters provided | ||
* @param {string} filterType type of filter applied | ||
* @param {string|object} value filter value or object containing filter values | ||
* @returns {object} search result object | ||
*/ | ||
|
||
const searchBy = async (filterType, value) => { | ||
let queryObject; | ||
let result; | ||
let message; | ||
let hasTitle = false; | ||
let isMultiple = false; | ||
const queryObjectArray = []; | ||
switch (filterType) { | ||
case 'tag': | ||
queryObject = await setTagQuery(value); | ||
result = await getSearchResult(isMultiple, queryObject, hasTitle); | ||
message = await setMessage(result); | ||
break; | ||
case 'author': | ||
queryObject = await setAuthorQuery(value); | ||
result = await getSearchResult(isMultiple, queryObject, hasTitle); | ||
message = await setMessage(result); | ||
break; | ||
case 'title': | ||
hasTitle = true; | ||
queryObject = { title: value }; | ||
result = await getSearchResult(isMultiple, queryObject, hasTitle); | ||
message = await setMessage(result); | ||
break; | ||
case 'multiple': | ||
isMultiple = true; | ||
if ('title' in value) { | ||
queryObjectArray.push({ title: value.title }); | ||
hasTitle = true; | ||
} | ||
if ('author' in value) { | ||
const authorQuery = await setAuthorQuery(value.author); | ||
queryObjectArray.push(authorQuery); | ||
} | ||
if ('tag' in value) { | ||
const tagQuery = await setTagQuery(value.tag); | ||
queryObjectArray.push(tagQuery); | ||
} | ||
|
||
result = await getSearchResult(isMultiple, queryObjectArray, hasTitle); | ||
message = await setMessage(result); | ||
break; | ||
} | ||
|
||
const searchResult = await formatResponse(result); | ||
return { message, searchResult }; | ||
}; | ||
|
||
export default searchBy; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.