Added voting. Improved comments.

This commit is contained in:
2023-06-25 19:52:00 +02:00
parent 754a89b913
commit f42daa2f83
29 changed files with 916 additions and 154 deletions

View File

@@ -1,41 +1,80 @@
import knex from './knex';
import { fetchUsers } from './users';
import { verifyPrivilege } from './privileges';
import { HttpError } from './errors';
function curateDatabaseComment(comment, { users }) {
return {
id: comment.id,
body: comment.body,
parentId: comment.parent_id,
userId: comment.user_id,
user: users.find((user) => user.id === comment.user_id),
createdAt: comment.created_at,
};
}
function threadComments(thread, commentsByParentId) {
if (!thread) {
return [];
}
return thread.map((comment) => ({
...comment,
comments: threadComments(commentsByParentId[comment.id], commentsByParentId),
}));
}
function nestComments(comments) {
const commentsByParentId = comments.reduce((acc, comment) => {
if (!acc[comment.parentId]) {
acc[comment.parentId] = [];
}
acc[comment.parentId].push(comment);
return acc;
}, {});
return threadComments(commentsByParentId.null, commentsByParentId);
}
async function fetchPostComments(postId, { limit = 100 } = {}) {
const comments = await knex('comments')
.where('post_id', postId)
.orderBy('created_at', 'asc')
.orderBy('created_at', 'desc')
.limit(limit);
const users = await fetchUsers(comments.map((comment) => comment.user_id));
const curatedComments = comments.map((comment) => curateDatabaseComment(comment, { users }));
return comments.map((comment) => curateDatabaseComment(comment, { users }));
const nestedComments = nestComments(curatedComments);
return nestedComments;
}
async function addComment(comment, postId, user) {
await verifyPrivilege('addComment', user);
const commentEntry = await knex('comments')
if (!comment.body) {
throw new HttpError({
statusMessage: 'Comment cannot be empty',
statusCode: 400,
});
}
const [commentEntry] = await knex('comments')
.insert({
body: comment.body,
post_id: postId,
parent_id: comment.parentId,
user_id: user.id,
})
.returning('*');
console.log(comment, user);
return curateDatabaseComment(commentEntry);
const users = await fetchUsers([commentEntry.user_id]);
return curateDatabaseComment(commentEntry, { users });
}
export {

View File

@@ -6,7 +6,9 @@ import { HttpError } from './errors';
import { fetchShelf } from './shelves';
import { fetchUsers } from './users';
function curatePost(post, { shelf, users }) {
function curateDatabasePost(post, {
shelf, users, votes,
}) {
const curatedPost = {
id: post.id,
title: post.title,
@@ -16,13 +18,40 @@ function curatePost(post, { shelf, users }) {
createdAt: post.created_at,
shelf,
user: users.find((user) => user.id === post.user_id),
votes: votes[post.id],
commentCount: Number(post.comment_count),
};
return curatedPost;
}
async function fetchShelfPosts(shelfId, { limit = 100 } = {}) {
function curatePostVote(vote) {
return {
tally: Number(vote.tally),
total: Number(vote.total),
bump: !!vote.bump,
sink: !!vote.sink,
};
}
async function fetchPostVotes(postIds, user) {
const votes = await knex('posts_votes')
.select(
'post_id',
knex.raw('sum(value) as tally'),
knex.raw('count(id) as total'),
...(user ? [
knex.raw('bool_or(user_id = :userId and value = 1) as bump', { userId: knex.raw(user.id) }),
knex.raw('bool_or(user_id = :userId and value = -1) as sink', { userId: knex.raw(user.id) })]
: []),
)
.whereIn('post_id', postIds)
.groupBy('post_id');
return Object.fromEntries(votes.map((vote) => [vote.post_id, curatePostVote(vote)]));
}
async function fetchShelfPosts(shelfId, { user, limit = 100 } = {}) {
const shelf = await fetchShelf(shelfId);
const posts = await knex('posts')
@@ -33,15 +62,19 @@ async function fetchShelfPosts(shelfId, { limit = 100 } = {}) {
.groupBy('posts.id')
.limit(limit);
const users = await fetchUsers(posts.map((post) => post.user_id));
const [users, votes] = await Promise.all([
fetchUsers(posts.map((post) => post.user_id)),
fetchPostVotes(posts.map((post) => post.id), user),
]);
return posts.map((post) => curatePost(post, { shelf, users }));
return posts.map((post) => curateDatabasePost(post, { shelf, users, votes }));
}
async function fetchPost(postId) {
async function fetchPost(postId, user) {
const post = await knex('posts')
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
.leftJoin('comments', 'comments.post_id', 'posts.id')
.leftJoin('posts_votes', 'posts_votes.post_id', 'posts.id')
.where('posts.id', postId)
.groupBy('posts.id')
.first();
@@ -53,12 +86,13 @@ async function fetchPost(postId) {
});
}
const [shelf, users] = await Promise.all([
const [shelf, users, votes] = await Promise.all([
fetchShelf(post.shelf_id),
fetchUsers([post.user_id]),
fetchPostVotes([post.id], user),
]);
return curatePost(post, { shelf, users });
return curateDatabasePost(post, { shelf, users, votes });
}
async function createPost(post, shelfId, user) {
@@ -73,9 +107,7 @@ async function createPost(post, shelfId, user) {
});
}
console.log(post);
const postId = await knex('posts')
const [postEntry] = await knex('posts')
.insert({
title: post.title,
body: post.body,
@@ -83,13 +115,32 @@ async function createPost(post, shelfId, user) {
shelf_id: shelf.id,
user_id: user.id,
})
.returning('id');
.returning('*');
return postId;
const users = await fetchUsers([postEntry.user_id]);
return curateDatabasePost(postEntry, { shelf, users });
}
async function votePost(postId, value, user) {
await knex('posts_votes')
.insert({
value,
post_id: postId,
user_id: user.id,
})
.onConflict(['post_id', 'user_id'])
.merge()
.returning('value');
const votes = await fetchPostVotes([postId], user);
return votes[postId];
}
export {
createPost,
fetchPost,
fetchShelfPosts,
votePost,
};

View File

@@ -36,15 +36,13 @@ async function fetchShelves({ limit = 10 } = {}) {
}
async function createShelf(shelf, user) {
const shelfEntry = await knex('shelves')
const [shelfEntry] = await knex('shelves')
.insert({
slug: shelf.slug,
founder_id: user.id,
})
.returning('*');
console.log('entry', shelfEntry);
return curateDatabaseShelf(shelfEntry);
}

View File

@@ -1,4 +1,4 @@
import { createPost } from '../posts';
import { createPost, votePost } from '../posts';
async function createPostApi(req, res) {
const post = await createPost(req.body, req.params.shelfId, req.user);
@@ -6,6 +6,13 @@ async function createPostApi(req, res) {
res.send(post);
}
async function votePostApi(req, res) {
const votes = await votePost(req.params.postId, req.body.value, req.user);
res.send(votes);
}
export {
createPostApi as createPost,
votePostApi as votePost,
};

View File

@@ -25,7 +25,7 @@ import {
import { createShelf } from './shelves';
import { createPost } from './posts';
import { createPost, votePost } from './posts';
import { addComment } from './comments';
const logger = initLogger();
@@ -70,6 +70,7 @@ async function startServer() {
// POSTS
router.post('/api/shelves/:shelfId/posts', createPost);
router.post('/api/posts/:postId/votes', votePost);
// COMMENTS
router.post('/api/posts/:postId/comments', addComment);

View File

@@ -1,9 +1,9 @@
import { createShelf } from '../shelves';
async function createShelfApi(req) {
async function createShelfApi(req, res) {
const shelf = await createShelf(req.body, req.user);
return shelf;
res.send(shelf);
}
export {