-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ft(create-article): create user article
- Write unit tests - Create model for article and association with user model - Create middleware to validate data to create article - Create helper function to create article - Create helper function to generate unique slugs - Add image upload feature using cloudinary - Create route and controller to create article - Create route and controller get an article using slug parameter - Create route and controller to get all articles [Delivers #159206054]
- Loading branch information
Olumide Ogundele
committed
Aug 3, 2018
1 parent
a42b857
commit 4e28259
Showing
18 changed files
with
993 additions
and
33 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,15 @@ | ||
module.exports = { | ||
secret: process.env.SECRET | ||
import dotenv from 'dotenv'; | ||
|
||
dotenv.config(); | ||
|
||
const config = { | ||
secret: process.env.SECRET, | ||
jwtSecret: process.env.JWT_TOKEN_SECRET, | ||
cloudinary: { | ||
cloud_name: process.env.CLOUD_NAME, | ||
api_key: process.env.API_KEY, | ||
api_secret: process.env.API_SECRET, | ||
} | ||
}; | ||
|
||
export default config; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import cloudinary from 'cloudinary'; | ||
|
||
import { Article, User } from '../models'; | ||
import createArticleHelper from '../helpers/createArticleHelper'; | ||
|
||
/** Class representing controller for articles. */ | ||
class ArticleController { | ||
/** | ||
* Create an article for a user | ||
* @param {object} req - The request object | ||
* @param {object} res - The response object sent to user | ||
* @return {object} A object containing created articles. | ||
*/ | ||
static createArticle(req, res) { | ||
const { | ||
title, description, body, tagList, imageUrl | ||
} = req.body.article; | ||
|
||
const { userId } = req; | ||
|
||
const articleObject = { | ||
title, description, body, tagList, imageUrl, userId | ||
}; | ||
/** check if image was provided in the request | ||
* upload the image to cloudinary, save the article | ||
* with the cloudinary URL in database but if an error | ||
* was encountered from cloudinary go ahead and create the article | ||
*/ | ||
if (imageUrl) { | ||
return cloudinary.v2.uploader.upload(imageUrl, { tags: 'basic_sample' }) | ||
.then(image => createArticleHelper(res, articleObject, image.url)) | ||
.catch(() => createArticleHelper(res, articleObject)); | ||
} | ||
|
||
/** if there no image was provided go ahead to create the article */ | ||
return createArticleHelper(res, articleObject); | ||
} | ||
|
||
/** | ||
* get an article using slug as query parameter | ||
* @param {object} req - request object | ||
* @param {object} res - response object | ||
* @returns {object} - the found article from database or error if not found | ||
*/ | ||
static getArticle(req, res) { | ||
const { slug } = req.params; | ||
|
||
return Article | ||
.findOne({ | ||
where: { slug, }, | ||
include: [{ | ||
model: User, | ||
attributes: { exclude: ['id', 'email', 'hashedPassword', 'createdAt', 'updatedAt'] } | ||
}], | ||
attributes: { exclude: ['id', 'userId'] } | ||
}) | ||
.then((article) => { | ||
/** if the article does not exist */ | ||
if (!article) { | ||
return res.status(404).json({ | ||
errors: { | ||
body: [ | ||
'Ooops! the article cannot be found.' | ||
] | ||
} | ||
}); | ||
} | ||
|
||
return res.status(200).json({ article }); | ||
}) | ||
.catch(() => res.status(501).send('oops seems there is an error find the article')); | ||
} | ||
|
||
/** | ||
* get all articles created | ||
* @param {object} req - request object | ||
* @param {object} res - response object | ||
* @returns {object} - the found article from database or empty if not found | ||
*/ | ||
static listAllArticles(req, res) { | ||
return Article | ||
.findAll({ | ||
include: [{ | ||
model: User, | ||
attributes: { exclude: ['id', 'email', 'hashedPassword', 'createdAt', 'updatedAt'] } | ||
}], | ||
attributes: { exclude: ['id', 'userId'] } | ||
}) | ||
.then((articles) => { | ||
/** check if there was no article created */ | ||
if (articles.length === 0) { | ||
return res.status(200).json({ | ||
message: 'Your request was successful but no articles created', | ||
articles, | ||
}); | ||
} | ||
|
||
return res.status(200).json({ articles, articlesCount: articles.length }); | ||
}) | ||
.catch(() => res.status(501).send('oops seems there is an error find all articles')); | ||
} | ||
} | ||
|
||
|
||
export default ArticleController; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import generateUniqueSlug from './generateUniqueSlug'; | ||
import { Article, User } from '../models'; | ||
|
||
|
||
/** | ||
* @description an helper function to help create article in database | ||
* @param {object} res - response object | ||
* @param {object} articleObject - contains extracted article fields | ||
* @param {string} imageUrl - image url from cloudinary | ||
* @returns object - the created article from the database | ||
*/ | ||
|
||
const createArticleHelper = (res, articleObject, imageUrl = null) => { | ||
const { | ||
title, description, body, tagList, userId | ||
} = articleObject; | ||
|
||
return Article | ||
.create({ | ||
slug: generateUniqueSlug(title), | ||
title, | ||
description, | ||
body, | ||
userId, | ||
tagList, | ||
imageUrl, | ||
}) | ||
.then(article => Article.findById(article.id, { | ||
include: [{ | ||
model: User, | ||
attributes: { exclude: ['id', 'email', 'hashedPassword', 'createdAt', 'updatedAt'] } | ||
}], | ||
attributes: { exclude: ['id', 'userId'] } | ||
})) | ||
.then(article => res.status(201).json({ article })) | ||
.catch(err => res.status(400).send({ | ||
errors: { | ||
body: [ | ||
'Sorry, there was an error creating your article', | ||
err | ||
] | ||
} | ||
})); | ||
}; | ||
|
||
export default createArticleHelper; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import slugify from 'slugify'; | ||
import cuid from 'cuid'; | ||
|
||
/** create a string as slug to make articles unique | ||
* @param {string} title - title of the article from request body | ||
* @returns {string} slug - timestamped string with randomly generated slug to avoid collision | ||
*/ | ||
const generateUniqueSlug = (title) => { | ||
const sluggedTitle = slugify(title); | ||
const slug = `${sluggedTitle}-${cuid()}`; | ||
return slug.toLowerCase(); | ||
}; | ||
|
||
|
||
export default generateUniqueSlug; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import db from '../models'; | ||
|
||
const { User } = db; | ||
const getUser = (req, res, next) => { | ||
User.findById(req.userId) | ||
.then((user) => { | ||
if (!user || user.rowCount === 0) { | ||
return res.status(404).json({ | ||
success: false, | ||
errors: { | ||
body: ['The user does not exist'] | ||
}, | ||
}); | ||
} | ||
req.userObject = user; | ||
next(); | ||
return null; | ||
}) | ||
.catch(err => res.status(500).json({ | ||
success: false, | ||
errors: { | ||
body: [err] | ||
}, | ||
})); | ||
}; | ||
|
||
export default getUser; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/** | ||
* @description this function checks that fields are not empty | ||
* @param {object} req | ||
* @param {object} res | ||
* @param {function} next | ||
* @returns {object} a status code and json object if theres error | ||
* or continue to the next middleware using next() | ||
*/ | ||
const validateArticle = (req, res, next) => { | ||
let { title, description, body } = req.body.article; | ||
|
||
if (!title || !description || !body) { | ||
return res.status(400).json({ | ||
errors: { | ||
body: [ | ||
'Please check that your title, description or body field is not empty' | ||
] | ||
} | ||
}); | ||
} | ||
|
||
title = title.trim(); | ||
description = description.trim(); | ||
body = body.trim(); | ||
|
||
next(); | ||
}; | ||
|
||
export default validateArticle; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import jwt from 'jsonwebtoken'; | ||
import config from '../config'; | ||
|
||
const verifyToken = (req, res, next) => { | ||
const token = req.headers.authorization.split(' ')[1]; | ||
if (!token || !req.headers.authorization) { | ||
return res.status(401).send('No token has been provided in the request'); | ||
} | ||
jwt.verify(token, config.jwtSecret, (err, decoded) => { | ||
if (err) { | ||
return res.status(401).json({ message: 'Could not authenticate the provided token' }); | ||
} | ||
req.userId = decoded.id; | ||
next(); | ||
return null; | ||
}); | ||
return null; | ||
}; | ||
|
||
export default verifyToken; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
module.exports = { | ||
up: (queryInterface, Sequelize) => { | ||
return queryInterface.createTable('Articles', { | ||
id: { | ||
allowNull: false, | ||
autoIncrement: true, | ||
primaryKey: true, | ||
type: Sequelize.INTEGER | ||
}, | ||
slug: { | ||
type: Sequelize.STRING | ||
}, | ||
title: { | ||
type: Sequelize.STRING | ||
}, | ||
description: { | ||
type: Sequelize.STRING | ||
}, | ||
body: { | ||
type: Sequelize.STRING | ||
}, | ||
tagList: { | ||
type: Sequelize.ARRAY(Sequelize.TEXT) | ||
}, | ||
favorited: { | ||
type: Sequelize.BOOLEAN | ||
}, | ||
favoritesCount: { | ||
type: Sequelize.INTEGER | ||
}, | ||
imageUrl: { | ||
type: Sequelize.STRING | ||
}, | ||
createdAt: { | ||
allowNull: false, | ||
type: Sequelize.DATE | ||
}, | ||
updatedAt: { | ||
allowNull: false, | ||
type: Sequelize.DATE | ||
}, | ||
userId: { | ||
type: Sequelize.INTEGER, | ||
onDelete: 'CASCADE', | ||
references: { | ||
model: 'Users', | ||
key: 'id', | ||
as: 'userId', | ||
}, | ||
}, | ||
}); | ||
}, | ||
down: (queryInterface, Sequelize) => { | ||
return queryInterface.dropTable('Articles'); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
module.exports = (sequelize, DataTypes) => { | ||
const Article = sequelize.define('Article', { | ||
slug: { | ||
type: DataTypes.STRING, | ||
allowNull: false, | ||
unique: true, | ||
}, | ||
title: { | ||
type: DataTypes.STRING, | ||
allowNull: false, | ||
}, | ||
description: { | ||
type: DataTypes.STRING, | ||
allowNull: false, | ||
}, | ||
body: { | ||
type: DataTypes.STRING, | ||
allowNull: false, | ||
}, | ||
tagList: DataTypes.ARRAY(DataTypes.STRING), | ||
favorited: DataTypes.BOOLEAN, | ||
favoritesCount: DataTypes.INTEGER, | ||
imageUrl: DataTypes.STRING | ||
}, {}); | ||
Article.associate = (models) => { | ||
// associations can be defined here | ||
Article.belongsTo(models.User, | ||
{ | ||
foreignKey: 'userId', | ||
onDelete: 'CASCADE', | ||
}); | ||
}; | ||
|
||
return Article; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.