Skip to content

Commit

Permalink
Merge 64abd19 into 5dc177e
Browse files Browse the repository at this point in the history
  • Loading branch information
codinger41 committed Mar 14, 2019
2 parents 5dc177e + 64abd19 commit 5bb0772
Show file tree
Hide file tree
Showing 24 changed files with 842 additions and 182 deletions.
1 change: 1 addition & 0 deletions .babelrc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"plugins": ["transform-es2015-destructuring", "transform-object-rest-spread"],
"presets": [ "es2015" ]
}
129 changes: 125 additions & 4 deletions controllers/articles.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Article } from '../models';

import { Article, User } from '../models';
/**
* @class ArticleController
* @override
Expand All @@ -17,13 +16,20 @@ export default class ArticleController {
static async createArticle(req, res) {
const images = req.images || [];
const {
title, description, body, slug
title, description, body, slug, categoryId
} = req.body;
const taglist = req.body.taglist ? req.body.taglist.split(',') : [];
const { id } = req.user;
try {
const result = await Article.create({
title, description, body, slug, images, taglist, userId: id
title,
description,
body,
slug,
images,
taglist,
userId: id,
categoryId: categoryId || 1
});
return res.status(201).json({
success: true,
Expand All @@ -34,4 +40,119 @@ export default class ArticleController {
return res.status(500).json({ success: false, error: [error.message] });
}
}

/**
* @description - Search for articles
* @static
* @param {Object} req - the request object
* @param {Object} res - the response object
* @memberof ArticleController
* @returns {Object} class instance
*/
static async searchForArticles(req, res) {
try {
const searchTerms = ArticleController.generateSearchQuery(req.query);
const results = await Article.findAll({
raw: true,
where: {
...searchTerms,
},
include: [{
model: User,
attributes: ['username', 'email', 'name', 'bio'],
as: 'author'
}]
});
return res.status(200).json({
results,
success: true
});
} catch (error) {
return res.status(200).json({
results: [],
success: true,
message: 'Oops, no article with your search terms was found.'
});
}
}

/**
* @description - Generate queries for search and filter
* @static
* @param {Object} searchTerms - the terms that the user wants to search for
* @memberof ArticleController
* @returns {Object} class instance
*/
static generateSearchQuery(searchTerms) {
const {
author, term, endDate, startDate, tags, categoryId
} = searchTerms;

const filterFields = {
'$user.username$': {
$like: author
},
createdAt: {
$between: [startDate, endDate]
},
title: {
$like: `%${term}%`,
},
description: {
$like: `%${term}%`,
},
taglist: {
$contains: tags ? [...tags.split(',')] : []
},
categoryId: Number(categoryId),
};

if (!author) {
delete filterFields['$user.username$'];
}
if (!startDate || !endDate) {
delete filterFields.createdAt;
}
if (!categoryId) {
delete filterFields.categoryId;
}
return filterFields;
}

/**
* @description - Get article by slug
* @static
* @param {Object} req - the request object
* @param {Object} res - the response object
* @memberof ArticleController
* @returns {Object} class instance
*/
static async getArticleBySlug(req, res) {
try {
const {
params: {
slug
}
} = req;
const article = await Article.findOne({
where: {
slug
},
include: [{
as: 'author',
model: User,
attributes: ['username', 'email', 'name', 'bio'],
}]
});
return res.status(200).json({
success: true,
article: article.dataValues
});
} catch (error) {
return res.status(404).json({
success: false,
errors: ['Article not found.'],
});
}
}
}
47 changes: 47 additions & 0 deletions controllers/category.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import dotenv from 'dotenv';
import { Category } from '../models';

dotenv.config();

