[#2] Refactored and added SQLX schema files.

This commit is contained in:
2025-08-29 19:09:05 -04:00
parent 0a16667b76
commit 7b5c69cc50
17 changed files with 242 additions and 54 deletions

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT OR IGNORE INTO tale_tags (tale_slug, tag_id)\n VALUES (?1, ?2)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "0f58e58ff3bcda95267629c8dbde46f85218872afb759b07d5aa217309555b99"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT OR REPLACE INTO tavern (key, value) VALUES ('title', ?1)",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "2b2ddeb7ea1690d809f237afc8d21aa67599591bcbc9671d31c6ed90aded502d"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT OR REPLACE INTO adventurers (\n handle, name, profile, image, blurb\n ) VALUES (?1, ?2, ?3, ?4, ?5)",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "40a4dcf62e6741b1e2b669469704325d06de1327c453945128ce2e0a1edf510d"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "CREATE TABLE IF NOT EXISTS tags (\n id INTEGER PRIMARY KEY,\n name TEXT NOT NULL UNIQUE\n )",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "50d891dc85cb19ce33378ced606b10ac982c6fdcd30e6d089a1d140210c9b11a"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT OR IGNORE INTO tags (name) VALUES (?1)",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "5bbbe0b55b0e8a775165a59c2344bf5cbd7028ca60047a869de25e7931920190"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "CREATE TABLE IF NOT EXISTS tales (\n slug TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n author TEXT NOT NULL,\n summary TEXT NOT NULL,\n publish_date TEXT NOT NULL,\n content TEXT NOT NULL\n )",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "63586c7d68985fedfb450a942ad03b81fe2f78bb0b317c75d9af6266c6ad6d55"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "CREATE TABLE IF NOT EXISTS adventurers (\n handle TEXT PRIMARY KEY,\n name TEXT NOT NULL,\n profile TEXT NOT NULL,\n image TEXT NOT NULL,\n blurb TEXT NOT NULL\n )",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "67c82ddcbec08a947ef28a28a439bbf3838edfc2e9ce27e6a6661505e5b936e2"
}

View File

@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "SELECT id FROM tags WHERE name = ?1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true
]
},
"hash": "b1fa9c554e3fe18b4117a314c644cc5bf969e512b9fb6b589bd09504317363c0"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "CREATE TABLE IF NOT EXISTS tale_tags (\n tale_slug TEXT,\n tag_id INTEGER,\n FOREIGN KEY(tale_slug) REFERENCES tales(slug),\n FOREIGN KEY(tag_id) REFERENCES tags(id),\n UNIQUE(tale_slug, tag_id)\n )",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "c32d614137f871a3f603c57e99d62b293515e469653452750ed9e5424be00320"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT OR REPLACE INTO tales (\n slug, title, author, summary, publish_date, content\n ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "e448c3365fa62303d143b2ed04ee4e230b99d780768c96de7966fbee252e7565"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "CREATE TABLE IF NOT EXISTS tavern (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n )",
"describe": {
"columns": [],
"parameters": {
"Right": 0
},
"nullable": []
},
"hash": "ec49fe1746763238c7ead570da9b7800e68e1e7311c16ea07d9e50904b40e817"
}

View File

@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT OR REPLACE INTO tavern (key, value) VALUES ('description', ?1)",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "ee6075930ca151fc036d2797b96b29c65de57982428e1a6f45579638b6c7442a"
}

View File

@ -1,24 +1,28 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use chrono::NaiveDate; use chrono::NaiveDate;
use tavern::{Adventurer, Database, FrontMatter, Tale, Tavern}; use tavern::{Adventurer, Database, Legend, Lore, Tale, Tavern};
#[cfg(feature = "publisher")] #[cfg(feature = "publisher")]
fn generate_tavern() -> Tavern fn generate_tavern() -> Tavern
{ {
let author: Adventurer = let legend: Legend = Legend
Adventurer { name: String::from("Jason Smith"), {
handle: String::from("myrddin"),
profile: profile:
String::from("https://cybermages.tech/about/myrddin"), String::from("https://cybermages.tech/about/myrddin"),
image: image:
String::from("https://cybermages.tech/about/myrddin/pic"), String::from("https://cybermages.tech/about/myrddin/pic"),
blurb: String::from("I love code!") }; blurb: String::from("I love code!") };
let fm: FrontMatter = let author: Adventurer =
FrontMatter { title: String::from("Test post"), Adventurer { name: String::from("Jason Smith"),
handle: String::from("myrddin"),
legend };
let lore: Lore =
Lore { title: String::from("Test post"),
slug: String::from("test_post"), slug: String::from("test_post"),
author: author.handle.clone(), author: author.handle.clone(),
summary: String::from("The Moon is made of cheese!"), summary: String::from("The Moon is made of cheese!"),
@ -30,8 +34,8 @@ fn generate_tavern() -> Tavern
10, 41) 10, 41)
.unwrap() }; .unwrap() };
let tale: Tale = Tale { front_matter: fm, let tale: Tale = Tale { lore,
content: PathBuf::from("posts/test_post.md") }; story: PathBuf::from("posts/test_post.md") };
// Create a dummy posts directory and file for this example to work // Create a dummy posts directory and file for this example to work

