Skip to content

Commit

Permalink
feat(bookmark): user can bookmark article to read later
Browse files Browse the repository at this point in the history
- user log in
- user specifies article slug they want to bookmark in the url
- user can bookmark a certain article only once
- when user tries to bookmark an already bookmarked article that bookmark is removed
from the database and user can bookmark again
- user can see all the bookmarks they made by logging in and using the get bookmarks
endpoint. they also see basic information about the article they bookmarked.
[wip #165413121]
  • Loading branch information
Kabalisa committed May 21, 2019
1 parent 554819c commit c774be7
Show file tree
Hide file tree
Showing 10 changed files with 400 additions and 3 deletions.
27 changes: 27 additions & 0 deletions controllers/article.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,33 @@ class ArticleController {
const result = await ArticleHelper.getDislikes(req);
return res.status(200).send({ dislikes: result, count: numberOfDislikes });
}

/**
* @param {object} req - Request object
* @param {object} res - Response object
* @returns {object} response
* @static
*/
static async bookmarkArticle(req, res) {
const { bookmark } = req.body;
if (bookmark === 'SequelizeUniqueConstraintError') {
const result = await ArticleHelper.deleteBookmark(req);
return res.status(200).send(result);
}
return res.status(201).send({ Bookmark: bookmark });
}

/**
* @param {object} req - Request object
* @param {object} res - Response object
* @returns {object} response
* @static
*/
static async getBookmarks(req, res) {
const { id } = req.user;
const bookmarks = await ArticleHelper.getBookmarks(id);
return res.status(200).send({ Bookmarks: bookmarks });
}
}

export default ArticleController;
75 changes: 74 additions & 1 deletion helpers/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import emitter from './eventEmitters';
import tagHelper from './tag.helper';

const {
Article, User, Tag, Like
Article, User, Tag, Like, Bookmark
} = db;

/**
Expand Down Expand Up @@ -391,5 +391,78 @@ class ArticleHelper {
const tagList = await Tag.aggregate('name', 'DISTINCT', { plain: false }).map(row => row.DISTINCT);
return tagList || [];
}

/**
* function for creating bookmarks on articles
* @function createBookmark
* @param {object} req
* @param {object} res
* @param {object} next
* @returns { number } appropriate message
*/
static async articleToBookmark(req, res, next) {
const { slug } = req.params;
const article = await Article.findOne({ where: { slug } });
if (!article) { return res.status(404).send({ errors: { body: ['article not found'] } }); }
next();
return true;
}

/**
* function for creating bookmarks on articles
* @function createBookmark
* @param {object} req
* @param {object} res
* @param {object} next
* @returns { number } appropriate message
*/
static createBookmark(req, res, next) {
const { slug } = req.params;
const { id } = req.user;
Bookmark.create({
userId: id,
titleSlug: slug,
createdAt: new Date(),
updatedAt: new Date()
}).then((bookmark) => {
req.body.bookmark = bookmark;
next();
return true;
})
.catch((error) => {
req.body.bookmark = error.name;
next();
return true;
});
}

/**
* @param {object} req - Request object
* @returns {object} response
* @static
*/
static async deleteBookmark(req) {
const { slug } = req.params;
const { id } = req.user;
await Bookmark.destroy({ where: { userId: id, titleSlug: slug } });
return { message: `your bookmark is for article with slug ${slug} is removed, bookmark again` };
}