/**
* @class CategoryController
* @override
* @export
*/
export default class CategoryController {
/**
* @description - Creates a new category
* @static
* @param {object} req - HTTP Request
* @param {object} res - HTTP Response
* @memberof CategoryController
* @returns {object} Class instance
*/
static async addNew(req, res) {
const {
body: {
category
}
} = req;
try {
const returnValue = await Category.create({ categoryName: category });
const { dataValues: { categoryName } } = returnValue;
return res.status(201).json({
success: true,
message: 'Category successfully added.',
categoryName
});
} catch (error) {
if (error.errors[0].type === 'unique violation') {
return res.status(409).json({
success: false,
errors: [error.errors[0].message]
});
}
return res.status(500).json({
success: false,
errors: ['Internal server error']
});
}
}
}
67 changes: 66 additions & 1 deletion doc.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"http"
],
"paths": {
"/user": {
"/user/signup": {
"post": {
"tags": [
"Authentication"
Expand Down Expand Up @@ -272,6 +272,71 @@
}
}
}
},
"/articles/search": {
"get": {
"tags": [
"Articles"
],
"summary": "Search for an article",
"description": "",
"operationId": "searchArticle",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"parameters": [
{
"name": "term",
"in": "query",
"description": "A string to search for ",
"required": true,
"type": "string"
},
{
"name": "categoryId",
"in": "query",
"description": "A categoryId to search from",
"required": false,
"type": "number"
},
{
"name": "author",
"in": "query",
"description": "Username or name of an author to search from",
"required": false,
"type": "string"
},
{
"name": "startDate",
"in": "query",
"description": "A start date for a date range search",
"required": false,
"type": "string"
},
{
"name": "endDate",
"in": "query",
"description": "An end date for a date range search",
"required": false,
"type": "string"
},
{
"name": "tags",
"in": "query",
"description": "Tags to search from",
"required": false,
"type": "string"
}
],
"responses": {
"200": {
"description": "Search results were found"
}
}
}
}
},
"definitions": {
Expand Down
4 changes: 2 additions & 2 deletions middleware/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ class Auth {
if (!token) {
return res.status(401).json({
success: false,
message: 'Unauthorized! You are required to be logged in to perform this operation.',
errors: ['Unauthorized! You are required to be logged in to perform this operation.'],
});
}
jwt.verify(token, JWT_SECRET, (err, decoded) => {
if (err) {
return res.status(401).json({
success: false,
message: 'Your session has expired, please login again to continue',
errors: ['Your session has expired, please login again to continue'],
});
}
req.user = decoded;
Expand Down
17 changes: 17 additions & 0 deletions middleware/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,20 @@ export const validateLogin = [
.custom(value => !/\s/.test(value))
.withMessage('Please provide a valid password.'),
];

export const validateCategory = [
check('category')
.exists()
.withMessage('No category provided. Please provide a category.')
.isLength({ min: 3, max: 30 })
.withMessage('Category must be at least 3 characters long and no more than 15.')
.isString()
.withMessage('Category must be alphanumeric characters, please remove leading and trailing whitespaces.')
];


export const validateSearch = [
check('term')
.isString()
.withMessage('Please provide a valid search term.')
];
2 changes: 1 addition & 1 deletion migrations/20190312203553-add-userId.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ module.exports = {
'userId'
);
},
};
};
24 changes: 24 additions & 0 deletions migrations/20190313193812-create-category.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Categories', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
categoryName: {
type: Sequelize.STRING,
allowNull: false,
unique: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('Categories')
};
18 changes: 18 additions & 0 deletions migrations/20190313195119-add_category_Id_to_article.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.addColumn(
'Articles',
'categoryId',
{
type: Sequelize.INTEGER,
allowNull: true,
unique: false
}
),

down(queryInterface) {
return queryInterface.removeColumn(
'Articles',
'categoryId'
);
},
};
21 changes: 21 additions & 0 deletions migrations/20190314104937-insert-default-category.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module.exports = {
up: queryInterface => queryInterface.bulkInsert(
'Categories',
[
{
categoryName: 'Uncategorized',
createdAt: new Date(),
updatedAt: new Date()
}
]
),

down: queryInterface => queryInterface.bulkDelete(
'Categories',
[
{
categoryName: 'Uncategorized'
}
]
),
};
Loading

0 comments on commit 5bb0772

Please sign in to comment.