ability to add columns
This commit is contained in:
parent
afafb49cd6
commit
389bd27b33
16 changed files with 328 additions and 120 deletions
|
|
@ -82,7 +82,7 @@ fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
|||
label_value: "",
|
||||
name_value: "",
|
||||
name_customized: False,
|
||||
field_type: "text",
|
||||
field_type: "Text",
|
||||
),
|
||||
effect.none(),
|
||||
)
|
||||
|
|
@ -144,7 +144,7 @@ fn view(model: Model) -> Element(Msg) {
|
|||
)
|
||||
True ->
|
||||
html.div([attr.class("header")], [
|
||||
html.form([attr.method("post"), attr.action("create-column")], [
|
||||
html.form([attr.method("post"), attr.action("add-column")], [
|
||||
label_input(value: model.label_value, on_input: UserUpdatedLabel),
|
||||
html.button(
|
||||
[attr.type_("button"), attr.popovertarget("config-popover")],
|
||||
|
|
@ -200,7 +200,6 @@ fn config_popover(model: Model) -> Element(Msg) {
|
|||
),
|
||||
html.select(
|
||||
[
|
||||
attr.type_("text"),
|
||||
attr.name("field_type"),
|
||||
attr.class("form-section__input"),
|
||||
attr.id("field-type-select"),
|
||||
|
|
@ -208,11 +207,11 @@ fn config_popover(model: Model) -> Element(Msg) {
|
|||
],
|
||||
[
|
||||
html.option(
|
||||
[attr.value("text"), attr.checked(model.field_type == "text")],
|
||||
[attr.value("Text"), attr.checked(model.field_type == "Text")],
|
||||
"Text",
|
||||
),
|
||||
html.option(
|
||||
[attr.value("decimal"), attr.checked(model.field_type == "decimal")],
|
||||
[attr.value("Decimal"), attr.checked(model.field_type == "Decimal")],
|
||||
"Decimal",
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
drop table if exists lens_selections;
|
||||
drop table if exists fields;
|
||||
drop table if exists lenses;
|
||||
drop type if exists lens_display_type;
|
||||
|
|
|
|||
|
|
@ -11,12 +11,11 @@ create table if not exists lenses (
|
|||
);
|
||||
create index on lenses (base_id);
|
||||
|
||||
create table if not exists lens_selections (
|
||||
create table if not exists fields (
|
||||
id uuid not null primary key,
|
||||
lens_id uuid not null references lenses(id) on delete cascade,
|
||||
attr_filters jsonb not null default '[]'::jsonb,
|
||||
name text not null,
|
||||
label text,
|
||||
field_type jsonb,
|
||||
visible boolean not null default true,
|
||||
field_type jsonb not null,
|
||||
width_px int not null default 200
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use derive_builder::Builder;
|
||||
use interim_pgtypes::pg_attribute::PgAttribute;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Decode, Postgres, Row as _, TypeInfo as _, ValueRef as _, postgres::PgRow};
|
||||
use sqlx::{
|
||||
Decode, PgExecutor, Postgres, Row as _, TypeInfo as _, ValueRef as _, postgres::PgRow, query_as,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
|
||||
const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S";
|
||||
pub const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S";
|
||||
|
||||
/// A single column which can be passed to a front-end viewer. A Selection may
|
||||
/// resolve to zero or more Fields.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Field {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub field_type: sqlx::types::Json<FieldType>,
|
||||
|
|
@ -18,8 +20,13 @@ pub struct Field {
|
|||
}
|
||||
|
||||
impl Field {
|
||||
pub fn insertable_builder() -> InsertableFieldBuilder {
|
||||
InsertableFieldBuilder::default()
|
||||
}
|
||||
|
||||
pub fn default_from_attr(attr: &PgAttribute) -> Self {
|
||||
Self {
|
||||
id: Uuid::now_v7(),
|
||||
name: attr.attname.clone(),
|
||||
label: None,
|
||||
field_type: sqlx::types::Json(FieldType::default_from_attr(attr)),
|
||||
|
|
@ -112,8 +119,73 @@ impl FieldType {
|
|||
_ => Self::Unknown,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a SQL fragment for the default data type for creating or
|
||||
/// altering a backing column, such as "integer", or "timestamptz". Returns
|
||||
/// None if the field type is Unknown.
|
||||
pub fn attr_data_type_fragment(&self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::Integer => Some("integer"),
|
||||
Self::InterimUser | Self::Text => Some("text"),
|
||||
Self::Timestamp { .. } => Some("timestamptz"),
|
||||
Self::Uuid => Some("uuid"),
|
||||
Self::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Insertable --------
|
||||
|
||||
#[derive(Builder, Clone, Debug)]
|
||||
pub struct InsertableField {
|
||||
lens_id: Uuid,
|
||||
name: String,
|
||||
#[builder(default)]
|
||||
label: Option<String>,
|
||||
field_type: FieldType,
|
||||
#[builder(default = 200)]
|
||||
width_px: i32,
|
||||
}
|
||||
|
||||
impl InsertableField {
|
||||
pub async fn insert<'a, E: PgExecutor<'a>>(self, app_db: E) -> Result<Field, sqlx::Error> {
|
||||
query_as!(
|
||||
Field,
|
||||
r#"
|
||||
insert into fields
|
||||
(id, lens_id, name, label, field_type, width_px)
|
||||
values ($1, $2, $3, $4, $5, $6)
|
||||
returning
|
||||
id,
|
||||
name,
|
||||
label,
|
||||
field_type as "field_type: sqlx::types::Json<FieldType>",
|
||||
width_px
|
||||
"#,
|
||||
Uuid::now_v7(),
|
||||
self.lens_id,
|
||||
self.name,
|
||||
self.label,
|
||||
sqlx::types::Json::<_>(self.field_type) as sqlx::types::Json<FieldType>,
|
||||
self.width_px,
|
||||
)
|
||||
.fetch_one(app_db)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
impl InsertableFieldBuilder {
|
||||
pub fn default_from_attr(attr: &PgAttribute) -> Self {
|
||||
Self {
|
||||
name: Some(attr.attname.clone()),
|
||||
field_type: Some(FieldType::default_from_attr(attr)),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -------- Errors --------
|
||||
|
||||
/// Error when parsing a sqlx value to JSON
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ParseError {
|
||||
|
|
@ -125,6 +197,9 @@ pub enum ParseError {
|
|||
UnknownType,
|
||||
}
|
||||
|
||||
// -------- Encodable --------
|
||||
// TODO this should probably be moved to another crate
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum Encodable {
|
||||
|
|
@ -147,26 +222,3 @@ impl Encodable {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// impl<'a> Encode<'a, Postgres> for Encodable {
|
||||
// fn encode_by_ref(
|
||||
// &self,
|
||||
// buf: &mut <Postgres as sqlx::Database>::ArgumentBuffer<'a>,
|
||||
// ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
|
||||
// match self {
|
||||
// Self::Integer(value) => Encode::<'a, Postgres>::encode(value, buf),
|
||||
// Self::Text(value) => Encode::<'a, Postgres>::encode(value, buf),
|
||||
// Self::Timestamptz(value) => Encode::<'a, Postgres>::encode(value, buf),
|
||||
// Self::Uuid(value) => Encode::<'a, Postgres>::encode(value, buf),
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl<'a> Decode<'a, Postgres> for FieldType {
|
||||
// fn decode(
|
||||
// value: <Postgres as sqlx::Database>::ValueRef<'a>,
|
||||
// ) -> Result<Self, sqlx::error::BoxDynError> {
|
||||
// let value: String = Decode::<'a, Postgres>::decode(value)?;
|
||||
// Ok(serde_json::from_str(&value)?)
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -3,10 +3,7 @@ use serde::Serialize;
|
|||
use sqlx::{PgExecutor, postgres::types::Oid, query_as};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
field::FieldType,
|
||||
selection::{AttrFilter, Selection},
|
||||
};
|
||||
use crate::field::{Field, FieldType};
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Lens {
|
||||
|
|
@ -68,21 +65,20 @@ where base_id = $1 and class_oid = $2
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn fetch_selections<'a, E: PgExecutor<'a>>(
|
||||
pub async fn fetch_fields<'a, E: PgExecutor<'a>>(
|
||||
&self,
|
||||
app_db: E,
|
||||
) -> Result<Vec<Selection>, sqlx::Error> {
|
||||
) -> Result<Vec<Field>, sqlx::Error> {
|
||||
query_as!(
|
||||
Selection,
|
||||
Field,
|
||||
r#"
|
||||
select
|
||||
id,
|
||||
attr_filters as "attr_filters: sqlx::types::Json<Vec<AttrFilter>>",
|
||||
name,
|
||||
label,
|
||||
field_type as "field_type: sqlx::types::Json<FieldType>",
|
||||
visible,
|
||||
width_px
|
||||
from lens_selections
|
||||
from fields
|
||||
where lens_id = $1
|
||||
"#,
|
||||
self.id
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
pub mod field;
|
||||
pub mod lens;
|
||||
pub mod selection;
|
||||
// pub mod selection;
|
||||
|
||||
pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!();
|
||||
|
|
|
|||
|
|
@ -4,3 +4,14 @@ pub mod pg_class;
|
|||
pub mod pg_database;
|
||||
pub mod pg_namespace;
|
||||
pub mod pg_role;
|
||||
|
||||
/// Given a raw identifier (such as a table name, column name, etc.), format it
|
||||
/// so that it may be safely interpolated into a SQL query.
|
||||
pub fn escape_identifier(identifier: &str) -> String {
|
||||
// Escaping identifiers for Postgres is fairly easy, provided that the input is
|
||||
// already known to contain no invalid multi-byte sequences. Backslashes may
|
||||
// remain as-is, and embedded double quotes are escaped simply by doubling
|
||||
// them (`"` becomes `""`). Refer to the PQescapeInternal() function in
|
||||
// libpq (fe-exec.c) and Diesel's PgQueryBuilder::push_identifier().
|
||||
format!("\"{}\"", identifier.replace('"', "\"\""))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use sqlx::{PgExecutor, postgres::types::Oid, query_as};
|
||||
|
||||
use crate::{pg_acl::PgAclItem, pg_namespace::PgNamespace};
|
||||
use crate::{escape_identifier, pg_acl::PgAclItem, pg_namespace::PgNamespace};
|
||||
|
||||
pub struct PgClass {
|
||||
/// Row identifier
|
||||
|
|
@ -9,6 +9,8 @@ pub struct PgClass {
|
|||
pub relname: String,
|
||||
/// The OID of the namespace that contains this relation
|
||||
pub relnamespace: Oid,
|
||||
/// SYNTHESIZED: name of this relation's namespace as text
|
||||
pub regnamespace: String,
|
||||
/// The OID of the data type that corresponds to this table's row type, if any; zero for indexes, sequences, and toast tables, which have no pg_type entry
|
||||
pub reltype: Oid,
|
||||
/// For typed tables, the OID of the underlying composite type; zero for all other relations
|
||||
|
|
@ -50,6 +52,7 @@ select
|
|||
oid,
|
||||
relname,
|
||||
relnamespace,
|
||||
relnamespace::regnamespace::text as "regnamespace!",
|
||||
reltype,
|
||||
reloftype,
|
||||
relowner,
|
||||
|
|
@ -89,6 +92,7 @@ select
|
|||
oid,
|
||||
relname,
|
||||
relnamespace,
|
||||
relnamespace::regnamespace::text as "regnamespace!",
|
||||
reltype,
|
||||
reloftype,
|
||||
relowner,
|
||||
|
|
@ -123,6 +127,15 @@ where
|
|||
// to the namespace that contains it. If not, that's an error.
|
||||
.ok_or(sqlx::Error::RowNotFound)
|
||||
}
|
||||
|
||||
/// Get ecaped identifier, including namespace.
|
||||
pub fn get_identifier(&self) -> String {
|
||||
format!(
|
||||
"{0}.{1}",
|
||||
escape_identifier(&self.regnamespace),
|
||||
escape_identifier(&self.relname)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum PgRelKind {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,17 @@ macro_rules! not_found {
|
|||
};
|
||||
}
|
||||
|
||||
macro_rules! bad_request {
|
||||
($message:literal) => {
|
||||
AppError::BadRequest($message.to_owned())
|
||||
};
|
||||
|
||||
($message:literal, $($param:expr),+) => {
|
||||
AppError::BadRequest(format!($message, $($param)+))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use bad_request;
|
||||
pub(crate) use not_found;
|
||||
|
||||
/// Custom error type that maps to appropriate HTTP responses.
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
use sqlx::{query, PgConnection, Row as _};
|
||||
|
||||
pub fn escape_identifier(identifier: &str) -> String {
|
||||
// Escaping identifiers for Postgres is fairly easy, provided that the input is
|
||||
// already known to contain no invalid multi-byte sequences. Backslashes may
|
||||
// remain as-is, and embedded double quotes are escaped simply by doubling
|
||||
// them (`"` becomes `""`). Refer to the PQescapeInternal() function in
|
||||
// libpq (fe-exec.c) and Diesel's PgQueryBuilder::push_identifier().
|
||||
format!("\"{}\"", identifier.replace('"', "\"\""))
|
||||
}
|
||||
use interim_pgtypes::escape_identifier;
|
||||
use sqlx::{PgConnection, Row as _, query};
|
||||
|
||||
pub async fn init_role(rolname: &str, client: &mut PgConnection) -> Result<(), sqlx::Error> {
|
||||
let session_user = query!("select session_user;")
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ mod cli;
|
|||
mod db_conns;
|
||||
mod lenses;
|
||||
mod middleware;
|
||||
mod navigator;
|
||||
mod rel_invitations;
|
||||
mod router;
|
||||
mod routes;
|
||||
|
|
|
|||
48
interim-server/src/navigator.rs
Normal file
48
interim-server/src/navigator.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use axum::{
|
||||
extract::FromRequestParts,
|
||||
http::request::Parts,
|
||||
response::{IntoResponse as _, Redirect, Response},
|
||||
};
|
||||
use interim_models::lens::Lens;
|
||||
|
||||
use crate::{app_error::AppError, app_state::AppState};
|
||||
|
||||
/// Helper type for semantically generating URI paths, e.g. for redirects.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Navigator {
|
||||
root_path: String,
|
||||
sub_path: String,
|
||||
}
|
||||
|
||||
impl Navigator {
|
||||
pub fn lens_page(&self, lens: &Lens) -> Self {
|
||||
Self {
|
||||
sub_path: format!(
|
||||
"/d/{0}/r/{1}/l/{2}/",
|
||||
lens.base_id.simple(),
|
||||
lens.class_oid.0,
|
||||
lens.id.simple()
|
||||
),
|
||||
..self.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redirect_to(&self) -> Response {
|
||||
Redirect::to(&format!("{0}{1}", self.root_path, self.sub_path)).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> FromRequestParts<S> for Navigator
|
||||
where
|
||||
S: Into<AppState> + Clone + Sync,
|
||||
{
|
||||
type Rejection = AppError;
|
||||
|
||||
async fn from_request_parts(_: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let app_state: AppState = state.clone().into();
|
||||
Ok(Navigator {
|
||||
root_path: app_state.settings.root_path.clone(),
|
||||
sub_path: "/".to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -69,9 +69,13 @@ pub fn new_router(state: AppState) -> Router<()> {
|
|||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/update-lens",
|
||||
post(routes::lenses::update_lens_page_post),
|
||||
)
|
||||
// .route(
|
||||
// "/d/{base_id}/r/{class_oid}/l/{lens_id}/add-selection",
|
||||
// post(routes::lenses::add_selection_page_post),
|
||||
// )
|
||||
.route(
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/add-selection",
|
||||
post(routes::lenses::add_selection_page_post),
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/add-column",
|
||||
post(routes::lenses::add_column_page_post),
|
||||
)
|
||||
.route(
|
||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/update-value",
|
||||
|
|
|
|||
|
|
@ -5,18 +5,14 @@ use axum::{
|
|||
response::{Html, IntoResponse as _, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Form;
|
||||
use interim_pgtypes::escape_identifier;
|
||||
use serde::Deserialize;
|
||||
use sqlx::{query, query_scalar};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_error::AppError,
|
||||
app_state::AppDbConn,
|
||||
base_pooler::BasePooler,
|
||||
base_user_perms::sync_perms_for_base,
|
||||
bases::Base,
|
||||
db_conns::{escape_identifier, init_role},
|
||||
settings::Settings,
|
||||
app_error::AppError, app_state::AppDbConn, base_pooler::BasePooler,
|
||||
base_user_perms::sync_perms_for_base, bases::Base, db_conns::init_role, settings::Settings,
|
||||
users::CurrentUser,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,15 +4,15 @@ use askama::Template;
|
|||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
response::{Html, IntoResponse as _, Redirect, Response},
|
||||
response::{Html, IntoResponse, Redirect, Response},
|
||||
};
|
||||
use axum_extra::extract::Form;
|
||||
use interim_models::{
|
||||
field::{Encodable, Field},
|
||||
field::{Encodable, Field, FieldType, InsertableFieldBuilder, RFC_3339_S},
|
||||
lens::{Lens, LensDisplayType},
|
||||
selection::{AttrFilter, Selection},
|
||||
};
|
||||
use interim_pgtypes::{
|
||||
escape_identifier,
|
||||
pg_attribute::{PgAttribute, fetch_attributes_for_rel, fetch_primary_keys_for_rel},
|
||||
pg_class::PgClass,
|
||||
};
|
||||
|
|
@ -25,11 +25,12 @@ use sqlx::{
|
|||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
app_error::{AppError, not_found},
|
||||
app_error::{AppError, bad_request, not_found},
|
||||
app_state::AppDbConn,
|
||||
base_pooler::BasePooler,
|
||||
bases::Base,
|
||||
db_conns::{escape_identifier, init_role},
|
||||
db_conns::init_role,
|
||||
navigator::Navigator,
|
||||
settings::Settings,
|
||||
users::CurrentUser,
|
||||
};
|
||||
|
|
@ -99,13 +100,19 @@ pub struct AddLensPagePostForm {
|
|||
}
|
||||
|
||||
pub async fn add_lens_page_post(
|
||||
State(Settings { root_path, .. }): State<Settings>,
|
||||
State(mut base_pooler): State<BasePooler>,
|
||||
navigator: Navigator,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
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).await?;
|
||||
|
||||
let attrs = fetch_attributes_for_rel(Oid(class_oid), &mut *client).await?;
|
||||
|
||||
let lens = Lens::insertable_builder()
|
||||
.base_id(base_id)
|
||||
.class_oid(Oid(class_oid))
|
||||
|
|
@ -114,12 +121,16 @@ pub async fn add_lens_page_post(
|
|||
.build()?
|
||||
.insert(&mut *app_db)
|
||||
.await?;
|
||||
Ok(Redirect::to(&format!(
|
||||
"{root_path}/d/{0}/r/{class_oid}/l/{1}",
|
||||
base_id.simple(),
|
||||
lens.id.simple()
|
||||
))
|
||||
.into_response())
|
||||
|
||||
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)]
|
||||
|
|
@ -134,27 +145,24 @@ pub async fn lens_page(
|
|||
State(mut base_pooler): State<BasePooler>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(LensPagePath {
|
||||
base_id,
|
||||
class_oid,
|
||||
lens_id,
|
||||
}): Path<LensPagePath>,
|
||||
Path(LensPagePath { lens_id, .. }): Path<LensPagePath>,
|
||||
) -> Result<Response, AppError> {
|
||||
// FIXME auth
|
||||
let base = Base::fetch_by_id(base_id, &mut *app_db)
|
||||
let lens = Lens::fetch_by_id(lens_id, &mut *app_db)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound("no base found with that id".to_owned()))?;
|
||||
let mut client = base_pooler.acquire_for(base_id).await?;
|
||||
.ok_or(not_found!("lens not found"))?;
|
||||
let base = Base::fetch_by_id(lens.base_id, &mut *app_db)
|
||||
.await?
|
||||
.ok_or(not_found!("no base found with that id"))?;
|
||||
|
||||
let mut client = base_pooler.acquire_for(lens.base_id).await?;
|
||||
init_role(
|
||||
&format!("{}{}", &base.user_role_prefix, ¤t_user.id.simple()),
|
||||
&mut client,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// FIXME auth
|
||||
|
||||
let class = PgClass::fetch_by_oid(Oid(class_oid), &mut *client)
|
||||
let class = PgClass::fetch_by_oid(lens.class_oid, &mut *client)
|
||||
.await?
|
||||
.ok_or(AppError::NotFound(
|
||||
"no relation found with that oid".to_owned(),
|
||||
|
|
@ -165,15 +173,9 @@ pub async fn lens_page(
|
|||
.await?
|
||||
.ok_or(not_found!("no lens found with that id"))?;
|
||||
|
||||
let attrs = fetch_attributes_for_rel(Oid(class_oid), &mut *client).await?;
|
||||
|
||||
let selections = lens.fetch_selections(&mut *app_db).await?;
|
||||
let mut fields: Vec<Field> = Vec::with_capacity(selections.len());
|
||||
for selection in selections.clone() {
|
||||
fields.append(&mut selection.resolve_fields_from_attrs(&attrs));
|
||||
}
|
||||
|
||||
let pkey_attrs = fetch_primary_keys_for_rel(Oid(class_oid), &mut *client).await?;
|
||||
let attrs = fetch_attributes_for_rel(lens.class_oid, &mut *client).await?;
|
||||
let fields = lens.fetch_fields(&mut *app_db).await?;
|
||||
let pkey_attrs = fetch_primary_keys_for_rel(lens.class_oid, &mut *client).await?;
|
||||
|
||||
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
||||
struct Row {
|
||||
|
|
@ -214,7 +216,6 @@ pub async fn lens_page(
|
|||
fields: Vec<Field>,
|
||||
all_columns: Vec<PgAttribute>,
|
||||
rows: Vec<Row>,
|
||||
selections_json: String,
|
||||
settings: Settings,
|
||||
}
|
||||
Ok(Html(
|
||||
|
|
@ -222,7 +223,6 @@ pub async fn lens_page(
|
|||
all_columns: attrs,
|
||||
fields,
|
||||
rows,
|
||||
selections_json: serde_json::to_string(&selections)?,
|
||||
settings,
|
||||
}
|
||||
.render()?,
|
||||
|
|
@ -231,42 +231,129 @@ pub async fn lens_page(
|
|||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AddSelectionPageForm {
|
||||
column: String,
|
||||
pub struct AddColumnPageForm {
|
||||
name: String,
|
||||
label: String,
|
||||
field_type: String,
|
||||
timestamp_format: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn add_selection_page_post(
|
||||
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(settings): State<Settings>,
|
||||
State(mut base_pooler): State<BasePooler>,
|
||||
navigator: Navigator,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
CurrentUser(current_user): CurrentUser,
|
||||
Path(LensPagePath {
|
||||
base_id,
|
||||
class_oid,
|
||||
lens_id,
|
||||
}): Path<LensPagePath>,
|
||||
Form(form): Form<AddSelectionPageForm>,
|
||||
Path(LensPagePath { lens_id, .. }): Path<LensPagePath>,
|
||||
Form(form): Form<AddColumnPageForm>,
|
||||
) -> Result<Response, AppError> {
|
||||
dbg!(&form);
|
||||
// FIXME auth
|
||||
// FIXME csrf
|
||||
// FIXME validate column name length is less than 64
|
||||
|
||||
let lens = Lens::fetch_by_id(lens_id, &mut *app_db)
|
||||
.await?
|
||||
.ok_or(not_found!("lens not found"))?;
|
||||
Selection::insertable_builder()
|
||||
let base = Base::fetch_by_id(lens.base_id, &mut *app_db)
|
||||
.await?
|
||||
.ok_or(not_found!("no base found with that id"))?;
|
||||
|
||||
let mut client = base_pooler.acquire_for(base.id).await?;
|
||||
init_role(
|
||||
&format!("{}{}", &base.user_role_prefix, ¤t_user.id.simple()),
|
||||
&mut client,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let class = PgClass::fetch_by_oid(lens.class_oid, &mut *client)
|
||||
.await?
|
||||
.ok_or(not_found!("pg class not found"))?;
|
||||
|
||||
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(&mut *client)
|
||||
.await?;
|
||||
|
||||
Field::insertable_builder()
|
||||
.lens_id(lens.id)
|
||||
.attr_filters(vec![AttrFilter::NameEq(form.column)])
|
||||
.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(Redirect::to(&format!(
|
||||
"{0}/d/{base_id}/r/{class_oid}/l/{lens_id}/",
|
||||
settings.root_path
|
||||
))
|
||||
.into_response())
|
||||
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())
|
||||
// }
|
||||
|
||||
pub async fn update_lens_page_post(
|
||||
State(settings): State<Settings>,
|
||||
AppDbConn(mut app_db): AppDbConn,
|
||||
|
|
|
|||
|
|
@ -46,5 +46,4 @@
|
|||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<lens-controls selections="{{ selections_json }}"></lens-controls>
|
||||
{% endblock %}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue