Skip to content

Commit

Permalink
feature-[167190542]: implement article tagging (#41)
Browse files Browse the repository at this point in the history
- create tag migration, model and seed files
- create articletag migration file
- enable user tag article
- write model, helper and end to end test
- enable admin create category
- implement get all tag route

[Delivers #167190542]
  • Loading branch information
henryade authored and dinobi committed Aug 16, 2019
1 parent 33804dd commit c09afc3
Show file tree
Hide file tree
Showing 27 changed files with 713 additions and 61 deletions.
42 changes: 0 additions & 42 deletions server/controllers/Article.js

This file was deleted.

80 changes: 80 additions & 0 deletions server/controllers/Articles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import model from '../database/models';
import { imageUpload, serverResponse, serverError } from '../helpers';
import Tags from './Tags';

const { Article } = model;
/**
* @export
* @class Articles
*/
class Articles {
/**
* @name createArticle
* @async
* @static
* @memberof Articles
* @param {Object} req express request object
* @param {Object} res express response object
* @returns {JSON} JSON object with details of new article
*/
static async createArticle(req, res) {
try {
let image;
const {
file,
body,
user: { id }
} = req;
const { status, articleBody, tags } = body;

const publishedAt = status === 'draft' || articleBody === undefined ? null : Date.now();
let createTags;
if (tags) {
createTags = await Tags.create(tags);
const error = Articles.canTag(createTags);
if (error) return serverResponse(res, error.status, error.message);
}
if (file) image = await imageUpload(req);
const myArticle = await Article.create({
...body,
image,
authorId: id,
publishedAt
});

const associateTags = (await Tags.associateArticle(myArticle.id, createTags)) || [];
myArticle.dataValues.tagList = associateTags;
return serverResponse(res, 200, myArticle.dataValues);
} catch (error) {
return serverError(res);
}
}

/**
* @name canTag
* @async
* @static
* @memberof Articles
* @param {Array} tagArray array of tags
* @returns {Object} response object
*/
static canTag(tagArray) {
if (tagArray === false) {
return {
status: 400,
message: {
error: 'each tag must be more than a character'
}
};
}
if (tagArray === null || tagArray.length < 1 || !tagArray[0].id) {
return {
status: 400,
message: {
error: 'tags should be an array of valid strings'
}
};
}
}
}
export default Articles;
87 changes: 87 additions & 0 deletions server/controllers/Tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import models from '../database/models';
import {
serverResponse,
serverError,
removeSpecialCharacters,
formatTag,
removeDuplicateTags
} from '../helpers';

const { Tag, Article } = models;

/**
* @export
* @class Tags
*/
class Tags {
/**
* @name getAll
* @async
* @static
* @memberof Tags
* @param {Object} req express request object
* @param {Object} res express response object
* @returns {JSON} JSON object with list of tags
*/
static async getAll(req, res) {
try {
const allTags = await Tag.findAll({ attributes: ['name'] });
const tags = allTags.map(({ name }) => name);
return serverResponse(res, 200, { tags });
} catch (error) {
serverError(res);
}
}

/**
* @name create
* @async
* @static
* @memberof Tags
* @param {String} tagList list of tags
* @returns {Array} array with accurate tag details
*/
static async create(tagList) {
if (!tagList || tagList.length < 1) return null;
const tagArray = tagList.split(',');
if (tagArray.some(tag => tag.length < 2)) return false;
const plainTags = tagArray
.map(eachTag => formatTag(eachTag))
.filter(tag => tag !== '');
const uniquePlainTags = removeDuplicateTags(plainTags);
const allTag = await Tag.findAll();
const SaveOrGetTagDetails = await Promise.all(
uniquePlainTags.map(async (eachTag) => {
const existingTag = allTag.find(
tag => removeSpecialCharacters(tag.dataValues.name)
=== removeSpecialCharacters(eachTag)
);
if (!existingTag) {
const newTag = await Promise.resolve(Tag.create({ name: eachTag }));
return newTag.dataValues;
}
return existingTag.dataValues;
})
);
return SaveOrGetTagDetails;
}

/**
* @name associateArticle
* @async
* @static
* @memberof Tags
* @param {Integer} articleId id of article to associate
* @param {Array} tagArray array of tags
* @returns {null} null
*/
static async associateArticle(articleId, tagArray) {
if (!articleId || !tagArray) return false;
const article = await Article.findById(articleId);
const arrayOfTagId = tagArray.map(({ id }) => id);
await article.addTags(arrayOfTagId);
return tagArray.map(({ name }) => name);
}
}

export default Tags;
24 changes: 24 additions & 0 deletions server/database/migrations/20190812113130-create-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export default {
up: (queryInterface, Sequelize) => queryInterface.createTable('Tags', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
allowNull: false,
type: Sequelize.STRING,
unqiue: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('Tags')
};
37 changes: 37 additions & 0 deletions server/database/migrations/20190813161118-create-article-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
module.exports = {
up: (queryInterface, Sequelize) => queryInterface.createTable('ArticleTags', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
articleId: {
type: Sequelize.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
references: {
model: 'Articles',
key: 'id'
}
},
tagId: {
type: Sequelize.INTEGER,
allowNull: false,
onUpdate: 'CASCADE',
references: {
model: 'Tags',
key: 'id'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('ArticleTags')
};
13 changes: 13 additions & 0 deletions server/database/models/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,25 @@ export default (sequelize, DataTypes) => {
},
{}
);
Article.findById = async (id) => {
const article = await Article.findOne({ where: { id } });
return article;
};

Article.associate = (models) => {
Article.belongsTo(models.User, {
foreignKey: 'authorId',
as: 'Author',
onDelete: 'CASCADE'
});
Article.belongsToMany(models.Tag, {
foreignKey: 'articleId',
otherKey: 'tagId',
through: 'ArticleTags',
as: 'tags',
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
});
};
SequelizeSlugify.slugifyModel(Article, {
source: ['title'],
Expand Down
31 changes: 31 additions & 0 deletions server/database/models/tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export default (sequelize, DataTypes) => {
const Tag = sequelize.define(
'Tag',
{
name: {
allowNull: false,
type: DataTypes.STRING,
unique: true,
validate: {
len: {
args: [2],
msg: 'tag name should be greater than one character'
}
}
}
},
{}
);

Tag.associate = (models) => {
Tag.belongsToMany(models.Article, {
foreignKey: 'tagId',
otherKey: 'articleId',
through: 'ArticleTags',
as: 'articles',
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
});
};
return Tag;
};
18 changes: 18 additions & 0 deletions server/database/seeds/20190730180408-my-seed-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ export default {
level: 4,
createdAt: new Date(),
updatedAt: new Date()
},
{
firstName: 'demo',
lastName: 'user',
userName: 'demoUser',
email: 'demoUser@gmail.com',
password:
'$2y$12$3t1adkk7/grjsz2cG5hlXOTO8LwZUmGeG7zs6udoH78MeoPNmXQ.y',
bio: 'This is a simple bio of demo user',
role: 'superadmin',
level: 5,
followingsCount: 1,
followersCount: 1,
identifiedBy: 'username',
location: 'Lagos, Nigeria',
occupation: 'Software Engineer',
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
Expand Down
20 changes: 20 additions & 0 deletions server/database/seeds/20190812113159-demo-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export default {
up: queryInterface => queryInterface.bulkInsert(
'Tags',
[
{
name: 'foot-ball',
createdAt: new Date(),
updatedAt: new Date()
},
{
name: 'fut-ball',
createdAt: new Date(),
updatedAt: new Date()
}
],
{}
),

down: queryInterface => queryInterface.bulkDelete('Tags', null, {})
};
Loading

0 comments on commit c09afc3

Please sign in to comment.