Skip to content

Commit

Permalink
Merge pull request #45 from andela/ft-implement-search-functionality-…
Browse files Browse the repository at this point in the history
…167392449

#167392449 Implement Search Functionality
  • Loading branch information
segunolalive committed Jul 26, 2019
2 parents 000a3e1 + 91dd531 commit 49b1fa2
Show file tree
Hide file tree
Showing 17 changed files with 473 additions and 14 deletions.
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ env:
- NODE_ENV=test
- JWT_SECRET=prfhdfdh
before_script:
- psql -c 'create database errorswag;' -U postgres
- psql -c "CREATE USER kifaru WITH PASSWORD null;" -U postgres
- psql -c 'create database errorswag;' -U postgres
- psql -U postgres -d errorswag -c 'CREATE EXTENSION pg_trgm'
- npm run build
- npm install -g sequelize-cli
- sequelize db:migrate
Expand Down
38 changes: 38 additions & 0 deletions src/controllers/search.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import BaseRepository from '../repository/base.repository';
import db from '../database/models';
import responseGenerator from '../helpers/responseGenerator';

/**
* @description class representing Article Controller
* @class SearchController
*/
class SearchController {
/**
* @description - This method is responsible for searching articles by author
* @static
* @param {object} req - Req sent to the router
* @param {object} res - Response sent from the controller
* @returns {object} - object representing response messages
* @memberof SearchController
*/
static async generateSearchQuery(req, res) {
try {
let searchResult;
if (req.query.search) {
const { search } = req.query;
const searchModified = search.toLowerCase();
searchResult = await BaseRepository.searchAll(searchModified);
return responseGenerator.sendSuccess(res, 200, searchResult, null);
}
searchResult = await BaseRepository.findAndCountAll(db.Article, {
attributes: ['id', 'authorId', 'title', 'body', 'image', 'status']
});
const { rows } = searchResult;
return responseGenerator.sendSuccess(res, 200, rows, null);
} catch (error) {
return responseGenerator.sendError(res, 500, error.message);
}
}
}

export default SearchController;
28 changes: 18 additions & 10 deletions src/controllers/user.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,17 +200,25 @@ class UserController {
*/
static async updateProfile(req, res) {
try {
const { avatar, bio } = req.body;
const { firstname, lastname, avatar, bio } = req.body;
const { id: userId } = req.currentUser;

await BaseRepository.update(db.User, { avatar, bio }, { id: userId });

return responseGenerator.sendSuccess(
res,
200,
null,
'Record successfully updated'
);
const userExist = await BaseRepository.findOneByField(db.User, {
id: userId
});
if (userExist) {
await BaseRepository.update(
db.User,
{ firstname, lastname, avatar, bio },
{ id: userId }
);
return responseGenerator.sendSuccess(
res,
200,
null,
'Record successfully updated'
);
}
return responseGenerator.sendSuccess(res, 404, null, 'User not found');
} catch (error) {
return responseGenerator.sendError(res, 500, error.message);
}
Expand Down
6 changes: 6 additions & 0 deletions src/database/migrations/20190702165504-create-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ module.exports = {
type: Sequelize.STRING,
unique: true
},
firstname: {
type: Sequelize.STRING
},
lastname: {
type: Sequelize.STRING
},
avatar: {
type: Sequelize.STRING
},
Expand Down
6 changes: 5 additions & 1 deletion src/database/migrations/20190711185759-create-article.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ module.exports = {
defaultValue: 'active'
},
authorId: {
type: Sequelize.INTEGER
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'id'
}
},
slug: {
type: Sequelize.STRING,
Expand Down
29 changes: 29 additions & 0 deletions src/database/migrations/20190719125528-create-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Tags', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING,
unique: true
},
createdAt: {
allowNull: false,
defaultValue: new Date(),
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
defaultValue: new Date(),
type: Sequelize.DATE
}
});
},
down: (queryInterface /* , Sequelize */) => {
return queryInterface.dropTable('Tags');
}
};
40 changes: 40 additions & 0 deletions src/database/migrations/20190722031409-create-article-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('ArticleTags', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
articleId: {
type: Sequelize.INTEGER,
primaryKey: true,
references: {
model: 'Articles',
key: 'id'
}
},
tagId: {
type: Sequelize.INTEGER,
primaryKey: true,
references: {
model: 'Tags',
key: 'id'
}
},
createdAt: {
allowNull: false,
defaultValue: new Date(),
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
defaultValue: new Date(),
type: Sequelize.DATE
}
});
},

