Skip to content

Commit

Permalink
Merge b5e1743 into 1073d22
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesenejo committed Sep 18, 2018
2 parents 1073d22 + b5e1743 commit 5f062ea
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 7 deletions.
97 changes: 97 additions & 0 deletions server/controllers/searchController.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Op } from 'sequelize';
import validator from 'validator';
import models from '../models';
import search from '../helpers/search';

const { escape } = validator;
const { getIds, searcher } = search;
const {
Articles, Users, Tags, Categories, ArticleTags
} = models;

const searchController = (req, res) => {
const {
keyword, author, tag, page
} = req.query;

const currentPage = /^[0-9]+$/.test(page) ? page : 1;
const limit = 10;
const offset = 10 * (currentPage - 1);
const order = [['updatedAt', 'DESC']];

const searchOptions = {
limit, offset, order
};

// Include details from other models
const include = [
{ model: Users, attributes: ['id', 'firstname', 'lastname', 'image'] },
{ model: Categories, as: 'category', attributes: ['name'] }
];

if (keyword) {
const queryOptions = {
where: {
[Op.or]: {
title: { [Op.iLike]: `%${escape(keyword)}%` },
body: { [Op.iLike]: `%${escape(keyword)}%` },
description: { [Op.iLike]: `%${escape(keyword)}%` }
}
},
include,
...searchOptions
};
return searcher(res, Articles, queryOptions, currentPage, 'keyword', escape(keyword));
}

if (tag) {
const where = {
name: {
[Op.iLike]: `%${escape(tag)}%`
}
};

// returns the list of ids of tags matching the query
return getIds(Tags, where)
.then((tagIds) => {
const queryOptions = {
where: { tagId: { [Op.in]: tagIds } },
include: [{
model: Articles,
include
}],
...searchOptions
};
return searcher(res, ArticleTags, queryOptions, currentPage, 'tag', escape(tag));
});
}

if (author) {
const where = {
[Op.or]: {
username: { [Op.iLike]: `%${escape(author)}%` },
firstname: { [Op.iLike]: `%${escape(author)}%` },
lastname: { [Op.iLike]: `%${escape(author)}%` }
}
};

// returns the list of ids of authors matching the query
return getIds(Users, where)
.then((authorIds) => {
const queryOptions = {
where: {
userId: { [Op.in]: authorIds }
},
include,
...searchOptions
};
return searcher(res, Articles, queryOptions, currentPage, 'author', escape(author));
});
}

return res.status(400).jsend.fail({
message: 'Request not understood. You can search with either a keyword, author or tag'
});
};

export default searchController;
39 changes: 39 additions & 0 deletions server/helpers/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const search = {
getIds: (model, where) => model.all({
where,
attributes: ['id']
})
.then(results => results.map(result => result.dataValues.id)),
searcher: (res, model, queryOptions, currentPage, type, searchQuery) => {
const { limit } = queryOptions;

model
.findAndCountAll(queryOptions)
.then((searchResult) => {
const { count } = searchResult;
const totalPages = Math.ceil(count / limit);

if (searchResult.count < 1) {
return res.status(404).jsend.fail({
message: `No results for the ${type} "${searchQuery}"`
});
}

if (currentPage > totalPages) {
return res.status(404).jsend.fail({
message: `End of results for the ${type} "${searchQuery}"`,
searchResult,
pageInfo: { currentPage, totalPages }
});
}
return res.status(200).jsend.success({
message: `Search results for the ${type} "${searchQuery}"`,
searchResult,
pageInfo: { currentPage, totalPages }
});
})
.catch(error => res.status(500).jsend.error(error));
}
};

export default search;
33 changes: 33 additions & 0 deletions server/migrations/20180830143198-create-articletags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('ArticleTags', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
articleId: {
type: Sequelize.INTEGER,
references: {
model: 'Articles',
key: 'id'
}
},
tagId: {
type: Sequelize.INTEGER,
references: {
model: 'Tags',
key: 'id'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}, { freezeTableName: true }),
down: queryInterface => queryInterface.dropTable('ArticleTags')
};
24 changes: 24 additions & 0 deletions server/models/ArticleTags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
const articleTags = (sequelize, DataTypes) => {
const ArticleTags = sequelize.define('ArticleTags', {
articleId: {
type: DataTypes.INTEGER
},
tagId: {
type: DataTypes.INTEGER
}
});

ArticleTags.associate = (models) => {
ArticleTags.belongsTo(models.Articles, {
foreignKey: 'articleId'
});

ArticleTags.belongsTo(models.Tags, {
foreignKey: 'tagId',
});
};

return ArticleTags;
};

