phonograph/interim-server/src/routes/lenses.rs
2025-08-04 13:59:50 -07:00

415 lines
11 KiB
Rust

use std::collections::HashMap;
use askama::Template;
use axum::{
Json,
extract::{Path, State},
response::{Html, IntoResponse, Response},
};
use axum_extra::extract::Form;
use interim_models::{
base::Base,
field::{Encodable, Field, FieldType, InsertableFieldBuilder, RFC_3339_S},
lens::{Lens, LensDisplayType},
};
use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass};
use serde::Deserialize;
use serde_json::json;
use sqlx::{
postgres::{PgRow, types::Oid},
query,
};
use uuid::Uuid;
use crate::{
app_error::{AppError, bad_request},
app_state::AppDbConn,
base_pooler::{BasePooler, RoleAssignment},
navbar::{NavLocation, Navbar, RelLocation},
navigator::Navigator,
settings::Settings,
user::CurrentUser,
};
#[derive(Deserialize)]
pub struct LensesPagePath {
base_id: Uuid,
class_oid: u32,
}
pub async fn lenses_page(
State(settings): State<Settings>,
AppDbConn(mut app_db): AppDbConn,
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
) -> Result<Response, AppError> {
// FIXME auth
let lenses = Lens::belonging_to_base(base_id)
.belonging_to_rel(Oid(class_oid))
.fetch_all(&mut app_db)
.await?;
#[derive(Template)]
#[template(path = "lenses.html")]
struct ResponseTemplate {
base_id: Uuid,
class_oid: u32,
lenses: Vec<Lens>,
settings: Settings,
}
Ok(Html(
ResponseTemplate {
base_id,
class_oid,
lenses,
settings,
}
.render()?,
)
.into_response())
}
pub async fn add_lens_page_get(
State(settings): State<Settings>,
AppDbConn(mut app_db): AppDbConn,
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
) -> Result<Response, AppError> {
// FIXME auth
#[derive(Template)]
#[template(path = "add_lens.html")]
struct ResponseTemplate {
base_id: Uuid,
class_oid: u32,
settings: Settings,
}
Ok(Html(
ResponseTemplate {
base_id,
class_oid,
settings,
}
.render()?,
)
.into_response())
}
#[derive(Deserialize)]
pub struct AddLensPagePostForm {
name: String,
}
pub async fn add_lens_page_post(
State(settings): State<Settings>,
State(mut base_pooler): State<BasePooler>,
navigator: Navigator,
AppDbConn(mut app_db): AppDbConn,
CurrentUser(current_user): CurrentUser,
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
Form(AddLensPagePostForm { name }): Form<AddLensPagePostForm>,
) -> Result<Response, AppError> {
// FIXME auth
// FIXME csrf
let mut client = base_pooler
.acquire_for(base_id, RoleAssignment::User(current_user.id))
.await?;
let attrs = PgAttribute::all_for_rel(Oid(class_oid))
.fetch_all(&mut client)
.await?;
let lens = Lens::insertable_builder()
.base_id(base_id)
.class_oid(Oid(class_oid))
.name(name)
.display_type(LensDisplayType::Table)
.build()?
.insert(&mut app_db)
.await?;
for attr in attrs {
InsertableFieldBuilder::default_from_attr(&attr)
.lens_id(lens.id)
.build()?
.insert(&mut app_db)
.await?;
}
Ok(navigator.lens_page(&lens).redirect_to())
}
#[derive(Deserialize)]
pub struct LensPagePath {
base_id: Uuid,
class_oid: u32,
lens_id: Uuid,
}
pub async fn lens_page(
State(settings): State<Settings>,
State(mut base_pooler): State<BasePooler>,
AppDbConn(mut app_db): AppDbConn,
CurrentUser(current_user): CurrentUser,
Path(LensPagePath {
lens_id,
base_id,
class_oid,
}): Path<LensPagePath>,
) -> Result<Response, AppError> {
// FIXME auth
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?;
let mut base_client = base_pooler
.acquire_for(lens.base_id, RoleAssignment::User(current_user.id))
.await?;
let rel = PgClass::with_oid(lens.class_oid)
.fetch_one(&mut base_client)
.await?;
let attrs = PgAttribute::all_for_rel(lens.class_oid)
.fetch_all(&mut base_client)
.await?;
let fields = Field::belonging_to_lens(lens.id)
.fetch_all(&mut app_db)
.await?;
let pkey_attrs = PgAttribute::pkeys_for_rel(lens.class_oid)
.fetch_all(&mut base_client)
.await?;
const FRONTEND_ROW_LIMIT: i64 = 1000;
let rows: Vec<PgRow> = query(&format!(
"select {0} from {1}.{2} limit $1",
pkey_attrs
.iter()
.chain(attrs.iter())
.map(|attr| escape_identifier(&attr.attname))
.collect::<Vec<_>>()
.join(", "),
escape_identifier(&rel.regnamespace),
escape_identifier(&rel.relname),
))
.bind(FRONTEND_ROW_LIMIT)
.fetch_all(base_client.get_conn())
.await?;
let pkeys: Vec<HashMap<String, Encodable>> = rows
.iter()
.map(|row| {
let mut pkey_values: HashMap<String, Encodable> = HashMap::new();
for attr in pkey_attrs.clone() {
let field = Field::default_from_attr(&attr);
pkey_values.insert(field.name.clone(), field.get_value_encodable(row).unwrap());
}
pkey_values
})
.collect();
#[derive(Template)]
#[template(path = "lens.html")]
struct ResponseTemplate {
fields: Vec<Field>,
all_columns: Vec<PgAttribute>,
rows: Vec<PgRow>,
pkeys: Vec<HashMap<String, Encodable>>,
settings: Settings,
navbar: Navbar,
}
Ok(Html(
ResponseTemplate {
all_columns: attrs,
fields,
pkeys,
rows,
navbar: Navbar::builder()
.root_path(settings.root_path.clone())
.base(base.clone())
.populate_rels(&mut app_db, &mut base_client)
.await?
.current(NavLocation::Rel(
Oid(class_oid),
Some(RelLocation::Lens(lens.id)),
))
.build()?,
settings,
}
.render()?,
)
.into_response())
}
#[derive(Debug, Deserialize)]
pub struct AddColumnPageForm {
name: String,
label: String,
field_type: String,
timestamp_format: Option<String>,
}
fn try_field_type_from_form(form: &AddColumnPageForm) -> Result<FieldType, AppError> {
let serialized = match form.field_type.as_str() {
"Timestamp" => {
json!({
"t": form.field_type,
"c": {
"format": form.timestamp_format.clone().unwrap_or(RFC_3339_S.to_owned()),
},
})
}
_ => json!({"t": form.field_type}),
};
serde_json::from_value(serialized).or(Err(bad_request!("unable to parse field type")))
}
pub async fn add_column_page_post(
State(mut base_pooler): State<BasePooler>,
navigator: Navigator,
AppDbConn(mut app_db): AppDbConn,
CurrentUser(current_user): CurrentUser,
Path(LensPagePath { lens_id, .. }): Path<LensPagePath>,
Form(form): Form<AddColumnPageForm>,
) -> Result<Response, AppError> {
// FIXME auth
// FIXME csrf
// FIXME validate column name length is less than 64
let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?;
let base = Base::with_id(lens.base_id).fetch_one(&mut app_db).await?;
let mut base_client = base_pooler
.acquire_for(base.id, RoleAssignment::User(current_user.id))
.await?;
let class = PgClass::with_oid(lens.class_oid)
.fetch_one(&mut base_client)
.await?;
let field_type = try_field_type_from_form(&form)?;
let data_type_fragment = field_type.attr_data_type_fragment().ok_or(bad_request!(
"cannot create column with type specified as Unknown"
))?;
query(&format!(
r#"
alter table {0}
add column if not exists {1} {2}
"#,
class.get_identifier(),
escape_identifier(&form.name),
data_type_fragment
))
.execute(base_client.get_conn())
.await?;
Field::insertable_builder()
.lens_id(lens.id)
.name(form.name)
.label(if form.label.is_empty() {
None
} else {
Some(form.label)
})
.field_type(field_type)
.build()?
.insert(&mut app_db)
.await?;
Ok(navigator.lens_page(&lens).redirect_to())
}
// #[derive(Debug, Deserialize)]
// pub struct AddSelectionPageForm {
// column: String,
// }
//
// pub async fn add_selection_page_post(
// State(settings): State<Settings>,
// AppDbConn(mut app_db): AppDbConn,
// CurrentUser(current_user): CurrentUser,
// Path(LensPagePath {
// base_id,
// class_oid,
// lens_id,
// }): Path<LensPagePath>,
// Form(form): Form<AddSelectionPageForm>,
// ) -> Result<Response, AppError> {
// dbg!(&form);
// // FIXME auth
// // FIXME csrf
//
// let lens = Lens::fetch_by_id(lens_id, &mut *app_db)
// .await?
// .ok_or(not_found!("lens not found"))?;
// Selection::insertable_builder()
// .lens_id(lens.id)
// .attr_filters(vec![AttrFilter::NameEq(form.column)])
// .build()?
// .insert(&mut *app_db)
// .await?;
//
// Ok(Redirect::to(&format!(
// "{0}/d/{base_id}/r/{class_oid}/l/{lens_id}/",
// settings.root_path
// ))
// .into_response())
// }
#[derive(Deserialize)]
pub struct UpdateValuePageForm {
column: String,
pkeys: HashMap<String, Encodable>,
value: Encodable,
}
pub async fn update_value_page_post(
State(mut base_pooler): State<BasePooler>,
CurrentUser(current_user): CurrentUser,
Path(LensPagePath {
base_id, class_oid, ..
}): Path<LensPagePath>,
Json(body): Json<UpdateValuePageForm>,
) -> Result<Response, AppError> {
// FIXME auth
// FIXME csrf
let mut base_client = base_pooler
.acquire_for(base_id, RoleAssignment::User(current_user.id))
.await?;
let rel = PgClass::with_oid(Oid(class_oid))
.fetch_one(&mut base_client)
.await?;
let pkey_attrs = PgAttribute::pkeys_for_rel(rel.oid)
.fetch_all(&mut base_client)
.await?;
body.pkeys
.get(&pkey_attrs.first().unwrap().attname)
.unwrap()
.bind_onto(body.value.bind_onto(query(&format!(
r#"update {0}.{1} set {2} = $1 where {3} = $2"#,
escape_identifier(&rel.regnamespace),
escape_identifier(&rel.relname),
escape_identifier(&body.column),
escape_identifier(&pkey_attrs.first().unwrap().attname),
))))
.execute(base_client.get_conn())
.await?;
Ok(Json(json!({ "ok": true })).into_response())
}
#[derive(Deserialize)]
pub struct ViewerPagePath {
base_id: Uuid,
class_oid: u32,
lens_id: Uuid,
}
pub async fn viewer_page(
State(settings): State<Settings>,
State(mut base_pooler): State<BasePooler>,
AppDbConn(mut app_db): AppDbConn,
CurrentUser(current_user): CurrentUser,
Path(params): Path<ViewerPagePath>,
) -> Result<Response, AppError> {
todo!("not yet implemented");
}