down: queryInterface => queryInterface.dropTable('ArticleTags')
};
9 changes: 9 additions & 0 deletions src/database/models/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ export default (sequelize, DataTypes) => {
as: 'articleRatings',
foreignKey: 'articleId'
});
Article.belongsTo(models.User, {
foreignKey: 'authorId',
as: 'author'
});
Article.belongsToMany(models.Tags, {
as: 'tags',
through: 'ArticleTags',
foreignKey: 'articleId'
});
};
return Article;
};
12 changes: 12 additions & 0 deletions src/database/models/articletags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module.exports = (sequelize, DataTypes) => {
const ArticleTags = sequelize.define(
'ArticleTags',
{
articleId: DataTypes.INTEGER,
tagId: DataTypes.INTEGER
},
{}
);
ArticleTags.associate = () => {};
return ArticleTags;
};
23 changes: 23 additions & 0 deletions src/database/models/tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
module.exports = (sequelize, DataTypes) => {
const Tags = sequelize.define(
'Tags',
{
name: {
type: DataTypes.STRING,
allowNull: false,
unique: {
args: true
}
}
},
{}
);
Tags.associate = models => {
Tags.belongsToMany(models.Article, {
through: 'ArticleTags',
as: 'Articles',
foreignKey: 'tagId'
});
};
return Tags;
};
9 changes: 8 additions & 1 deletion src/database/models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.STRING,
unique: true
},
firstname: DataTypes.STRING,
lastname: DataTypes.STRING,
avatar: DataTypes.STRING,
bio: DataTypes.STRING,
password: DataTypes.STRING,
Expand Down Expand Up @@ -70,6 +72,11 @@ module.exports = (sequelize, DataTypes) => {
as: 'articleId'
});
};

User.associate = models => {
User.hasMany(models.Article, {
foreignKey: 'authorId',
as: 'Article'
});
};
return User;
};
49 changes: 49 additions & 0 deletions src/repository/base.repository.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
import dotenv from 'dotenv';
import Sequelize from 'sequelize';

dotenv.config();

const sequelize = new Sequelize({
username: process.env.DB_TEST_USERNAME,
password: process.env.DB_TEST_PASSWORD || '',
database: process.env.DB_TEST_DATABASE,
host: process.env.DB_TEST_HOST,
port: process.env.DB_PORT,
dialect: 'postgres'
});

/**
* @class BaseRepository
*/
Expand Down Expand Up @@ -185,6 +199,41 @@ class BaseRepository {
static findOne(model, options) {
return model.findByPk(options);
}

/**
*
*
* @static
* @param {object} searchModified
* @returns {object} - returns a database object
* @memberof BaseRepository
*/
static async searchAll(searchModified) {
return sequelize.query(
`SELECT a.id, a.title, a.body, a.description, u.username,
(similarity(?, u.username)) as user_score,
(similarity(?, a.title)) as title_score,
(similarity(?, tg.name)) as tag_score
from "Articles" a
JOIN "Users" u ON u.id = a."authorId"
JOIN "ArticleTags" at1 ON at1."articleId" = a.id
JOIN "Tags" tg ON tg.id = at1."tagId"
WHERE
a."publishedDate" is not null
and a.status = 'active' and a."publishedDate" <= NOW()
and (u.username ilike ?
or a.title ilike ?
or tg.name ilike ?)
ORDER BY user_score, title_score, tag_score desc
LIMIT 10 OFFSET 0`,
{
replacements: new Array(3)
.fill(searchModified)
.concat(new Array(3).fill(`%${searchModified}%`)),
type: sequelize.QueryTypes.SELECT
}
);
}
}

export default BaseRepository;
2 changes: 2 additions & 0 deletions src/routes/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import authRoutes from './auth.route';
import userRoutes from './user.route';
import articleRoutes from './article.route';
import ratingRoutes from './rating.route';
import searchRoute from './search.route';

export default app => {
app.use('/auth', authRoutes);
app.use('/api/v1/users', userRoutes);
app.use('/api/v1/auth', userRoutes);
app.use('/api/v1/articles', articleRoutes);
app.use('/api/v1/articles', ratingRoutes);
app.use('/api/v1', searchRoute);
};
8 changes: 8 additions & 0 deletions src/routes/v1/search.route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Router } from 'express';
import SearchController from '../../controllers/search.controller';

const router = Router();

router.get('/search', SearchController.generateSearchQuery);

export default router;
Loading

0 comments on commit 49b1fa2

Please sign in to comment.