Skip to content

Commit

Permalink
#163446771 Allow a User to Search by Tag and Title on Article (#39)
Browse files Browse the repository at this point in the history
* feat(search): allow search articles by author

- create search route
- create search controller
- add new line to test file
- update yaml file
- modify user model
- allow a user to search by tags and words
- write test

finishes: 163405801
  • Loading branch information
tejiri4 authored and seunkoko committed Jan 29, 2019
1 parent b48d6c8 commit f166a24
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 16 deletions.
8 changes: 6 additions & 2 deletions server/controllers/ArticleController.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,9 +314,13 @@ class ArticleController {
* @returns {object} api route response
*/
static async search(req, res) {
const { author } = req.query;
const { author, tag, title } = req.query;
if (author) {
SearchController.byAuthor(author, res);
SearchController.byAuthor(author, req, res);
} else if (tag) {
SearchController.byTags(tag, req, res);
} else if (title) {
SearchController.byTitle(title, req, res);
} else {
return response(
res,
Expand Down
130 changes: 120 additions & 10 deletions server/controllers/SearchController.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Sequelize from 'sequelize';
import response from '../helpers/response';
import db from '../models';
import pagination from '../helpers/pagination';


const { User } = db;
const {
User, Tag, Article
} = db;
const { Op } = Sequelize;
/**
* @class SearchController
Expand All @@ -13,37 +15,145 @@ class SearchController {
* @static
* @description a function that handles searching articles by author
* @param {String} author
* @param {object} req HTTP request object
* @param {object} res HTTP response object
* @returns {object} api route response with created article info
*/
static async byAuthor(author, res) {
static async byAuthor(author, req, res) {
try {
const findAuthor = await User.findAll({
const { query } = req;
const limit = Number(query.limit) || 20;
const currentPage = Number(query.page) || 1;
const offset = (currentPage - 1) * limit;
const findAuthor = await User.findAndCountAll({
where: {
[Op.or]: [
{
userName: {
[Op.like]: `%${author}%`
[Op.iLike]: `%${author}%`
}
},
{
fullName: {
[Op.like]: `%${author}%`
[Op.iLike]: `%${author}%`
}
}
]
},
attributes: ['userName', 'fullName', 'bio', 'img']
attributes: ['id', 'userName', 'fullName', 'bio', 'img'],
limit,
offset
});
if (findAuthor.length > 0) {
return response(res, 200, 'success', 'Author found', null, findAuthor);
const totalAuthor = findAuthor.count;
const paginatedData = pagination(findAuthor.rows.length, limit, currentPage, totalAuthor);
const data = {
authors: findAuthor,
paginatedData
};
if (totalAuthor > 0) {
return response(res, 200, 'success', 'Author found', null, data);
}
return response(res, 404, 'failure', 'Author not found', null, null);
} catch (error) {
return response(res, 500, 'failure', 'Something went wrong on the server', null, null);
}
}
}

/**
* @static
* @description a function that handles searching articles by author
* @param {String} tag
* @param {object} req HTTP request object
* @param {object} res HTTP response object
* @returns {object} api route response with created article info
*/
static async byTags(tag, req, res) {
try {
const { query } = req;
const limit = Number(query.limit) || 20;
const currentPage = Number(query.page) || 1;
const offset = (currentPage - 1) * limit;
const findTag = await Tag.findAndCountAll({
where: {
name: {
[Op.iLike]: `%${tag}%`
}
},
attributes: ['id', 'name'],
include:
[{
model: Article,
as: 'articles',
attributes: ['id', 'slug', 'title', 'content'],
through: {
attributes: []
}
}],
limit,
offset
});
const totalTag = findTag.count;
const paginatedData = pagination(findTag.rows.length, limit, currentPage, totalTag);
const data = {
tags: findTag,
paginatedData
};
if (totalTag > 0) {
return response(res, 200, 'success', 'Tag found', null, data);
}
return response(res, 404, 'failure', 'Tag not found', null, null);
} catch (error) {
return response(res, 500, 'failure', 'Something went wrong on the server', null, error);
}
}

/**
* @static
* @description a function that handles searching articles by author
* @param {String} title
* @param {object} req HTTP request object
* @param {object} res HTTP response object
* @returns {object} api route response with created article info
*/
static async byTitle(title, req, res) {
try {
const { query } = req;
const limit = Number(query.limit) || 20;
const currentPage = Number(query.page) || 1;
const offset = (currentPage - 1) * limit;
const findArticles = await Article.findAndCountAll({
where: {
title: {
[Op.iLike]: `%${title}%`
}
},
attributes: { exclude: ['userId'] },
include: [
{
model: Tag,
as: 'tags',
attributes: ['name'],
through: { attributes: [] }
}
],
offset,
limit
});

const totalArticle = findArticles.count;
const paginatedData = pagination(findArticles.rows.length, limit, currentPage, totalArticle);
const data = {
articles: findArticles,
paginatedData
};
if (totalArticle > 0) {
return response(res, 200, 'success', 'Article found', null, data);
}
return response(res, 404, 'failure', 'Article not found', null, null);
} catch (error) {
return response(res, 500, 'failure', 'Something went wrong on the server', null, null);
}
}
}

export default SearchController;
15 changes: 12 additions & 3 deletions server/models/articleTag.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@ export default (sequelize, DataTypes) => {
tagId: {
type: DataTypes.UUID,
allowNull: false
}
},
{}
},
}, {}
);
ArticleTag.associate = (models) => {
ArticleTag.belongsTo(models.Article, {
foreignKey: 'articleId',
onDelete: 'CASCADE'
});
ArticleTag.belongsTo(models.Tag, {
foreignKey: 'tagId',
onDelete: 'CASCADE'
});
};
return ArticleTag;
};
13 changes: 13 additions & 0 deletions server/seeders/20190124084130-demoTag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export default {
up: (queryInterface, Sequelize) => queryInterface.bulkInsert('Tags', [{
id: '00143c60-7b1a-11e8-9c9c-2d42b21b1a3e',
name: 'Politics'
},
{
id: '00155c60-6b1a-11e8-9c9c-2d42b21b1a3e',
name: 'Technology'
}
], {}),

down: (queryInterface, Sequelize) => queryInterface.bulkDelete('Tags', null, {})
};
10 changes: 10 additions & 0 deletions server/seeders/20190124084901-demoArticleTag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export default {
up: (queryInterface, Sequelize) => queryInterface.bulkInsert('ArticleTags', [{
id: '00143c60-7b1a-11e8-9c9c-2d42b21b1a3e',
articleId: '95745c60-7b1a-11e8-9c9c-2d42b21b1a3e',
tagId: '00143c60-7b1a-11e8-9c9c-2d42b21b1a3e'
}
], {}),

down: (queryInterface, Sequelize) => queryInterface.bulkDelete('ArticleTags', null, {})
};
36 changes: 35 additions & 1 deletion server/test/controllers/searchController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,22 @@ describe('Search Model', () => {
expect(response.status).to.eqls(200);
expect(response.body.status).to.eqls('success');
});
it('It should be to search articles by tags', async () => {
const response = await chai
.request(app)
.get('/api/v1/search?tag=technology')
.set('Authorization', `Bearer ${token}`)
expect(response.status).to.eqls(200);
expect(response.body.status).to.eqls('success');
});
it('It should be to search articles by title or content', async () => {
const response = await chai
.request(app)
.get('/api/v1/search?title=to')
.set('Authorization', `Bearer ${token}`)
expect(response.status).to.eqls(200);
expect(response.body.status).to.eqls('success');
});
it('It should not be able to search for authors if inputed value dont exits ', async () => {
const response = await chai
.request(app)
Expand All @@ -29,6 +45,24 @@ describe('Search Model', () => {
expect(response.body.status).to.eqls('failure');
expect(response.body.data.message).to.eqls('Author not found');
});
it('It should not be able to search for tags if inputed value dont exits ', async () => {
const response = await chai
.request(app)
.get('/api/v1/search?tag=3')
.set('Authorization', `Bearer ${token}`)
expect(response.status).to.eqls(404);
expect(response.body.status).to.eqls('failure');
expect(response.body.data.message).to.eqls('Tag not found');
});
it('It should not be able to search for articles if inputed value dont exits ', async () => {
const response = await chai
.request(app)
.get('/api/v1/search?title=*')
.set('Authorization', `Bearer ${token}`)
expect(response.status).to.eqls(404);
expect(response.body.status).to.eqls('failure');
expect(response.body.data.message).to.eqls('Article not found');
});
it('It should not be able to search for authors if inputed value is empty', async () => {
const response = await chai
.request(app)
Expand All @@ -46,9 +80,9 @@ describe('Search Model', () => {
.request(app)
.get('/api/v1/search?author=s')
.set('Authorization', `Bearer ${token}`)
console.log(response.body)
expect(response.body.data.statusCode).to.equal(500);
stub.restore();
});

});
});

0 comments on commit f166a24

Please sign in to comment.