Working data and basic demo pages
This commit is contained in:
parent
e2d2599a95
commit
9983720bd1
@ -21,6 +21,7 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"node-cache": "^5.1.2",
|
||||
"postgres": "^3.4.4",
|
||||
"ssh2": "^1.15.0"
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<h3>{post.name}</h3>
|
||||
<a href="/posts/{post.id}">{post.name}</a>
|
||||
<!-- <p>{post.author.name}</p> -->
|
||||
<p>{post.content}</p>
|
||||
</div>
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
5
src/lib/cache.server.js
Normal file
5
src/lib/cache.server.js
Normal 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
32
src/lib/db.server.js
Normal 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
|
||||
})
|
||||
})
|
||||
}
|
||||
);
|
||||
@ -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;
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
// export function runQuery(
|
||||
// /** @type {import('postgres').Sql} */ sql,
|
||||
// /** @type {string} */ query,
|
||||
// /** @type {any[]} */ args = []) {
|
||||
// sql`${query}`
|
||||
// }
|
||||
@ -5,4 +5,5 @@ dotenv.config();
|
||||
/**
|
||||
* @type {import('$types/env').Env}
|
||||
*/
|
||||
// @ts-ignore
|
||||
export const env = process.env;
|
||||
|
||||
@ -1 +0,0 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
||||
75
src/lib/server/db/category.js
Normal file
75
src/lib/server/db/category.js
Normal 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
123
src/lib/server/db/post.js
Normal 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
46
src/lib/server/db/root.js
Normal 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
75
src/lib/server/db/user.js
Normal 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];
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
11
src/routes/(app)/posts/[id]/+page.server.js
Normal file
11
src/routes/(app)/posts/[id]/+page.server.js
Normal 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
|
||||
};
|
||||
}
|
||||
12
src/routes/(app)/posts/[id]/+page.svelte
Normal file
12
src/routes/(app)/posts/[id]/+page.svelte
Normal 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
0
src/stores.js
Normal file
@ -1,21 +1,27 @@
|
||||
export type Result<T> = {[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
|
||||
};
|
||||
|
||||
4
src/types/error.ts
Normal file
4
src/types/error.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type Error = {
|
||||
error: boolean,
|
||||
msg: string
|
||||
};
|
||||
12
yarn.lock
12
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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user