diff --git a/src/comp/comment.svelte b/src/comp/comment.svelte new file mode 100644 index 0000000..8a58d3f --- /dev/null +++ b/src/comp/comment.svelte @@ -0,0 +1,16 @@ + + +
+
{commentNode.parent.author?.name}
+

{commentNode.parent.content}

+
+ {#each commentNode.children as reply} + + {/each} +
+
diff --git a/src/lib/client/nodetree.js b/src/lib/client/nodetree.js new file mode 100644 index 0000000..e94afc9 --- /dev/null +++ b/src/lib/client/nodetree.js @@ -0,0 +1,41 @@ +/** + * @typedef {import('$types/base').Comment} Comment + */ + +/** + * @param {import('$types/base').Result} comments + * @returns {import('$types/base').CommentTreeNode[]} + */ +export function buildCommentTree(comments) { + /** @type {Comment[]} */ + let roots = []; + /** @type {Map} */ + let refs = new Map(); + + comments.forEach((comment, id) => { + if (comment.parentCommentId == null) { + roots.push(comment); + } else { + if (!refs.has(comment.parentCommentId)) { + refs.set(comment.parentCommentId, []); + } + refs.get(comment.parentCommentId)?.push(comment); + } + }); + + return roots.map(r => buildFromRoot(refs, r)); +} + +/** + * @param {Map} refs + * @param {Comment} comment + * @returns {import('$types/base').CommentTreeNode} + */ +function buildFromRoot(refs, comment) { + return { + parent: comment, + children: refs.get(comment.id)?.map(c => { + return buildFromRoot(refs, c); + }) || [] + } +} diff --git a/src/lib/server/db/category.js b/src/lib/server/db/category.js index cea443d..d2bf150 100644 --- a/src/lib/server/db/category.js +++ b/src/lib/server/db/category.js @@ -1,5 +1,5 @@ import { createCache } from '$lib/cache.server'; -import { cacheUpdater, cachedMethod } from './root'; +import { cacheUpdater, cachedMethod, refExtendCachedMethod } from './root'; const cache = createCache(); @@ -12,6 +12,17 @@ const cache = createCache(); * @typedef {import('$types/base').Category} Category */ +/** + * @param {import('postgres').Row} row + * @returns {Category} + */ +function parseCategoryFromRow(row) { + return { + id: row['id'], + name: row['name'] + }; +} + /** * @param {Result} categories * @returns {Result} @@ -25,13 +36,15 @@ const updateCategoryCache = cacheUpdater(cache); */ export const getCategoriesCached = cachedMethod(cache, getCategories); +export const getCategoriesCachedByRef = refExtendCachedMethod(getCategoriesCached); + /** * @param {import('postgres').Sql} sql * @param {number[]} category_ids * @returns {Promise>} */ export async function getCategories(sql, category_ids) { - if (category_ids.length == 0) return {}; + if (category_ids.length == 0) return new Map(); const query = sql` SELECT id, name @@ -43,13 +56,10 @@ export async function getCategories(sql, category_ids) { /** * @type {Result} */ - let result = {}; + let result = new Map(); categories.forEach(row => { - result[row['id']] = { - id: row['id'], - name: row['name'] - } + result.set(row['id'], parseCategoryFromRow(row)); }) return updateCategoryCache(result); @@ -64,12 +74,8 @@ export async function getCategories(sql, category_ids) { export async function getCategoryCached(sql, category_id) { const categories = await getCategoriesCached(sql, [category_id]); - if (Object.keys(categories).length == 0) { - return { - error: true, - msg: `Could not find Category of ID ${category_id}` - }; - } - - return categories[category_id]; + return categories.get(category_id) || { + error: true, + msg: `Could not find Category of ID ${category_id}` + }; } \ No newline at end of file diff --git a/src/lib/server/db/comment.js b/src/lib/server/db/comment.js new file mode 100644 index 0000000..84ddbf9 --- /dev/null +++ b/src/lib/server/db/comment.js @@ -0,0 +1,48 @@ +import { getUser, getUsersCached, getUsersCachedByRef } from './user'; + +/** + * @typedef {import('$types/base').Comment} Comment + */ + +/** + * @param {import('$types/base').User | null} author + * @param {import('postgres').Row} row + * @returns {Comment} + */ +function parseCommentFromRow(author, row) { + return { + id: row['id'], + author: author, + parentCommentId: row['parent_comment_id'], + content: row['latest_content'], + commentDate: row['created_date'], + rating: { + likes: BigInt(row['likes']), + dislikes: BigInt(row['dislikes']), + } + }; +} + +/** + * @param {import('postgres').Sql} sql + * @param {number} post_id + * @returns {Promise} + */ +export async function getCommentsForPost(sql, post_id) { + const query = sql` + SELECT id, author_id, latest_content, parent_comment_id, created_date, likes, dislikes + FROM doki8902.message_comment + WHERE post_id = ${ post_id };`; + + const comments = await query; + + const users = await getUsersCachedByRef(sql, comments, c => c['author_id']); + + /** + * @type {Comment[]} + */ + return comments.map(row => parseCommentFromRow( + users.get(row['author_id']) || null, + row + )); +} diff --git a/src/lib/server/db/post.js b/src/lib/server/db/post.js index 339c16b..81ad5c2 100644 --- a/src/lib/server/db/post.js +++ b/src/lib/server/db/post.js @@ -1,10 +1,31 @@ -import { getCategoriesCached, getCategoryCached } from './category'; -import { getUser, getUsersCached } from './user'; +import { getCategoriesCached, getCategoriesCachedByRef, getCategoryCached } from './category'; +import { getUser, getUsersCached, getUsersCachedByRef } from './user'; /** * @typedef {import('$types/base').Post} Post */ +/** + * @param {import('$types/base').User | null} author + * @param {import('$types/base').Category} category + * @param {import('postgres').Row} row + * @returns {Post} + */ +function parsePostFromRow(author, category, row) { + return { + id: row['id'], + author: author, + name: row['name'], + category: category, + content: row['latest_content'], + postDate: row['created_date'], + rating: { + likes: BigInt(row['likes']), + dislikes: BigInt(row['dislikes']), + } + }; +} + /** * @param {import('postgres').Sql} sql * @param {import('$types/base').Category | undefined} category @@ -30,31 +51,17 @@ export async function getPosts(sql, category = undefined, limit = 10, offset = 0 const posts = await query; - const users = await getUsersCached(sql, posts.map(row => { - return row['author_id']; - })); - - const categories = await getCategoriesCached(sql, posts.map(row => { - return row['category_id']; - })); + const users = await getUsersCachedByRef(sql, posts, p => p['author_id']); + const categories = await getCategoriesCachedByRef(sql, posts, p => p['category_id']); /** * @type {Post[]} */ - return posts.map(row => { - return { - id: row['id'], - author: users[row['author_id']] || null, - name: row['name'], - category: categories[row['category_id']], - content: row['latest_content'], - post_date: row['created_date'], - rating: { - likes: BigInt(row['likes']), - dislikes: BigInt(row['dislikes']), - } - }; - }); + return posts.map(row => parsePostFromRow( + users.get(row['author_id']) || null, + /** @type {import('$types/base').Category} */ (categories.get(row['category_id'])), + row + )); } /** @@ -108,16 +115,5 @@ export async function getPost(sql, post_id) { /** * @type {Post} */ - return { - id: post['id'], - author: author, - name: post['name'], - category: category, - content: post['latest_content'], - post_date: post['created_date'], - rating: { - likes: BigInt(post['likes']), - dislikes: BigInt(post['dislikes']), - } - }; + return parsePostFromRow(author, category, post); } diff --git a/src/lib/server/db/root.js b/src/lib/server/db/root.js index 41076b7..ce73d8a 100644 --- a/src/lib/server/db/root.js +++ b/src/lib/server/db/root.js @@ -1,12 +1,12 @@ /** * @template T * @param {import('node-cache')} cache - * @returns {function({[id: number]: T})} + * @returns {function(import('$types/base').Result)} */ export const cacheUpdater = (cache) => { return function updateUserCache(data) { - Object.keys(data).forEach(id => { - cache.set(parseInt(id), data[parseInt(id)]); + data.forEach((val, id) => { + cache.set(id, val); }); return data; } @@ -15,15 +15,15 @@ export const cacheUpdater = (cache) => { /** * @template T * @param {import('node-cache')} cache - * @param {function(import('postgres').Sql, number[]): Promise<{[id: number]: T}>} method - * @returns {function(import('postgres').Sql, number[]): Promise<{[id: number]: T}>} + * @param {function(import('postgres').Sql, number[]): Promise>} method + * @returns {function(import('postgres').Sql, number[]): Promise>} */ export const cachedMethod = (cache, method) => { return async function(sql, ids) { /** - * @type {{[id: number]: T}} + * @type {import('$types/base').Result} */ - let results = {}; + let results = new Map(); /** * @type {number[]} */ @@ -34,13 +34,24 @@ export const cachedMethod = (cache, method) => { return; let user = cache.get(id); if (user) - results[id] = user; + results.set(id, user); else missing.push(id); }); const remaining = await method(sql, missing); - return Object.assign({}, results, remaining); + return new Map([...results, ...remaining]); }; } + +/** + * @template T + * @param {function(import('postgres').Sql, number[]): Promise>} cachedMethod + * @returns {function(import('postgres').Sql, import('postgres').RowList, function(import('postgres').Row): number): Promise>} + */ +export const refExtendCachedMethod = (cachedMethod) => { + return async function(sql, rows, getRef) { + return await cachedMethod(sql, rows.map(r => getRef(r))); + } +} diff --git a/src/lib/server/db/user.js b/src/lib/server/db/user.js index c64f724..6844d1f 100644 --- a/src/lib/server/db/user.js +++ b/src/lib/server/db/user.js @@ -1,5 +1,5 @@ import { createCache } from '$lib/cache.server'; -import { cacheUpdater, cachedMethod } from './root'; +import { cacheUpdater, cachedMethod, refExtendCachedMethod } from './root'; const cache = createCache(); @@ -12,6 +12,18 @@ const cache = createCache(); * @typedef {import('$types/base').User} User */ +/** + * @param {import('postgres').Row} row + * @returns {User} + */ +function parseUserFromRow(row) { + return { + id: row['id'], + name: row['username'], + joinDate: row['join_time'] + }; +} + /** * @param {Result} users * @returns {Result} @@ -25,13 +37,15 @@ const updateUserCache = cacheUpdater(cache); */ export const getUsersCached = cachedMethod(cache, getUsers); +export const getUsersCachedByRef = refExtendCachedMethod(getUsersCached); + /** * @param {import('postgres').Sql} sql * @param {number[]} user_ids * @returns {Promise>} */ export async function getUsers(sql, user_ids) { - if (user_ids.length == 0) return {}; + if (user_ids.length == 0) return new Map(); const query = sql` SELECT id, username, join_time @@ -43,14 +57,10 @@ export async function getUsers(sql, user_ids) { /** * @type {Result} */ - let result = {}; + let result = new Map(); users.forEach(row => { - result[row['id']] = { - id: row['id'], - name: row['username'], - join_date: row['join_time'] - } + result.set(row['id'], parseUserFromRow(row)); }) return updateUserCache(result); @@ -64,12 +74,8 @@ export async function getUsers(sql, user_ids) { export async function getUser(sql, user_id) { const users = await getUsers(sql, [user_id]); - if (Object.keys(users).length == 0) { - return { - error: true, - msg: `Could not find user of ID ${user_id}` - }; - } - - return users[user_id]; + return users.get(user_id) || { + error: true, + msg: `Could not find user of ID ${user_id}` + }; } diff --git a/src/routes/(app)/posts/+page.server.js b/src/routes/(app)/posts/+page.server.js index 66312b8..6c4b64c 100644 --- a/src/routes/(app)/posts/+page.server.js +++ b/src/routes/(app)/posts/+page.server.js @@ -5,8 +5,6 @@ import { getPosts } from '$lib/server/db/post'; export async function load({ locals }) { let result = await getPosts(locals.sql); - console.log(result); - return { posts: result }; diff --git a/src/routes/(app)/posts/[id]/+page.server.js b/src/routes/(app)/posts/[id]/+page.server.js index cebdf3b..7be79e2 100644 --- a/src/routes/(app)/posts/[id]/+page.server.js +++ b/src/routes/(app)/posts/[id]/+page.server.js @@ -1,11 +1,18 @@ +import { getCommentsForPost } from "$lib/server/db/comment"; import { getPost } from "$lib/server/db/post"; /** @type {import("@sveltejs/kit").ServerLoad} */ export async function load({ params, locals }) { - /** @type {import("$types/base").Post | import("$types/error").Error} */ - const post = await getPost(locals.sql, Number(params.id)); + const post_id = Number(params.id); + + const post = await getPost(locals.sql, post_id); + + const comments = await getCommentsForPost(locals.sql, post_id); + + console.log(comments); return { - post: post + post: post, + comments: comments }; } diff --git a/src/routes/(app)/posts/[id]/+page.svelte b/src/routes/(app)/posts/[id]/+page.svelte index 7fd84a1..9e6a6be 100644 --- a/src/routes/(app)/posts/[id]/+page.svelte +++ b/src/routes/(app)/posts/[id]/+page.svelte @@ -1,12 +1,29 @@

{data.post.name}

{data.post.author?.name}

{data.post.content}

+
+ {#each commentTree as reply} + + {/each} +
diff --git a/src/types/base.ts b/src/types/base.ts index b278c70..ab74ab8 100644 --- a/src/types/base.ts +++ b/src/types/base.ts @@ -1,9 +1,9 @@ -export type Result = {[id: number]: T}; +export type Result = Map; export type User = { id: number, name: string, - join_date: Date + joinDate: Date }; export type Rating = { @@ -22,6 +22,20 @@ export type Post = { name: string, category: Category, content: string, - post_date: Date, + postDate: Date, rating: Rating }; + +export type Comment = { + id: number, + author: User | null, + content: string, + commentDate: Date, + rating: Rating, + parentCommentId: number +}; + +export type CommentTreeNode = { + parent: Comment, + children: (CommentTreeNode | number)[] +};