shack/src/posts.js

224 lines
5.7 KiB
JavaScript

// import knex from './knex';
import { verifyPrivilege } from './privileges';
import knex from './knex';
import { HttpError } from './errors';
import { fetchShelf, fetchShelves } from './shelves';
import { fetchUsers } from './users';
import { generateThumbnail } from './thumbnails';
import slugify from './utils/slugify';
import initLogger from './logger';
const logger = initLogger();
const emptyVote = {
tally: 0,
total: 0,
bump: false,
sink: false,
};
function curateDatabasePost(post, {
shelf, shelves, users, vote, votes,
}) {
const curatedPost = {
id: post.id,
title: post.title,
slug: slugify(post.title, { limit: 50 }),
body: post.body,
link: post.link,
shelfId: post.shelf_id,
createdAt: post.created_at,
hasThumbnail: post.thumbnail,
shelf: shelf || shelves?.[post.shelf_id],
user: users.find((user) => user.id === post.user_id),
vote: vote || votes?.[post.id] || emptyVote,
commentCount: Number(post.comment_count),
};
return curatedPost;
}
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(shelfIds, { user, limit = 100 } = {}) {
const shelves = await fetchShelves([].concat(shelfIds));
const posts = await knex('posts')
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
.leftJoin('comments', 'comments.post_id', 'posts.id')
.whereIn('shelf_id', Object.keys(shelves))
.orderBy('created_at', 'desc')
.groupBy('posts.id')
.limit(limit);
const [users, votes] = await Promise.all([
fetchUsers(posts.map((post) => post.user_id)),
fetchPostVotes(posts.map((post) => post.id), user),
]);
return posts.map((post) => curateDatabasePost(post, { shelves, users, votes }));
}
async function fetchUserPosts(user, { limit = 20 } = {}) {
if (!user) {
throw new HttpError({
statusMessage: 'You are not logged in',
statusCode: 401,
});
}
const posts = await knex('shelves_subscriptions')
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
.leftJoin('posts', 'posts.shelf_id', 'shelves_subscriptions.shelf_id')
.leftJoin('comments', 'comments.post_id', 'posts.id')
.where('shelves_subscriptions.user_id', user.id)
.groupBy('posts.id')
.orderBy('created_at', 'desc')
.limit(limit);
const [shelves, users, votes] = await Promise.all([
fetchShelves(posts.map((post) => post.shelf_id)),
fetchUsers(posts.map((post) => post.user_id)),
fetchPostVotes(posts.map((post) => post.id), user),
]);
return posts.map((post) => curateDatabasePost(post, { shelves, users, votes }));
}
async function fetchAllPosts({ user, limit = 20 } = {}) {
const posts = await knex('posts')
.select('posts.*', knex.raw('count(comments.id) as comment_count'))
.leftJoin('comments', 'comments.post_id', 'posts.id')
.orderBy('created_at', 'desc')
.groupBy('posts.id')
.limit(limit);
const [shelves, users, votes] = await Promise.all([
fetchShelves(posts.map((post) => post.shelf_id)),
fetchUsers(posts.map((post) => post.user_id)),
fetchPostVotes(posts.map((post) => post.id), user),
]);
return posts.map((post) => curateDatabasePost(post, { shelves, users, votes }));
}
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();
if (!post) {
throw new HttpError({
statusMessage: 'This post does not exist',
statusCode: 404,
});
}
const [shelf, users, votes] = await Promise.all([
fetchShelf(post.shelf_id),
fetchUsers([post.user_id]),
fetchPostVotes([post.id], user),
]);
return curateDatabasePost(post, { shelf, users, votes });
}
async function votePost(postId, value, user) {
if (value === 0) {
await knex('posts_votes')
.where({
post_id: postId,
user_id: user.id,
})
.delete();
} else {
await knex('posts_votes')
.insert({
value,
post_id: postId,
user_id: user.id,
})
.onConflict(['post_id', 'user_id'])
.merge();
}
logger.silly(`User ${user.username} voted ${value} on post ${postId}`);
const votes = await fetchPostVotes([postId], user);
return votes[postId] || emptyVote;
}
async function createPost(post, shelfId, user) {
await verifyPrivilege('createPost', user);
const shelf = await fetchShelf(shelfId);
if (!shelf) {
throw new HttpError({
statusMessage: 'The target shelf does not exist',
statusCode: 404,
});
}
const [postEntry] = await knex('posts')
.insert({
title: post.title,
body: post.body,
link: post.link,
shelf_id: shelf.id,
user_id: user.id,
})
.returning('*');
const [users, vote] = await Promise.all([
fetchUsers([postEntry.user_id]),
votePost(postEntry.id, 1, user),
]);
logger.verbose(`User ${user.username} created post ${postEntry.id} on s/${shelf.slug}`);
generateThumbnail(postEntry.id);
return curateDatabasePost(postEntry, { shelf, users, vote });
}
export {
createPost,
fetchPost,
fetchShelfPosts,
fetchUserPosts,
fetchAllPosts,
votePost,
};