Skip to content

Commit

Permalink
166816219-feat(search): implement search feature
Browse files Browse the repository at this point in the history
- create search routes
- write test
- create swagger documentation

[Delivers #166816219]
  • Loading branch information
caleb-42 authored and kevoese committed Jul 17, 2019
1 parent 8d026bd commit 91994be
Show file tree
Hide file tree
Showing 14 changed files with 220 additions and 30 deletions.
10 changes: 3 additions & 7 deletions controllers/articles/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,9 @@ export default {
as: 'author',
attributes: ['username', 'bio', 'image']
}, {
model: db.ArticleTag,
as: 'articleTag',
include: [{
model: db.Tag,
as: 'tag',
attributes: ['name']
}]
model: db.Tag,
as: 'tags',
attributes: ['name']
}]
});

Expand Down
57 changes: 57 additions & 0 deletions controllers/search/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import Sequelize from 'sequelize';
import db from '../../db/models';

const { Op } = Sequelize;
let include = [];
const setInclude = association => [...include, association];

export const tagFilter = async (req, res, next) => {
const { tag } = req.query;

const tagObj = {
model: db.Tag,
as: 'tags',
};

include = setInclude(
tag ? {
...tagObj,
where: {
name: { [Op.iLike]: `%${tag}%` }
}
} : tagObj
);
return next();
};

export const authorFilter = async (req, res, next) => {
const { author } = req.query;
include = setInclude({
model: db.User,
as: 'author',
where:
(author) ? {
[Op.or]: [
{ firstName: { [Op.iLike]: `%${author}%` } },
{ lastName: { [Op.iLike]: `%${author}%` } },
{ username: { [Op.iLike]: `%${author}%` } }
]
}
: {},
attributes: ['username', 'firstName', 'lastName']
});
return next();
};

export const end = async (req, res) => {
const { title } = req.query;
const search = await db.Article.findAll(
{
where: !title ? {} : { title: { [Op.iLike]: `%${title}%` } },
include
}
);

include = [];
res.status(200).json({ search });
};
7 changes: 6 additions & 1 deletion db/models/Article.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ module.exports = (sequelize, DataTypes) => {
as: 'author',
});

Article.belongsToMany(models.Tag, {
through: 'ArticleTag',
foreignKey: 'articleId',
as: 'tags'
});

Article.hasMany(models.ArticleVote, {
foreignKey: 'articleId',
as: 'articleVote',
Expand All @@ -45,6 +51,5 @@ module.exports = (sequelize, DataTypes) => {
cascade: true,
});
};

return Article;
};
6 changes: 2 additions & 4 deletions db/models/ArticleTag.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ module.exports = (sequelize) => {
}, {});
ArticleTag.associate = (models) => {
ArticleTag.belongsTo(models.Article, {
foreignKey: 'articleId',
as: 'article'
foreignKey: 'articleId'
});

ArticleTag.belongsTo(models.Tag, {
foreignKey: 'tagId',
as: 'tag'
foreignKey: 'tagId'
});
};
return ArticleTag;
Expand Down
1 change: 1 addition & 0 deletions db/models/Tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = (sequelize, DataTypes) => {
as: 'articleTag',
cascade: true
});
Tag.belongsToMany(models.Article, { through: 'ArticleTag', foreignKey: 'tagId', as: 'article' });
};
return Tag;
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"seed": "sequelize db:seed:all",
"coverage": "nyc report --reporter=text-lcov | coveralls",
"heroku-postbuild": " npm run migrate",
"dev:test": "NODE_ENV=test npm run rollback:migration && npm test"
"dev:test": "NODE_ENV=test npm run rollback:migration && npm test"
},
"nyc": {
"exclude": [
Expand Down
2 changes: 2 additions & 0 deletions routes/v1/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import profiles from './profiles';
import notification from './notifications';
import members from './members';
import role from './role';
import search from './search';

const router = express.Router();

Expand All @@ -17,5 +18,6 @@ router.use('/articles', article);
router.use('/notifications', notification);
router.use('/members', members);
router.use('/role', role);
router.use('/search', search);

export default router;
13 changes: 13 additions & 0 deletions routes/v1/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import express from 'express';
import * as Search from '../../controllers/search';
import Validator from '../../validators/search';

const router = express.Router();

router.get('/',
Validator.search,
Search.tagFilter,
Search.authorFilter,
Search.end);

export default router;
30 changes: 30 additions & 0 deletions swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,36 @@ paths:
description: Input your content
404:
description: Article does not exist
/search:
get:
tags:
- Search
summary: Get search results for author, articles or tags
operationId: search
consumes:
- application/json
- application/x-www-form-urlencoded
produces:
- application/json
parameters:
- name: author
in: query
description: filters out authors from the search result
required: false
type: string
- name: tag
in: query
description: filters out tags from the search result
required: false
type: string
- name: title
in: query
description: filters out articles from the search result
required: false
type: string
responses:
'200':
description: an object containg search result
definitions:
Notify:
type: object
Expand Down
3 changes: 3 additions & 0 deletions tests/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ export const createTestFakeUsers = () => {
await createUser(user);
});
};
export const createTag = async tag => db.Tag.create(tag);

