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"
}

904
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -13,6 +13,4 @@ serde = { version = "1", features = ["derive"] }
time = { version = "0.3.34" }
uuid = { version = "1.7.0", features = ["v4", "serde"] }
serde_json = "1.0.114"
joinrs = { path = "../joinrs/joinrs" }
leptos = { version = "0.6.11", features = ["ssr"] }
regex = "1.10.4"

@ -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,3 +1,5 @@
#![feature(type_alias_impl_trait)]
#[macro_use]
extern crate rocket;
@ -6,8 +8,6 @@ use dotenv::dotenv;
mod db;
mod routes;
mod ui;
mod util;
#[rocket::main]
async fn main() {

@ -1,17 +1,11 @@
use rocket::{http::Status, State};
use crate::db::DB;
use crate::ui::layouts::index::Index;
use crate::util::render_bare;
#[get("/")]
pub async fn site_index(db : &State<DB>) -> Result<String, (Status, String)> {
let site = db.get_site_data().await.unwrap();
let response = render_bare(leptos::view! {
<Index site={site}/>
});
let response = serde_json::to_string(&site).unwrap();
Ok(response)
}

@ -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…
Cancel
Save