diff --git a/package.json b/package.json
index 5b65544..b1d1830 100644
--- a/package.json
+++ b/package.json
@@ -21,6 +21,7 @@
"type": "module",
"dependencies": {
"dotenv": "^16.4.5",
+ "node-cache": "^5.1.2",
"postgres": "^3.4.4",
"ssh2": "^1.15.0"
}
diff --git a/src/comp/post.svelte b/src/comp/post.svelte
index b3ababa..6f84e3e 100644
--- a/src/comp/post.svelte
+++ b/src/comp/post.svelte
@@ -6,7 +6,7 @@
diff --git a/src/hooks.server.js b/src/hooks.server.js
index 7e337ad..e7b1ac5 100644
--- a/src/hooks.server.js
+++ b/src/hooks.server.js
@@ -1,35 +1,6 @@
-// @ts-nocheck
-import { env } from '$lib/env';
-import postgres from 'postgres';
-import ssh2 from 'ssh2';
+import { sql } from '$lib/db.server';
export const handle = async ({event, resolve}) => {
- const sql = postgres(
- // 'postgres://doki8902:ChangeItN8w@pgsql3.mif:5432/studentu',
- {
- host: env.PG_HOST,
- port: parseInt(env.PG_PORT),
- database: env.PG_DATABASE,
- username: env.PG_USERNAME,
- password: env.PG_PASSWORD,
- socket: ({ host: [host], port: [port] }) => new Promise((resolve, reject) => {
- const ssh = new ssh2.Client();
- ssh
- .on('error', reject)
- .on('ready', () =>
- ssh.forwardOut('127.0.0.1', 12345, host, port,
- (err, socket) => err ? reject(err) : resolve(socket)
- )
- )
- .connect({
- host: env.SSH_HOST,
- port: parseInt(env.SSH_PORT),
- username: env.SSH_USERNAME,
- password: env.SSH_PASSWORD
- })
- })
- }
- );
// https://github.com/porsager/postgres/issues/762
diff --git a/src/lib/cache.server.js b/src/lib/cache.server.js
new file mode 100644
index 0000000..4b6488c
--- /dev/null
+++ b/src/lib/cache.server.js
@@ -0,0 +1,5 @@
+import NodeCache from "node-cache";
+
+export const createCache = () => new NodeCache({
+ stdTTL: 100
+});
diff --git a/src/lib/db.server.js b/src/lib/db.server.js
new file mode 100644
index 0000000..7cb3293
--- /dev/null
+++ b/src/lib/db.server.js
@@ -0,0 +1,32 @@
+import { env } from '$lib/env';
+import postgres from 'postgres';
+import ssh2 from 'ssh2';
+
+// @ts-ignore
+export const sql = postgres(
+ // 'postgres://doki8902:ChangeItN8w@pgsql3.mif:5432/studentu',
+ {
+ host: env.PG_HOST,
+ port: parseInt(env.PG_PORT),
+ database: env.PG_DATABASE,
+ username: env.PG_USERNAME,
+ password: env.PG_PASSWORD,
+ // @ts-ignore
+ socket: ({ host: [host], port: [port] }) => new Promise((resolve, reject) => {
+ const ssh = new ssh2.Client();
+ ssh
+ .on('error', reject)
+ .on('ready', () =>
+ ssh.forwardOut('127.0.0.1', 12345, host, port,
+ (err, socket) => err ? reject(err) : resolve(socket)
+ )
+ )
+ .connect({
+ host: env.SSH_HOST,
+ port: parseInt(env.SSH_PORT),
+ username: env.SSH_USERNAME,
+ password: env.SSH_PASSWORD
+ })
+ })
+ }
+);
diff --git a/src/lib/db/post.js b/src/lib/db/post.js
deleted file mode 100644
index b300649..0000000
--- a/src/lib/db/post.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * @param {import('postgres').Sql} sql
- * @param {number | undefined} category
- * @param {number} limit
- * @param {number} offset
- * @returns {Promise}
- */
-export async function getPosts(sql, category = undefined, limit = 10, offset = 0) {
- let query;
-
- if (category === undefined) {
- query = sql`
- SELECT name, latest_content
- FROM doki8902.message_post
- FETCH FIRST ${limit} ROWS ONLY
- OFFSET ${offset};`;
- } else {
- query = sql`
- SELECT name, latest_content
- FROM doki8902.message_post
- WHERE category_id = ${category}
- FETCH FIRST ${limit} ROWS ONLY
- OFFSET ${offset};`;
- }
-
- let posts = await query;
-
- /**
- * @type {import('$types/base').Post[]}
- */
- let result = [];
-
- posts.forEach(row => {
- result.push({
- name: row['name'],
- content: row['latest_content']
- });
- });
-
- return result;
-}
diff --git a/src/lib/db/root.js b/src/lib/db/root.js
deleted file mode 100644
index ce29d4f..0000000
--- a/src/lib/db/root.js
+++ /dev/null
@@ -1,6 +0,0 @@
-// export function runQuery(
-// /** @type {import('postgres').Sql} */ sql,
-// /** @type {string} */ query,
-// /** @type {any[]} */ args = []) {
-// sql`${query}`
-// }
\ No newline at end of file
diff --git a/src/lib/env.js b/src/lib/env.js
index 1bbb1b7..beaa7ef 100644
--- a/src/lib/env.js
+++ b/src/lib/env.js
@@ -5,4 +5,5 @@ dotenv.config();
/**
* @type {import('$types/env').Env}
*/
+// @ts-ignore
export const env = process.env;
diff --git a/src/lib/index.js b/src/lib/index.js
deleted file mode 100644
index 856f2b6..0000000
--- a/src/lib/index.js
+++ /dev/null
@@ -1 +0,0 @@
-// place files you want to import through the `$lib` alias in this folder.
diff --git a/src/lib/server/db/category.js b/src/lib/server/db/category.js
new file mode 100644
index 0000000..cea443d
--- /dev/null
+++ b/src/lib/server/db/category.js
@@ -0,0 +1,75 @@
+import { createCache } from '$lib/cache.server';
+import { cacheUpdater, cachedMethod } from './root';
+
+const cache = createCache();
+
+/**
+ * @template T
+ * @typedef {import('$types/base').Result} Result
+ */
+
+/**
+ * @typedef {import('$types/base').Category} Category
+ */
+
+/**
+ * @param {Result} categories
+ * @returns {Result}
+ */
+const updateCategoryCache = cacheUpdater(cache);
+
+/**
+ * @param {import('postgres').Sql} sql
+ * @param {number[]} user_ids
+ * @returns {Promise>}
+ */
+export const getCategoriesCached = cachedMethod(cache, getCategories);
+
+/**
+ * @param {import('postgres').Sql} sql
+ * @param {number[]} category_ids
+ * @returns {Promise>}
+ */
+export async function getCategories(sql, category_ids) {
+ if (category_ids.length == 0) return {};
+
+ const query = sql`
+ SELECT id, name
+ FROM doki8902.post_category
+ WHERE id IN ${ sql(category_ids) };`;
+
+ let categories = await query;
+
+ /**
+ * @type {Result}
+ */
+ let result = {};
+
+ categories.forEach(row => {
+ result[row['id']] = {
+ id: row['id'],
+ name: row['name']
+ }
+ })
+
+ return updateCategoryCache(result);
+}
+
+/**
+ *
+ * @param {import('postgres').Sql} sql
+ * @param {number} category_id
+ * @returns {Promise}
+ */
+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];
+}
\ No newline at end of file
diff --git a/src/lib/server/db/post.js b/src/lib/server/db/post.js
new file mode 100644
index 0000000..339c16b
--- /dev/null
+++ b/src/lib/server/db/post.js
@@ -0,0 +1,123 @@
+import { getCategoriesCached, getCategoryCached } from './category';
+import { getUser, getUsersCached } from './user';
+
+/**
+ * @typedef {import('$types/base').Post} Post
+ */
+
+/**
+ * @param {import('postgres').Sql} sql
+ * @param {import('$types/base').Category | undefined} category
+ * @param {number} limit
+ * @param {number} offset
+ * @returns {Promise}
+ */
+export async function getPosts(sql, category = undefined, limit = 10, offset = 0) {
+ let filter;
+
+ if (category === undefined) {
+ filter = sql``;
+ } else {
+ filter = sql`WHERE category_id = ${ category.id }`;
+ }
+
+ const query = sql`
+ SELECT id, author_id, name, category_id, latest_content, created_date, likes, dislikes
+ FROM doki8902.message_post
+ ${ filter }
+ FETCH FIRST ${ limit } ROWS ONLY
+ OFFSET ${ offset };`;
+
+ 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'];
+ }));
+
+ /**
+ * @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']),
+ }
+ };
+ });
+}
+
+/**
+ *
+ * @param {import('postgres').Sql} sql
+ * @param {number} post_id
+ * @returns {Promise}
+ */
+export async function getPost(sql, post_id) {
+ const query = sql`
+ SELECT id, author_id, name, category_id, latest_content, created_date, likes, dislikes
+ 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(sql, 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(sql, 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 {
+ 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']),
+ }
+ };
+}
diff --git a/src/lib/server/db/root.js b/src/lib/server/db/root.js
new file mode 100644
index 0000000..41076b7
--- /dev/null
+++ b/src/lib/server/db/root.js
@@ -0,0 +1,46 @@
+/**
+ * @template T
+ * @param {import('node-cache')} cache
+ * @returns {function({[id: number]: T})}
+ */
+export const cacheUpdater = (cache) => {
+ return function updateUserCache(data) {
+ Object.keys(data).forEach(id => {
+ cache.set(parseInt(id), data[parseInt(id)]);
+ });
+ return data;
+ }
+};
+
+/**
+ * @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}>}
+ */
+export const cachedMethod = (cache, method) => {
+ return async function(sql, ids) {
+ /**
+ * @type {{[id: number]: T}}
+ */
+ let results = {};
+ /**
+ * @type {number[]}
+ */
+ let missing = [];
+
+ ids.forEach(id => {
+ if (id === null || id === undefined)
+ return;
+ let user = cache.get(id);
+ if (user)
+ results[id] = user;
+ else
+ missing.push(id);
+ });
+
+ const remaining = await method(sql, missing);
+
+ return Object.assign({}, results, remaining);
+ };
+}
diff --git a/src/lib/server/db/user.js b/src/lib/server/db/user.js
new file mode 100644
index 0000000..c64f724
--- /dev/null
+++ b/src/lib/server/db/user.js
@@ -0,0 +1,75 @@
+import { createCache } from '$lib/cache.server';
+import { cacheUpdater, cachedMethod } from './root';
+
+const cache = createCache();
+
+/**
+ * @template T
+ * @typedef {import('$types/base').Result} Result
+ */
+
+/**
+ * @typedef {import('$types/base').User} User
+ */
+
+/**
+ * @param {Result} users
+ * @returns {Result}
+ */
+const updateUserCache = cacheUpdater(cache);
+
+/**
+ * @param {import('postgres').Sql} sql
+ * @param {number[]} user_ids
+ * @returns {Promise>}
+ */
+export const getUsersCached = cachedMethod(cache, getUsers);
+
+/**
+ * @param {import('postgres').Sql} sql
+ * @param {number[]} user_ids
+ * @returns {Promise>}
+ */
+export async function getUsers(sql, user_ids) {
+ if (user_ids.length == 0) return {};
+
+ const query = sql`
+ SELECT id, username, join_time
+ FROM doki8902.user
+ WHERE id IN ${ sql(user_ids) };`;
+
+ let users = await query;
+
+ /**
+ * @type {Result}
+ */
+ let result = {};
+
+ users.forEach(row => {
+ result[row['id']] = {
+ id: row['id'],
+ name: row['username'],
+ join_date: row['join_time']
+ }
+ })
+
+ return updateUserCache(result);
+}
+
+/**
+ * @param {import('postgres').Sql} sql
+ * @param {number} user_id
+ * @returns {Promise}
+ */
+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];
+}
diff --git a/src/routes/(app)/posts/+page.server.js b/src/routes/(app)/posts/+page.server.js
index 83104e1..66312b8 100644
--- a/src/routes/(app)/posts/+page.server.js
+++ b/src/routes/(app)/posts/+page.server.js
@@ -1,12 +1,10 @@
-import { getPosts } from '$lib/db/post';
-import { env } from '$lib/env';
+import { getPosts } from '$lib/server/db/post';
+
/** @type {import('./$types').PageServerLoad} */
export async function load({ locals }) {
let result = await getPosts(locals.sql);
- // console.log();
-
console.log(result);
return {
diff --git a/src/routes/(app)/posts/+page.svelte b/src/routes/(app)/posts/+page.svelte
index 9c3220d..8ddc015 100644
--- a/src/routes/(app)/posts/+page.svelte
+++ b/src/routes/(app)/posts/+page.svelte
@@ -1,5 +1,5 @@
+
+{data.post.name}
+{data.post.author?.name}
+{data.post.content}
diff --git a/src/stores.js b/src/stores.js
new file mode 100644
index 0000000..e69de29
diff --git a/src/types/base.ts b/src/types/base.ts
index c87f1e3..b278c70 100644
--- a/src/types/base.ts
+++ b/src/types/base.ts
@@ -1,21 +1,27 @@
+export type Result = {[id: number]: T};
+
export type User = {
- name: string
+ id: number,
+ name: string,
+ join_date: Date
};
export type Rating = {
- likes: number,
- dislikes: number
+ likes: BigInt,
+ dislikes: BigInt
}
export type Category = {
+ id: number,
name: string
}
export type Post = {
- // author: User,
+ id: number,
+ author: User | null,
name: string,
- // category: Category,
+ category: Category,
content: string,
- // post_date: Date,
- // rating: Rating
+ post_date: Date,
+ rating: Rating
};
diff --git a/src/types/error.ts b/src/types/error.ts
new file mode 100644
index 0000000..a644d06
--- /dev/null
+++ b/src/types/error.ts
@@ -0,0 +1,4 @@
+export type Error = {
+ error: boolean,
+ msg: string
+};
diff --git a/yarn.lock b/yarn.lock
index 9132936..51f2773 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -419,6 +419,11 @@ chokidar@^3.4.1:
optionalDependencies:
fsevents "~2.3.2"
+clone@2.x:
+ version "2.1.2"
+ resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
+ integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
+
code-red@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/code-red/-/code-red-1.0.4.tgz#59ba5c9d1d320a4ef795bc10a28bd42bfebe3e35"
@@ -744,6 +749,13 @@ nanoid@^3.3.7:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
+node-cache@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
+ integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
+ dependencies:
+ clone "2.x"
+
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"