Working data and basic demo pages

This commit is contained in:
Donatas Kirda 2024-05-10 00:28:23 +03:00
parent e2d2599a95
commit 9983720bd1
Signed by: bloodwiing
GPG Key ID: 63020D8D3F4A164F
21 changed files with 415 additions and 91 deletions

View File

@ -21,6 +21,7 @@
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"node-cache": "^5.1.2",
"postgres": "^3.4.4", "postgres": "^3.4.4",
"ssh2": "^1.15.0" "ssh2": "^1.15.0"
} }

View File

@ -6,7 +6,7 @@
</script> </script>
<div> <div>
<h3>{post.name}</h3> <a href="/posts/{post.id}">{post.name}</a>
<!-- <p>{post.author.name}</p> --> <!-- <p>{post.author.name}</p> -->
<p>{post.content}</p> <p>{post.content}</p>
</div> </div>

View File

@ -1,35 +1,6 @@
// @ts-nocheck import { sql } from '$lib/db.server';
import { env } from '$lib/env';
import postgres from 'postgres';
import ssh2 from 'ssh2';
export const handle = async ({event, resolve}) => { 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 // https://github.com/porsager/postgres/issues/762

5
src/lib/cache.server.js Normal file
View File

@ -0,0 +1,5 @@
import NodeCache from "node-cache";
export const createCache = () => new NodeCache({
stdTTL: 100
});

32
src/lib/db.server.js Normal file
View File

@ -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
})
})
}
);

View File

@ -1,41 +0,0 @@
/**
* @param {import('postgres').Sql} sql
* @param {number | undefined} category
* @param {number} limit
* @param {number} offset
* @returns {Promise<import('$types/base').Post[]>}
*/
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;
}

View File

@ -1,6 +0,0 @@
// export function runQuery(
// /** @type {import('postgres').Sql} */ sql,
// /** @type {string} */ query,
// /** @type {any[]} */ args = []) {
// sql`${query}`
// }

View File

@ -5,4 +5,5 @@ dotenv.config();
/** /**
* @type {import('$types/env').Env} * @type {import('$types/env').Env}
*/ */
// @ts-ignore
export const env = process.env; export const env = process.env;

View File

@ -1 +0,0 @@
// place files you want to import through the `$lib` alias in this folder.

View File

@ -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<T>} Result<T>
*/
/**
* @typedef {import('$types/base').Category} Category
*/
/**
* @param {Result<Category>} categories
* @returns {Result<Category>}
*/
const updateCategoryCache = cacheUpdater(cache);
/**
* @param {import('postgres').Sql} sql
* @param {number[]} user_ids
* @returns {Promise<Result<Category>>}
*/
export const getCategoriesCached = cachedMethod(cache, getCategories);
/**
* @param {import('postgres').Sql} sql
* @param {number[]} category_ids
* @returns {Promise<Result<Category>>}
*/
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<Category>}
*/
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<Category | import('$types/error').Error>}
*/
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];
}

123
src/lib/server/db/post.js Normal file
View File

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

46
src/lib/server/db/root.js Normal file
View File

@ -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);
};
}

75
src/lib/server/db/user.js Normal file
View File

@ -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<T>} Result<T>
*/
/**
* @typedef {import('$types/base').User} User
*/
/**
* @param {Result<User>} users
* @returns {Result<User>}
*/
const updateUserCache = cacheUpdater(cache);
/**
* @param {import('postgres').Sql} sql
* @param {number[]} user_ids
* @returns {Promise<Result<User>>}
*/
export const getUsersCached = cachedMethod(cache, getUsers);
/**
* @param {import('postgres').Sql} sql
* @param {number[]} user_ids
* @returns {Promise<Result<User>>}
*/
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<User>}
*/
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<User | import('$types/error').Error>}
*/
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];
}

View File

@ -1,12 +1,10 @@
import { getPosts } from '$lib/db/post'; import { getPosts } from '$lib/server/db/post';
import { env } from '$lib/env';
/** @type {import('./$types').PageServerLoad} */ /** @type {import('./$types').PageServerLoad} */
export async function load({ locals }) { export async function load({ locals }) {
let result = await getPosts(locals.sql); let result = await getPosts(locals.sql);
// console.log();
console.log(result); console.log(result);
return { return {

View File

@ -1,5 +1,5 @@
<script> <script>
import Postlist from "$comp/postlist.svelte"; import Postlist from "$comp/postlist.svelte";
/** /**
* @type {{posts: import("$types/base").Post[]}} * @type {{posts: import("$types/base").Post[]}}

View File

@ -0,0 +1,11 @@
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));
return {
post: post
};
}

View File

@ -0,0 +1,12 @@
<script>
/**
* @type {{post: import("$types/base").Post}}
*/
export let data;
console.log(data);
</script>
<h1>{data.post.name}</h1>
<a href="#">{data.post.author?.name}</a>
<p>{data.post.content}</p>

0
src/stores.js Normal file
View File

View File

@ -1,21 +1,27 @@
export type Result<T> = {[id: number]: T};
export type User = { export type User = {
name: string id: number,
name: string,
join_date: Date
}; };
export type Rating = { export type Rating = {
likes: number, likes: BigInt,
dislikes: number dislikes: BigInt
} }
export type Category = { export type Category = {
id: number,
name: string name: string
} }
export type Post = { export type Post = {
// author: User, id: number,
author: User | null,
name: string, name: string,
// category: Category, category: Category,
content: string, content: string,
// post_date: Date, post_date: Date,
// rating: Rating rating: Rating
}; };

4
src/types/error.ts Normal file
View File

@ -0,0 +1,4 @@
export type Error = {
error: boolean,
msg: string
};

View File

@ -419,6 +419,11 @@ chokidar@^3.4.1:
optionalDependencies: optionalDependencies:
fsevents "~2.3.2" 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: code-red@^1.0.3:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/code-red/-/code-red-1.0.4.tgz#59ba5c9d1d320a4ef795bc10a28bd42bfebe3e35" 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" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== 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: normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"