2024-05-19 01:04:17 +03:00

244 lines
5.9 KiB
JavaScript

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<number>}
*/
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<Post[]>}
*/
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<Post | import('$types/status').Error>}
*/
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<import('$types/status').Success | import('$types/status').Error>}
*/
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,
};
}