Compare commits
No commits in common. 'main' and 'db-macros' have entirely different histories.
@ -1,128 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n select\n site.*,\n board.*,\n thread.*,\n post_uuid as \"post_uuid?\",\n post_contents as \"post_contents?\",\n \"user\".*\n from forum.site\n left join forum.board on board_site = site_uuid\n left join forum.thread on thread_board = board_uuid\n left join forum.post on post_thread = thread_uuid\n left join forum.user on post_author = user_uuid\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "site_uuid",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "site_title",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "site_base_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "site_theme",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "site_singleton",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "board_uuid",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "board_site",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "board_title",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"name": "board_description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "thread_uuid",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "thread_board",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"name": "thread_title",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"name": "post_uuid?",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"name": "post_contents?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"name": "user_uuid",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"name": "user_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"name": "user_username",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"name": "user_password_hash",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"name": "user_is_admin",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4a732f8bab8e6fab48a383d41bce82b59cb5030b9507c1718dc8d2e862955778"
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@
|
||||
[toolchain]
|
||||
channel = "nightly"
|
||||
|
@ -0,0 +1,294 @@
|
||||
use std::{iter::Peekable, vec::IntoIter};
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::{postgres::PgRow, Pool, Postgres, Row};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DB {
|
||||
connection_pool: Pool::<Postgres>
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Site {
|
||||
uuid: Uuid,
|
||||
title: String,
|
||||
base_url: String,
|
||||
theme: String,
|
||||
boards: Vec<Board>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Board {
|
||||
uuid: Uuid,
|
||||
title: String,
|
||||
description: String,
|
||||
threads: Vec<Thread>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Thread {
|
||||
uuid: Uuid,
|
||||
title: String,
|
||||
posts: Vec<Post>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Post {
|
||||
uuid: Uuid,
|
||||
contents: String,
|
||||
author: User
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct User {
|
||||
uuid: Uuid,
|
||||
email: String,
|
||||
username: String,
|
||||
password_hash: String,
|
||||
is_admin: bool
|
||||
}
|
||||
|
||||
impl DB {
|
||||
pub async fn init() -> Self {
|
||||
let db_url = std::env::var("DATABASE_URL").expect("Please provide DATABASE_URL in environment");
|
||||
let pool = Pool::<Postgres>::connect(db_url.as_str()).await.expect("Could not connect to database");
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("Could not run database migrations");
|
||||
|
||||
DB {
|
||||
connection_pool: pool
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_site_data(&self) -> Result<Site, String> {
|
||||
let rows : Vec<PgRow> = sqlx::query(
|
||||
r#"select
|
||||
site_uuid,
|
||||
site_title,
|
||||
site_base_url,
|
||||
site_theme,
|
||||
board_uuid,
|
||||
board_title,
|
||||
board_description,
|
||||
thread_uuid,
|
||||
thread_title,
|
||||
post_uuid as "post_uuid?",
|
||||
post_contents as "post_contents?",
|
||||
user_uuid,
|
||||
user_email,
|
||||
user_username,
|
||||
user_password_hash,
|
||||
user_is_admin
|
||||
from forum.site
|
||||
left join forum.board on board_site = site_uuid
|
||||
left join forum.thread on thread_board = board_uuid
|
||||
left join forum.post on post_thread = thread_uuid
|
||||
left join forum.user on post_author = user_uuid
|
||||
"#).fetch_all(&self.connection_pool).await.expect("Could not connect to database");
|
||||
|
||||
let mut rows_iter = rows.into_iter().peekable();
|
||||
|
||||
let site = Site::parse_from_rows(&mut rows_iter, "site_".to_string());
|
||||
|
||||
if let Some(site) = site {
|
||||
Ok(site)
|
||||
} else {
|
||||
Err("Could not find site in DB".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Rows = Peekable<IntoIter<PgRow>>;
|
||||
trait ParseFromRows {
|
||||
fn parse_from_rows<'a>(rows: &'a mut Rows, item_prefix: String) -> Option<Self> where Self: Sized;
|
||||
}
|
||||
|
||||
macro_rules! parse_field {
|
||||
( $row_name:ident, $item_prefix:ident, $field_name:literal ) => {
|
||||
$row_name.get(format!("{}{}", $item_prefix, $field_name).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! parse_array {
|
||||
( $rows:ident, $current_uuid:ident, $uuid_field:ident, $array_type:ty, $field_prefix:literal) => {
|
||||
{
|
||||
let mut items = Vec::new();
|
||||
|
||||
loop {
|
||||
if let Some(item) = <$array_type>::parse_from_rows($rows, $field_prefix.to_string()) {
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
$rows.next();
|
||||
let next = $rows.peek();
|
||||
|
||||
if next.is_none() {
|
||||
break
|
||||
}
|
||||
|
||||
let uuid_result = next.unwrap().try_get::<Option<Uuid>, &str>(&$uuid_field.as_str());
|
||||
|
||||
if uuid_result.is_err() || uuid_result.as_ref().unwrap().is_none() {
|
||||
break
|
||||
}
|
||||
|
||||
let next_uuid = uuid_result.as_ref().unwrap().unwrap().clone();
|
||||
|
||||
if next_uuid != $current_uuid {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
items
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl ParseFromRows for User {
|
||||
fn parse_from_rows(rows: &mut Rows, item_prefix: String) -> Option<Self> {
|
||||
let next = rows.peek();
|
||||
if next.is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let next = next.unwrap();
|
||||
let uuid_field = format!("{}uuid", item_prefix) ;
|
||||
let uuid_result = next.try_get::<Option<Uuid>, &str>(&uuid_field.as_str());
|
||||
|
||||
if uuid_result.is_err() || uuid_result.as_ref().unwrap().is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let uuid = uuid_result.as_ref().unwrap().unwrap().clone();
|
||||
let email = parse_field!(next, item_prefix, "email");
|
||||
let username = parse_field!(next, item_prefix, "username");
|
||||
let password_hash = parse_field!(next, item_prefix, "password_hash");
|
||||
let is_admin = parse_field!(next, item_prefix, "is_admin");
|
||||
|
||||
Some(Self {
|
||||
uuid,
|
||||
email,
|
||||
username,
|
||||
password_hash,
|
||||
is_admin
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseFromRows for Post {
|
||||
fn parse_from_rows(rows: &mut Rows, item_prefix: String) -> Option<Self> {
|
||||
let next = rows.peek();
|
||||
if next.is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let next = next.unwrap();
|
||||
let uuid_field = format!("{}uuid", item_prefix) ;
|
||||
let uuid_result = next.try_get::<Option<Uuid>, &str>(&uuid_field.as_str());
|
||||
|
||||
if uuid_result.is_err() || uuid_result.as_ref().unwrap().is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let uuid = uuid_result.as_ref().unwrap().unwrap().clone();
|
||||
let contents = parse_field!(next, item_prefix, "contents");
|
||||
let author = User::parse_from_rows(rows, "user_".to_string()).unwrap();
|
||||
|
||||
Some(Self {
|
||||
uuid,
|
||||
author,
|
||||
contents
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseFromRows for Thread {
|
||||
fn parse_from_rows(rows: &mut Rows, item_prefix: String) -> Option<Self> {
|
||||
let next = rows.peek();
|
||||
if next.is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let next = next.unwrap();
|
||||
let uuid_field = format!("{}uuid", item_prefix) ;
|
||||
let uuid_result = next.try_get::<Option<Uuid>, &str>(&uuid_field.as_str());
|
||||
|
||||
if uuid_result.is_err() || uuid_result.as_ref().unwrap().is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let uuid = uuid_result.as_ref().unwrap().unwrap().clone();
|
||||
let title = parse_field!(next, item_prefix, "title");
|
||||
let posts = parse_array!(rows, uuid, uuid_field, Post, "post_");
|
||||
|
||||
Some(Self {
|
||||
uuid,
|
||||
title,
|
||||
posts
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseFromRows for Board {
|
||||
fn parse_from_rows(rows: &mut Rows, item_prefix: String) -> Option<Self> {
|
||||
let next = rows.peek();
|
||||
if next.is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let next = next.unwrap();
|
||||
let uuid_field = format!("{}uuid", item_prefix) ;
|
||||
let uuid_result = next.try_get::<Option<Uuid>, &str>(&uuid_field.as_str());
|
||||
|
||||
if uuid_result.is_err() || uuid_result.as_ref().unwrap().is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let uuid = uuid_result.as_ref().unwrap().unwrap().clone();
|
||||
let title = parse_field!(next, item_prefix, "title");
|
||||
let description = parse_field!(next, item_prefix, "description");
|
||||
let threads = parse_array!(rows, uuid, uuid_field, Thread, "thread_");
|
||||
|
||||
Some(Self {
|
||||
uuid,
|
||||
title,
|
||||
description,
|
||||
threads
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ParseFromRows for Site {
|
||||
fn parse_from_rows(rows: &mut Rows, item_prefix: String) -> Option<Self> {
|
||||
let next = rows.peek();
|
||||
if next.is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let next = next.unwrap();
|
||||
let uuid_field = format!("{}uuid", item_prefix) ;
|
||||
let uuid_result = next.try_get::<Option<Uuid>, &str>(&uuid_field.as_str());
|
||||
|
||||
if uuid_result.is_err() || uuid_result.as_ref().unwrap().is_none() {
|
||||
return None
|
||||
}
|
||||
|
||||
let uuid = uuid_result.as_ref().unwrap().unwrap().clone();
|
||||
let title = parse_field!(next, item_prefix, "title");
|
||||
let base_url = parse_field!(next, item_prefix, "base_url");
|
||||
let theme = parse_field!(next, item_prefix, "theme");
|
||||
let boards = parse_array!(rows, uuid, uuid_field, Board, "board_");
|
||||
|
||||
Some(Self {
|
||||
uuid,
|
||||
title,
|
||||
base_url,
|
||||
theme,
|
||||
boards
|
||||
})
|
||||
}
|
||||
}
|
@ -1,81 +0,0 @@
|
||||
use sqlx::{Pool, Postgres};
|
||||
|
||||
pub mod objects;
|
||||
use objects::{Board, Post, Site, Thread, User};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DB {
|
||||
connection_pool: Pool<Postgres>,
|
||||
}
|
||||
|
||||
impl DB {
|
||||
pub async fn init() -> Self {
|
||||
let db_url =
|
||||
std::env::var("DATABASE_URL").expect("Please provide DATABASE_URL in environment");
|
||||
let pool = Pool::<Postgres>::connect(db_url.as_str())
|
||||
.await
|
||||
.expect("Could not connect to database");
|
||||
|
||||
sqlx::migrate!()
|
||||
.run(&pool)
|
||||
.await
|
||||
.expect("Could not run database migrations");
|
||||
|
||||
DB {
|
||||
connection_pool: pool,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_site_data(&self) -> Result<Site, String> {
|
||||
let site = joinrs::query_parsed!(
|
||||
connection = &self.connection_pool,
|
||||
query = r#"
|
||||
select
|
||||
site.*,
|
||||
board.*,
|
||||
thread.*,
|
||||
post_uuid as "post_uuid?",
|
||||
post_contents as "post_contents?",
|
||||
"user".*
|
||||
from forum.site
|
||||
left join forum.board on board_site = site_uuid
|
||||
left join forum.thread on thread_board = board_uuid
|
||||
left join forum.post on post_thread = thread_uuid
|
||||
left join forum.user on post_author = user_uuid
|
||||
"#,
|
||||
return_type = Site {
|
||||
uuid: site_uuid,
|
||||
title: site_title,
|
||||
base_url: site_base_url,
|
||||
theme: site_theme,
|
||||
boards: Vec<Board {
|
||||
uuid: board_uuid,
|
||||
title: board_title,
|
||||
description: board_description,
|
||||
threads: Vec<Thread {
|
||||
uuid: thread_uuid,
|
||||
title: thread_title,
|
||||
posts: Vec<Post {
|
||||
uuid: post_uuid,
|
||||
contents: post_contents,
|
||||
author: User {
|
||||
uuid: user_uuid,
|
||||
username: user_username,
|
||||
email: user_email,
|
||||
password_hash: user_password_hash,
|
||||
is_admin: user_is_admin
|
||||
}
|
||||
}>
|
||||
}>
|
||||
}>
|
||||
}
|
||||
);
|
||||
|
||||
if let Err(err) = site {
|
||||
let message = format!("Error getting site from database: {}", err);
|
||||
return Err(message)
|
||||
}
|
||||
|
||||
Ok(site.unwrap())
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Site {
|
||||
pub uuid: Uuid,
|
||||
pub title: String,
|
||||
pub base_url: String,
|
||||
pub theme: String,
|
||||
pub boards: Vec<Board>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Board {
|
||||
pub uuid: Uuid,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub threads: Vec<Thread>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Thread {
|
||||
pub uuid: Uuid,
|
||||
pub title: String,
|
||||
pub posts: Vec<Post>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct Post {
|
||||
pub uuid: Uuid,
|
||||
pub contents: String,
|
||||
pub author: User,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct User {
|
||||
pub uuid: Uuid,
|
||||
pub email: String,
|
||||
pub username: String,
|
||||
pub password_hash: String,
|
||||
pub is_admin: bool,
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
use leptos::{component, view, IntoView};
|
||||
|
||||
use crate::db::objects::Site;
|
||||
|
||||
#[component]
|
||||
pub fn Header(site: Site) -> impl IntoView {
|
||||
view!(
|
||||
<h1>{site.title}</h1>
|
||||
)
|
||||
}
|
@ -1 +0,0 @@
|
||||
pub mod header;
|
@ -1,11 +0,0 @@
|
||||
use leptos::{component, view, IntoView};
|
||||
use crate::{db::objects::Site, ui::components::header::Header};
|
||||
|
||||
#[component]
|
||||
pub fn Index(site: Site) -> impl IntoView {
|
||||
view!(
|
||||
<>
|
||||
<Header site={site}/>
|
||||
</>
|
||||
)
|
||||
}
|
@ -1 +0,0 @@
|
||||
pub mod index;
|
@ -1,2 +0,0 @@
|
||||
pub mod components;
|
||||
pub mod layouts;
|
@ -1,10 +0,0 @@
|
||||
pub fn render_bare<N>(view: N) -> String
|
||||
where N: leptos::IntoView + 'static {
|
||||
let render_result = leptos::ssr::render_to_string(|| {view});
|
||||
|
||||
let html_comment: regex::Regex = regex::Regex::new(r"<!--.*-->").unwrap();
|
||||
let leptos_string = render_result.to_string();
|
||||
let bare_string = html_comment.replace_all(&leptos_string, "");
|
||||
|
||||
bare_string.to_string()
|
||||
}
|
Loading…
Reference in New Issue