1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
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()
        }
    }
}