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: "",
|
label_value: "",
|
||||||
name_value: "",
|
name_value: "",
|
||||||
name_customized: False,
|
name_customized: False,
|
||||||
field_type: "text",
|
field_type: "Text",
|
||||||
),
|
),
|
||||||
effect.none(),
|
effect.none(),
|
||||||
)
|
)
|
||||||
|
|
@ -144,7 +144,7 @@ fn view(model: Model) -> Element(Msg) {
|
||||||
)
|
)
|
||||||
True ->
|
True ->
|
||||||
html.div([attr.class("header")], [
|
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),
|
label_input(value: model.label_value, on_input: UserUpdatedLabel),
|
||||||
html.button(
|
html.button(
|
||||||
[attr.type_("button"), attr.popovertarget("config-popover")],
|
[attr.type_("button"), attr.popovertarget("config-popover")],
|
||||||
|
|
@ -200,7 +200,6 @@ fn config_popover(model: Model) -> Element(Msg) {
|
||||||
),
|
),
|
||||||
html.select(
|
html.select(
|
||||||
[
|
[
|
||||||
attr.type_("text"),
|
|
||||||
attr.name("field_type"),
|
attr.name("field_type"),
|
||||||
attr.class("form-section__input"),
|
attr.class("form-section__input"),
|
||||||
attr.id("field-type-select"),
|
attr.id("field-type-select"),
|
||||||
|
|
@ -208,11 +207,11 @@ fn config_popover(model: Model) -> Element(Msg) {
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
html.option(
|
html.option(
|
||||||
[attr.value("text"), attr.checked(model.field_type == "text")],
|
[attr.value("Text"), attr.checked(model.field_type == "Text")],
|
||||||
"Text",
|
"Text",
|
||||||
),
|
),
|
||||||
html.option(
|
html.option(
|
||||||
[attr.value("decimal"), attr.checked(model.field_type == "decimal")],
|
[attr.value("Decimal"), attr.checked(model.field_type == "Decimal")],
|
||||||
"Decimal",
|
"Decimal",
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
drop table if exists lens_selections;
|
drop table if exists fields;
|
||||||
drop table if exists lenses;
|
drop table if exists lenses;
|
||||||
drop type if exists lens_display_type;
|
drop type if exists lens_display_type;
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,11 @@ create table if not exists lenses (
|
||||||
);
|
);
|
||||||
create index on lenses (base_id);
|
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,
|
id uuid not null primary key,
|
||||||
lens_id uuid not null references lenses(id) on delete cascade,
|
lens_id uuid not null references lenses(id) on delete cascade,
|
||||||
attr_filters jsonb not null default '[]'::jsonb,
|
name text not null,
|
||||||
label text,
|
label text,
|
||||||
field_type jsonb,
|
field_type jsonb not null,
|
||||||
visible boolean not null default true,
|
|
||||||
width_px int not null default 200
|
width_px int not null default 200
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,18 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use derive_builder::Builder;
|
||||||
use interim_pgtypes::pg_attribute::PgAttribute;
|
use interim_pgtypes::pg_attribute::PgAttribute;
|
||||||
use serde::{Deserialize, Serialize};
|
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 thiserror::Error;
|
||||||
use uuid::Uuid;
|
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)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub struct Field {
|
pub struct Field {
|
||||||
|
pub id: Uuid,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
pub field_type: sqlx::types::Json<FieldType>,
|
pub field_type: sqlx::types::Json<FieldType>,
|
||||||
|
|
@ -18,8 +20,13 @@ pub struct Field {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Field {
|
impl Field {
|
||||||
|
pub fn insertable_builder() -> InsertableFieldBuilder {
|
||||||
|
InsertableFieldBuilder::default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn default_from_attr(attr: &PgAttribute) -> Self {
|
pub fn default_from_attr(attr: &PgAttribute) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
id: Uuid::now_v7(),
|
||||||
name: attr.attname.clone(),
|
name: attr.attname.clone(),
|
||||||
label: None,
|
label: None,
|
||||||
field_type: sqlx::types::Json(FieldType::default_from_attr(attr)),
|
field_type: sqlx::types::Json(FieldType::default_from_attr(attr)),
|
||||||
|
|
@ -112,8 +119,73 @@ impl FieldType {
|
||||||
_ => Self::Unknown,
|
_ => 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
|
/// Error when parsing a sqlx value to JSON
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum ParseError {
|
pub enum ParseError {
|
||||||
|
|
@ -125,6 +197,9 @@ pub enum ParseError {
|
||||||
UnknownType,
|
UnknownType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -------- Encodable --------
|
||||||
|
// TODO this should probably be moved to another crate
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
||||||
#[serde(tag = "t", content = "c")]
|
#[serde(tag = "t", content = "c")]
|
||||||
pub enum Encodable {
|
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 sqlx::{PgExecutor, postgres::types::Oid, query_as};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::field::{Field, FieldType};
|
||||||
field::FieldType,
|
|
||||||
selection::{AttrFilter, Selection},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct Lens {
|
pub struct Lens {
|
||||||
|
|
@ -68,21 +65,20 @@ where base_id = $1 and class_oid = $2
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_selections<'a, E: PgExecutor<'a>>(
|
pub async fn fetch_fields<'a, E: PgExecutor<'a>>(
|
||||||
&self,
|
&self,
|
||||||
app_db: E,
|
app_db: E,
|
||||||
) -> Result<Vec<Selection>, sqlx::Error> {
|
) -> Result<Vec<Field>, sqlx::Error> {
|
||||||
query_as!(
|
query_as!(
|
||||||
Selection,
|
Field,
|
||||||
r#"
|
r#"
|
||||||
select
|
select
|
||||||
id,
|
id,
|
||||||
attr_filters as "attr_filters: sqlx::types::Json<Vec<AttrFilter>>",
|
name,
|
||||||
label,
|
label,
|
||||||
field_type as "field_type: sqlx::types::Json<FieldType>",
|
field_type as "field_type: sqlx::types::Json<FieldType>",
|
||||||
visible,
|
|
||||||
width_px
|
width_px
|
||||||
from lens_selections
|
from fields
|
||||||
where lens_id = $1
|
where lens_id = $1
|
||||||
"#,
|
"#,
|
||||||
self.id
|
self.id
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
pub mod field;
|
pub mod field;
|
||||||
pub mod lens;
|
pub mod lens;
|
||||||
pub mod selection;
|
// pub mod selection;
|
||||||
|
|
||||||
pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!();
|
pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!();
|
||||||
|
|
|
||||||
|
|
@ -4,3 +4,14 @@ pub mod pg_class;
|
||||||
pub mod pg_database;
|
pub mod pg_database;
|
||||||
pub mod pg_namespace;
|
pub mod pg_namespace;
|
||||||
pub mod pg_role;
|
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 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 {
|
pub struct PgClass {
|
||||||
/// Row identifier
|
/// Row identifier
|
||||||
|
|
@ -9,6 +9,8 @@ pub struct PgClass {
|
||||||
pub relname: String,
|
pub relname: String,
|
||||||
/// The OID of the namespace that contains this relation
|
/// The OID of the namespace that contains this relation
|
||||||
pub relnamespace: Oid,
|
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
|
/// 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,
|
pub reltype: Oid,
|
||||||
/// For typed tables, the OID of the underlying composite type; zero for all other relations
|
/// For typed tables, the OID of the underlying composite type; zero for all other relations
|
||||||
|
|
@ -50,6 +52,7 @@ select
|
||||||
oid,
|
oid,
|
||||||
relname,
|
relname,
|
||||||
relnamespace,
|
relnamespace,
|
||||||
|
relnamespace::regnamespace::text as "regnamespace!",
|
||||||
reltype,
|
reltype,
|
||||||
reloftype,
|
reloftype,
|
||||||
relowner,
|
relowner,
|
||||||
|
|
@ -89,6 +92,7 @@ select
|
||||||
oid,
|
oid,
|
||||||
relname,
|
relname,
|
||||||
relnamespace,
|
relnamespace,
|
||||||
|
relnamespace::regnamespace::text as "regnamespace!",
|
||||||
reltype,
|
reltype,
|
||||||
reloftype,
|
reloftype,
|
||||||
relowner,
|
relowner,
|
||||||
|
|
@ -123,6 +127,15 @@ where
|
||||||
// to the namespace that contains it. If not, that's an error.
|
// to the namespace that contains it. If not, that's an error.
|
||||||
.ok_or(sqlx::Error::RowNotFound)
|
.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 {
|
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;
|
pub(crate) use not_found;
|
||||||
|
|
||||||
/// Custom error type that maps to appropriate HTTP responses.
|
/// Custom error type that maps to appropriate HTTP responses.
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,5 @@
|
||||||
use sqlx::{query, PgConnection, Row as _};
|
use interim_pgtypes::escape_identifier;
|
||||||
|
use sqlx::{PgConnection, Row as _, 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('"', "\"\""))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn init_role(rolname: &str, client: &mut PgConnection) -> Result<(), sqlx::Error> {
|
pub async fn init_role(rolname: &str, client: &mut PgConnection) -> Result<(), sqlx::Error> {
|
||||||
let session_user = query!("select session_user;")
|
let session_user = query!("select session_user;")
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ mod cli;
|
||||||
mod db_conns;
|
mod db_conns;
|
||||||
mod lenses;
|
mod lenses;
|
||||||
mod middleware;
|
mod middleware;
|
||||||
|
mod navigator;
|
||||||
mod rel_invitations;
|
mod rel_invitations;
|
||||||
mod router;
|
mod router;
|
||||||
mod routes;
|
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",
|
"/d/{base_id}/r/{class_oid}/l/{lens_id}/update-lens",
|
||||||
post(routes::lenses::update_lens_page_post),
|
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(
|
.route(
|
||||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/add-selection",
|
"/d/{base_id}/r/{class_oid}/l/{lens_id}/add-column",
|
||||||
post(routes::lenses::add_selection_page_post),
|
post(routes::lenses::add_column_page_post),
|
||||||
)
|
)
|
||||||
.route(
|
.route(
|
||||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/update-value",
|
"/d/{base_id}/r/{class_oid}/l/{lens_id}/update-value",
|
||||||
|
|
|
||||||
|
|
@ -5,18 +5,14 @@ use axum::{
|
||||||
response::{Html, IntoResponse as _, Redirect, Response},
|
response::{Html, IntoResponse as _, Redirect, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
|
use interim_pgtypes::escape_identifier;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{query, query_scalar};
|
use sqlx::{query, query_scalar};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_error::AppError,
|
app_error::AppError, app_state::AppDbConn, base_pooler::BasePooler,
|
||||||
app_state::AppDbConn,
|
base_user_perms::sync_perms_for_base, bases::Base, db_conns::init_role, settings::Settings,
|
||||||
base_pooler::BasePooler,
|
|
||||||
base_user_perms::sync_perms_for_base,
|
|
||||||
bases::Base,
|
|
||||||
db_conns::{escape_identifier, init_role},
|
|
||||||
settings::Settings,
|
|
||||||
users::CurrentUser,
|
users::CurrentUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,15 @@ use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{Html, IntoResponse as _, Redirect, Response},
|
response::{Html, IntoResponse, Redirect, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use interim_models::{
|
use interim_models::{
|
||||||
field::{Encodable, Field},
|
field::{Encodable, Field, FieldType, InsertableFieldBuilder, RFC_3339_S},
|
||||||
lens::{Lens, LensDisplayType},
|
lens::{Lens, LensDisplayType},
|
||||||
selection::{AttrFilter, Selection},
|
|
||||||
};
|
};
|
||||||
use interim_pgtypes::{
|
use interim_pgtypes::{
|
||||||
|
escape_identifier,
|
||||||
pg_attribute::{PgAttribute, fetch_attributes_for_rel, fetch_primary_keys_for_rel},
|
pg_attribute::{PgAttribute, fetch_attributes_for_rel, fetch_primary_keys_for_rel},
|
||||||
pg_class::PgClass,
|
pg_class::PgClass,
|
||||||
};
|
};
|
||||||
|
|
@ -25,11 +25,12 @@ use sqlx::{
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_error::{AppError, not_found},
|
app_error::{AppError, bad_request, not_found},
|
||||||
app_state::AppDbConn,
|
app_state::AppDbConn,
|
||||||
base_pooler::BasePooler,
|
base_pooler::BasePooler,
|
||||||
bases::Base,
|
bases::Base,
|
||||||
db_conns::{escape_identifier, init_role},
|
db_conns::init_role,
|
||||||
|
navigator::Navigator,
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
users::CurrentUser,
|
users::CurrentUser,
|
||||||
};
|
};
|
||||||
|
|
@ -99,13 +100,19 @@ pub struct AddLensPagePostForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_lens_page_post(
|
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,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
|
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
|
||||||
Form(AddLensPagePostForm { name }): Form<AddLensPagePostForm>,
|
Form(AddLensPagePostForm { name }): Form<AddLensPagePostForm>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME auth
|
// FIXME auth
|
||||||
// FIXME csrf
|
// 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()
|
let lens = Lens::insertable_builder()
|
||||||
.base_id(base_id)
|
.base_id(base_id)
|
||||||
.class_oid(Oid(class_oid))
|
.class_oid(Oid(class_oid))
|
||||||
|
|
@ -114,12 +121,16 @@ pub async fn add_lens_page_post(
|
||||||
.build()?
|
.build()?
|
||||||
.insert(&mut *app_db)
|
.insert(&mut *app_db)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Redirect::to(&format!(
|
|
||||||
"{root_path}/d/{0}/r/{class_oid}/l/{1}",
|
for attr in attrs {
|
||||||
base_id.simple(),
|
InsertableFieldBuilder::default_from_attr(&attr)
|
||||||
lens.id.simple()
|
.lens_id(lens.id)
|
||||||
))
|
.build()?
|
||||||
.into_response())
|
.insert(&mut *app_db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(navigator.lens_page(&lens).redirect_to())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -134,27 +145,24 @@ pub async fn lens_page(
|
||||||
State(mut base_pooler): State<BasePooler>,
|
State(mut base_pooler): State<BasePooler>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(current_user): CurrentUser,
|
||||||
Path(LensPagePath {
|
Path(LensPagePath { lens_id, .. }): Path<LensPagePath>,
|
||||||
base_id,
|
|
||||||
class_oid,
|
|
||||||
lens_id,
|
|
||||||
}): Path<LensPagePath>,
|
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME auth
|
// 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?
|
.await?
|
||||||
.ok_or(AppError::NotFound("no base found with that id".to_owned()))?;
|
.ok_or(not_found!("lens not found"))?;
|
||||||
let mut client = base_pooler.acquire_for(base_id).await?;
|
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(
|
init_role(
|
||||||
&format!("{}{}", &base.user_role_prefix, ¤t_user.id.simple()),
|
&format!("{}{}", &base.user_role_prefix, ¤t_user.id.simple()),
|
||||||
&mut client,
|
&mut client,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// FIXME auth
|
let class = PgClass::fetch_by_oid(lens.class_oid, &mut *client)
|
||||||
|
|
||||||
let class = PgClass::fetch_by_oid(Oid(class_oid), &mut *client)
|
|
||||||
.await?
|
.await?
|
||||||
.ok_or(AppError::NotFound(
|
.ok_or(AppError::NotFound(
|
||||||
"no relation found with that oid".to_owned(),
|
"no relation found with that oid".to_owned(),
|
||||||
|
|
@ -165,15 +173,9 @@ pub async fn lens_page(
|
||||||
.await?
|
.await?
|
||||||
.ok_or(not_found!("no lens found with that id"))?;
|
.ok_or(not_found!("no lens found with that id"))?;
|
||||||
|
|
||||||
let attrs = fetch_attributes_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 selections = lens.fetch_selections(&mut *app_db).await?;
|
let pkey_attrs = fetch_primary_keys_for_rel(lens.class_oid, &mut *client).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?;
|
|
||||||
|
|
||||||
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
||||||
struct Row {
|
struct Row {
|
||||||
|
|
@ -214,7 +216,6 @@ pub async fn lens_page(
|
||||||
fields: Vec<Field>,
|
fields: Vec<Field>,
|
||||||
all_columns: Vec<PgAttribute>,
|
all_columns: Vec<PgAttribute>,
|
||||||
rows: Vec<Row>,
|
rows: Vec<Row>,
|
||||||
selections_json: String,
|
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
}
|
}
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
|
|
@ -222,7 +223,6 @@ pub async fn lens_page(
|
||||||
all_columns: attrs,
|
all_columns: attrs,
|
||||||
fields,
|
fields,
|
||||||
rows,
|
rows,
|
||||||
selections_json: serde_json::to_string(&selections)?,
|
|
||||||
settings,
|
settings,
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
|
|
@ -231,42 +231,129 @@ pub async fn lens_page(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct AddSelectionPageForm {
|
pub struct AddColumnPageForm {
|
||||||
column: String,
|
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(settings): State<Settings>,
|
||||||
|
State(mut base_pooler): State<BasePooler>,
|
||||||
|
navigator: Navigator,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(current_user): CurrentUser,
|
||||||
Path(LensPagePath {
|
Path(LensPagePath { lens_id, .. }): Path<LensPagePath>,
|
||||||
base_id,
|
Form(form): Form<AddColumnPageForm>,
|
||||||
class_oid,
|
|
||||||
lens_id,
|
|
||||||
}): Path<LensPagePath>,
|
|
||||||
Form(form): Form<AddSelectionPageForm>,
|
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
dbg!(&form);
|
|
||||||
// FIXME auth
|
// FIXME auth
|
||||||
// FIXME csrf
|
// FIXME csrf
|
||||||
|
// FIXME validate column name length is less than 64
|
||||||
|
|
||||||
let lens = Lens::fetch_by_id(lens_id, &mut *app_db)
|
let lens = Lens::fetch_by_id(lens_id, &mut *app_db)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(not_found!("lens not found"))?;
|
.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)
|
.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()?
|
.build()?
|
||||||
.insert(&mut *app_db)
|
.insert(&mut *app_db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Redirect::to(&format!(
|
Ok(navigator.lens_page(&lens).redirect_to())
|
||||||
"{0}/d/{base_id}/r/{class_oid}/l/{lens_id}/",
|
|
||||||
settings.root_path
|
|
||||||
))
|
|
||||||
.into_response())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #[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(
|
pub async fn update_lens_page_post(
|
||||||
State(settings): State<Settings>,
|
State(settings): State<Settings>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
|
|
|
||||||
|
|
@ -46,5 +46,4 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<lens-controls selections="{{ selections_json }}"></lens-controls>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue