Skip to content

Commit

Permalink
Merge 8b91af9 into 9ccf88c
Browse files Browse the repository at this point in the history
  • Loading branch information
Anguandia committed Aug 19, 2019
2 parents 9ccf88c + 8b91af9 commit 06a68d0
Show file tree
Hide file tree
Showing 21 changed files with 1,931 additions and 1,269 deletions.
Binary file added .DS_Store
Binary file not shown.
2,536 changes: 1,270 additions & 1,266 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 0 additions & 3 deletions sequelize-data.json

This file was deleted.

211 changes: 211 additions & 0 deletions src/controllers/tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/* eslint-disable require-jsdoc */
// import isEmpty from 'utils';
import models from '../models';
import Util from '../helpers/util';

const util = new Util();
const internalError = () => {
util.setError(500, 'Internal Error');
return util;
};

const notFound = (msg) => {
util.setError(404, `${msg} not found`);
return util;
};

const {
Tag, Article, ArticleTag
} = models;

class TagController {
static async createTag(req, res) {
// create orphan tag
const { name } = req.body;
Tag.findOrCreate({
where: { name }
}).then((tag) => {
if (tag[1]) {
return res.status(201).json({ msg: `tag ${name} created`, data: tag[0] });
}
res.status(200).json({ msg: `tag ${name} exists` });
}).catch(() => {
internalError.send(res);
});
}

static async createArticleTag(req, res) {
const article = await Article.findOne({
where: { id: req.params.articleId },
});
const { name } = req.body;
Tag.findOrCreate({
where: { name },
raw: true
}).then((tag) => {
if (tag[0]) {
article.addTag(tag[0]);
return res.status(201).json({ msg: `tag ${name} added to article`, data: article });
}
}).catch(() => internalError().send(res));
}

static async editTag(req, res) {
// changes the name of a given tag if new name still unique or merges tags
// with all its relationships if new name exists.
// must confirm merger from user
const { name } = req.params;
const newName = req.body.update;
Tag.update({ name: newName }, {
where: { name },
returning: true,
}).then((updated) => {
if (updated[0] !== 0) {
return res.status(200).json({
msg: `tag ${name} updated to ${newName}`, data: updated[1]
});
}
notFound(`tag ${name}`).send(res);
}).catch(() => internalError().send(res));
}

static async editArticleTag(req, res) {
const newTag = await Tag.findOrCreate({
where: { name: req.body.name }
});
const oldTag = await Tag.findOne({
where: { name: req.params.tagName }
});
ArticleTag.update(
{ tagId: newTag.id, articleId: req.params.articleId },
{
where: {
tagId: oldTag.id, articleId: req.params.articleId
},
returning: true,
raw: true
}
).then((updated) => {
if (updated[0] !== 0) {
res.status(200).json({ msg: 'update successful' });
}
notFound(oldTag.name).send(res);
})
.catch(() => internalError());
}

static async getTags(req, res) {
// gets and returns an array of objects having tags and the number of
// articles for each tags
Tag.findAll()
.then((tags) => {
if (tags.length === 0) {
notFound('tags').send(res);// not tested
}
// reduce tags list to give article numbers only
res.status(200).json({ tags });
}).catch(() => internalError().send(res));
}

static getTag(req, res) {
// gets and returns a given tag and the number of articles with this tag
Tag.findOne({
where: { name: req.params.name },
include: ['articles']
}).then((tag) => {
if (tag) {
return res.status(200).json({
tag: {
id: tag.id, name: tag.name, articleCount: tag.articles.length
}
});
}
notFound('tag').send(res);
}).catch(() => internalError().send(res));
}

static getTagArticles(req, res) {
// gets an array of all articles with the given tag
Tag.findAll({
// alternatively use tag.getArticles()
where: { name: req.params.name },
include: ['articles'],
}).then((articles) => {
if (articles[0].articles.length === 0) {
return notFound(`articles about ${req.params.name}`).send(res);
}
res.status(200).json({
articles: articles.map(element => element.articles)
});
// res.status(404).json({ msg: `no articles about ${req.params.name}` });
notFound(`articles about ${req.params.name}`).send(res);
}).catch(() => internalError().send(res));
}

static async getArticleTags(req, res) {
// returns an array of all tags for a given article
Article.findOne({
where: { id: req.params.articleId },
include: ['tags']
}).then((article) => {
if (!article) {
return res.status(404).json({ error: 'article not found' }); // untested
}
if (article.tags.length === 0) {
return res.status(404).json({ msg: 'article not tagged' });
}
res.status(200).json({ tags: article.tags.map(tag => tag.name) });
}).catch(() => internalError().send(res));
}

static async deleteArticleTag(req, res) {
// deletes a given tag of a given article technically the tag and article
// remain undeleted, only the association in the ArticleTags table is
// deleted
ArticleTag.destroy({
// alternatively, use article.removeTag(tag)
where: { articleId: req.params.articleId, tagId: req.params.tagId }
}).then((deleted) => {
if (deleted) {
return res.status(200).json({
message: `tag with Id ${req.params.tagId} removed from article`
});
}
res.status(404).json({
message: `this article has no tag ${req.params.tagId}`
});
}).catch(() => internalError().send(res));
}

static async deleteTagArticles(req, res) {
// deletes all articles about a particular item/tag
Tag.findOne({
where: { name: req.params.name },
include: ['articles']
}).then((tag) => {
const failed = [];
if (tag.articles.length === 0) {
return res.status(404).json({
error: `no articles about ${req.params.name}`
});
}
tag.articles.forEach((article) => {
article.destroy((success) => {
if (success !== {}) {
failed.push(article.id);
}
});
});
if (failed.length !== 0) {
return res.status(500).json({
error: `articles with Ids ${failed.join(', ')} not deleted`
});
}
res.status(200).json({
msg: `all articles about ${req.params.name} deleted`
});
}).catch(() => internalError().send(res));
}
}

