feat: initial page rendering; db stuff
This commit is contained in:
parent
a410e4f9ec
commit
08f6faef52
7 changed files with 692 additions and 23 deletions
78
src/main.rs
78
src/main.rs
|
|
@ -1,39 +1,60 @@
|
|||
mod server;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use r2d2_sqlite::{rusqlite, SqliteConnectionManager};
|
||||
use tera::Tera;
|
||||
|
||||
pub type DbPool = r2d2::Pool<SqliteConnectionManager>;
|
||||
|
||||
const MIGRATIONS: [&str; 1] = [include_str!("migrations/000_initial.sql")];
|
||||
const MIGRATIONS: [&str; 2] = [
|
||||
include_str!("migrations/000_initial.sql"),
|
||||
include_str!("migrations/001_plants.sql"),
|
||||
];
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Context {
|
||||
pool: crate::DbPool,
|
||||
tera: Arc<Tera>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let manager = SqliteConnectionManager::file("db.sqlite");
|
||||
let pool = r2d2::Pool::new(manager).unwrap();
|
||||
run_migrations(&pool, true).unwrap();
|
||||
run_migrations(&pool).unwrap();
|
||||
|
||||
let tera = load_templates();
|
||||
let ctx = Context {
|
||||
pool,
|
||||
tera: Arc::new(tera),
|
||||
};
|
||||
let app = server::app(ctx);
|
||||
|
||||
let address = "0.0.0.0:8000";
|
||||
|
||||
tracing::info!("Starting server on {address}");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
||||
axum::serve(listener, app.into_make_service())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn run_migrations(pool: &DbPool, new: bool) -> rusqlite::Result<()> {
|
||||
fn run_migrations(pool: &DbPool) -> rusqlite::Result<()> {
|
||||
let mut conn = pool.get().unwrap();
|
||||
|
||||
// Run very first migration
|
||||
if new {
|
||||
let tx = conn.transaction()?;
|
||||
|
||||
tx.execute(MIGRATIONS[0], ())?;
|
||||
|
||||
let cur_time = chrono::Local::now().timestamp();
|
||||
tx.execute(
|
||||
"insert into migration_version values ($1, $2)",
|
||||
[0, cur_time],
|
||||
)?;
|
||||
|
||||
tx.commit()?;
|
||||
}
|
||||
|
||||
let mut next_version = conn.query_row(
|
||||
"select version from migration_version order by updated_at desc limit 1",
|
||||
(),
|
||||
|row| row.get::<_, usize>(0),
|
||||
)? + 1;
|
||||
// If the migration version query fails, we assume it's because the table isn't there yet so we
|
||||
// try to run the first migration
|
||||
let mut next_version = conn
|
||||
.query_row(
|
||||
"select version from migration_version order by updated_at desc limit 1",
|
||||
(),
|
||||
|row| row.get::<_, usize>(0).map(|n| n + 1),
|
||||
)
|
||||
.unwrap_or(0);
|
||||
|
||||
while next_version < MIGRATIONS.len() {
|
||||
let tx = conn.transaction()?;
|
||||
|
|
@ -48,8 +69,19 @@ fn run_migrations(pool: &DbPool, new: bool) -> rusqlite::Result<()> {
|
|||
|
||||
tx.commit()?;
|
||||
|
||||
tracing::info!("Applied migration {next_version}");
|
||||
|
||||
next_version += 1;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_templates() -> Tera {
|
||||
let mut tera = Tera::default();
|
||||
|
||||
tera.add_raw_templates(vec![("index.html", include_str!("templates/index.html"))])
|
||||
.unwrap();
|
||||
|
||||
tera
|
||||
}
|
||||
|
|
|
|||
6
src/migrations/001_plants.sql
Normal file
6
src/migrations/001_plants.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
create table plants (
|
||||
id integer primary key,
|
||||
name text not null,
|
||||
species text not null,
|
||||
description text
|
||||
);
|
||||
50
src/server/mod.rs
Normal file
50
src/server/mod.rs
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
use axum::{extract::State, response::Html, routing::get, Router};
|
||||
use r2d2_sqlite::rusqlite::{self, Row};
|
||||
use serde::Serialize;
|
||||
use tera::Context;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Plant {
|
||||
id: i32,
|
||||
name: String,
|
||||
species: String,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
impl TryFrom<&Row<'_>> for Plant {
|
||||
type Error = rusqlite::Error;
|
||||
|
||||
fn try_from(row: &Row<'_>) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
species: row.get(2)?,
|
||||
description: row.get(3)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn app(ctx: crate::Context) -> axum::Router {
|
||||
Router::new().route("/", get(get_index)).with_state(ctx)
|
||||
}
|
||||
|
||||
async fn get_index(State(ctx): State<crate::Context>) -> Html<String> {
|
||||
let plants = tokio::task::spawn_blocking(move || {
|
||||
let conn = ctx.pool.get().unwrap();
|
||||
|
||||
let mut stmt = conn.prepare("select * from plants").unwrap();
|
||||
let mut plants = Vec::new();
|
||||
|
||||
for plant in stmt.query_map((), |row| Plant::try_from(row)).unwrap() {
|
||||
plants.push(plant.unwrap());
|
||||
}
|
||||
|
||||
plants
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut context = Context::new();
|
||||
context.insert("plants", &plants);
|
||||
Html(ctx.tera.render("index.html", &context).unwrap())
|
||||
}
|
||||
11
src/templates/index.html
Normal file
11
src/templates/index.html
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<html>
|
||||
<body>
|
||||
<h1>Calathea</h1>
|
||||
<h2>Plants</h2>
|
||||
<ul>
|
||||
{% for plant in plants %}
|
||||
<li>{{ plant.name }} ({{ plant.species }})</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Add table
Add a link
Reference in a new issue