/**
* @param {number} id - Request object
* @returns {object} response
* @static
*/
static async getBookmarks(id) {
const bookmarks = await Bookmark.findAll({
where: { userId: id },
include: [{ model: Article, as: 'article', attributes: ['title', 'description', 'slug'] }],
attributes: ['id', 'userId', 'createdAt']
});
if (bookmarks.length < 1) {
return 'no bookmarks made';
}
return bookmarks;
}
}
export default ArticleHelper;
39 changes: 39 additions & 0 deletions migrations/20190518210158-create-bookmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const bookmarkMigration = {
up: (queryInterface, Sequelize) => queryInterface.createTable('Bookmarks', {
id: {
allowNull: false,
autoIncrement: true,
type: Sequelize.INTEGER
},
userId: {
type: Sequelize.INTEGER,
primaryKey: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
references: {
model: 'Users',
key: 'id'
}
},
titleSlug: {
type: Sequelize.STRING,
primaryKey: true,
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
references: {
model: 'Articles',
key: 'slug'
}
},
createdAt: {
allowNull: false,
type: Sequelize.DATE
},
updatedAt: {
allowNull: false,
type: Sequelize.DATE
}
}),
down: queryInterface => queryInterface.dropTable('Bookmarks')
};
export default bookmarkMigration;
4 changes: 4 additions & 0 deletions models/article.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ const articles = (sequelize, DataTypes) => {
foreignKey: 'articleSlug',
sourceKey: 'slug'
});
Article.hasMany(models.Bookmark, {
foreignKey: 'titleSlug',
sourceKey: 'slug'
});
};
return Article;
};
Expand Down
29 changes: 29 additions & 0 deletions models/bookmark.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
const bookmarks = (sequelize, DataTypes) => {
const Bookmark = sequelize.define('Bookmark', {
userId: {
type: DataTypes.INTEGER,
primaryKey: true,
},
titleSlug: {
type: DataTypes.STRING,
primaryKey: true,
}
}, {});
Bookmark.associate = (models) => {
Bookmark.belongsTo(models.User, {
foreignKey: 'userId',
as: 'author',
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
});
Bookmark.belongsTo(models.Article, {
foreignKey: 'titleSlug',
as: 'article',
targetKey: 'slug',
onDelete: 'CASCADE',
onUpdate: 'CASCADE'
});
};
return Bookmark;
};
export default bookmarks;
4 changes: 2 additions & 2 deletions models/like.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ const likes = (sequelize, DataTypes) => {
foreignKey: 'userId',
as: 'author',
onDelete: 'CASCADE',
onupdate: 'CASCADE'
onUpdate: 'CASCADE'
});
Like.belongsTo(models.Article, {
foreignKey: 'titleSlug',
targetKey: 'slug',
onDelete: 'CASCADE',
onupdate: 'CASCADE'
onUpdate: 'CASCADE'
});
};
return Like;
Expand Down
1 change: 1 addition & 0 deletions models/users.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const users = (sequelize, DataTypes) => {
User.hasMany(models.Follows, { foreignKey: 'following' });
User.hasMany(models.Follows, { foreignKey: 'follower' });
User.hasMany(models.Notification, { as: 'user', foreignKey: 'userId' });
User.hasMany(models.Bookmark, { foreignKey: 'userId' });
};
return User;
};
Expand Down
2 changes: 2 additions & 0 deletions routes/api/article/articles.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ router.get('/:slug/likes', Auth, ArticleHelper.likesNumber, articleController.ge
router.get('/:slug/dislikes', Auth, ArticleHelper.dislikesNumber, articleController.getDislikes);
router.post('/:slug/tag', Auth, ArticleHelper.isOwner, TagMiddleware.isNotTagAdded, articleController.tagArticle);
router.get('/tag/list', articleController.getAllTags);
router.post('/:slug/bookmark', Auth, ArticleHelper.articleToBookmark, ArticleHelper.createBookmark, articleController.bookmarkArticle);
router.get('/user/bookmarks', Auth, articleController.getBookmarks);

export default router;
58 changes: 58 additions & 0 deletions routes/api/article/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,3 +249,61 @@
* '400':
* dislikes have been failed to be fetched
*/

/**
* @swagger
* /articles/{slug}/bookmark:
* post:
* tags:
* - bookmark
* name: bookmark
* summary: create bookmark by a user an article
* produces:
* - application/json
* consumes:
* - application/json
* parameters:
* - name: slug
* in: path
* schema:
* type: string
* required:
* - slug
* - name: authorization
* in: header
* schema:
* type: string
* required:
* - authorization
* responses:
* '201':
* description: bookmark have been created successfully
* '400':
* bookmark have been failed to be created
*/

/**
* @swagger
* /articles/user/bookmarks:
* get:
* tags:
* - bookmark
* name: bookmark
* summary: get bookmark made by a user an article
* produces:
* - application/json
* consumes:
* - application/json
* parameters:
* - name: authorization
* in: header
* schema:
* type: string
* required:
* - authorization
* responses:
* '200':
* description: bookmarks have been fetched successfully
* '400':
* bookmarks have been failed to be fetched
*/
Loading

0 comments on commit c774be7

Please sign in to comment.