From 1866cf40fa91444606a8d0e10c41b4f8a539ad45 Mon Sep 17 00:00:00 2001 From: Nkunzi Innocent Date: Sat, 13 Apr 2019 20:53:42 +0200 Subject: [PATCH 1/3] Feature(article): Adding routes and migration Creates the route in index.js Creates routes for article Installing useful package adding test fot the home route Adding moch data to be used in tests [Finishes #164489786] --- migrations/20190403113724-create-article.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/migrations/20190403113724-create-article.js b/migrations/20190403113724-create-article.js index 3f13459..2248961 100644 --- a/migrations/20190403113724-create-article.js +++ b/migrations/20190403113724-create-article.js @@ -4,7 +4,11 @@ module.exports = { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, slug: { type: Sequelize.STRING, allowNull: false, unique: true }, title: { type: Sequelize.STRING, required: true }, +<<<<<<< HEAD description: { type: Sequelize.TEXT, allowNull: false }, +======= + description: { type: Sequelize.TEXT, allowNull: true }, +>>>>>>> Feature(article): Adding routes and migration body: { type: Sequelize.TEXT, required: true }, taglist: { type: Sequelize.ARRAY(Sequelize.STRING), defaultValue: [] }, authorid: { type: Sequelize.INTEGER, allowNull: false }, From f1ed85ad59e2d640880e12f187f986519b118ca6 Mon Sep 17 00:00:00 2001 From: Nkunzi Innocent Date: Mon, 15 Apr 2019 00:27:24 +0200 Subject: [PATCH 2/3] Feature(article): A user should create an article Modifies migrations Modifies models Adds a slug class to create a slug Create route to the controller Adds tests [Finishes #164489786] --- controllers/article.js | 35 ++++++++ helpers/slug.js | 47 ++++++++++ migrations/20190403113724-create-article.js | 4 - models/article.js | 25 ++++-- package.json | 1 + routes/articles.js | 3 +- test/article.js | 99 +++++++++++++++++++++ test/mockData/articleMockData.js | 4 +- 8 files changed, 204 insertions(+), 14 deletions(-) create mode 100644 controllers/article.js create mode 100644 helpers/slug.js create mode 100644 test/article.js diff --git a/controllers/article.js b/controllers/article.js new file mode 100644 index 0000000..a105ce4 --- /dev/null +++ b/controllers/article.js @@ -0,0 +1,35 @@ +import models from '../models'; +import Slug from '../helpers/slug'; + +const { article: ArticleModel } = models; +/** + * @description CRUD for article Class + */ +class Article { + /** + *@author: Innocent Nkunzi + * @param {Object} req + * @param {Object} res + * @returns {Object} Article + */ + static async createArticle(req, res) { + if (!req.body.title) { + return res.status(400).json({ error: 'title can not be null' }); + } + const slugInstance = new Slug(req.body.title); + const descriptData = req.body.description || `${req.body.body.substring(0, 100)}...`; + try { + const { + title, body, taglist + } = req.body; + const slug = slugInstance.returnSlug(title); + const authorid = 2; + const newArticle = { + title, body, description: descriptData, slug, authorid, taglist + }; + const article = await ArticleModel.createArticle(newArticle); + return res.status(201).json({ article }); + } catch (error) { return res.status(400).json({ message: error.errors[0].message }); } + } +} +export default Article; diff --git a/helpers/slug.js b/helpers/slug.js new file mode 100644 index 0000000..212de62 --- /dev/null +++ b/helpers/slug.js @@ -0,0 +1,47 @@ +import slugify from 'slugify'; +import hasha from 'hasha'; +/** + * This class to maake a slug + */ +class Slug { + /** + * @author: Innocent Nkunzi + * @param {param} strings + * @returns {*} returns a slug + */ + constructor(strings) { + this.strings = strings; + this.randomNumber = Math.floor(Math.random() * 10000); + this.randomString = Math.random().toString(36).substr(2, 1); + this.date = new Date(); + this.slugMaker(); + } + + /** + * @author: Innocent Nkunzi + * @returns {*} it returns a slug without an ID + */ + slugMaker() { + if (this.strings.length <= 40) { + return slugify(this.strings); + } + const slug = this.strings.substring(0, 40); + return slugify(slug); + } + + /** + * @author: Innocent Nkunzi + * @returns {*} it returns a full slug with a hashed ID + */ + returnSlug() { + const slug = this.slugMaker(); + const thisDate = Date.now().toString(); + + let nowHash = hasha(thisDate); + nowHash = nowHash.substring(0, 7); + const newSlug = `${slug}-${nowHash}`; + return newSlug; + } +} + +export default Slug; diff --git a/migrations/20190403113724-create-article.js b/migrations/20190403113724-create-article.js index 2248961..dfc85ae 100644 --- a/migrations/20190403113724-create-article.js +++ b/migrations/20190403113724-create-article.js @@ -4,11 +4,7 @@ module.exports = { id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true }, slug: { type: Sequelize.STRING, allowNull: false, unique: true }, title: { type: Sequelize.STRING, required: true }, -<<<<<<< HEAD - description: { type: Sequelize.TEXT, allowNull: false }, -======= description: { type: Sequelize.TEXT, allowNull: true }, ->>>>>>> Feature(article): Adding routes and migration body: { type: Sequelize.TEXT, required: true }, taglist: { type: Sequelize.ARRAY(Sequelize.STRING), defaultValue: [] }, authorid: { type: Sequelize.INTEGER, allowNull: false }, diff --git a/models/article.js b/models/article.js index f5150e6..e93a889 100644 --- a/models/article.js +++ b/models/article.js @@ -1,13 +1,24 @@ -module.exports = (sequelize, DataTypes) => { +import sequelizeTrasform from 'sequelize-transforms'; + +const ArticleModel = (sequelize, DataTypes) => { const Article = sequelize.define('article', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, slug: { type: DataTypes.STRING, allowNull: false, unique: true }, - title: { type: DataTypes.STRING, allowNull: false }, - body: { type: DataTypes.TEXT, allowNull: false }, - taglist: { type: DataTypes.ARRAY(DataTypes.STRING), allowNull: true }, - description: { type: DataTypes.TEXT, allowNull: true }, + title: { + type: DataTypes.STRING, + allowNull: false, + trim: true, + validate: { len: { args: 5 }, notEmpty: true } + }, + body: { + type: DataTypes.TEXT, trim: true, allowNull: false, validate: { len: { args: 255, msg: 'Body needs to be above 255 characters' }, notEmpty: true } + }, + taglist: { type: DataTypes.ARRAY(DataTypes.STRING), allowNull: true, defaultValue: [] }, + description: { type: DataTypes.TEXT, trim: true }, authorid: { type: DataTypes.INTEGER, allowNull: false } - }, {}); - + }); + sequelizeTrasform(Article); + Article.createArticle = article => Article.create(article); return Article; }; +export default ArticleModel; diff --git a/package.json b/package.json index 2cc14d7..f1f9ce5 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "request": "^2.87.0", "sequelize": "^4.42.0", "sequelize-cli": "^5.4.0", + "sequelize-transforms": "^2.0.0", "slug": "^1.0.0", "slugify": "^1.3.4", "swagger-ui-express": "^4.0.2", diff --git a/routes/articles.js b/routes/articles.js index 0730d59..2fa612c 100644 --- a/routes/articles.js +++ b/routes/articles.js @@ -1,8 +1,9 @@ import express from 'express'; +import articleController from '../controllers/article'; const router = express.Router(); -router.post('/'); +router.post('/', articleController.createArticle); router.get('/'); router.delete('/:slug'); router.put('/:slug'); diff --git a/test/article.js b/test/article.js new file mode 100644 index 0000000..b31211d --- /dev/null +++ b/test/article.js @@ -0,0 +1,99 @@ +import chai from 'chai'; +import faker from 'faker'; +import chaiHttp from 'chai-http'; +import db from '../models'; +import fakeData from './mockData/articleMockData'; +import index from '../index'; + +const articleModel = db.article; + +chai.should(); +chai.use(chaiHttp); + +/** + * @author: Innocent Nkunzi + * @description: tests related to article + */ + +before('Cleaning the database first', (done) => { + articleModel.destroy({ truncate: true, cascade: true }); + done(); +}); +describe('Create an article', () => { + it('It should create an article', (done) => { + chai.request(index).post('/api/articles').send(fakeData).then((res) => { + res.should.have.status(201); + res.body.should.have.property('article'); + res.body.article.should.be.a('object'); + res.body.article.should.have.property('id'); + res.body.article.should.have.property('slug'); + res.body.article.should.have.property('title'); + res.body.article.should.have.property('description'); + res.body.article.should.have.property('createdAt'); + res.body.article.should.have.property('updatedAt'); + done(); + }) + .catch(err => err); + }); +}); +describe('It checks title errors', () => { + it('Should not create an article if the title is empty', (done) => { + const newArticle = { + title: '', + description: faker.lorem.paragraph(), + body: faker.lorem.paragraphs(), + }; + chai.request(index).post('/api/articles').send(newArticle).then((res) => { + res.should.have.status(400); + res.body.should.be.a('object'); + res.body.should.have.property('error').eql('title can not be null'); + done(); + }) + .catch(err => err); + }); +}); +describe('Test the body', () => { + it('should not create and article if the body is empty', (done) => { + const newArticle = { + title: faker.random.words(), + description: faker.lorem.paragraph(), + body: '' + }; + chai.request(index).post('/api/articles').send(newArticle).then((res) => { + res.should.have.status(400); + res.body.should.be.a('object'); + // res.body.should.have.property('error').eql('The body can\'t be empty'); + done(); + }) + .catch(err => err); + }); + it('should return an error if the body is not predefined', (done) => { + const longTitleArticle = { + title: faker.lorem.sentences(), + description: faker.lorem.paragraph(), + }; + chai.request(index).post('/api/articles').send(longTitleArticle).then((res) => { + res.should.have.status(400); + res.body.should.be.a('object'); + res.body.should.have.property('message').eql('article.body cannot be null'); + done(); + }) + .catch(err => err); + }); +}); +describe('Test the title', () => { + it('should substring a long title to only 40 characters', (done) => { + const longTitleArticle = { + title: faker.lorem.sentence(), + body: faker.lorem.paragraphs(), + description: faker.lorem.paragraph(), + }; + chai.request(index).post('/api/articles').send(longTitleArticle).then((res) => { + res.should.have.status(201); + res.body.should.be.a('object'); + res.body.should.have.property('article'); + done(); + }) + .catch(err => err); + }); +}); diff --git a/test/mockData/articleMockData.js b/test/mockData/articleMockData.js index 8e8274c..9856824 100644 --- a/test/mockData/articleMockData.js +++ b/test/mockData/articleMockData.js @@ -2,7 +2,7 @@ import faker from 'faker'; module.exports = { title: faker.random.words(), - description: faker.lorem.sentences(), - body: faker.lorem.sentences(), + description: faker.lorem.paragraphs(), + body: faker.lorem.paragraphs(), authorid: faker.random.number(), }; From 348dc83dfff79ea48212f3056f84d8d1475f84fe Mon Sep 17 00:00:00 2001 From: Nkunzi Innocent Date: Tue, 16 Apr 2019 09:09:35 +0200 Subject: [PATCH 3/3] adding article to user relations --- controllers/article.js | 2 +- ...le.js => 20190403114724-create-article.js} | 2 +- models/article.js | 6 +++++ models/user.js | 6 +++++ test/article.js | 26 ++++++++++++++++--- test/mockData/articleMockData.js | 2 +- 6 files changed, 37 insertions(+), 7 deletions(-) rename migrations/{20190403113724-create-article.js => 20190403114724-create-article.js} (85%) diff --git a/controllers/article.js b/controllers/article.js index a105ce4..c35b3da 100644 --- a/controllers/article.js +++ b/controllers/article.js @@ -23,7 +23,7 @@ class Article { title, body, taglist } = req.body; const slug = slugInstance.returnSlug(title); - const authorid = 2; + const authorid = 1; const newArticle = { title, body, description: descriptData, slug, authorid, taglist }; diff --git a/migrations/20190403113724-create-article.js b/migrations/20190403114724-create-article.js similarity index 85% rename from migrations/20190403113724-create-article.js rename to migrations/20190403114724-create-article.js index dfc85ae..1812250 100644 --- a/migrations/20190403113724-create-article.js +++ b/migrations/20190403114724-create-article.js @@ -7,7 +7,7 @@ module.exports = { description: { type: Sequelize.TEXT, allowNull: true }, body: { type: Sequelize.TEXT, required: true }, taglist: { type: Sequelize.ARRAY(Sequelize.STRING), defaultValue: [] }, - authorid: { type: Sequelize.INTEGER, allowNull: false }, + authorid: { type: Sequelize.INTEGER, allowNull: false, references: { model: 'users', key: 'id', onDelete: 'CASCADE' } }, createdAt: { allowNull: false, type: Sequelize.DATE }, updatedAt: { allowNull: false, type: Sequelize.DATE }, }), diff --git a/models/article.js b/models/article.js index e93a889..0a0db39 100644 --- a/models/article.js +++ b/models/article.js @@ -19,6 +19,12 @@ const ArticleModel = (sequelize, DataTypes) => { }); sequelizeTrasform(Article); Article.createArticle = article => Article.create(article); + + Article.associate = (models) => { + Article.belongsTo(models.user, { + foreignKey: 'authorid', onDelete: 'CASCADE' + }); + }; return Article; }; export default ArticleModel; diff --git a/models/user.js b/models/user.js index 8eddbbe..53f7019 100644 --- a/models/user.js +++ b/models/user.js @@ -2,6 +2,7 @@ import helper from '../helpers/helper'; const UserModel = (Sequelize, DataTypes) => { const User = Sequelize.define('user', { + id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, firstname: { type: DataTypes.STRING, allowNull: true }, lastname: { type: DataTypes.STRING, allowNull: true }, username: { type: DataTypes.STRING, allowNull: false }, @@ -43,6 +44,11 @@ const UserModel = (Sequelize, DataTypes) => { }); return result[0].dataValues; }; + User.associate = (models) => { + User.hasMany(models.article, { + foreignKey: 'authorid', onDelete: 'CASCADE' + }); + }; return User; }; export default UserModel; diff --git a/test/article.js b/test/article.js index b31211d..65e79a1 100644 --- a/test/article.js +++ b/test/article.js @@ -6,6 +6,7 @@ import fakeData from './mockData/articleMockData'; import index from '../index'; const articleModel = db.article; +const userModel = db.user; chai.should(); chai.use(chaiHttp); @@ -15,12 +16,29 @@ chai.use(chaiHttp); * @description: tests related to article */ -before('Cleaning the database first', (done) => { - articleModel.destroy({ truncate: true, cascade: true }); - done(); +before('Cleaning the database first', async () => { + await articleModel.destroy({ truncate: true, cascade: true }); + await userModel.destroy({ where: { email: userModel.email }, truncate: true, cascade: true }); +}); +const user = { + id: 1, + username: 'nkunziinnocent', + email: 'nkunzi@gmail.com', + password: '@Nkunzi1234', +}; +describe('Create a user to be used in in creating article', () => { + it('should create a user', (done) => { + chai.request(index).post('/api/users').send(user).then((res) => { + res.should.have.status(201); + res.body.user.should.be.a('object'); + res.body.user.should.have.property('username'); + done(); + }) + .catch(err => err); + }); }); describe('Create an article', () => { - it('It should create an article', (done) => { + it('should create an article', (done) => { chai.request(index).post('/api/articles').send(fakeData).then((res) => { res.should.have.status(201); res.body.should.have.property('article'); diff --git a/test/mockData/articleMockData.js b/test/mockData/articleMockData.js index 9856824..dc8c36b 100644 --- a/test/mockData/articleMockData.js +++ b/test/mockData/articleMockData.js @@ -4,5 +4,5 @@ module.exports = { title: faker.random.words(), description: faker.lorem.paragraphs(), body: faker.lorem.paragraphs(), - authorid: faker.random.number(), + authorid: 1 };