tide-admin 0.1.0

Admin dashboard for tide-based web apps
Documentation
mod information_schema;

use sqlx::types::chrono::{DateTime, Utc};
use sqlx::{Column, PgPool, Row, TypeInfo};
use tera::Tera;
use tide::{Request, Server};
use tide_tera::prelude::*;

#[derive(Clone, Debug)]
pub struct State {
    pool: sqlx::PgPool,
    tera: Tera,
}

impl State {
    async fn new(database_url: &str, tera: Tera) -> Self {
        Self {
            pool: PgPool::connect(database_url).await.unwrap(),
            tera,
        }
    }
}

pub struct Admin {
    server: Server<State>,
}

impl Admin {
    pub async fn new(database_url: &str) -> Result<Self, Box<dyn std::error::Error>> {
        // TODO: Should read files dynamically from current dir
        let tera = Tera::new("../templates/*")?;
        let mut server = tide::with_state(State::new(&database_url, tera).await);
        server.at("/").get(index);
        server.at("/tables/:table_name").get(table_overview);
        Ok(Self { server })
    }

    pub fn get_server(&self) -> tide::Server<State> {
        self.server.clone()
    }
}

async fn index(request: Request<State>) -> tide::Result {
    let pool = &request.state().pool;

    let tables = information_schema::tables(pool).await?;
    let tera = &request.state().tera;
    tera.render_response("index.html", &context! { "tables" => tables })
}

async fn table_overview(request: Request<State>) -> tide::Result {
    let table_name: String = request.param("table_name")?.parse()?;
    let pool = &request.state().pool;

    let tables = information_schema::tables(pool).await?;
    let columns = information_schema::columns(&table_name, pool).await?;
    let data = get_data_from_table(&table_name, pool).await?;
    let tera = &request.state().tera;
    tera.render_response(
        "overview.html",
        &context! { "table_name" => table_name, "tables" => tables, "columns" => columns, "data" => data },
    )
}

async fn get_data_from_table(table: &str, pool: &PgPool) -> Result<Vec<Vec<String>>, sqlx::Error> {
    let sql = format!("SELECT * FROM {} LIMIT 10", table);
    let rows = sqlx::query(&sql).fetch_all(pool).await?;

    let mut data = Vec::new();
    for row in rows {
        let mut row_data = Vec::new();
        for col in row.columns() {
            let val: String = get_column_data(&row, col);
            row_data.push(val);
        }
        data.push(row_data);
    }

    Ok(data)
}

fn get_column_data(row: &sqlx::postgres::PgRow, col: &sqlx::postgres::PgColumn) -> String {
    let col_name = col.name();
    let col_type = col.type_info().name();
    match col_type {
        "VARCHAR" => row.try_get(col_name).unwrap(),
        "TEXT" => row.try_get(col_name).unwrap(),
        "CHAR" => row.try_get(col_name).unwrap(),
        "INT4" => row.try_get::<i32, &str>(col_name).unwrap().to_string(),
        "INT8" => row.try_get::<i64, &str>(col_name).unwrap().to_string(),
        "BOOL" => row.try_get::<bool, &str>(col_name).unwrap().to_string(),
        "FLOAT8" => row.try_get::<f64, &str>(col_name).unwrap().to_string(),
        "TIMESTAMPTZ" => row
            .try_get::<DateTime<Utc>, &str>(col_name)
            .unwrap()
            .to_string(),
        _ => {
            println!("Col type {} not found", col_type);
            String::new()
        }
    }
}