import { sql } from '$lib/db.server'; import { getCategoriesCachedByRef, getCategoryCached } from './category'; import { isPostgresError } from './root'; import { getUser, getUsersCachedByRef, sqlUserFromToken } from './user'; /** * @typedef {import('$types/base').Post} Post * @typedef {import('$types/base').PostMetrics} PostMetrics */ /** * @param {import('postgres').Row} row * @returns {PostMetrics} */ function parsePostMetricsFromRow(row) { return { commentCount: BigInt(row['comment_count']), userCount: BigInt(row['user_count']), latestActivity: row['latest_activity'], engagement: BigInt(row['engagement']), age: row['age'], relevancy: row['relevancy'], }; } /** * @param {import('$types/base').User | null} author * @param {import('$types/base').Category} category * @param {import('postgres').Row} row * @param {boolean} withMetrics * @returns {Post} */ function parsePostFromRow(author, category, row, withMetrics = false) { return { id: row['id'], author: author, name: row['name'], category: category, content: row['latest_content'], edited: row['edit_count'] > 1, editCount: row['edit_count'] - 1, postDate: row['created_date'], rating: { likes: BigInt(row['likes']), dislikes: BigInt(row['dislikes']), }, metrics: withMetrics ? parsePostMetricsFromRow(row) : null, }; } /** * @param {{ * category?: import('$types/base').Category | undefined * }} opts * @returns {Promise} */ export async function getPostCount(opts = {}) { const { category = undefined } = opts; const filter = category ? sql`WHERE category_id = ${ category.id }` : sql``; const query = sql` SELECT COUNT(*) FROM doki8902.message_post ${ filter };`; const count = await query; return count[0]['count']; } /** * @param {{ * category?: import('$types/base').Category | undefined, * limit?: number, * offset?: number, * withMetrics?: boolean * }} opts * @returns {Promise} */ export async function getPosts(opts = {}) { const { category = undefined, limit = 10, offset = 0, withMetrics = false, } = opts; const filter = category ? sql`WHERE category_id = ${ category.id }` : sql``; const metrics = withMetrics ? sql`, comment_count, user_count, latest_activity, engagement, age, relevancy` : sql``; const query = sql` SELECT id, author_id, name, category_id, latest_content, edit_count, created_date, likes, dislikes ${ metrics } FROM doki8902.message_post ${ filter } FETCH FIRST ${ limit } ROWS ONLY OFFSET ${ offset };`; const posts = await query; const users = await getUsersCachedByRef(posts, p => p['author_id']); const categories = await getCategoriesCachedByRef(posts, p => p['category_id']); /** * @type {Post[]} */ return posts.map(row => parsePostFromRow( users.get(row['author_id']) || null, /** @type {import('$types/base').Category} */ (categories.get(row['category_id'])), row, withMetrics )); } /** * * @param {number} post_id * @param {{ * withMetrics?: boolean * }} opts * @returns {Promise} */ export async function getPost(post_id, opts = {}) { const { withMetrics = false } = opts; const metrics = withMetrics ? sql`, comment_count, user_count, latest_activity, engagement, age, relevancy` : sql``; const query = sql` SELECT id, author_id, name, category_id, latest_content, edit_count, created_date, likes, dislikes ${ metrics } FROM doki8902.message_post WHERE id = ${ post_id };`; const post = (await query).at(0); if (!post) { return { error: true, msg: `Could not find Post of ID ${ post_id }` }; } const user_guess = await getUser(post['author_id']); /** * @type {import('$types/base').User | null} */ const author = function () { if (Object.hasOwn(user_guess, 'error')) { return null; } else { return /** @type {import('$types/base').User} */ (user_guess); } }(); const category_guess = await getCategoryCached(post['category_id']); if (Object.hasOwn(category_guess, 'error')) { return { error: true, msg: `Post of ID ${ post_id } has an invalid Category ID ${ post['category_id'] }` }; } /** * @type {import('$types/base').Category} */ const category = function () { return /** @type {import('$types/base').Category} */ (category_guess); }(); /** * @type {Post} */ return parsePostFromRow(author, category, post, withMetrics); } /** * @param {string} token * @param {import('$types/base').Category} category * @param {string} name * @param {string} content * @returns {Promise} */ export async function createPost(token, category, name, content) { const insert = sql` INSERT INTO doki8902.message_post (author_id, category_id, name, latest_content) VALUES ( (${ sqlUserFromToken(token) }), ${ category.id }, ${ name }, ${ content } ) RETURNING id;`; let result; try { result = await insert; } catch (e) { const pgerr = isPostgresError(e); switch (pgerr?.constraint_name) { case 'message_require_user': return { error: true, msg: 'User token was invalid', }; case 'post_category_id_fkey': return { error: true, msg: 'Post category does not exist', }; case 'content_min_length': return { error: true, msg: 'Post content cannot be empty', }; case 'name_min_length': return { error: true, msg: 'Post name cannot be empty', }; default: console.log(e); return { error: true, msg: 'Unknown error (notify dev)', }; } } const postId = result[0]['id']; return { success: true, result: postId, }; }