View File

@ -1,6 +1,20 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
///
#[derive(Deserialize, Serialize)]
pub struct Legend
{
/// A link to the adventurer's profile (e.g., personal website, GitHub,
/// etc.).
pub profile: String,
/// A URL or path to an image representing the adventurer (e.g., avatar or
/// portrait).
pub image: String,
/// A short descriptive text or tagline about the adventurer.
pub blurb: String
}
/// Represents an author or contributor of a tale. /// Represents an author or contributor of a tale.
/// ///
@ -16,16 +30,9 @@ pub struct Adventurer
/// mentions). /// mentions).
pub handle: String, pub handle: String,
/// A link to the adventurer's profile (e.g., personal website, GitHub, ///
/// etc.). #[serde(flatten)]
pub profile: String, pub legend: Legend
/// A URL or path to an image representing the adventurer (e.g., avatar or
/// portrait).
pub image: String,
/// A short descriptive text or tagline about the adventurer.
pub blurb: String
} }

View File

@ -7,14 +7,14 @@ use sqlx::{Error, Result};
#[cfg(not(feature = "publisher"))] #[cfg(not(feature = "publisher"))]
use sqlx::Row; use sqlx::Row;
use crate::adventurer::Adventurer; use crate::adventurer::{Adventurer, Legend};
use crate::tale::Tale; use crate::tale::Tale;
#[cfg(feature = "publisher")] #[cfg(feature = "publisher")]
use crate::converter::Converter; use crate::converter::Converter;
#[cfg(not(feature = "publisher"))] #[cfg(not(feature = "publisher"))]
use crate::tale::FrontMatter; use crate::tale::Lore;
@ -136,7 +136,7 @@ impl Database
-> Result<(), Box<dyn std::error::Error>> -> Result<(), Box<dyn std::error::Error>>
{ {
// Convert the tales content from Markdown to HTML. // Convert the tales content from Markdown to HTML.
let markdown_content = std::fs::read_to_string(&tale.content)?; let markdown_content = std::fs::read_to_string(&tale.story)?;
let html_content: String = Converter::markdown_to_html(&markdown_content); let html_content: String = Converter::markdown_to_html(&markdown_content);
// Start a transaction. // Start a transaction.
@ -147,18 +147,18 @@ impl Database
"INSERT OR REPLACE INTO tales ( "INSERT OR REPLACE INTO tales (
slug, title, author, summary, publish_date, content slug, title, author, summary, publish_date, content
) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", ) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
tale.front_matter.slug, tale.lore.slug,
tale.front_matter.title, tale.lore.title,
tale.front_matter.author, tale.lore.author,
tale.front_matter.summary, tale.lore.summary,
tale.front_matter.publish_date, tale.lore.publish_date,
html_content html_content
).execute(&mut *tx) // Pass mutable reference to the transaction ).execute(&mut *tx) // Pass mutable reference to the transaction
.await?; .await?;
// Store the tags. // Store the tags.
// For each tag ... // For each tag ...
for tag_name in &tale.front_matter.tags for tag_name in &tale.lore.tags
{ {
// Insert a new tag, ignore if it already exists. // Insert a new tag, ignore if it already exists.
sqlx::query!("INSERT OR IGNORE INTO tags (name) VALUES (?1)", sqlx::query!("INSERT OR IGNORE INTO tags (name) VALUES (?1)",
@ -176,7 +176,7 @@ impl Database
sqlx::query!( sqlx::query!(
"INSERT OR IGNORE INTO tale_tags (tale_slug, tag_id) "INSERT OR IGNORE INTO tale_tags (tale_slug, tag_id)
VALUES (?1, ?2)", VALUES (?1, ?2)",
tale.front_matter.slug, tale.lore.slug,
id id
).execute(&mut *tx) // Pass mutable reference to the transaction ).execute(&mut *tx) // Pass mutable reference to the transaction
.await?; .await?;
@ -202,9 +202,9 @@ impl Database
) VALUES (?1, ?2, ?3, ?4, ?5)", ) VALUES (?1, ?2, ?3, ?4, ?5)",
adventurer.handle, adventurer.handle,
adventurer.name, adventurer.name,
adventurer.profile, adventurer.legend.profile,
adventurer.image, adventurer.legend.image,
adventurer.blurb adventurer.legend.blurb
).execute(&mut *tx) ).execute(&mut *tx)
.await?; .await?;
@ -242,7 +242,7 @@ impl Database
#[cfg(not(feature = "publisher"))] #[cfg(not(feature = "publisher"))]
pub async fn get_tales_summary(&self, categories: &[String]) pub async fn get_tales_summary(&self, categories: &[String])
-> Result<Vec<FrontMatter>> -> Result<Vec<Lore>>
{ {
let mut tales = Vec::new(); let mut tales = Vec::new();
@ -292,7 +292,7 @@ impl Database
let publish_date = chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%dT%H:%M:%S") let publish_date = chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%dT%H:%M:%S")
.map_err(|e| sqlx::Error::Decode(e.into()))?; .map_err(|e| sqlx::Error::Decode(e.into()))?;
tales.push(FrontMatter { title: row.try_get("title")?, tales.push(Lore { title: row.try_get("title")?,
slug: row.try_get("slug")?, slug: row.try_get("slug")?,
summary: row.try_get("summary")?, summary: row.try_get("summary")?,
author: row.try_get("author")?, author: row.try_get("author")?,
@ -336,15 +336,15 @@ impl Database
let publish_date = chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%dT%H:%M:%S") let publish_date = chrono::NaiveDateTime::parse_from_str(&date_str, "%Y-%m-%dT%H:%M:%S")
.map_err(|e| Error::Decode(e.into()))?; .map_err(|e| Error::Decode(e.into()))?;
let front_matter = FrontMatter { title: row.try_get("title")?, let lore = Lore { title: row.try_get("title")?,
slug: row.try_get("slug")?, slug: row.try_get("slug")?,
summary: row.try_get("summary")?, summary: row.try_get("summary")?,
author: row.try_get("author")?, author: row.try_get("author")?,
publish_date, publish_date,
tags }; tags };
Ok(Some(Tale { front_matter, Ok(Some(Tale { lore,
content: row.try_get("content")? })) story: row.try_get("content")? }))
} }
else else
{ {
@ -358,22 +358,47 @@ impl Database
{ {
let mut tx = self.pool.begin().await?; let mut tx = self.pool.begin().await?;
let adventurer = sqlx::query_as!( let legend = sqlx::query_as!(
Adventurer, Legend,
"SELECT "SELECT
handle AS 'handle!', profile AS profile,
name AS 'name!', image AS image,
profile AS 'profile!', blurb AS blurb
image AS 'image!',
blurb AS 'blurb!'
FROM adventurers FROM adventurers
WHERE handle = ?1", WHERE handle = ?1",
handle handle
).fetch_optional(&mut *tx) // Use transaction here )
.fetch_optional(&mut *tx)
.await?;
let hero = sqlx::query!(
"SELECT
name,
handle AS 'handle!'
FROM adventurers
WHERE handle = ?1",
handle
)
.fetch_optional(&mut *tx)
.await?; .await?;
tx.commit().await?; tx.commit().await?;
let adventurer = match (hero, legend)
{
(Some(h), Some(l)) =>
{
Some(Adventurer
{
name: h.name,
handle: h.handle,
legend: l,
})
}
_ => { None }
};
Ok(adventurer) Ok(adventurer)
} }
} }

View File

@ -12,8 +12,8 @@ mod tavern;
pub use crate::adventurer::Adventurer; pub use crate::adventurer::{Adventurer, Legend};
pub use crate::database::Database; pub use crate::database::Database;
pub use crate::info::{get_name, get_version}; pub use crate::info::{get_name, get_version};
pub use crate::tale::{FrontMatter, Tale}; pub use crate::tale::{Lore, Tale};
pub use crate::tavern::Tavern; pub use crate::tavern::Tavern;

View File

@ -20,7 +20,7 @@ pub type Markdown = String;
/// This includes details such as the title, author, summary, and /// This includes details such as the title, author, summary, and
/// associated tags. /// associated tags.
#[derive(Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct FrontMatter pub struct Lore
{ {
/// The title of the tale. /// The title of the tale.
pub title: String, pub title: String,
@ -49,8 +49,8 @@ pub struct Tale
{ {
/// Metadata of the post. /// Metadata of the post.
#[serde(flatten)] #[serde(flatten)]
pub front_matter: FrontMatter, pub lore: Lore,
/// The file path to the Markdown content of the tale. /// The file path to the Markdown content of the tale.
pub content: Markdown pub story: Markdown
} }