export default articleTags;
2 changes: 1 addition & 1 deletion server/models/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const articles = (sequelize, DataTypes) => {

Articles.belongsToMany(models.Tags, {
as: 'articleTags',
through: 'Tagging',
through: 'ArticleTags',
foreignKey: 'articleId'
});

Expand Down
2 changes: 1 addition & 1 deletion server/models/tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const tags = (sequelize, DataTypes) => {
Tags.associate = (models) => {
Tags.belongsToMany(models.Articles, {
as: 'tagArticle',
through: 'Tagging',
through: 'ArticleTags',
foreignKey: 'tagId'
});
};
Expand Down
6 changes: 6 additions & 0 deletions server/models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ const users = (sequelize, DataTypes) => {
allowNull: false,
unique: true
},
firstname: {
type: DataTypes.STRING
},
lastname: {
type: DataTypes.STRING
},
email: {
type: DataTypes.STRING,
allowNull: false,
Expand Down
3 changes: 2 additions & 1 deletion server/routes/articleRoutes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express from 'express';
import articleController from '../controllers/articleController';
import commentController from '../controllers/commentController';
import likeController from '../controllers/likeController';
import searchController from '../controllers/searchController';
import auth from '../middleware/auth';
import ratingValidation from '../middleware/ratingValidation';
import inputValidator from '../middleware/inputValidator';
Expand Down Expand Up @@ -60,5 +61,5 @@ articleRoutes.post(
validArticleId,
reportArticle
);

articleRoutes.get('/search', searchController);
export default articleRoutes;
28 changes: 28 additions & 0 deletions server/seeders/20180909082852-demo-article.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 25 additions & 0 deletions server/seeders/20180909083506-demo-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module.exports = {
up: queryInterface => queryInterface.bulkInsert('Tags', [{
name: 'SomethingFishy',
createdAt: '2018-09-09',
updatedAt: '2018-09-09'
}, {
name: 'CodeForALiving',
createdAt: '2018-09-09',
updatedAt: '2018-09-09'
}, {
name: 'IAmMe',
createdAt: '2018-09-09',
updatedAt: '2018-09-09'
}, {
name: 'Purposefulness',
createdAt: '2018-09-09',
updatedAt: '2018-09-09'
}, {
name: 'GoalGetter',
createdAt: '2018-09-09',
updatedAt: '2018-09-09'
}]),

down: queryInterface => queryInterface.bulkDelete('Tags')
};
21 changes: 21 additions & 0 deletions server/seeders/20180910285904-demo-articletags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module.exports = {
up: queryInterface => queryInterface.bulkInsert('ArticleTags', [{
articleId: 1,
tagId: 1,
createdAt: '2018-09-08',
updatedAt: '2018-09-08'
},
{
articleId: 2,
tagId: 2,
createdAt: '2018-09-08',
updatedAt: '2018-09-08'
},
{
articleId: 2,
tagId: 1,
createdAt: '2018-09-08',
updatedAt: '2018-09-08'
}]),
down: queryInterface => queryInterface.bulkDelete('ArticleTags')
};
32 changes: 28 additions & 4 deletions swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -451,11 +451,9 @@
}
}
},
"/api/v1/articles/:articleId/comments/:commentId/like": {
"/api/v1/articles/:articleId/comments/like": {
"post": {
"tags": [
"Like"
],
"tags": [ "Like" ],
"summary": "Route for authenticated users to like or unlike a comments on an article",
"description": "Returns a confirmation is like or unlike is successful",
"parameters": [
Expand Down Expand Up @@ -721,6 +719,32 @@
}
}
}
},
"/api/v1/articles/search?{query}": {
"post": {
"tags": [ "Articles" ],
"summary": "Route for authenticated users to post comments on an article",
"description": "Returns the user and the user's comment",
"parameters": [
{
"in": "query",
"name": "query",
"description": "The query string helps the user filter search results",
"required": true
}
],
"responses": {
"200": {
"description": "Search successfully returns results"
},
"400": {
"description": "bad request; empty query string"
},
"400": {
"description": "invalid query; invalid page number"
}
}
}
}
},
"definitions": {
Expand Down
Loading

0 comments on commit 5f062ea

Please sign in to comment.