export const createArticleTag = async tag => db.ArticleTag.create(tag);
2 changes: 1 addition & 1 deletion tests/routes/profiles.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import chai from 'chai';
import chaiHttp from 'chai-http';
import sinon from 'sinon';
import { app, db } from '../../server';
import { createUser } from '../../utils';
import { createUser } from '../helpers';
import { transporter } from '../../utils/mailer';

const { expect } = chai;
Expand Down
72 changes: 72 additions & 0 deletions tests/routes/search.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import chai from 'chai';
import chaiHttp from 'chai-http';
import { app, db } from '../../server';
import {
createUser, createArticle, createTag, createArticleTag
} from '../helpers';

const { expect } = chai;

chai.use(chaiHttp);
let user, article, newUser, newArticle, newTag = {};
describe('USER PROFILE', () => {
before(async () => {
await db.Article.destroy({ truncate: true, cascade: true });
user = {
firstName: 'vincent',
lastName: 'hamza',
username: 'damy',
password: '12345678',
email: 'pronew@gmail.com'
};
article = {
title: 'React course by hamza',
description: 'very good book',
body: 'learning react is good for your career...'
};
newUser = await createUser(user);
newArticle = await createArticle({ ...article, authorId: newUser.id });
newTag = await createTag({ name: 'react' });
await createArticleTag({ articleId: newArticle.id, tagId: newTag.id });
});
after(async () => {
await db.Article.destroy({ truncate: true, cascade: true });
});
describe('SEARCH', () => {
it('Should not get articles if no filter is entered', async () => {
const res = await chai
.request(app)
.get('/api/v1/search');
expect(res.body).to.be.an('object');
expect(res.body).to.include.all.keys('message');
expect(res.body.message).to.be.an('array');
});
it('Should get article with tag filter entered', async () => {
const res = await chai
.request(app)
.get('/api/v1/search?tag=react');
expect(res.body).to.be.an('object');
expect(res.body.search).to.be.an('array');
expect(res.body.search[0]).to.be.an('object');
expect(res.body.search[0].tags[0].name).to.include('react');
});
it('Should get article with author filter entered', async () => {
const res = await chai
.request(app)
.get('/api/v1/search?author=vincent');
expect(res.body).to.be.an('object');
expect(res.body.search).to.be.an('array');
expect(res.body.search[0]).to.be.an('object');
expect(res.body.search[0].author.username).to.include('damy');
});
it('Should get article with title filter entered', async () => {
const res = await chai
.request(app)
.get('/api/v1/search?title=React');
expect(res.body).to.be.an('object');
expect(res.body.search).to.be.an('array');
expect(res.body.search[0]).to.be.an('object');
expect(res.body.search[0].title).to.include('React course by hamza');
});
});
});
20 changes: 4 additions & 16 deletions utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export const createUserFromSocials = async (data) => {
export const messages = {
required: 'Input your {{ field }}',
required_with_any: 'You have to provide a {{ field }} for any {{ argument.0 }}',
requiredWithoutAll: 'Search Failed, no Filter provided',
min: '{{ field }} should not be less than {{ argument.0 }}',
max: '{{ field }} should not be more than {{ argument.0 }}',
unique: '{{ field }} already existed',
Expand All @@ -86,6 +87,9 @@ export const sanitizeRules = {
username: 'trim',
email: 'trim',
password: 'trim',
tag: 'to_boolean',
author: 'to_boolean',
title: 'to_boolean',
};

validations.unique = async (data, field, message, args, get) => {
Expand All @@ -97,22 +101,6 @@ validations.unique = async (data, field, message, args, get) => {

export const validatorInstance = Validator(validations, Vanilla);

export const createUser = async (user) => {
const {
firstName, lastName, username, email, password
} = user;

const newUser = await db.User.create({
firstName,
lastName,
username,
email,
password
});

return newUser;
};

export const randomString = () => crypto.randomBytes(11).toString('hex');

export const hashPassword = password => bcrypt.hash(password, 10);
Expand Down
25 changes: 25 additions & 0 deletions validators/search/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { sanitize } from 'indicative';
import { messages, validatorInstance, sanitizeRules } from '../../utils';

export default {
search: async (req, res, next) => {
const rules = {
tag: 'string',
author: 'string',
title: 'string|requiredWithoutAll:tag,author'
};

const data = { ...req.query };

sanitize(data, sanitizeRules);
try {
await validatorInstance.validateAll(data, rules, messages);
req.search = [];
next();
} catch (e) {
return res.status(400).json({
message: e,
});
}
}
};

0 comments on commit 91994be

Please sign in to comment.