export default TagController;
13 changes: 13 additions & 0 deletions src/middlewares/validators/validateTags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import isAlnum from './is.alphanumeric';

const validateReq = (req, res, next) => {
if (req.params.articleId && !/^\d+$/.test(req.params.articleId)) {
res.status(400).json({ error: 'articleId must be an integer' });
} else if (req.params.name && !isAlnum(req.params.name)) {
res.status(400).json({ error: 'tagName invalid' });
} else if (req.body.name && !isAlnum(req.body.name)) {
res.status(400).json({ error: 'tagName invalid' });
} else { next(); }
};

export default validateReq;
28 changes: 28 additions & 0 deletions src/migrations/20190816062502-create-tags.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('Tags', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
name: {
type: Sequelize.STRING,
primaryKey: true
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('Tags');
}
};
24 changes: 24 additions & 0 deletions src/migrations/20190817003558-create-article-tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('ArticleTags', {
articleId: {
type: Sequelize.INTEGER
},
tagId: {
type: Sequelize.INTEGER
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('ArticleTags');
}
};
5 changes: 5 additions & 0 deletions src/models/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ module.exports = (sequelize, DataTypes) => {
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
})
Article.belongsToMany(models.Tag, {
through: 'ArticleTags',
foreignKey: 'articleId',
as: 'tags'
})
};
return Article;
};
12 changes: 12 additions & 0 deletions src/models/articletag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
'use strict';
module.exports = (sequelize, DataTypes) => {
const ArticleTag = sequelize.define('ArticleTag', {
articleId: DataTypes.INTEGER,
tagId: DataTypes.INTEGER
}, {});
ArticleTag.associate = function (models) {
ArticleTag.belongsTo(models.Article, { foreignKey: 'articleId' })
ArticleTag.belongsTo(models.Tag, { foreignKey: 'tagId' })
};
return ArticleTag;
};
13 changes: 13 additions & 0 deletions src/models/tag.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module.exports = (sequelize, DataTypes) => {
const Tag = sequelize.define('Tag', {
name: DataTypes.STRING,
}, {});
Tag.associate = function (models) {
Tag.belongsToMany(models.Article, {
through: 'ArticleTags',
foreignKey: 'tagId',
as: 'articles'
})
};
return Tag;
};
9 changes: 9 additions & 0 deletions src/routes/api/article/article.routes.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import express from 'express';
import auth from '../../../middlewares/auth';
import articleController from '../../../controllers/articles.controller';
import tagController from '../../../controllers/tag';
import imageUpload from '../../../middlewares/multer';
import validate from '../../../middlewares/validators/general.validation';
import { schema } from '../../../middlewares/validators/schemas';
import fakeCloud from '../../../middlewares/fakecloud';
import confirmEmailAuth from '../../../middlewares/emailVarification.middleware';
import validateReq from '../../../middlewares/validators/validateTags';

const router = express.Router();

Expand All @@ -17,4 +19,11 @@ router.get('/articles', [auth, confirmEmailAuth], articleController.getAllArticl
router.get('/articles/:slug', [auth, confirmEmailAuth], articleController.getOneArticle);
router.delete('articles/:slug', [auth, confirmEmailAuth], articleController.deleteArticle);
router.patch('articles/:slug', [auth, confirmEmailAuth], imageUpload.array('images', 10), articleController.UpdateArticle);


router.post('/articles/:articleId/tags', auth, validateReq, tagController.createArticleTag);
router.get('/articles/:articleId/tags', validateReq, tagController.getArticleTags);
router.patch('/articles/:articleId/:tagName', auth, validateReq, tagController.editArticleTag);
router.delete('/articles/:articleId/:tagId', auth, validateReq, tagController.deleteArticleTag);

export default router;
Empty file added src/routes/api/article/doc.js
Empty file.
2 changes: 2 additions & 0 deletions src/routes/api/index.route.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import article from './article/article.routes';
import profile from './profile/profile.route';
import rate from './rate/rate.route';
import Comments from './comment/comments.route';
import tag from './tag/tag.routes';

const router = express.Router();
router.use('/images', express.static(path.join(__dirname, 'images')));

router.use('/comments', Comments);
router.use(oauth);
router.use('/profile', profile);
router.use('/', tag);
router.use('/users', user);
router.use('/', article);
router.use('/rate', rate);
Expand Down
16 changes: 16 additions & 0 deletions src/routes/api/tag/tag.routes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import express from 'express';
import TagController from '../../../controllers/tag';
import auth from '../../../middlewares/auth';
import validateReq from '../../../middlewares/validators/validateTags';


const router = express.Router();

router.post('/tags', auth, TagController.createTag);
router.get('/tags', TagController.getTags);
router.get('/tags/:name', TagController.getTag);
router.patch('/tags/:name', auth, TagController.editTag);
router.get('/tags/:name/articles', validateReq, TagController.getTagArticles);
router.delete('/tags/:name/articles', auth, TagController.deleteTagArticles);

export default router;
Loading

0 comments on commit 06a68d0

Please sign in to comment.