Skip to content

Commit

Permalink
Merge d456545 into 0c892de
Browse files Browse the repository at this point in the history
  • Loading branch information
GodswillOnuoha committed Sep 5, 2018
2 parents 0c892de + d456545 commit b8e7cee
Show file tree
Hide file tree
Showing 15 changed files with 574 additions and 8 deletions.
6 changes: 6 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ PROD_DB_DIALECT=postgres

USER_PASSWORD=
ADMIN_PASSWORD=
<<<<<<< HEAD

SECRET=

Expand All @@ -31,3 +32,8 @@ GOOGLE_APP_SECRET=
RESET_URL=

SENDGRID_API_KEY=
=======
AUTHOR_PASSWORD=
AUTHOR_EMAIL=
SECRET=
>>>>>>> feat(API): add Article CRUD features
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ before_script:

script:
- npm test

after_success:
- npm run coveralls
- npm run coverage
Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
"main": "index.js",
"scripts": {
"lint": "eslint ./ --fix",
"migrate": "sequelize db:migrate:undo:all && sequelize db:migrate",
"migrate": "sequelize db:migrate:undo:all && sequelize db:migrate && sequelize db:seed:all",
"pretest": "NODE_ENV=test npm run migrate",
"test": "NODE_ENV=test nyc mocha --compilers js:babel-register ./server/tests/* --timeout 5000ms --exit --no-deprecation",
"test": "NODE_ENV=test nyc mocha --compilers js:babel-register ./server/tests/ --timeout 5000ms --exit --no-deprecation",
"start": "node index.js",
"start:dev": "nodemon --watch server --exec babel-node ./index.js",
"seed": "sequelize db:seed:all",
Expand All @@ -22,7 +22,7 @@
"bcrypt": "^3.0.0",
"body-parser": "^1.18.3",
"chai": "^4.1.2",
"chai-http": "^4.2.0",
"chai-http": "^4.0.0",
"cors": "^2.8.4",
"dotenv": "^6.0.0",
"ejs": "^2.6.1",
Expand All @@ -48,11 +48,12 @@
"request": "^2.87.0",
"sequelize": "^4.38.0",
"sequelize-cli": "^4.1.1",
"sequelize-test-helpers": "^1.0.4",
"sinon": "^6.1.5",
"slug": "^0.9.1",
"swagger-ui-express": "^4.0.1",
"underscore": "^1.9.1",
"validator": "^10.7.0"
"validator": "^10.7.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
Expand Down
196 changes: 196 additions & 0 deletions server/controllers/article.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@

import db from '../models';
import articleValidation from '../validation/articles';

const { Article, User } = db;

/**
* Article controller function
*/
class ArticleController {
/**
* @static
* @description returns slug from article title
* @param {string} title
* @returns {string} slug
*/
static slugify(title) {
return title.replace(/\s+/g, '-') + Math.floor((Math.random() * 1000000) + 1).toString();
}

/**
* @static
* @param {reuest} req
* @param {response} res
* @return {json} res
* @description creates article.
*/
static create(req, res) {
// validation file :(to be moved to validations file)
const { errors, isValid } = articleValidation.validateArticle(req.body);
if (!isValid) {
return res.status(400).json({ errors });
}

// get parameters from request
const {
title, description, body,
} = req.body;


// set author id to current user id
const authorId = req.userId;

// generate slug
const slug = ArticleController.slugify(title);

User.findById(authorId, {
attributes: ['username', 'email', 'bio', 'image']
}).then((user) => {
// create article
Article.create({
title, description, body, authorId, slug
})
.then(article => res.status(201).json({
article: {
slug: article.slug,
title: article.title,
description: article.description,
body: article.body,
author: user,
createdAt: article.createdAt,
updatedAt: article.updatedAt,
}
}));
});
}

/**
* @static
* @param {reuest} req
* @param {response} res
* @return {json} res
* @description returns all article.
*/
static getAll(req, res) {
return Article.findAll({
include: [{
model: User,
as: 'author',
attributes: ['username', 'email', 'bio', 'image']
}],
attributes: ['slug', 'title', 'description', 'body', 'createdAt', 'updatedAt']
})
.then((article) => {
res.status(200).json({
articles: article
});
})
.catch(error => res.status(500).json(error));
}

/**
* @static
* @param {reuest} req
* @param {response} res
* @return {json} res
* @description returns specific article that has the slug passes as req param (article_slug).
*/
static getSpecific(req, res) {
return Article.findOne({
where: { slug: req.params.article_slug },
include: [{
model: User,
as: 'author',
attributes: ['username', 'email', 'bio', 'image']
}],
attributes: ['slug', 'title', 'description', 'body', 'createdAt', 'updatedAt']
})
.then(article => res.status(200).json({
article
}))
.catch(error => res.status(400).json(error));
}

/**
* @static
* @param {reuest} req
* @param {response} res
* @return {json} res
* @description returns specific article with given slug.
*/
static update(req, res) {
return Article.findOne({
where: { slug: req.params.article_slug },
include: [{
model: User,
as: 'author',
attributes: ['username', 'email', 'bio', 'image']
}],
})
.then((article) => {
// return 404 if article not found
if (!article) {
return res.status(404).json({
errors: { message: 'article not found' }
});
}

// check if article belongs to current user
if (parseInt(article.authorId, 10) !== parseInt(req.userId, 10)) {
return res.status(403).json({
errors: { message: 'forbidden from editing another user\'s article' }
});
}

return article.update({
title: req.body.title || article.title,
slug: ArticleController.slugify(req.body.title) || article.slug,
description: req.body.description || article.description,
body: req.body.body || article.body
})
.then((self) => {
const updated = self;
delete updated.id;
delete updated.authorId;
res.status(200).json({ article: updated });
})
.catch(error => res.status(400).json(error));
})
.catch(error => res.status(400).json(error));
}

/**
* @static
* @param {reuest} req
* @param {response} res
* @return {json} res
* @description deletes an article object with given slug in param.
*/
static delete(req, res) {
return Article.findOne({ where: { slug: req.params.article_slug } })
.then((article) => {
if (!article) {
return res.status(404).json({
message: 'article not found',
});
}

// check if article belongs to current user
if (parseInt(article.authorId, 10) !== parseInt(req.userId, 10)) {
return res.status(403).json({
errors: { message: 'forbidden from deleting another user\'s article' }
});
}

return article.destroy()
.then(() => res.status(200).json({
message: 'article successfully deleted',
}))
.catch(error => res.status(400).json(error));
})
.catch(error => res.status(400).json(error));
}
}

export default ArticleController;
53 changes: 53 additions & 0 deletions server/migrations/20180901112322-create-article.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Articles', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
title: {
allowNull: false,
type: Sequelize.STRING
},
slug: {
type: Sequelize.STRING,
unique: true,
allowNull: false
},
description: {
type: Sequelize.STRING
},
body: {
allowNull: false,
type: Sequelize.TEXT
},
authorId: {
allowNull: false,
type: Sequelize.INTEGER,
onDelete: 'CASCADE',
references: {
model: 'Users',
key: 'id',
as: 'authorId',
},
},
likeDislikeId: {
type: Sequelize.INTEGER,
references: {
model: 'Users',
key: 'id',
as: 'authorId',
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('Articles')
};
31 changes: 31 additions & 0 deletions server/models/article.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
module.exports = (sequelize, DataTypes) => {
const Article = sequelize.define('Article', {
title: {
type: DataTypes.STRING,
allowNull: false,
},
slug: {
type: DataTypes.STRING,
unique: true,
allowNull: false,
},
description: DataTypes.STRING,
body: {
type: DataTypes.TEXT,
allowNull: false
},
authorId: {
type: DataTypes.INTEGER,
allowNull: false,
},
likeDislikeId: DataTypes.INTEGER
}, {});

Article.associate = (models) => {
// 1:m relationship
Article.belongsTo(models.User, {
as: 'author', foreignKey: 'authorId'
});
};
return Article;
};
14 changes: 14 additions & 0 deletions server/routes/api/articles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import ArticleController from '../../controllers/article';
import auth from '../../middleware/auth';

// get authenticateUser method
const { authenticateUser, authorizeAuthor } = auth;
const router = require('express').Router();

router.post('/', authenticateUser, authorizeAuthor, ArticleController.create);
router.get('/', authenticateUser, ArticleController.getAll);
router.get('/:article_slug', authenticateUser, ArticleController.getSpecific);
router.put('/:article_slug', authenticateUser, ArticleController.update);
router.delete('/:article_slug', authenticateUser, ArticleController.delete);

export default router;
2 changes: 2 additions & 0 deletions server/routes/api/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Router } from 'express';
import articlesRouter from './articles';

import userRoutes from './users';
import welcomeRoute from './welcome';
Expand All @@ -21,5 +22,6 @@ routes.use((err, req, res, next) => {
routes.use('/users', userRoutes);
routes.use('/', welcomeRoute);
routes.use('/', socialAuth);
routes.use('/articles', articlesRouter);

export default routes;
10 changes: 10 additions & 0 deletions server/seeders/20180904121828-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ module.exports = {
hash: bcrypt.hashSync(process.env.USER_PASSWORD, 10),
createdAt: new Date(),
updatedAt: new Date(),
},
{
firstName: 'author1',
lastName: 'Author',
username: 'randomAuthor1',
email: 'author1@mail.com',
role: 'author',
hash: bcrypt.hashSync(process.env.AUTHOR_PASSWORD, 10),
createdAt: new Date(),
updatedAt: new Date(),
}
], {}),

Expand Down
Loading

0 comments on commit b8e7cee

Please sign in to comment.