refactor db clients
This commit is contained in:
parent
aa8bf34642
commit
53b4dfa130
49 changed files with 1241 additions and 1256 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
FROM lukemathwalker/cargo-chef:latest-rust-1.85.0 AS chef
|
FROM lukemathwalker/cargo-chef:latest-rust-1.87.0 AS chef
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
FROM chef AS planner
|
FROM chef AS planner
|
||||||
|
|
@ -18,5 +18,9 @@ FROM debian:bookworm-slim AS runtime
|
||||||
RUN apt-get update && apt-get install -y libpq-dev
|
RUN apt-get update && apt-get install -y libpq-dev
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=builder /app/target/release/interim /usr/local/bin
|
COPY --from=builder /app/target/release/interim /usr/local/bin
|
||||||
|
|
||||||
|
COPY ./css_dist ./css_dist
|
||||||
|
COPY ./js_dist ./js_dist
|
||||||
COPY ./static ./static
|
COPY ./static ./static
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/interim"]
|
ENTRYPOINT ["/usr/local/bin/interim"]
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use sqlx::{Encode, postgres::Postgres};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub enum Sqlizable {
|
|
||||||
Integer(i32),
|
|
||||||
Text(String),
|
|
||||||
Timestamptz(DateTime<Utc>),
|
|
||||||
Uuid(Uuid),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Encode<'a, Postgres> for Sqlizable {}
|
|
||||||
113
interim-models/src/base.rs
Normal file
113
interim-models/src/base.rs
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
use derive_builder::Builder;
|
||||||
|
use sqlx::query_as;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::client::AppDbClient;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Base {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub name: String,
|
||||||
|
pub url: String,
|
||||||
|
pub owner_id: Uuid,
|
||||||
|
pub user_role_prefix: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Base {
|
||||||
|
pub fn insertable_builder() -> InsertableBaseBuilder {
|
||||||
|
InsertableBaseBuilder::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_id(id: Uuid) -> WithIdQuery {
|
||||||
|
WithIdQuery { id }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_permission_in<I: IntoIterator<Item = &'static str>>(
|
||||||
|
perms: I,
|
||||||
|
) -> WithPermissionInQueryPartial {
|
||||||
|
let perms: Vec<String> = perms.into_iter().map(ToOwned::to_owned).collect();
|
||||||
|
WithPermissionInQueryPartial { perms }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WithPermissionInQueryPartial {
|
||||||
|
perms: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithPermissionInQueryPartial {
|
||||||
|
pub fn for_user(self, user_id: Uuid) -> WithPermissionInQuery {
|
||||||
|
WithPermissionInQuery {
|
||||||
|
perms: self.perms,
|
||||||
|
user_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WithPermissionInQuery {
|
||||||
|
perms: Vec<String>,
|
||||||
|
user_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithPermissionInQuery {
|
||||||
|
pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<Base>, sqlx::Error> {
|
||||||
|
query_as!(
|
||||||
|
Base,
|
||||||
|
"
|
||||||
|
select bases.*
|
||||||
|
from bases inner join base_user_perms as p
|
||||||
|
on p.base_id = bases.id
|
||||||
|
where p.user_id = $1 and perm = ANY($2)
|
||||||
|
",
|
||||||
|
self.user_id,
|
||||||
|
self.perms.as_slice(),
|
||||||
|
)
|
||||||
|
.fetch_all(&mut *app_db.conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WithIdQuery {
|
||||||
|
id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithIdQuery {
|
||||||
|
pub async fn fetch_optional(
|
||||||
|
self,
|
||||||
|
app_db: &mut AppDbClient,
|
||||||
|
) -> Result<Option<Base>, sqlx::Error> {
|
||||||
|
query_as!(Base, "select * from bases where id = $1", &self.id)
|
||||||
|
.fetch_optional(&mut *app_db.conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_one(self, app_db: &mut AppDbClient) -> Result<Base, sqlx::Error> {
|
||||||
|
query_as!(Base, "select * from bases where id = $1", &self.id)
|
||||||
|
.fetch_one(&mut *app_db.conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Builder)]
|
||||||
|
pub struct InsertableBase {
|
||||||
|
url: String,
|
||||||
|
owner_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InsertableBase {
|
||||||
|
pub async fn insert(self, app_db: &mut AppDbClient) -> Result<Base, sqlx::Error> {
|
||||||
|
query_as!(
|
||||||
|
Base,
|
||||||
|
"
|
||||||
|
insert into bases
|
||||||
|
(id, url, owner_id)
|
||||||
|
values ($1, $2, $3)
|
||||||
|
returning *
|
||||||
|
",
|
||||||
|
Uuid::now_v7(),
|
||||||
|
self.url,
|
||||||
|
self.owner_id
|
||||||
|
)
|
||||||
|
.fetch_one(&mut *app_db.conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
15
interim-models/src/client.rs
Normal file
15
interim-models/src/client.rs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
use sqlx::{PgConnection, Postgres, pool::PoolConnection};
|
||||||
|
|
||||||
|
pub struct AppDbClient {
|
||||||
|
pub(crate) conn: PoolConnection<Postgres>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppDbClient {
|
||||||
|
pub fn from_pool_conn(conn: PoolConnection<Postgres>) -> Self {
|
||||||
|
Self { conn }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_conn(&mut self) -> &mut PgConnection {
|
||||||
|
&mut self.conn
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,12 +2,12 @@ use chrono::{DateTime, Utc};
|
||||||
use derive_builder::Builder;
|
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::{
|
use sqlx::{Decode, Postgres, Row as _, TypeInfo as _, ValueRef as _, postgres::PgRow, query_as};
|
||||||
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;
|
||||||
|
|
||||||
|
use crate::client::AppDbClient;
|
||||||
|
|
||||||
pub const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S";
|
pub const RFC_3339_S: &str = "%Y-%m-%dT%H:%M:%S";
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
|
@ -49,27 +49,6 @@ impl Field {
|
||||||
vec![]
|
vec![]
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn render(&self, value: &Encodable) -> String {
|
|
||||||
// match (self.field_type.0.clone(), value) {
|
|
||||||
// (FieldType::Integer, Encodable::Integer(Some(value))) => value.to_string(),
|
|
||||||
// (FieldType::Integer, Encodable::Integer(None)) => "".to_owned(),
|
|
||||||
// (FieldType::Integer, _) => "###".to_owned(),
|
|
||||||
// (FieldType::InterimUser, Encodable::Text(value)) => todo!(),
|
|
||||||
// (FieldType::InterimUser, _) => "###".to_owned(),
|
|
||||||
// (FieldType::Text, Encodable::Text(Some(value))) => value.clone(),
|
|
||||||
// (FieldType::Text, Encodable::Text(None)) => "".to_owned(),
|
|
||||||
// (FieldType::Text, _) => "###".to_owned(),
|
|
||||||
// (FieldType::Timestamp { format }, Encodable::Timestamptz(value)) => value
|
|
||||||
// .map(|value| value.format(&format).to_string())
|
|
||||||
// .unwrap_or("".to_owned()),
|
|
||||||
// (FieldType::Timestamp { .. }, _) => "###".to_owned(),
|
|
||||||
// (FieldType::Uuid, Encodable::Uuid(Some(value))) => value.hyphenated().to_string(),
|
|
||||||
// (FieldType::Uuid, Encodable::Uuid(None)) => "".to_owned(),
|
|
||||||
// (FieldType::Uuid, _) => "###".to_owned(),
|
|
||||||
// (FieldType::Unknown, _) => "###".to_owned(),
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
pub fn get_value_encodable(&self, row: &PgRow) -> Result<Encodable, ParseError> {
|
pub fn get_value_encodable(&self, row: &PgRow) -> Result<Encodable, ParseError> {
|
||||||
let value_ref = row
|
let value_ref = row
|
||||||
.try_get_raw(self.name.as_str())
|
.try_get_raw(self.name.as_str())
|
||||||
|
|
@ -89,6 +68,36 @@ impl Field {
|
||||||
_ => return Err(ParseError::UnknownType),
|
_ => return Err(ParseError::UnknownType),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn belonging_to_lens(lens_id: Uuid) -> BelongingToLensQuery {
|
||||||
|
BelongingToLensQuery { lens_id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BelongingToLensQuery {
|
||||||
|
lens_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BelongingToLensQuery {
|
||||||
|
pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<Field>, sqlx::Error> {
|
||||||
|
query_as!(
|
||||||
|
Field,
|
||||||
|
r#"
|
||||||
|
select
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
field_type as "field_type: sqlx::types::Json<FieldType>",
|
||||||
|
width_px
|
||||||
|
from fields
|
||||||
|
where lens_id = $1
|
||||||
|
"#,
|
||||||
|
self.lens_id
|
||||||
|
)
|
||||||
|
.fetch_all(&mut *app_db.conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
|
@ -148,7 +157,7 @@ pub struct InsertableField {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InsertableField {
|
impl InsertableField {
|
||||||
pub async fn insert<'a, E: PgExecutor<'a>>(self, app_db: E) -> Result<Field, sqlx::Error> {
|
pub async fn insert(self, app_db: &mut AppDbClient) -> Result<Field, sqlx::Error> {
|
||||||
query_as!(
|
query_as!(
|
||||||
Field,
|
Field,
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -169,7 +178,7 @@ returning
|
||||||
sqlx::types::Json::<_>(self.field_type) as sqlx::types::Json<FieldType>,
|
sqlx::types::Json::<_>(self.field_type) as sqlx::types::Json<FieldType>,
|
||||||
self.width_px,
|
self.width_px,
|
||||||
)
|
)
|
||||||
.fetch_one(app_db)
|
.fetch_one(&mut *app_db.conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::{PgExecutor, postgres::types::Oid, query_as};
|
use sqlx::{postgres::types::Oid, query_as};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::field::{Field, FieldType};
|
use crate::client::AppDbClient;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
pub struct Lens {
|
pub struct Lens {
|
||||||
|
|
@ -19,12 +19,27 @@ impl Lens {
|
||||||
InsertableLensBuilder::default()
|
InsertableLensBuilder::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_by_id<'a, E: PgExecutor<'a>>(
|
pub fn with_id(id: Uuid) -> WithIdQuery {
|
||||||
id: Uuid,
|
WithIdQuery { id }
|
||||||
app_db: E,
|
}
|
||||||
) -> Result<Option<Self>, sqlx::Error> {
|
|
||||||
|
pub fn belonging_to_base(base_id: Uuid) -> BelongingToBaseQuery {
|
||||||
|
BelongingToBaseQuery { base_id }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WithIdQuery {
|
||||||
|
id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithIdQuery {
|
||||||
|
pub async fn fetch_optional(
|
||||||
|
self,
|
||||||
|
app_db: &mut AppDbClient,
|
||||||
|
) -> Result<Option<Lens>, sqlx::Error> {
|
||||||
query_as!(
|
query_as!(
|
||||||
Self,
|
Lens,
|
||||||
r#"
|
r#"
|
||||||
select
|
select
|
||||||
id,
|
id,
|
||||||
|
|
@ -35,19 +50,56 @@ select
|
||||||
from lenses
|
from lenses
|
||||||
where id = $1
|
where id = $1
|
||||||
"#,
|
"#,
|
||||||
id
|
self.id
|
||||||
)
|
)
|
||||||
.fetch_optional(app_db)
|
.fetch_optional(&mut *app_db.conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_by_rel<'a, E: PgExecutor<'a>>(
|
pub async fn fetch_one(self, app_db: &mut AppDbClient) -> Result<Lens, sqlx::Error> {
|
||||||
base_id: Uuid,
|
|
||||||
rel_oid: Oid,
|
|
||||||
app_db: E,
|
|
||||||
) -> Result<Vec<Self>, sqlx::Error> {
|
|
||||||
query_as!(
|
query_as!(
|
||||||
Self,
|
Lens,
|
||||||
|
r#"
|
||||||
|
select
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
base_id,
|
||||||
|
class_oid,
|
||||||
|
display_type as "display_type: LensDisplayType"
|
||||||
|
from lenses
|
||||||
|
where id = $1
|
||||||
|
"#,
|
||||||
|
self.id
|
||||||
|
)
|
||||||
|
.fetch_one(&mut *app_db.conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BelongingToBaseQuery {
|
||||||
|
base_id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BelongingToBaseQuery {
|
||||||
|
pub fn belonging_to_rel(self, rel_oid: Oid) -> BelongingToRelQuery {
|
||||||
|
BelongingToRelQuery {
|
||||||
|
base_id: self.base_id,
|
||||||
|
rel_oid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BelongingToRelQuery {
|
||||||
|
base_id: Uuid,
|
||||||
|
rel_oid: Oid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BelongingToRelQuery {
|
||||||
|
pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<Lens>, sqlx::Error> {
|
||||||
|
query_as!(
|
||||||
|
Lens,
|
||||||
r#"
|
r#"
|
||||||
select
|
select
|
||||||
id,
|
id,
|
||||||
|
|
@ -58,32 +110,10 @@ select
|
||||||
from lenses
|
from lenses
|
||||||
where base_id = $1 and class_oid = $2
|
where base_id = $1 and class_oid = $2
|
||||||
"#,
|
"#,
|
||||||
base_id,
|
self.base_id,
|
||||||
rel_oid
|
self.rel_oid
|
||||||
)
|
)
|
||||||
.fetch_all(app_db)
|
.fetch_all(&mut *app_db.conn)
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_fields<'a, E: PgExecutor<'a>>(
|
|
||||||
&self,
|
|
||||||
app_db: E,
|
|
||||||
) -> Result<Vec<Field>, sqlx::Error> {
|
|
||||||
query_as!(
|
|
||||||
Field,
|
|
||||||
r#"
|
|
||||||
select
|
|
||||||
id,
|
|
||||||
name,
|
|
||||||
label,
|
|
||||||
field_type as "field_type: sqlx::types::Json<FieldType>",
|
|
||||||
width_px
|
|
||||||
from fields
|
|
||||||
where lens_id = $1
|
|
||||||
"#,
|
|
||||||
self.id
|
|
||||||
)
|
|
||||||
.fetch_all(app_db)
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +133,7 @@ pub struct InsertableLens {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InsertableLens {
|
impl InsertableLens {
|
||||||
pub async fn insert<'a, E: PgExecutor<'a>>(self, app_db: E) -> Result<Lens, sqlx::Error> {
|
pub async fn insert(self, app_db: &mut AppDbClient) -> Result<Lens, sqlx::Error> {
|
||||||
query_as!(
|
query_as!(
|
||||||
Lens,
|
Lens,
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -123,7 +153,7 @@ returning
|
||||||
self.name,
|
self.name,
|
||||||
self.display_type as LensDisplayType
|
self.display_type as LensDisplayType
|
||||||
)
|
)
|
||||||
.fetch_one(app_db)
|
.fetch_one(&mut *app_db.conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
|
pub mod base;
|
||||||
|
pub mod client;
|
||||||
pub mod field;
|
pub mod field;
|
||||||
pub mod lens;
|
pub mod lens;
|
||||||
// pub mod selection;
|
pub mod rel_invitation;
|
||||||
|
pub mod user;
|
||||||
|
|
||||||
pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!();
|
pub static MIGRATOR: sqlx::migrate::Migrator = sqlx::migrate!();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use interim_pgtypes::pg_acl::PgPrivilegeType;
|
use interim_pgtypes::pg_acl::PgPrivilegeType;
|
||||||
use sqlx::{PgExecutor, postgres::types::Oid, query_as};
|
use sqlx::{postgres::types::Oid, query_as};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::client::AppDbClient;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RelInvitation {
|
pub struct RelInvitation {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|
@ -16,20 +18,8 @@ pub struct RelInvitation {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RelInvitation {
|
impl RelInvitation {
|
||||||
pub async fn fetch_by_class_oid<'a, E: PgExecutor<'a>>(
|
pub fn belonging_to_rel(rel_oid: Oid) -> BelongingToRelQuery {
|
||||||
oid: Oid,
|
BelongingToRelQuery { rel_oid }
|
||||||
app_db: E,
|
|
||||||
) -> Result<Vec<Self>, sqlx::Error> {
|
|
||||||
query_as!(
|
|
||||||
Self,
|
|
||||||
"
|
|
||||||
select * from rel_invitations
|
|
||||||
where class_oid = $1
|
|
||||||
",
|
|
||||||
oid
|
|
||||||
)
|
|
||||||
.fetch_all(app_db)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsertable() -> UpsertableRelInvitationBuilder {
|
pub fn upsertable() -> UpsertableRelInvitationBuilder {
|
||||||
|
|
@ -37,6 +27,29 @@ where class_oid = $1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct BelongingToRelQuery {
|
||||||
|
rel_oid: Oid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BelongingToRelQuery {
|
||||||
|
pub async fn fetch_all(
|
||||||
|
self,
|
||||||
|
app_db: &mut AppDbClient,
|
||||||
|
) -> Result<Vec<RelInvitation>, sqlx::Error> {
|
||||||
|
query_as!(
|
||||||
|
RelInvitation,
|
||||||
|
"
|
||||||
|
select * from rel_invitations
|
||||||
|
where class_oid = $1
|
||||||
|
",
|
||||||
|
self.rel_oid
|
||||||
|
)
|
||||||
|
.fetch_all(&mut *app_db.conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Builder, Clone, Debug)]
|
#[derive(Builder, Clone, Debug)]
|
||||||
pub struct UpsertableRelInvitation {
|
pub struct UpsertableRelInvitation {
|
||||||
email: String,
|
email: String,
|
||||||
|
|
@ -49,10 +62,7 @@ pub struct UpsertableRelInvitation {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertableRelInvitation {
|
impl UpsertableRelInvitation {
|
||||||
pub async fn upsert<'a, E: PgExecutor<'a>>(
|
pub async fn upsert(self, app_db: &mut AppDbClient) -> Result<RelInvitation, sqlx::Error> {
|
||||||
self,
|
|
||||||
app_db: E,
|
|
||||||
) -> Result<RelInvitation, sqlx::Error> {
|
|
||||||
query_as!(
|
query_as!(
|
||||||
RelInvitation,
|
RelInvitation,
|
||||||
"
|
"
|
||||||
|
|
@ -72,7 +82,7 @@ returning *
|
||||||
self.created_by,
|
self.created_by,
|
||||||
self.expires_at,
|
self.expires_at,
|
||||||
)
|
)
|
||||||
.fetch_one(app_db)
|
.fetch_one(&mut *app_db.conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
38
interim-models/src/user.rs
Normal file
38
interim-models/src/user.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
use sqlx::query_as;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::client::AppDbClient;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct User {
|
||||||
|
pub id: Uuid,
|
||||||
|
pub uid: String,
|
||||||
|
pub email: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl User {
|
||||||
|
pub fn with_id_in<I: IntoIterator<Item = Uuid>>(ids: I) -> WithIdInQuery {
|
||||||
|
let ids: Vec<Uuid> = ids.into_iter().collect();
|
||||||
|
WithIdInQuery { ids }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WithIdInQuery {
|
||||||
|
ids: Vec<Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithIdInQuery {
|
||||||
|
pub async fn fetch_all(self, app_db: &mut AppDbClient) -> Result<Vec<User>, sqlx::Error> {
|
||||||
|
query_as!(
|
||||||
|
User,
|
||||||
|
"
|
||||||
|
select * from users
|
||||||
|
where id = any($1)
|
||||||
|
",
|
||||||
|
self.ids.as_slice()
|
||||||
|
)
|
||||||
|
.fetch_all(&mut *app_db.conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
55
interim-pgtypes/src/client.rs
Normal file
55
interim-pgtypes/src/client.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
use sqlx::{PgConnection, Postgres, Row as _, pool::PoolConnection, query};
|
||||||
|
|
||||||
|
use crate::escape_identifier;
|
||||||
|
|
||||||
|
pub struct BaseClient {
|
||||||
|
pub(crate) conn: PoolConnection<Postgres>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseClient {
|
||||||
|
pub fn from_pool_conn(conn: PoolConnection<Postgres>) -> Self {
|
||||||
|
Self { conn }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_conn(&mut self) -> &mut PgConnection {
|
||||||
|
&mut self.conn
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs the Postgres `set role` command for the underlying connection. If
|
||||||
|
/// the given role does not exist, it is created and granted to the
|
||||||
|
/// session_user.
|
||||||
|
///
|
||||||
|
/// Note that while using `set role` simulates impersonation for most data
|
||||||
|
/// access and RLS purposes, it is both incomplete and easily reversible:
|
||||||
|
/// some commands and system tables will still behave according to the
|
||||||
|
/// privileges of the session user, and clients relying on this abstraction
|
||||||
|
/// should **NEVER** execute untrusted SQL.
|
||||||
|
pub async fn init_role(&mut self, rolname: &str) -> Result<(), sqlx::Error> {
|
||||||
|
let session_user = query!("select session_user;")
|
||||||
|
.fetch_one(&mut *self.conn)
|
||||||
|
.await?
|
||||||
|
.session_user
|
||||||
|
.unwrap();
|
||||||
|
if !query("select exists(select 1 from pg_roles where rolname = $1)")
|
||||||
|
.bind(rolname)
|
||||||
|
.fetch_one(&mut *self.conn)
|
||||||
|
.await?
|
||||||
|
.try_get(0)?
|
||||||
|
{
|
||||||
|
query(&format!("create role {}", escape_identifier(rolname)))
|
||||||
|
.execute(&mut *self.conn)
|
||||||
|
.await?;
|
||||||
|
query(&format!(
|
||||||
|
"grant {} to {}",
|
||||||
|
escape_identifier(rolname),
|
||||||
|
escape_identifier(&session_user),
|
||||||
|
))
|
||||||
|
.execute(&mut *self.conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
query(&format!("set role {}", escape_identifier(rolname)))
|
||||||
|
.execute(&mut *self.conn)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
pub mod client;
|
||||||
pub mod pg_acl;
|
pub mod pg_acl;
|
||||||
pub mod pg_attribute;
|
pub mod pg_attribute;
|
||||||
pub mod pg_class;
|
pub mod pg_class;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::{PgExecutor, postgres::types::Oid, query_as};
|
use sqlx::{postgres::types::Oid, query_as};
|
||||||
|
|
||||||
|
use crate::client::BaseClient;
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
pub struct PgAttribute {
|
pub struct PgAttribute {
|
||||||
|
|
@ -37,13 +39,26 @@ pub struct PgAttribute {
|
||||||
pub attfdwoptions: Option<Vec<String>>,
|
pub attfdwoptions: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_attributes_for_rel<'a, E: PgExecutor<'a>>(
|
impl PgAttribute {
|
||||||
oid: Oid,
|
pub fn all_for_rel(rel_oid: Oid) -> AllForRelQuery {
|
||||||
client: E,
|
AllForRelQuery { rel_oid }
|
||||||
) -> Result<Vec<PgAttribute>, sqlx::Error> {
|
}
|
||||||
query_as!(
|
|
||||||
PgAttribute,
|
pub fn pkeys_for_rel(rel_oid: Oid) -> PkeysForRelQuery {
|
||||||
r#"
|
PkeysForRelQuery { rel_oid }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AllForRelQuery {
|
||||||
|
rel_oid: Oid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AllForRelQuery {
|
||||||
|
pub async fn fetch_all(self, client: &mut BaseClient) -> Result<Vec<PgAttribute>, sqlx::Error> {
|
||||||
|
query_as!(
|
||||||
|
PgAttribute,
|
||||||
|
r#"
|
||||||
select
|
select
|
||||||
attrelid,
|
attrelid,
|
||||||
attname,
|
attname,
|
||||||
|
|
@ -63,19 +78,23 @@ select
|
||||||
from pg_attribute
|
from pg_attribute
|
||||||
where attrelid = $1 and attnum > 0 and not attisdropped
|
where attrelid = $1 and attnum > 0 and not attisdropped
|
||||||
"#,
|
"#,
|
||||||
&oid
|
&self.rel_oid
|
||||||
)
|
)
|
||||||
.fetch_all(client)
|
.fetch_all(&mut *client.conn)
|
||||||
.await
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_primary_keys_for_rel<'a, E: PgExecutor<'a>>(
|
#[derive(Clone, Debug)]
|
||||||
oid: Oid,
|
pub struct PkeysForRelQuery {
|
||||||
client: E,
|
rel_oid: Oid,
|
||||||
) -> Result<Vec<PgAttribute>, sqlx::Error> {
|
}
|
||||||
query_as!(
|
|
||||||
PgAttribute,
|
impl PkeysForRelQuery {
|
||||||
r#"
|
pub async fn fetch_all(self, client: &mut BaseClient) -> Result<Vec<PgAttribute>, sqlx::Error> {
|
||||||
|
query_as!(
|
||||||
|
PgAttribute,
|
||||||
|
r#"
|
||||||
select
|
select
|
||||||
a.attrelid as attrelid,
|
a.attrelid as attrelid,
|
||||||
a.attname as attname,
|
a.attname as attname,
|
||||||
|
|
@ -98,8 +117,9 @@ from pg_attribute a
|
||||||
and a.attnum = any(i.indkey)
|
and a.attnum = any(i.indkey)
|
||||||
where i.indrelid = $1 and i.indisprimary;
|
where i.indrelid = $1 and i.indisprimary;
|
||||||
"#,
|
"#,
|
||||||
&oid
|
&self.rel_oid
|
||||||
)
|
)
|
||||||
.fetch_all(client)
|
.fetch_all(&mut *client.conn)
|
||||||
.await
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use sqlx::{PgExecutor, postgres::types::Oid, query_as};
|
use sqlx::{postgres::types::Oid, query_as};
|
||||||
|
|
||||||
use crate::{escape_identifier, pg_acl::PgAclItem, pg_namespace::PgNamespace};
|
use crate::{client::BaseClient, escape_identifier, pg_acl::PgAclItem, pg_namespace::PgNamespace};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub struct PgClass {
|
pub struct PgClass {
|
||||||
/// Row identifier
|
/// Row identifier
|
||||||
pub oid: Oid,
|
pub oid: Oid,
|
||||||
|
|
@ -41,12 +42,46 @@ pub struct PgClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PgClass {
|
impl PgClass {
|
||||||
pub async fn fetch_by_oid<'a, E: PgExecutor<'a>>(
|
pub async fn fetch_namespace(
|
||||||
oid: Oid,
|
&self,
|
||||||
client: E,
|
client: &mut BaseClient,
|
||||||
) -> Result<Option<Self>, sqlx::Error> {
|
) -> Result<PgNamespace, sqlx::Error> {
|
||||||
|
PgNamespace::fetch_by_oid(self.relnamespace, &mut *client.conn)
|
||||||
|
.await?
|
||||||
|
// If client has access to the class, it would expect to have access
|
||||||
|
// 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 fn with_oid(oid: Oid) -> WithOidQuery {
|
||||||
|
WithOidQuery { oid }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_kind_in<I: IntoIterator<Item = PgRelKind>>(kinds: I) -> WithKindInQuery {
|
||||||
|
WithKindInQuery {
|
||||||
|
kinds: kinds.into_iter().collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WithOidQuery {
|
||||||
|
oid: Oid,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extracted as macro so that fetch_one() and fetch_optional() methods can
|
||||||
|
// reuse the same code.
|
||||||
|
macro_rules! with_oid_sqlx_query {
|
||||||
|
($value:expr) => {
|
||||||
query_as!(
|
query_as!(
|
||||||
Self,
|
PgClass,
|
||||||
r#"
|
r#"
|
||||||
select
|
select
|
||||||
oid,
|
oid,
|
||||||
|
|
@ -71,22 +106,41 @@ from pg_class
|
||||||
where
|
where
|
||||||
oid = $1
|
oid = $1
|
||||||
"#,
|
"#,
|
||||||
oid,
|
$value,
|
||||||
)
|
)
|
||||||
.fetch_optional(client)
|
};
|
||||||
.await
|
}
|
||||||
|
|
||||||
|
impl WithOidQuery {
|
||||||
|
pub async fn fetch_one(self, client: &mut BaseClient) -> Result<PgClass, sqlx::Error> {
|
||||||
|
with_oid_sqlx_query!(self.oid)
|
||||||
|
.fetch_one(&mut *client.conn)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_all_by_kind_any<'a, I: IntoIterator<Item = PgRelKind>, E: PgExecutor<'a>>(
|
pub async fn fetch_optional(
|
||||||
kinds: I,
|
self,
|
||||||
client: E,
|
client: &mut BaseClient,
|
||||||
) -> Result<Vec<Self>, sqlx::Error> {
|
) -> Result<Option<PgClass>, sqlx::Error> {
|
||||||
let kinds_i8 = kinds
|
with_oid_sqlx_query!(self.oid)
|
||||||
|
.fetch_optional(&mut *client.conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WithKindInQuery {
|
||||||
|
kinds: Vec<PgRelKind>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithKindInQuery {
|
||||||
|
pub async fn fetch_all(self, client: &mut BaseClient) -> Result<Vec<PgClass>, sqlx::Error> {
|
||||||
|
let kinds_i8: Vec<_> = self
|
||||||
|
.kinds
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|kind| kind.to_u8() as i8)
|
.map(|kind| kind.to_u8() as i8)
|
||||||
.collect::<Vec<i8>>();
|
.collect();
|
||||||
query_as!(
|
query_as!(
|
||||||
Self,
|
PgClass,
|
||||||
r#"
|
r#"
|
||||||
select
|
select
|
||||||
oid,
|
oid,
|
||||||
|
|
@ -113,29 +167,9 @@ where
|
||||||
"#,
|
"#,
|
||||||
kinds_i8.as_slice(),
|
kinds_i8.as_slice(),
|
||||||
)
|
)
|
||||||
.fetch_all(client)
|
.fetch_all(&mut *client.conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_namespace<'a, E: PgExecutor<'a>>(
|
|
||||||
&self,
|
|
||||||
client: E,
|
|
||||||
) -> Result<PgNamespace, sqlx::Error> {
|
|
||||||
PgNamespace::fetch_by_oid(self.relnamespace, client)
|
|
||||||
.await?
|
|
||||||
// If client has access to the class, it would expect to have access
|
|
||||||
// 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 {
|
pub enum PgRelKind {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use sqlx::{PgExecutor, postgres::types::Oid, query_as};
|
use sqlx::{postgres::types::Oid, query_as};
|
||||||
|
|
||||||
use crate::pg_acl::PgAclItem;
|
use crate::{client::BaseClient, pg_acl::PgAclItem};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PgDatabase {
|
pub struct PgDatabase {
|
||||||
|
|
@ -39,9 +39,16 @@ pub struct PgDatabase {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PgDatabase {
|
impl PgDatabase {
|
||||||
pub async fn fetch_current<'a, E: PgExecutor<'a>>(
|
pub fn current() -> CurrentQuery {
|
||||||
client: E,
|
CurrentQuery {}
|
||||||
) -> Result<PgDatabase, sqlx::Error> {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CurrentQuery {}
|
||||||
|
|
||||||
|
impl CurrentQuery {
|
||||||
|
pub async fn fetch_one(self, client: &mut BaseClient) -> Result<PgDatabase, sqlx::Error> {
|
||||||
query_as!(
|
query_as!(
|
||||||
PgDatabase,
|
PgDatabase,
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -66,7 +73,7 @@ from pg_database
|
||||||
where datname = current_database()
|
where datname = current_database()
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.fetch_one(client)
|
.fetch_one(&mut *client.conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use sqlx::{postgres::types::Oid, prelude::FromRow, query_as, PgExecutor};
|
use sqlx::{postgres::types::Oid, prelude::FromRow, query_as};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::client::BaseClient;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, FromRow, PartialEq)]
|
#[derive(Clone, Debug, Eq, Hash, FromRow, PartialEq)]
|
||||||
pub struct PgRole {
|
pub struct PgRole {
|
||||||
/// ID of role
|
/// ID of role
|
||||||
|
|
@ -30,10 +32,18 @@ pub struct PgRole {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PgRole {
|
impl PgRole {
|
||||||
pub async fn fetch_by_names_any<'a, E: PgExecutor<'a>>(
|
pub fn with_name_in(names: Vec<String>) -> WithNameInQuery {
|
||||||
names: Vec<String>,
|
WithNameInQuery { names }
|
||||||
client: E,
|
}
|
||||||
) -> Result<Vec<PgRole>, sqlx::Error> {
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct WithNameInQuery {
|
||||||
|
names: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WithNameInQuery {
|
||||||
|
pub async fn fetch_all(&self, client: &mut BaseClient) -> Result<Vec<PgRole>, sqlx::Error> {
|
||||||
query_as!(
|
query_as!(
|
||||||
PgRole,
|
PgRole,
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -50,9 +60,9 @@ select
|
||||||
rolvaliduntil,
|
rolvaliduntil,
|
||||||
rolbypassrls as "rolbypassrls!"
|
rolbypassrls as "rolbypassrls!"
|
||||||
from pg_roles where rolname = any($1)"#,
|
from pg_roles where rolname = any($1)"#,
|
||||||
names.as_slice()
|
self.names.as_slice()
|
||||||
)
|
)
|
||||||
.fetch_all(client)
|
.fetch_all(&mut *client.conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -64,7 +74,7 @@ pub struct RoleTree {
|
||||||
pub inherit: bool,
|
pub inherit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, FromRow)]
|
#[derive(Clone, Debug, FromRow)]
|
||||||
struct RoleTreeRow {
|
struct RoleTreeRow {
|
||||||
#[sqlx(flatten)]
|
#[sqlx(flatten)]
|
||||||
role: PgRole,
|
role: PgRole,
|
||||||
|
|
@ -73,9 +83,37 @@ struct RoleTreeRow {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RoleTree {
|
impl RoleTree {
|
||||||
pub async fn fetch_members<'a, E: PgExecutor<'a>>(
|
pub fn members_of(role_oid: Oid) -> MembersOfQuery {
|
||||||
role_oid: Oid,
|
MembersOfQuery { role_oid }
|
||||||
client: E,
|
}
|
||||||
|
|
||||||
|
pub fn granted_to(role_oid: Oid) -> GrantedToQuery {
|
||||||
|
GrantedToQuery { role_oid }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flatten_inherited(&self) -> Vec<&PgRole> {
|
||||||
|
[
|
||||||
|
vec![&self.role],
|
||||||
|
self.branches
|
||||||
|
.iter()
|
||||||
|
.filter(|member| member.inherit)
|
||||||
|
.map(|member| member.flatten_inherited())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.concat(),
|
||||||
|
]
|
||||||
|
.concat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MembersOfQuery {
|
||||||
|
role_oid: Oid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MembersOfQuery {
|
||||||
|
pub async fn fetch_tree(
|
||||||
|
self,
|
||||||
|
client: &mut BaseClient,
|
||||||
) -> Result<Option<RoleTree>, sqlx::Error> {
|
) -> Result<Option<RoleTree>, sqlx::Error> {
|
||||||
let rows: Vec<RoleTreeRow> = query_as(
|
let rows: Vec<RoleTreeRow> = query_as(
|
||||||
"
|
"
|
||||||
|
|
@ -95,8 +133,8 @@ from (
|
||||||
join pg_roles on pg_roles.oid = subquery.roleid
|
join pg_roles on pg_roles.oid = subquery.roleid
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.bind(role_oid)
|
.bind(self.role_oid)
|
||||||
.fetch_all(client)
|
.fetch_all(&mut *client.conn)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -107,10 +145,17 @@ from (
|
||||||
inherit: root_row.inherit,
|
inherit: root_row.inherit,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn fetch_granted<'a, E: PgExecutor<'a>>(
|
#[derive(Clone, Debug)]
|
||||||
role_oid: Oid,
|
pub struct GrantedToQuery {
|
||||||
client: E,
|
role_oid: Oid,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GrantedToQuery {
|
||||||
|
pub async fn fetch_tree(
|
||||||
|
self,
|
||||||
|
client: &mut BaseClient,
|
||||||
) -> Result<Option<RoleTree>, sqlx::Error> {
|
) -> Result<Option<RoleTree>, sqlx::Error> {
|
||||||
let rows: Vec<RoleTreeRow> = query_as(
|
let rows: Vec<RoleTreeRow> = query_as(
|
||||||
"
|
"
|
||||||
|
|
@ -130,8 +175,8 @@ from (
|
||||||
join pg_roles on pg_roles.oid = subquery.roleid
|
join pg_roles on pg_roles.oid = subquery.roleid
|
||||||
",
|
",
|
||||||
)
|
)
|
||||||
.bind(role_oid)
|
.bind(self.role_oid)
|
||||||
.fetch_all(client)
|
.fetch_all(&mut *client.conn)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(rows
|
Ok(rows
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -142,19 +187,6 @@ from (
|
||||||
inherit: root_row.inherit,
|
inherit: root_row.inherit,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn flatten_inherited(&self) -> Vec<&PgRole> {
|
|
||||||
[
|
|
||||||
vec![&self.role],
|
|
||||||
self.branches
|
|
||||||
.iter()
|
|
||||||
.filter(|member| member.inherit)
|
|
||||||
.map(|member| member.flatten_inherited())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.concat(),
|
|
||||||
]
|
|
||||||
.concat()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compute_members(rows: &Vec<RoleTreeRow>, root: Oid) -> Vec<RoleTree> {
|
fn compute_members(rows: &Vec<RoleTreeRow>, root: Oid) -> Vec<RoleTree> {
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
|
|
||||||
|
|
@ -5,8 +5,9 @@ use axum::{
|
||||||
extract::{FromRef, FromRequestParts},
|
extract::{FromRef, FromRequestParts},
|
||||||
http::request::Parts,
|
http::request::Parts,
|
||||||
};
|
};
|
||||||
|
use interim_models::client::AppDbClient;
|
||||||
use oauth2::basic::BasicClient;
|
use oauth2::basic::BasicClient;
|
||||||
use sqlx::{pool::PoolConnection, postgres::PgPoolOptions, Postgres};
|
use sqlx::postgres::PgPoolOptions;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_error::AppError, auth, base_pooler::BasePooler, sessions::PgStore, settings::Settings,
|
app_error::AppError, auth, base_pooler::BasePooler, sessions::PgStore, settings::Settings,
|
||||||
|
|
@ -64,7 +65,7 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extractor to automatically obtain a Deadpool Diesel connection
|
/// Extractor to automatically obtain a Deadpool Diesel connection
|
||||||
pub struct AppDbConn(pub PoolConnection<Postgres>);
|
pub struct AppDbConn(pub AppDbClient);
|
||||||
|
|
||||||
impl<S> FromRequestParts<S> for AppDbConn
|
impl<S> FromRequestParts<S> for AppDbConn
|
||||||
where
|
where
|
||||||
|
|
@ -77,6 +78,6 @@ where
|
||||||
.app_db
|
.app_db
|
||||||
.acquire()
|
.acquire()
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Self(conn))
|
Ok(Self(AppDbClient::from_pool_conn(conn)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
use std::{collections::HashMap, sync::Arc, time::Duration};
|
use std::{collections::HashMap, sync::Arc, time::Duration};
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::Result;
|
||||||
use axum::extract::FromRef;
|
use axum::extract::FromRef;
|
||||||
use sqlx::{pool::PoolConnection, postgres::PgPoolOptions, raw_sql, Executor, PgPool, Postgres};
|
use interim_models::{base::Base, client::AppDbClient};
|
||||||
|
use interim_pgtypes::client::BaseClient;
|
||||||
|
use sqlx::{Executor, PgPool, postgres::PgPoolOptions, raw_sql};
|
||||||
use tokio::sync::{OnceCell, RwLock};
|
use tokio::sync::{OnceCell, RwLock};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{app_state::AppState, bases::Base};
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
const MAX_CONNECTIONS: u32 = 4;
|
const MAX_CONNECTIONS: u32 = 4;
|
||||||
const IDLE_SECONDS: u64 = 3600;
|
const IDLE_SECONDS: u64 = 3600;
|
||||||
|
|
||||||
// NOTE: The Arc<RwLock> this uses will probably need to be cleaned up for
|
// TODO: The Arc<RwLock> this uses will probably need to be cleaned up for
|
||||||
// performance eventually.
|
// performance eventually.
|
||||||
|
|
||||||
/// A collection of multiple SQLx Pools.
|
/// A collection of multiple SQLx Pools.
|
||||||
|
|
@ -31,9 +33,8 @@ impl BasePooler {
|
||||||
|
|
||||||
async fn get_pool_for(&mut self, base_id: Uuid) -> Result<PgPool> {
|
async fn get_pool_for(&mut self, base_id: Uuid) -> Result<PgPool> {
|
||||||
let init_cell = || async {
|
let init_cell = || async {
|
||||||
let base = Base::fetch_by_id(base_id, &self.app_db)
|
let mut app_db = AppDbClient::from_pool_conn(self.app_db.acquire().await?);
|
||||||
.await?
|
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||||
.context("no such base")?;
|
|
||||||
Ok(PgPoolOptions::new()
|
Ok(PgPoolOptions::new()
|
||||||
.min_connections(0)
|
.min_connections(0)
|
||||||
.max_connections(MAX_CONNECTIONS)
|
.max_connections(MAX_CONNECTIONS)
|
||||||
|
|
@ -78,9 +79,29 @@ discard sequences;
|
||||||
.clone())
|
.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn acquire_for(&mut self, base_id: Uuid) -> Result<PoolConnection<Postgres>> {
|
/// Note that while using `set role` simulates impersonation for most data
|
||||||
|
/// access and RLS purposes, it is both incomplete and easily reversible:
|
||||||
|
/// some commands and system tables will still behave according to the
|
||||||
|
/// privileges of the session user, and clients relying on this abstraction
|
||||||
|
/// should **NEVER** execute untrusted SQL.
|
||||||
|
pub async fn acquire_for(
|
||||||
|
&mut self,
|
||||||
|
base_id: Uuid,
|
||||||
|
set_role: RoleAssignment,
|
||||||
|
) -> Result<BaseClient> {
|
||||||
|
let mut app_db = AppDbClient::from_pool_conn(self.app_db.acquire().await?);
|
||||||
let pool = self.get_pool_for(base_id).await?;
|
let pool = self.get_pool_for(base_id).await?;
|
||||||
Ok(pool.acquire().await?)
|
let mut client = BaseClient::from_pool_conn(pool.acquire().await?);
|
||||||
|
match set_role {
|
||||||
|
RoleAssignment::User(id) => {
|
||||||
|
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||||
|
let prefix = base.user_role_prefix;
|
||||||
|
let user_id = id.simple();
|
||||||
|
client.init_role(&format!("{prefix}{user_id}")).await?;
|
||||||
|
}
|
||||||
|
RoleAssignment::Root => {}
|
||||||
|
}
|
||||||
|
Ok(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn close_for(&mut self, base_id: Uuid) -> Result<()> {
|
pub async fn close_for(&mut self, base_id: Uuid) -> Result<()> {
|
||||||
|
|
@ -109,3 +130,8 @@ where
|
||||||
Into::<AppState>::into(state.clone()).base_pooler.clone()
|
Into::<AppState>::into(state.clone()).base_pooler.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum RoleAssignment {
|
||||||
|
Root,
|
||||||
|
User(Uuid),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::Result;
|
||||||
|
use interim_models::{base::Base, client::AppDbClient};
|
||||||
use interim_pgtypes::{
|
use interim_pgtypes::{
|
||||||
|
client::BaseClient,
|
||||||
pg_acl::PgPrivilegeType,
|
pg_acl::PgPrivilegeType,
|
||||||
pg_database::PgDatabase,
|
pg_database::PgDatabase,
|
||||||
pg_role::{PgRole, RoleTree, user_id_from_rolname},
|
pg_role::{PgRole, RoleTree, user_id_from_rolname},
|
||||||
};
|
};
|
||||||
use sqlx::{PgConnection, query};
|
use sqlx::query;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::bases::Base;
|
|
||||||
|
|
||||||
pub struct BaseUserPerm {
|
pub struct BaseUserPerm {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub base_id: Uuid,
|
pub base_id: Uuid,
|
||||||
|
|
@ -20,13 +20,13 @@ pub struct BaseUserPerm {
|
||||||
|
|
||||||
pub async fn sync_perms_for_base(
|
pub async fn sync_perms_for_base(
|
||||||
base_id: Uuid,
|
base_id: Uuid,
|
||||||
app_db: &mut PgConnection,
|
app_db: &mut AppDbClient,
|
||||||
client: &mut PgConnection,
|
base_client: &mut BaseClient,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let db = PgDatabase::fetch_current(&mut *client).await?;
|
let db = PgDatabase::current().fetch_one(base_client).await?;
|
||||||
let explicit_roles = PgRole::fetch_by_names_any(
|
let explicit_roles = PgRole::with_name_in(
|
||||||
db.datacl
|
db.datacl
|
||||||
.unwrap_or(vec![])
|
.unwrap_or_default()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|item| {
|
.filter(|item| {
|
||||||
item.privileges
|
item.privileges
|
||||||
|
|
@ -35,20 +35,21 @@ pub async fn sync_perms_for_base(
|
||||||
})
|
})
|
||||||
.map(|item| item.grantee)
|
.map(|item| item.grantee)
|
||||||
.collect(),
|
.collect(),
|
||||||
&mut *client,
|
|
||||||
)
|
)
|
||||||
|
.fetch_all(base_client)
|
||||||
.await?;
|
.await?;
|
||||||
let mut all_roles: HashSet<PgRole> = HashSet::new();
|
let mut all_roles: HashSet<PgRole> = HashSet::new();
|
||||||
for explicit_role in explicit_roles {
|
for explicit_role in explicit_roles {
|
||||||
if let Some(role_tree) = RoleTree::fetch_members(explicit_role.oid, &mut *client).await? {
|
if let Some(role_tree) = RoleTree::members_of(explicit_role.oid)
|
||||||
|
.fetch_tree(base_client)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
for implicit_role in role_tree.flatten_inherited() {
|
for implicit_role in role_tree.flatten_inherited() {
|
||||||
all_roles.insert(implicit_role.clone());
|
all_roles.insert(implicit_role.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let base = Base::fetch_by_id(base_id, &mut *app_db)
|
let base = Base::with_id(base_id).fetch_one(app_db).await?;
|
||||||
.await?
|
|
||||||
.context("base with that id not found")?;
|
|
||||||
let user_ids: Vec<Uuid> = all_roles
|
let user_ids: Vec<Uuid> = all_roles
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|role| user_id_from_rolname(&role.rolname, &base.user_role_prefix).ok())
|
.filter_map(|role| user_id_from_rolname(&role.rolname, &base.user_role_prefix).ok())
|
||||||
|
|
@ -58,7 +59,7 @@ pub async fn sync_perms_for_base(
|
||||||
base_id,
|
base_id,
|
||||||
user_ids.as_slice(),
|
user_ids.as_slice(),
|
||||||
)
|
)
|
||||||
.execute(&mut *app_db)
|
.execute(app_db.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
for user_id in user_ids {
|
for user_id in user_ids {
|
||||||
query!(
|
query!(
|
||||||
|
|
@ -72,7 +73,7 @@ on conflict (base_id, user_id, perm) do nothing
|
||||||
base.id,
|
base.id,
|
||||||
user_id
|
user_id
|
||||||
)
|
)
|
||||||
.execute(&mut *app_db)
|
.execute(app_db.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
use derive_builder::Builder;
|
|
||||||
use sqlx::{query_as, PgExecutor};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
pub struct Base {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub name: String,
|
|
||||||
pub url: String,
|
|
||||||
pub owner_id: Uuid,
|
|
||||||
pub user_role_prefix: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Base {
|
|
||||||
pub fn insertable_builder() -> InsertableBaseBuilder {
|
|
||||||
InsertableBaseBuilder::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_by_id<'a, E: PgExecutor<'a>>(
|
|
||||||
id: Uuid,
|
|
||||||
app_db: E,
|
|
||||||
) -> Result<Option<Base>, sqlx::Error> {
|
|
||||||
query_as!(Self, "select * from bases where id = $1", &id)
|
|
||||||
.fetch_optional(app_db)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn fetch_by_perm_any<'a, E: PgExecutor<'a>>(
|
|
||||||
user_id: Uuid,
|
|
||||||
perms: Vec<&str>,
|
|
||||||
app_db: E,
|
|
||||||
) -> Result<Vec<Base>, sqlx::Error> {
|
|
||||||
let perms = perms
|
|
||||||
.into_iter()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.collect::<Vec<String>>();
|
|
||||||
query_as!(
|
|
||||||
Self,
|
|
||||||
"
|
|
||||||
select bases.*
|
|
||||||
from bases inner join base_user_perms as p
|
|
||||||
on p.base_id = bases.id
|
|
||||||
where p.user_id = $1 and perm = ANY($2)
|
|
||||||
",
|
|
||||||
user_id,
|
|
||||||
perms.as_slice(),
|
|
||||||
)
|
|
||||||
.fetch_all(app_db)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Builder)]
|
|
||||||
pub struct InsertableBase {
|
|
||||||
url: String,
|
|
||||||
owner_id: Uuid,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InsertableBase {
|
|
||||||
pub async fn insert<'a, E: PgExecutor<'a>>(self, app_db: E) -> Result<Base, sqlx::Error> {
|
|
||||||
query_as!(
|
|
||||||
Base,
|
|
||||||
"
|
|
||||||
insert into bases
|
|
||||||
(id, url, owner_id)
|
|
||||||
values ($1, $2, $3)
|
|
||||||
returning *
|
|
||||||
",
|
|
||||||
Uuid::now_v7(),
|
|
||||||
self.url,
|
|
||||||
self.owner_id
|
|
||||||
)
|
|
||||||
.fetch_one(app_db)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use interim_models::selection::SelectionDisplayType;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{
|
|
||||||
ColumnIndex, Decode, Encode, Postgres, Row as _, TypeInfo as _, ValueRef as _,
|
|
||||||
error::BoxDynError,
|
|
||||||
postgres::{PgRow, PgTypeInfo, PgValueRef},
|
|
||||||
};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
const DEFAULT_TIMESTAMP_FORMAT: &str = "%Y-%m-%dT%H:%M:%S%.f%:z";
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
#[serde(tag = "t", content = "c")]
|
|
||||||
pub enum Value {
|
|
||||||
Text(Option<String>),
|
|
||||||
Integer(Option<i32>),
|
|
||||||
Timestamptz(Option<DateTime<Utc>>),
|
|
||||||
Uuid(Option<Uuid>),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait ToHtmlString {
|
|
||||||
fn to_html_string(&self, display_type: &Option<SelectionDisplayType>) -> String;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO rewrite with thiserror
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct FromSqlError {
|
|
||||||
message: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for FromSqlError {}
|
|
||||||
|
|
||||||
impl Display for FromSqlError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromSqlError {
|
|
||||||
fn new(message: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
message: message.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Value {
|
|
||||||
pub fn get_from_row<I: ColumnIndex<PgRow> + Display>(
|
|
||||||
row: &PgRow,
|
|
||||||
idx: I,
|
|
||||||
) -> Result<Self, BoxDynError> {
|
|
||||||
let value_ref = row.try_get_raw(idx)?;
|
|
||||||
Self::decode(value_ref)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn webc_tag(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Text(_) => "cell-text",
|
|
||||||
Self::Integer(_) => todo!(),
|
|
||||||
Self::Timestamptz(_) => todo!(),
|
|
||||||
Self::Uuid(_) => "cell-uuid",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_json(&self) -> Result<String, serde_json::Error> {
|
|
||||||
match self {
|
|
||||||
Self::Text(value) => serde_json::to_string(&value),
|
|
||||||
Self::Integer(value) => serde_json::to_string(&value),
|
|
||||||
Self::Timestamptz(value) => serde_json::to_string(&value),
|
|
||||||
Self::Uuid(value) => serde_json::to_string(&value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToHtmlString for Value {
|
|
||||||
fn to_html_string(&self, display_type: &Option<SelectionDisplayType>) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Text(Some(value)) => value.clone(),
|
|
||||||
Self::Integer(Some(value)) => format!("{value}"),
|
|
||||||
Self::Timestamptz(_) => todo!(),
|
|
||||||
Self::Uuid(Some(value)) => value.to_string(),
|
|
||||||
_ => "-".to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl sqlx::Type<Postgres> for Value {
|
|
||||||
fn type_info() -> <Postgres as sqlx::Database>::TypeInfo {
|
|
||||||
PgTypeInfo::with_name("XXX");
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Decode<'a, Postgres> for Value {
|
|
||||||
fn decode(value: PgValueRef<'a>) -> Result<Self, BoxDynError> {
|
|
||||||
let type_info = value.type_info();
|
|
||||||
let ty = type_info.name();
|
|
||||||
match ty {
|
|
||||||
"INT" | "INT4" => Ok(Self::Integer(if value.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(<i32 as Decode<Postgres>>::decode(value)?)
|
|
||||||
})),
|
|
||||||
"TEXT" | "VARCHAR" => Ok(Self::Text(if value.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(<String as Decode<Postgres>>::decode(value)?)
|
|
||||||
})),
|
|
||||||
"TIMESTAMPTZ" => Ok(Self::Timestamptz(if value.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(<DateTime<Utc> as Decode<Postgres>>::decode(value)?)
|
|
||||||
})),
|
|
||||||
"UUID" => Ok(Self::Uuid(if value.is_null() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(<Uuid as Decode<Postgres>>::decode(value)?)
|
|
||||||
})),
|
|
||||||
_ => Err(Box::new(FromSqlError::new(
|
|
||||||
"unsupported pg type for interim Value",
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Encode<'a, Postgres> for Value {
|
|
||||||
fn encode_by_ref(
|
|
||||||
&self,
|
|
||||||
buf: &mut <Postgres as sqlx::Database>::ArgumentBuffer<'a>,
|
|
||||||
) -> std::result::Result<sqlx::encode::IsNull, BoxDynError> {
|
|
||||||
match self {
|
|
||||||
Self::Text(value) => Encode::<'a, Postgres>::encode_by_ref(&value, buf),
|
|
||||||
Self::Integer(value) => Encode::<'a, Postgres>::encode_by_ref(&value, buf),
|
|
||||||
Self::Timestamptz(value) => value.encode_by_ref(buf),
|
|
||||||
Self::Uuid(value) => value.encode_by_ref(buf),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
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;")
|
|
||||||
.fetch_one(&mut *client)
|
|
||||||
.await?
|
|
||||||
.session_user
|
|
||||||
.unwrap();
|
|
||||||
if !query("select exists(select 1 from pg_roles where rolname = $1)")
|
|
||||||
.bind(rolname)
|
|
||||||
.fetch_one(&mut *client)
|
|
||||||
.await?
|
|
||||||
.try_get(0)?
|
|
||||||
{
|
|
||||||
query(&format!("create role {}", escape_identifier(rolname)))
|
|
||||||
.execute(&mut *client)
|
|
||||||
.await?;
|
|
||||||
query(&format!(
|
|
||||||
"grant {} to {}",
|
|
||||||
escape_identifier(rolname),
|
|
||||||
escape_identifier(&session_user),
|
|
||||||
))
|
|
||||||
.execute(&mut *client)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
query(&format!("set role {}", escape_identifier(rolname)))
|
|
||||||
.execute(&mut *client)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use diesel::{
|
|
||||||
pg::{Pg,
|
|
||||||
row::{NamedRow, Row},
|
|
||||||
QueryableByName,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Internally a HashMap mapping field names to a custom sum type capable of
|
|
||||||
/// deserializing common SQL types. This allows Diesel to load rows without a
|
|
||||||
/// hard-coded structure.
|
|
||||||
pub struct FlexiRow {
|
|
||||||
internal: HashMap<String, FlexiField>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sum type representing a range of SQL data types.
|
|
||||||
pub enum FlexiField {
|
|
||||||
Text(String),
|
|
||||||
Int(i32),
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QueryableByName<Pg> for FlexiRow {
|
|
||||||
fn build<'a>(row: &impl NamedRow<'a, Pg>) -> diesel::deserialize::Result<Self> {
|
|
||||||
let mut hm: HashMap<String, FlexiField> = HashMap::new();
|
|
||||||
for i in 0..row.field_count() {
|
|
||||||
if let Some(field) = diesel::row::Row::<'a, Pg>::get(&row, i) {
|
|
||||||
let name = field.field_name().or("Unnamed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
diesel::deserialize::Result::Ok(FlexiRow { internal: hm })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use sqlx::{postgres::types::Oid, query, query_as, PgConnection, Row as _};
|
|
||||||
|
|
||||||
use crate::abstract_::escape_identifier;
|
|
||||||
|
|
||||||
pub struct PgRole {
|
|
||||||
oid: Option<Oid>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct DbSession {
|
|
||||||
conn: PgConnection,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DbSession {
|
|
||||||
pub async fn set_role(&mut self, rolname: &str) -> Result<()> {
|
|
||||||
if !query("select exists(select 1 from pg_roles where rolname = $1)")
|
|
||||||
.bind(&rolname)
|
|
||||||
.fetch_one(&mut self.conn)
|
|
||||||
.await?
|
|
||||||
.try_get(0)?
|
|
||||||
{
|
|
||||||
query(&format!("create role {}", escape_identifier(&rolname)))
|
|
||||||
.execute(&mut self.conn)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
query(&format!("set role {}", escape_identifier(&rolname)))
|
|
||||||
.execute(&mut self.conn)
|
|
||||||
.await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_users_with_db_connect(
|
|
||||||
&mut self,
|
|
||||||
user_role_prefix: &str,
|
|
||||||
) -> Result<Vec<PgRole>> {
|
|
||||||
Ok(query_as!(
|
|
||||||
PgRole,
|
|
||||||
"
|
|
||||||
select oid
|
|
||||||
from pg_roles
|
|
||||||
where has_database_privilege(rolname, current_database(), 'connect')
|
|
||||||
and starts_with(rolname, $1)
|
|
||||||
",
|
|
||||||
&user_role_prefix,
|
|
||||||
)
|
|
||||||
.fetch_all(&mut self.conn)
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
use derive_builder::Builder;
|
|
||||||
use interim_pgtypes::pg_attribute::PgAttribute;
|
|
||||||
use regex::Regex;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{PgExecutor, postgres::types::Oid, query_as};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub struct Selection {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub attr_filters: sqlx::types::Json<Vec<AttrFilter>>,
|
|
||||||
pub label: Option<String>,
|
|
||||||
pub display_type: Option<SelectionDisplayType>,
|
|
||||||
pub visible: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Selection {
|
|
||||||
pub fn resolve_fields_from_attrs(&self, all_attrs: &[PgAttribute]) -> Vec<Field> {
|
|
||||||
if self.visible {
|
|
||||||
let mut filtered_attrs = all_attrs.to_owned();
|
|
||||||
for attr_filter in self.attr_filters.0.clone() {
|
|
||||||
filtered_attrs.retain(|attr| attr_filter.matches(attr));
|
|
||||||
}
|
|
||||||
filtered_attrs
|
|
||||||
.into_iter()
|
|
||||||
.map(|attr| Field {
|
|
||||||
name: attr.attname.clone(),
|
|
||||||
label: self.label.clone(),
|
|
||||||
display_type: self.display_type.clone(),
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
vec![]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, sqlx::Type)]
|
|
||||||
#[sqlx(rename_all = "lowercase")]
|
|
||||||
pub enum SelectionDisplayType {
|
|
||||||
Text,
|
|
||||||
InterimUser,
|
|
||||||
Timestamp,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
||||||
pub enum AttrFilter {
|
|
||||||
NameEq(String),
|
|
||||||
NameMatches(String),
|
|
||||||
TypeEq(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AttrFilter {
|
|
||||||
pub fn matches(&self, attr: &PgAttribute) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::NameEq(name) => &attr.attname == name,
|
|
||||||
Self::NameMatches(pattern) => Regex::new(pattern)
|
|
||||||
.map(|re| re.is_match(&attr.attname))
|
|
||||||
.unwrap_or(false),
|
|
||||||
Self::TypeEq(_) => todo!("attr type filter is not yet implemented"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A single column which can be passed to a front-end viewer. A Selection may
|
|
||||||
/// resolve to zero or more Fields.
|
|
||||||
#[derive(Clone, Debug, Serialize)]
|
|
||||||
pub struct Field {
|
|
||||||
pub name: String,
|
|
||||||
pub label: Option<String>,
|
|
||||||
pub display_type: Option<SelectionDisplayType>,
|
|
||||||
}
|
|
||||||
|
|
@ -14,18 +14,15 @@ mod app_state;
|
||||||
mod auth;
|
mod auth;
|
||||||
mod base_pooler;
|
mod base_pooler;
|
||||||
mod base_user_perms;
|
mod base_user_perms;
|
||||||
mod bases;
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod db_conns;
|
|
||||||
mod lenses;
|
|
||||||
mod middleware;
|
mod middleware;
|
||||||
|
mod navbar;
|
||||||
mod navigator;
|
mod navigator;
|
||||||
mod rel_invitations;
|
|
||||||
mod router;
|
mod router;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod sessions;
|
mod sessions;
|
||||||
mod settings;
|
mod settings;
|
||||||
mod users;
|
mod user;
|
||||||
mod worker;
|
mod worker;
|
||||||
|
|
||||||
/// Run CLI
|
/// Run CLI
|
||||||
|
|
|
||||||
|
|
@ -1,236 +0,0 @@
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use axum::extract::FromRef;
|
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
|
||||||
|
|
||||||
pub const NAVBAR_ITEM_TEAMS: &str = "teams";
|
|
||||||
pub const NAVBAR_ITEM_PROJECTS: &str = "projects";
|
|
||||||
pub const NAVBAR_ITEM_CHANNELS: &str = "channels";
|
|
||||||
pub const NAVBAR_ITEM_TEAM_MEMBERS: &str = "team-members";
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct BreadcrumbTrail {
|
|
||||||
base_path: String,
|
|
||||||
breadcrumbs: Vec<Breadcrumb>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BreadcrumbTrail {
|
|
||||||
/// Initialize with a non-empty base path.
|
|
||||||
pub fn from_base_path(base_path: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
base_path: base_path.to_owned(),
|
|
||||||
breadcrumbs: Vec::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append an i18n path segment to the base path.
|
|
||||||
pub fn with_i18n_slug(mut self, language_code: &str) -> Self {
|
|
||||||
self.base_path.push('/');
|
|
||||||
self.base_path.push_str(language_code);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a breadcrumb by name and slug. If other breadcrumbs have already
|
|
||||||
/// been added, href will be generated by appending it to the previous href
|
|
||||||
/// as "<previous>/<slug>". Otherwise, it will be appended to the base path
|
|
||||||
/// with i18n slug (if any).
|
|
||||||
pub fn push_slug(mut self, label: &str, slug: &str) -> Self {
|
|
||||||
let href = if let Some(prev_breadcrumb) = self.iter().last() {
|
|
||||||
format!(
|
|
||||||
"{}/{}",
|
|
||||||
prev_breadcrumb.href,
|
|
||||||
percent_encoding::percent_encode(
|
|
||||||
slug.as_bytes(),
|
|
||||||
percent_encoding::NON_ALPHANUMERIC
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("{}/{}", self.base_path, slug)
|
|
||||||
};
|
|
||||||
self.breadcrumbs.push(Breadcrumb {
|
|
||||||
label: label.to_owned(),
|
|
||||||
href,
|
|
||||||
});
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn iter(&self) -> std::slice::Iter<'_, Breadcrumb> {
|
|
||||||
self.breadcrumbs.iter()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get an absolute URI path, starting from the child of the last
|
|
||||||
/// breadcrumb. For example, if the last breadcrumb has an href of
|
|
||||||
/// "/en/teams/team123" and the relative path is "../team456", the result
|
|
||||||
/// will be "/en/teams/team456". If no breadcrumbs exist, the base path
|
|
||||||
/// with i18n slug (if any) will be used.
|
|
||||||
pub fn join(&self, rel_path: &str) -> String {
|
|
||||||
let base = if let Some(breadcrumb) = self.iter().last() {
|
|
||||||
&breadcrumb.href
|
|
||||||
} else {
|
|
||||||
&self.base_path
|
|
||||||
};
|
|
||||||
let mut path_buf: Vec<&str> = base.split('/').collect();
|
|
||||||
for rel_segment in rel_path.split('/') {
|
|
||||||
if rel_segment == "." {
|
|
||||||
continue;
|
|
||||||
} else if rel_segment == ".." {
|
|
||||||
path_buf.pop();
|
|
||||||
} else {
|
|
||||||
path_buf.push(rel_segment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
path_buf.join("/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl IntoIterator for BreadcrumbTrail {
|
|
||||||
type Item = Breadcrumb;
|
|
||||||
type IntoIter = std::vec::IntoIter<Breadcrumb>;
|
|
||||||
|
|
||||||
fn into_iter(self) -> Self::IntoIter {
|
|
||||||
self.breadcrumbs.into_iter()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Breadcrumb {
|
|
||||||
pub href: String,
|
|
||||||
pub label: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct NavbarBuilder {
|
|
||||||
base_path: String,
|
|
||||||
items: Vec<NavbarItem>,
|
|
||||||
active_item: Option<String>,
|
|
||||||
params: HashMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NavbarBuilder {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
base_path: "".to_owned(),
|
|
||||||
items: Vec::new(),
|
|
||||||
active_item: None,
|
|
||||||
params: HashMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_base_path(mut self, base_path: &str) -> Self {
|
|
||||||
self.base_path = base_path.to_owned();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add a navbar item. Subpath is a path relative to the base path, and it
|
|
||||||
/// may contain placeholders for path params, such as "/{lang}/teams".
|
|
||||||
/// The navbar item will only be displayed if all corresponding path params
|
|
||||||
/// are registered using .with_param().
|
|
||||||
pub fn push_item(mut self, id: &str, label: &str, subpath: &str) -> Self {
|
|
||||||
self.items.push(NavbarItem {
|
|
||||||
id: id.to_owned(),
|
|
||||||
href: subpath.to_owned(),
|
|
||||||
label: label.to_owned(),
|
|
||||||
});
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Registers a path param with the navbar builder.
|
|
||||||
pub fn with_param(mut self, k: &str, v: &str) -> Self {
|
|
||||||
self.params.insert(k.to_owned(), v.to_owned());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// If a visible navbar item matches the provided ID, it will render as
|
|
||||||
/// active. Calling this method overrides any previously specified value.
|
|
||||||
pub fn with_active_item(mut self, item_id: &str) -> Self {
|
|
||||||
self.active_item = Some(item_id.to_owned());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(self) -> Navbar {
|
|
||||||
let mut built_items: Vec<NavbarItem> = Vec::with_capacity(self.items.len());
|
|
||||||
for item in self.items {
|
|
||||||
let path_segments = item.href.split('/');
|
|
||||||
let substituted_segments: Vec<Option<&str>> = path_segments
|
|
||||||
.map(|segment| {
|
|
||||||
if segment.starts_with("{") && segment.ends_with("}") {
|
|
||||||
let param_k = segment[1..segment.len() - 1].trim();
|
|
||||||
self.params.get(param_k).map(|v| v.as_str())
|
|
||||||
} else {
|
|
||||||
Some(segment)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if substituted_segments.iter().all(|segment| segment.is_some()) {
|
|
||||||
built_items.push(NavbarItem {
|
|
||||||
id: item.id,
|
|
||||||
href: format!(
|
|
||||||
"{}{}",
|
|
||||||
self.base_path,
|
|
||||||
substituted_segments
|
|
||||||
.into_iter()
|
|
||||||
.map(|segment| {
|
|
||||||
segment.expect(
|
|
||||||
"should already have checked that all path segments are Some",
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("/")
|
|
||||||
),
|
|
||||||
label: item.label,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Navbar {
|
|
||||||
active_item: self.active_item,
|
|
||||||
items: built_items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for NavbarBuilder {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
.push_item(NAVBAR_ITEM_TEAMS, "Teams", "/en/teams")
|
|
||||||
.push_item(
|
|
||||||
NAVBAR_ITEM_PROJECTS,
|
|
||||||
"Projects",
|
|
||||||
"/en/teams/{team_id}/projects",
|
|
||||||
)
|
|
||||||
.push_item(
|
|
||||||
NAVBAR_ITEM_CHANNELS,
|
|
||||||
"Channels",
|
|
||||||
"/en/teams/{team_id}/channels",
|
|
||||||
)
|
|
||||||
.push_item(
|
|
||||||
NAVBAR_ITEM_TEAM_MEMBERS,
|
|
||||||
"Team Members",
|
|
||||||
"/en/teams/{team_id}/members",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> FromRef<S> for NavbarBuilder
|
|
||||||
where
|
|
||||||
S: Into<AppState> + Clone,
|
|
||||||
{
|
|
||||||
fn from_ref(state: &S) -> Self {
|
|
||||||
Into::<AppState>::into(state.clone())
|
|
||||||
.navbar_template
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Navbar {
|
|
||||||
pub items: Vec<NavbarItem>,
|
|
||||||
pub active_item: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct NavbarItem {
|
|
||||||
pub href: String,
|
|
||||||
pub id: String,
|
|
||||||
pub label: String,
|
|
||||||
}
|
|
||||||
108
interim-server/src/navbar.rs
Normal file
108
interim-server/src/navbar.rs
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use askama::Template;
|
||||||
|
use derive_builder::Builder;
|
||||||
|
use interim_models::{base::Base, client::AppDbClient, lens::Lens};
|
||||||
|
use interim_pgtypes::{
|
||||||
|
client::BaseClient,
|
||||||
|
pg_class::{PgClass, PgRelKind},
|
||||||
|
};
|
||||||
|
use sqlx::postgres::types::Oid;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Builder, Clone, Template)]
|
||||||
|
#[template(path = "navbar.html")]
|
||||||
|
pub struct Navbar {
|
||||||
|
pub base: Base,
|
||||||
|
pub namespaces: Vec<NamespaceItem>,
|
||||||
|
#[builder(setter(strip_option))]
|
||||||
|
pub current: Option<NavLocation>,
|
||||||
|
pub root_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Navbar {
|
||||||
|
pub fn builder() -> NavbarBuilder {
|
||||||
|
NavbarBuilder::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct NamespaceItem {
|
||||||
|
pub name: String,
|
||||||
|
pub rels: Vec<RelItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct RelItem {
|
||||||
|
pub name: String,
|
||||||
|
pub class_oid: Oid,
|
||||||
|
pub lenses: Vec<LensItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct LensItem {
|
||||||
|
pub name: String,
|
||||||
|
pub id: Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum NavLocation {
|
||||||
|
Rel(Oid, Option<RelLocation>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum RelLocation {
|
||||||
|
Lens(Uuid),
|
||||||
|
Rbac,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NavbarBuilder {
|
||||||
|
/// Helper function to populate relations and lenses automatically.
|
||||||
|
pub async fn populate_rels(
|
||||||
|
&mut self,
|
||||||
|
app_db: &mut AppDbClient,
|
||||||
|
base_client: &mut BaseClient,
|
||||||
|
) -> Result<&mut Self> {
|
||||||
|
let rels = PgClass::with_kind_in([PgRelKind::OrdinaryTable])
|
||||||
|
.fetch_all(base_client)
|
||||||
|
.await?;
|
||||||
|
let mut namespaces: HashMap<String, Vec<RelItem>> = HashMap::new();
|
||||||
|
for rel in rels {
|
||||||
|
if rel.regnamespace.as_str() != "pg_catalog"
|
||||||
|
&& rel.regnamespace.as_str() != "information_schema"
|
||||||
|
{
|
||||||
|
let lenses = Lens::belonging_to_base(
|
||||||
|
self.base
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(NavbarBuilderError::UninitializedField("base"))?
|
||||||
|
.id,
|
||||||
|
)
|
||||||
|
.belonging_to_rel(rel.oid)
|
||||||
|
.fetch_all(app_db)
|
||||||
|
.await?;
|
||||||
|
let rel_items = namespaces.entry(rel.regnamespace).or_default();
|
||||||
|
rel_items.push(RelItem {
|
||||||
|
name: rel.relname,
|
||||||
|
class_oid: rel.oid,
|
||||||
|
lenses: lenses
|
||||||
|
.into_iter()
|
||||||
|
.map(|lens| LensItem {
|
||||||
|
name: lens.name,
|
||||||
|
id: lens.id,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(self.namespaces(
|
||||||
|
namespaces
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, rel_items)| NamespaceItem {
|
||||||
|
name,
|
||||||
|
rels: rel_items,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -65,10 +65,6 @@ pub fn new_router(state: AppState) -> Router<()> {
|
||||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/",
|
"/d/{base_id}/r/{class_oid}/l/{lens_id}/",
|
||||||
get(routes::lenses::lens_page),
|
get(routes::lenses::lens_page),
|
||||||
)
|
)
|
||||||
.route(
|
|
||||||
"/d/{base_id}/r/{class_oid}/l/{lens_id}/update-lens",
|
|
||||||
post(routes::lenses::update_lens_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-selection",
|
||||||
// post(routes::lenses::add_selection_page_post),
|
// post(routes::lenses::add_selection_page_post),
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,19 @@ 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_models::base::Base;
|
||||||
use interim_pgtypes::escape_identifier;
|
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_state::AppDbConn, base_pooler::BasePooler,
|
app_error::AppError,
|
||||||
base_user_perms::sync_perms_for_base, bases::Base, db_conns::init_role, settings::Settings,
|
app_state::AppDbConn,
|
||||||
users::CurrentUser,
|
base_pooler::{self, BasePooler},
|
||||||
|
base_user_perms::sync_perms_for_base,
|
||||||
|
settings::Settings,
|
||||||
|
user::CurrentUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn list_bases_page(
|
pub async fn list_bases_page(
|
||||||
|
|
@ -21,9 +25,10 @@ pub async fn list_bases_page(
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(current_user): CurrentUser,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
let bases =
|
let bases = Base::with_permission_in(["configure", "connect"])
|
||||||
Base::fetch_by_perm_any(current_user.id, vec!["configure", "connect"], &mut *app_db)
|
.for_user(current_user.id)
|
||||||
.await?;
|
.fetch_all(&mut app_db)
|
||||||
|
.await?;
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "list_bases.html")]
|
#[template(path = "list_bases.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
|
@ -43,7 +48,7 @@ pub async fn add_base_page(
|
||||||
.url("".to_owned())
|
.url("".to_owned())
|
||||||
.owner_id(current_user.id)
|
.owner_id(current_user.id)
|
||||||
.build()?
|
.build()?
|
||||||
.insert(&mut *app_db)
|
.insert(&mut app_db)
|
||||||
.await?;
|
.await?;
|
||||||
query!(
|
query!(
|
||||||
"
|
"
|
||||||
|
|
@ -54,7 +59,7 @@ values ($1, $2, $3, 'configure')",
|
||||||
base.id,
|
base.id,
|
||||||
current_user.id
|
current_user.id
|
||||||
)
|
)
|
||||||
.execute(&mut *app_db)
|
.execute(app_db.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Redirect::to(&format!("{}/d/{}/config", settings.root_path, base.id)).into_response())
|
Ok(Redirect::to(&format!("{}/d/{}/config", settings.root_path, base.id)).into_response())
|
||||||
}
|
}
|
||||||
|
|
@ -67,13 +72,11 @@ pub struct BaseConfigPagePath {
|
||||||
pub async fn base_config_page_get(
|
pub async fn base_config_page_get(
|
||||||
State(settings): State<Settings>,
|
State(settings): State<Settings>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(_current_user): CurrentUser,
|
||||||
Path(params): Path<BaseConfigPagePath>,
|
Path(params): Path<BaseConfigPagePath>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME: auth
|
// FIXME: auth
|
||||||
let base = Base::fetch_by_id(params.base_id, &mut *app_db)
|
let base = Base::with_id(params.base_id).fetch_one(&mut app_db).await?;
|
||||||
.await?
|
|
||||||
.ok_or(AppError::NotFound("no base found with that id".to_owned()))?;
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "base_config.html")]
|
#[template(path = "base_config.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
|
@ -99,39 +102,38 @@ pub async fn base_config_page_post(
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME: CSRF
|
// FIXME: CSRF
|
||||||
// FIXME: auth
|
// FIXME: auth
|
||||||
let base = Base::fetch_by_id(base_id, &mut *app_db)
|
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||||
.await?
|
|
||||||
.ok_or(AppError::NotFound("no base found with that id".to_owned()))?;
|
|
||||||
query!(
|
query!(
|
||||||
"update bases set name = $1, url = $2 where id = $3",
|
"update bases set name = $1, url = $2 where id = $3",
|
||||||
&form.name,
|
&form.name,
|
||||||
&form.url,
|
&form.url,
|
||||||
&base_id
|
&base_id
|
||||||
)
|
)
|
||||||
.execute(&mut *app_db)
|
.execute(app_db.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
if form.url != base.url {
|
if form.url != base.url {
|
||||||
base_pooler.close_for(base_id).await?;
|
base_pooler.close_for(base_id).await?;
|
||||||
let mut client = base_pooler.acquire_for(base.id).await?;
|
|
||||||
let rolname = format!("{}{}", base.user_role_prefix, current_user.id.simple());
|
let rolname = format!("{}{}", base.user_role_prefix, current_user.id.simple());
|
||||||
// Bootstrap user role with database connect privilege. If the user was
|
// Bootstrap user role with database connect privilege. If the user was
|
||||||
// able to successfully authenticate a connection string, it should be
|
// able to successfully authenticate a connection string, it should be
|
||||||
// safe to say that they should be allowed to connect as an Interim
|
// safe to say that they should be allowed to connect as an Interim
|
||||||
// user.
|
// user.
|
||||||
init_role(&rolname, &mut client).await?;
|
let mut root_client = base_pooler
|
||||||
|
.acquire_for(base.id, base_pooler::RoleAssignment::Root)
|
||||||
|
.await?;
|
||||||
let db_name: String = query_scalar!("select current_database()")
|
let db_name: String = query_scalar!("select current_database()")
|
||||||
.fetch_one(&mut *client)
|
.fetch_one(root_client.get_conn())
|
||||||
.await?
|
.await?
|
||||||
.context("unable to select current_database()")?;
|
.context("unable to select current_database()")?;
|
||||||
query!("reset role").execute(&mut *client).await?;
|
|
||||||
query(&format!(
|
query(&format!(
|
||||||
"grant connect on database {} to {}",
|
"grant connect on database {} to {}",
|
||||||
escape_identifier(&db_name),
|
escape_identifier(&db_name),
|
||||||
escape_identifier(&rolname)
|
escape_identifier(&rolname)
|
||||||
))
|
))
|
||||||
.execute(&mut *client)
|
.execute(root_client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
sync_perms_for_base(base.id, &mut app_db, &mut client).await?;
|
sync_perms_for_base(base.id, &mut app_db, &mut root_client).await?;
|
||||||
}
|
}
|
||||||
Ok(Redirect::to(&format!("{}/d/{}/config", settings.root_path, base_id)).into_response())
|
Ok(Redirect::to(&format!("{}/d/{}/config", settings.root_path, base_id)).into_response())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,15 @@ use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
Json,
|
Json,
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{Html, IntoResponse, Redirect, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use interim_models::{
|
use interim_models::{
|
||||||
|
base::Base,
|
||||||
field::{Encodable, Field, FieldType, InsertableFieldBuilder, RFC_3339_S},
|
field::{Encodable, Field, FieldType, InsertableFieldBuilder, RFC_3339_S},
|
||||||
lens::{Lens, LensDisplayType},
|
lens::{Lens, LensDisplayType},
|
||||||
};
|
};
|
||||||
use interim_pgtypes::{
|
use interim_pgtypes::{escape_identifier, pg_attribute::PgAttribute, pg_class::PgClass};
|
||||||
escape_identifier,
|
|
||||||
pg_attribute::{PgAttribute, fetch_attributes_for_rel, fetch_primary_keys_for_rel},
|
|
||||||
pg_class::PgClass,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
|
|
@ -25,14 +22,13 @@ use sqlx::{
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_error::{AppError, bad_request, not_found},
|
app_error::{AppError, bad_request},
|
||||||
app_state::AppDbConn,
|
app_state::AppDbConn,
|
||||||
base_pooler::BasePooler,
|
base_pooler::{BasePooler, RoleAssignment},
|
||||||
bases::Base,
|
navbar::{NavLocation, Navbar, RelLocation},
|
||||||
db_conns::init_role,
|
|
||||||
navigator::Navigator,
|
navigator::Navigator,
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
users::CurrentUser,
|
user::CurrentUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -47,7 +43,10 @@ pub async fn lenses_page(
|
||||||
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
|
Path(LensesPagePath { base_id, class_oid }): Path<LensesPagePath>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME auth
|
// FIXME auth
|
||||||
let lenses = Lens::fetch_by_rel(base_id, Oid(class_oid), &mut *app_db).await?;
|
let lenses = Lens::belonging_to_base(base_id)
|
||||||
|
.belonging_to_rel(Oid(class_oid))
|
||||||
|
.fetch_all(&mut app_db)
|
||||||
|
.await?;
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "lenses.html")]
|
#[template(path = "lenses.html")]
|
||||||
struct ResponseTemplate {
|
struct ResponseTemplate {
|
||||||
|
|
@ -100,18 +99,24 @@ pub struct AddLensPagePostForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_lens_page_post(
|
pub async fn add_lens_page_post(
|
||||||
|
State(settings): State<Settings>,
|
||||||
State(mut base_pooler): State<BasePooler>,
|
State(mut base_pooler): State<BasePooler>,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
|
CurrentUser(current_user): CurrentUser,
|
||||||
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 mut client = base_pooler
|
||||||
|
.acquire_for(base_id, RoleAssignment::User(current_user.id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
let attrs = fetch_attributes_for_rel(Oid(class_oid), &mut *client).await?;
|
let attrs = PgAttribute::all_for_rel(Oid(class_oid))
|
||||||
|
.fetch_all(&mut client)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let lens = Lens::insertable_builder()
|
let lens = Lens::insertable_builder()
|
||||||
.base_id(base_id)
|
.base_id(base_id)
|
||||||
|
|
@ -119,14 +124,14 @@ pub async fn add_lens_page_post(
|
||||||
.name(name)
|
.name(name)
|
||||||
.display_type(LensDisplayType::Table)
|
.display_type(LensDisplayType::Table)
|
||||||
.build()?
|
.build()?
|
||||||
.insert(&mut *app_db)
|
.insert(&mut app_db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for attr in attrs {
|
for attr in attrs {
|
||||||
InsertableFieldBuilder::default_from_attr(&attr)
|
InsertableFieldBuilder::default_from_attr(&attr)
|
||||||
.lens_id(lens.id)
|
.lens_id(lens.id)
|
||||||
.build()?
|
.build()?
|
||||||
.insert(&mut *app_db)
|
.insert(&mut app_db)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,37 +150,32 @@ 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 { lens_id, .. }): Path<LensPagePath>,
|
Path(LensPagePath {
|
||||||
|
lens_id,
|
||||||
|
base_id,
|
||||||
|
class_oid,
|
||||||
|
}): Path<LensPagePath>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME auth
|
// FIXME auth
|
||||||
let lens = Lens::fetch_by_id(lens_id, &mut *app_db)
|
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||||
.await?
|
let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).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?;
|
let mut base_client = base_pooler
|
||||||
init_role(
|
.acquire_for(lens.base_id, RoleAssignment::User(current_user.id))
|
||||||
&format!("{}{}", &base.user_role_prefix, ¤t_user.id.simple()),
|
.await?;
|
||||||
&mut client,
|
let rel = PgClass::with_oid(lens.class_oid)
|
||||||
)
|
.fetch_one(&mut base_client)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let class = PgClass::fetch_by_oid(lens.class_oid, &mut *client)
|
let attrs = PgAttribute::all_for_rel(lens.class_oid)
|
||||||
.await?
|
.fetch_all(&mut base_client)
|
||||||
.ok_or(AppError::NotFound(
|
.await?;
|
||||||
"no relation found with that oid".to_owned(),
|
let fields = Field::belonging_to_lens(lens.id)
|
||||||
))?;
|
.fetch_all(&mut app_db)
|
||||||
let namespace = class.fetch_namespace(&mut *client).await?;
|
.await?;
|
||||||
|
let pkey_attrs = PgAttribute::pkeys_for_rel(lens.class_oid)
|
||||||
let lens = Lens::fetch_by_id(lens_id, &mut *app_db)
|
.fetch_all(&mut base_client)
|
||||||
.await?
|
.await?;
|
||||||
.ok_or(not_found!("no lens found with that id"))?;
|
|
||||||
|
|
||||||
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;
|
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
||||||
let rows: Vec<PgRow> = query(&format!(
|
let rows: Vec<PgRow> = query(&format!(
|
||||||
|
|
@ -186,11 +186,11 @@ pub async fn lens_page(
|
||||||
.map(|attr| escape_identifier(&attr.attname))
|
.map(|attr| escape_identifier(&attr.attname))
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(", "),
|
.join(", "),
|
||||||
escape_identifier(&namespace.nspname),
|
escape_identifier(&rel.regnamespace),
|
||||||
escape_identifier(&class.relname),
|
escape_identifier(&rel.relname),
|
||||||
))
|
))
|
||||||
.bind(FRONTEND_ROW_LIMIT)
|
.bind(FRONTEND_ROW_LIMIT)
|
||||||
.fetch_all(&mut *client)
|
.fetch_all(base_client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
let pkeys: Vec<HashMap<String, Encodable>> = rows
|
let pkeys: Vec<HashMap<String, Encodable>> = rows
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -212,6 +212,7 @@ pub async fn lens_page(
|
||||||
rows: Vec<PgRow>,
|
rows: Vec<PgRow>,
|
||||||
pkeys: Vec<HashMap<String, Encodable>>,
|
pkeys: Vec<HashMap<String, Encodable>>,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
|
navbar: Navbar,
|
||||||
}
|
}
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ResponseTemplate {
|
ResponseTemplate {
|
||||||
|
|
@ -219,6 +220,16 @@ pub async fn lens_page(
|
||||||
fields,
|
fields,
|
||||||
pkeys,
|
pkeys,
|
||||||
rows,
|
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,
|
settings,
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
|
|
@ -250,7 +261,6 @@ fn try_field_type_from_form(form: &AddColumnPageForm) -> Result<FieldType, AppEr
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_column_page_post(
|
pub async fn add_column_page_post(
|
||||||
State(settings): State<Settings>,
|
|
||||||
State(mut base_pooler): State<BasePooler>,
|
State(mut base_pooler): State<BasePooler>,
|
||||||
navigator: Navigator,
|
navigator: Navigator,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
AppDbConn(mut app_db): AppDbConn,
|
||||||
|
|
@ -262,23 +272,16 @@ pub async fn add_column_page_post(
|
||||||
// FIXME csrf
|
// FIXME csrf
|
||||||
// FIXME validate column name length is less than 64
|
// FIXME validate column name length is less than 64
|
||||||
|
|
||||||
let lens = Lens::fetch_by_id(lens_id, &mut *app_db)
|
let lens = Lens::with_id(lens_id).fetch_one(&mut app_db).await?;
|
||||||
.await?
|
let base = Base::with_id(lens.base_id).fetch_one(&mut app_db).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(base.id).await?;
|
let mut base_client = base_pooler
|
||||||
init_role(
|
.acquire_for(base.id, RoleAssignment::User(current_user.id))
|
||||||
&format!("{}{}", &base.user_role_prefix, ¤t_user.id.simple()),
|
.await?;
|
||||||
&mut client,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
let class = PgClass::fetch_by_oid(lens.class_oid, &mut *client)
|
let class = PgClass::with_oid(lens.class_oid)
|
||||||
.await?
|
.fetch_one(&mut base_client)
|
||||||
.ok_or(not_found!("pg class not found"))?;
|
.await?;
|
||||||
|
|
||||||
let field_type = try_field_type_from_form(&form)?;
|
let field_type = try_field_type_from_form(&form)?;
|
||||||
let data_type_fragment = field_type.attr_data_type_fragment().ok_or(bad_request!(
|
let data_type_fragment = field_type.attr_data_type_fragment().ok_or(bad_request!(
|
||||||
|
|
@ -294,7 +297,7 @@ add column if not exists {1} {2}
|
||||||
escape_identifier(&form.name),
|
escape_identifier(&form.name),
|
||||||
data_type_fragment
|
data_type_fragment
|
||||||
))
|
))
|
||||||
.execute(&mut *client)
|
.execute(base_client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Field::insertable_builder()
|
Field::insertable_builder()
|
||||||
|
|
@ -307,7 +310,7 @@ add column if not exists {1} {2}
|
||||||
})
|
})
|
||||||
.field_type(field_type)
|
.field_type(field_type)
|
||||||
.build()?
|
.build()?
|
||||||
.insert(&mut *app_db)
|
.insert(&mut app_db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(navigator.lens_page(&lens).redirect_to())
|
Ok(navigator.lens_page(&lens).redirect_to())
|
||||||
|
|
@ -350,28 +353,6 @@ add column if not exists {1} {2}
|
||||||
// .into_response())
|
// .into_response())
|
||||||
// }
|
// }
|
||||||
|
|
||||||
pub async fn update_lens_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<HashMap<String, String>>,
|
|
||||||
) -> Result<Response, AppError> {
|
|
||||||
dbg!(&form);
|
|
||||||
// FIXME auth
|
|
||||||
// FIXME csrf
|
|
||||||
|
|
||||||
Ok(Redirect::to(&format!(
|
|
||||||
"{0}/d/{base_id}/r/{class_oid}/l/{lens_id}/",
|
|
||||||
settings.root_path
|
|
||||||
))
|
|
||||||
.into_response())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct UpdateValuePageForm {
|
pub struct UpdateValuePageForm {
|
||||||
column: String,
|
column: String,
|
||||||
|
|
@ -380,44 +361,37 @@ pub struct UpdateValuePageForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_value_page_post(
|
pub async fn update_value_page_post(
|
||||||
State(settings): State<Settings>,
|
|
||||||
State(mut base_pooler): State<BasePooler>,
|
State(mut base_pooler): State<BasePooler>,
|
||||||
AppDbConn(mut app_db): AppDbConn,
|
|
||||||
CurrentUser(current_user): CurrentUser,
|
CurrentUser(current_user): CurrentUser,
|
||||||
Path(LensPagePath {
|
Path(LensPagePath {
|
||||||
base_id,
|
base_id, class_oid, ..
|
||||||
class_oid,
|
|
||||||
lens_id: _,
|
|
||||||
}): Path<LensPagePath>,
|
}): Path<LensPagePath>,
|
||||||
Json(body): Json<UpdateValuePageForm>,
|
Json(body): Json<UpdateValuePageForm>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME auth
|
// FIXME auth
|
||||||
// FIXME csrf
|
// FIXME csrf
|
||||||
|
|
||||||
let base = Base::fetch_by_id(base_id, &mut *app_db)
|
let mut base_client = base_pooler
|
||||||
.await?
|
.acquire_for(base_id, RoleAssignment::User(current_user.id))
|
||||||
.ok_or(not_found!("no base found with that id"))?;
|
.await?;
|
||||||
|
let rel = PgClass::with_oid(Oid(class_oid))
|
||||||
let mut client = base_pooler.acquire_for(base_id).await?;
|
.fetch_one(&mut base_client)
|
||||||
|
.await?;
|
||||||
let class = PgClass::fetch_by_oid(Oid(class_oid), &mut *client)
|
let pkey_attrs = PgAttribute::pkeys_for_rel(rel.oid)
|
||||||
.await?
|
.fetch_all(&mut base_client)
|
||||||
.ok_or(not_found!("unable to load table"))?;
|
.await?;
|
||||||
let namespace = class.fetch_namespace(&mut *client).await?;
|
|
||||||
|
|
||||||
let pkey_attrs = fetch_primary_keys_for_rel(Oid(class_oid), &mut *client).await?;
|
|
||||||
|
|
||||||
body.pkeys
|
body.pkeys
|
||||||
.get(&pkey_attrs.first().unwrap().attname)
|
.get(&pkey_attrs.first().unwrap().attname)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.bind_onto(body.value.bind_onto(query(&format!(
|
.bind_onto(body.value.bind_onto(query(&format!(
|
||||||
r#"update {0}.{1} set {2} = $1 where {3} = $2"#,
|
r#"update {0}.{1} set {2} = $1 where {3} = $2"#,
|
||||||
escape_identifier(&namespace.nspname),
|
escape_identifier(&rel.regnamespace),
|
||||||
escape_identifier(&class.relname),
|
escape_identifier(&rel.relname),
|
||||||
escape_identifier(&body.column),
|
escape_identifier(&body.column),
|
||||||
escape_identifier(&pkey_attrs.first().unwrap().attname),
|
escape_identifier(&pkey_attrs.first().unwrap().attname),
|
||||||
))))
|
))))
|
||||||
.execute(&mut *client)
|
.execute(base_client.get_conn())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(Json(json!({ "ok": true })).into_response())
|
Ok(Json(json!({ "ok": true })).into_response())
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ 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_models::{base::Base, rel_invitation::RelInvitation, user::User};
|
||||||
use interim_pgtypes::{
|
use interim_pgtypes::{
|
||||||
pg_acl::PgPrivilegeType,
|
pg_acl::PgPrivilegeType,
|
||||||
pg_class::{PgClass, PgRelKind},
|
pg_class::{PgClass, PgRelKind},
|
||||||
|
|
@ -17,14 +18,11 @@ use sqlx::postgres::types::Oid;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app_error::{AppError, not_found},
|
app_error::AppError,
|
||||||
app_state::AppDbConn,
|
app_state::AppDbConn,
|
||||||
base_pooler::BasePooler,
|
base_pooler::{self, BasePooler},
|
||||||
bases::Base,
|
|
||||||
db_conns::init_role,
|
|
||||||
rel_invitations::RelInvitation,
|
|
||||||
settings::Settings,
|
settings::Settings,
|
||||||
users::{CurrentUser, User},
|
user::CurrentUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -40,16 +38,18 @@ pub async fn list_relations_page(
|
||||||
Path(ListRelationsPagePath { base_id }): Path<ListRelationsPagePath>,
|
Path(ListRelationsPagePath { base_id }): Path<ListRelationsPagePath>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME auth
|
// FIXME auth
|
||||||
let base = Base::fetch_by_id(base_id, &mut *app_db)
|
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||||
.await?
|
|
||||||
.ok_or(AppError::NotFound("no base found with that id".to_owned()))?;
|
|
||||||
let mut client = base_pooler.acquire_for(base_id).await?;
|
|
||||||
let rolname = format!("{}{}", &base.user_role_prefix, current_user.id.simple());
|
let rolname = format!("{}{}", &base.user_role_prefix, current_user.id.simple());
|
||||||
init_role(&rolname, &mut client).await?;
|
let mut client = base_pooler
|
||||||
|
.acquire_for(base_id, base_pooler::RoleAssignment::User(current_user.id))
|
||||||
|
.await?;
|
||||||
|
|
||||||
let roles = PgRole::fetch_by_names_any(vec![rolname], &mut *client).await?;
|
let roles = PgRole::with_name_in(vec![rolname])
|
||||||
|
.fetch_all(&mut client)
|
||||||
|
.await?;
|
||||||
let role = roles.first().context("role not found in pg_roles")?;
|
let role = roles.first().context("role not found in pg_roles")?;
|
||||||
let granted_role_tree = RoleTree::fetch_granted(role.oid, &mut *client)
|
let granted_role_tree = RoleTree::granted_to(role.oid)
|
||||||
|
.fetch_tree(&mut client)
|
||||||
.await?
|
.await?
|
||||||
.context("unable to construct role tree")?;
|
.context("unable to construct role tree")?;
|
||||||
let granted_roles: HashSet<String> = granted_role_tree
|
let granted_roles: HashSet<String> = granted_role_tree
|
||||||
|
|
@ -58,7 +58,9 @@ pub async fn list_relations_page(
|
||||||
.map(|role| role.rolname.clone())
|
.map(|role| role.rolname.clone())
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let all_rels = PgClass::fetch_all_by_kind_any([PgRelKind::OrdinaryTable], &mut *client).await?;
|
let all_rels = PgClass::with_kind_in([PgRelKind::OrdinaryTable])
|
||||||
|
.fetch_all(&mut client)
|
||||||
|
.await?;
|
||||||
let accessible_rels: Vec<PgClass> = all_rels
|
let accessible_rels: Vec<PgClass> = all_rels
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|rel| {
|
.filter(|rel| {
|
||||||
|
|
@ -117,15 +119,13 @@ pub async fn rel_rbac_page(
|
||||||
Path(RelPagePath { base_id, class_oid }): Path<RelPagePath>,
|
Path(RelPagePath { base_id, class_oid }): Path<RelPagePath>,
|
||||||
) -> Result<Response, AppError> {
|
) -> Result<Response, AppError> {
|
||||||
// FIXME: auth
|
// FIXME: auth
|
||||||
let base = Base::fetch_by_id(base_id, &mut *app_db)
|
let base = Base::with_id(base_id).fetch_one(&mut app_db).await?;
|
||||||
.await?
|
let mut client = base_pooler
|
||||||
.ok_or(not_found!("no base found with id {}", base_id))?;
|
.acquire_for(base_id, base_pooler::RoleAssignment::User(current_user.id))
|
||||||
let mut client = base_pooler.acquire_for(base_id).await?;
|
.await?;
|
||||||
let rolname = format!("{}{}", &base.user_role_prefix, current_user.id.simple());
|
let class = PgClass::with_oid(Oid(class_oid))
|
||||||
init_role(&rolname, &mut client).await?;
|
.fetch_one(&mut client)
|
||||||
let class = PgClass::fetch_by_oid(Oid(class_oid), &mut *client)
|
.await?;
|
||||||
.await?
|
|
||||||
.ok_or(not_found!("no relation found with oid {}", class_oid))?;
|
|
||||||
let user_ids: Vec<Uuid> = class
|
let user_ids: Vec<Uuid> = class
|
||||||
.relacl
|
.relacl
|
||||||
.clone()
|
.clone()
|
||||||
|
|
@ -133,7 +133,7 @@ pub async fn rel_rbac_page(
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|item| user_id_from_rolname(&item.grantee, &base.user_role_prefix).ok())
|
.filter_map(|item| user_id_from_rolname(&item.grantee, &base.user_role_prefix).ok())
|
||||||
.collect();
|
.collect();
|
||||||
let all_users = User::fetch_by_ids_any(user_ids, &mut *app_db).await?;
|
let all_users = User::with_id_in(user_ids).fetch_all(&mut app_db).await?;
|
||||||
let interim_users: HashMap<String, User> = all_users
|
let interim_users: HashMap<String, User> = all_users
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|user| {
|
.map(|user| {
|
||||||
|
|
@ -144,7 +144,9 @@ pub async fn rel_rbac_page(
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let all_invites = RelInvitation::fetch_by_class_oid(Oid(class_oid), &mut *app_db).await?;
|
let all_invites = RelInvitation::belonging_to_rel(Oid(class_oid))
|
||||||
|
.fetch_all(&mut app_db)
|
||||||
|
.await?;
|
||||||
let mut invites_by_email: HashMap<String, Vec<RelInvitation>> = HashMap::new();
|
let mut invites_by_email: HashMap<String, Vec<RelInvitation>> = HashMap::new();
|
||||||
for invite in all_invites {
|
for invite in all_invites {
|
||||||
let entry = invites_by_email.entry(invite.email.clone()).or_default();
|
let entry = invites_by_email.entry(invite.email.clone()).or_default();
|
||||||
|
|
@ -215,7 +217,7 @@ pub async fn rel_rbac_invite_page_post(
|
||||||
.privilege(privilege)
|
.privilege(privilege)
|
||||||
.created_by(current_user.id)
|
.created_by(current_user.id)
|
||||||
.build()?
|
.build()?
|
||||||
.upsert(&mut *app_db)
|
.upsert(&mut app_db)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(Redirect::to(&format!(
|
Ok(Redirect::to(&format!(
|
||||||
|
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
// @generated automatically by Diesel CLI.
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
browser_sessions (id) {
|
|
||||||
id -> Text,
|
|
||||||
serialized -> Text,
|
|
||||||
created_at -> Timestamptz,
|
|
||||||
expiry -> Nullable<Timestamptz>,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::table! {
|
|
||||||
users (id) {
|
|
||||||
id -> Uuid,
|
|
||||||
uid -> Text,
|
|
||||||
email -> Text,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
diesel::allow_tables_to_appear_in_same_query!(
|
|
||||||
browser_sessions,
|
|
||||||
users,
|
|
||||||
);
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_session::{async_trait, Session, SessionStore};
|
use async_session::{Session, SessionStore, async_trait};
|
||||||
use axum::{
|
use axum::{
|
||||||
|
RequestPartsExt as _,
|
||||||
extract::{FromRef, FromRequestParts},
|
extract::{FromRef, FromRequestParts},
|
||||||
http::request::Parts,
|
http::request::Parts,
|
||||||
RequestPartsExt as _,
|
|
||||||
};
|
};
|
||||||
use axum_extra::extract::CookieJar;
|
use axum_extra::extract::CookieJar;
|
||||||
use chrono::{DateTime, TimeDelta, Utc};
|
use chrono::{DateTime, TimeDelta, Utc};
|
||||||
use sqlx::{query, query_as, Executor, PgPool};
|
use sqlx::{PgPool, query, query_as};
|
||||||
use tracing::{trace_span, Instrument};
|
use tracing::{Instrument, trace_span};
|
||||||
|
|
||||||
use crate::{app_error::AppError, app_state::AppState};
|
use crate::{app_error::AppError, app_state::AppState};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,16 @@
|
||||||
use async_session::{Session, SessionStore as _};
|
use async_session::{Session, SessionStore as _};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{FromRequestParts, OriginalUri},
|
|
||||||
http::{request::Parts, Method},
|
|
||||||
response::{IntoResponse, Redirect, Response},
|
|
||||||
RequestPartsExt,
|
RequestPartsExt,
|
||||||
|
extract::{FromRequestParts, OriginalUri},
|
||||||
|
http::{Method, request::Parts},
|
||||||
|
response::{IntoResponse, Redirect, Response},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::{
|
use axum_extra::extract::{
|
||||||
cookie::{Cookie, SameSite},
|
|
||||||
CookieJar,
|
CookieJar,
|
||||||
|
cookie::{Cookie, SameSite},
|
||||||
};
|
};
|
||||||
use sqlx::{query_as, PgExecutor};
|
use interim_models::user::User;
|
||||||
|
use sqlx::query_as;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -19,33 +20,6 @@ use crate::{
|
||||||
sessions::AppSession,
|
sessions::AppSession,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct User {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub uid: String,
|
|
||||||
pub email: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl User {
|
|
||||||
pub async fn fetch_by_ids_any<'a, I: IntoIterator<Item = Uuid>, E: PgExecutor<'a>>(
|
|
||||||
ids: I,
|
|
||||||
app_db: E,
|
|
||||||
) -> Result<Vec<Self>, sqlx::Error> {
|
|
||||||
let ids: Vec<Uuid> = ids.into_iter().collect();
|
|
||||||
query_as!(
|
|
||||||
Self,
|
|
||||||
"
|
|
||||||
select * from users
|
|
||||||
where id = any($1)
|
|
||||||
",
|
|
||||||
ids.as_slice()
|
|
||||||
)
|
|
||||||
.fetch_all(app_db)
|
|
||||||
.await
|
|
||||||
.map_err(Into::into)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CurrentUser(pub User);
|
pub struct CurrentUser(pub User);
|
||||||
|
|
||||||
|
|
@ -3,8 +3,6 @@
|
||||||
<head>
|
<head>
|
||||||
<title>{% block title %}Interim{% endblock %}</title>
|
<title>{% block title %}Interim{% endblock %}</title>
|
||||||
{% include "meta_tags.html" %}
|
{% include "meta_tags.html" %}
|
||||||
<link rel="stylesheet" href="{{ settings.root_path }}/modern-normalize.min.css">
|
|
||||||
<link rel="stylesheet" href="{{ settings.root_path }}/main.css">
|
|
||||||
<link rel="stylesheet" href="{{ settings.root_path }}/css_dist/main.css">
|
<link rel="stylesheet" href="{{ settings.root_path }}/css_dist/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -2,51 +2,62 @@
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<link rel="stylesheet" href="{{ settings.root_path }}/css_dist/viewer.css">
|
<link rel="stylesheet" href="{{ settings.root_path }}/css_dist/viewer.css">
|
||||||
<viewer-controller root-path="{{ settings.root_path }}" pkeys="{{ pkeys | json }}" fields="{{ fields | json }}">
|
<div class="page-grid">
|
||||||
<table class="viewer">
|
<div class="page-grid__toolbar"></div>
|
||||||
<thead>
|
<div class="page-grid__sidebar">
|
||||||
<tr>
|
{{ navbar | safe }}
|
||||||
{% for field in fields %}
|
</div>
|
||||||
<th width="{{ field.width_px }}">
|
<main class="page-grid__main">
|
||||||
<div class="padded-cell">{{ field.label.clone().unwrap_or(field.name.clone()) }}</div>
|
<viewer-controller root-path="{{ settings.root_path }}" pkeys="{{ pkeys | json }}" fields="{{ fields | json }}">
|
||||||
</th>
|
<table class="viewer-table">
|
||||||
{% endfor %}
|
<thead>
|
||||||
<th class="column-adder">
|
<tr>
|
||||||
<field-adder root-path="{{ settings.root_path }}" columns="{{ all_columns | json }}"></field-adder>
|
{% for field in fields %}
|
||||||
</th>
|
<th class="viewer-table__column-header" width="{{ field.width_px }}">
|
||||||
</tr>
|
{{ field.label.clone().unwrap_or(field.name.clone()) }}
|
||||||
</thead>
|
</th>
|
||||||
<tbody>
|
|
||||||
{% for (i, row) in rows.iter().enumerate() %}
|
|
||||||
{# TODO: store primary keys in a Vec separate from rows #}
|
|
||||||
<tr>
|
|
||||||
{% for (j, field) in fields.iter().enumerate() %}
|
|
||||||
{# Setting max-width is required for overflow to work properly. #}
|
|
||||||
<td style="width: {{ field.width_px }}px; max-width: {{ field.width_px }}px;">
|
|
||||||
{% match field.get_value_encodable(row) %}
|
|
||||||
{% when Ok with (encodable) %}
|
|
||||||
<{{ field.webc_tag() | safe }}
|
|
||||||
{% for (k, v) in field.webc_custom_attrs() %}
|
|
||||||
{{ k }}="{{ v }}"
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
row="{{ i }}"
|
<th class="viewer-table__actions-header">
|
||||||
column="{{ j }}"
|
<field-adder root-path="{{ settings.root_path }}" columns="{{ all_columns | json }}"></field-adder>
|
||||||
value="{{ encodable | json }}"
|
</th>
|
||||||
class="cell"
|
</tr>
|
||||||
>
|
</thead>
|
||||||
{{ encodable.inner_as_value() | json }}
|
<tbody>
|
||||||
</{{ field.webc_tag() | safe }}
|
{% for (i, row) in rows.iter().enumerate() %}
|
||||||
{% when Err with (err) %}
|
{# TODO: store primary keys in a Vec separate from rows #}
|
||||||
<span class="pg-value-error">{{ err }}</span>
|
<tr>
|
||||||
{% endmatch %}
|
{% for (j, field) in fields.iter().enumerate() %}
|
||||||
</td>
|
{# Setting max-width is required for overflow to work properly. #}
|
||||||
{% endfor %}
|
<td
|
||||||
</tr>
|
class="viewer-table__td"
|
||||||
{% endfor %}
|
style="width: {{ field.width_px }}px; max-width: {{ field.width_px }}px;"
|
||||||
</tbody>
|
>
|
||||||
</table>
|
{% match field.get_value_encodable(row) %}
|
||||||
<viewer-hoverbar root-path="{{ settings.root_path }}"></viewer-hoverbar>
|
{% when Ok with (encodable) %}
|
||||||
</viewer-controller>
|
<{{ field.webc_tag() | safe }}
|
||||||
|
{% for (k, v) in field.webc_custom_attrs() %}
|
||||||
|
{{ k }}="{{ v }}"
|
||||||
|
{% endfor %}
|
||||||
|
row="{{ i }}"
|
||||||
|
column="{{ j }}"
|
||||||
|
value="{{ encodable | json }}"
|
||||||
|
class="cell"
|
||||||
|
>
|
||||||
|
{{ encodable.inner_as_value() | json }}
|
||||||
|
</{{ field.webc_tag() | safe }}
|
||||||
|
{% when Err with (err) %}
|
||||||
|
<span class="pg-value-error">{{ err }}</span>
|
||||||
|
{% endmatch %}
|
||||||
|
</td>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<viewer-hoverbar root-path="{{ settings.root_path }}"></viewer-hoverbar>
|
||||||
|
</viewer-controller>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
<script type="module" src="{{ settings.root_path }}/js_dist/field_adder_component.mjs"></script>
|
<script type="module" src="{{ settings.root_path }}/js_dist/field_adder_component.mjs"></script>
|
||||||
<script type="module" src="{{ settings.root_path }}/js_dist/viewer_controller_component.mjs"></script>
|
<script type="module" src="{{ settings.root_path }}/js_dist/viewer_controller_component.mjs"></script>
|
||||||
<script type="module" src="{{ settings.root_path }}/js_dist/cell_text_component.mjs"></script>
|
<script type="module" src="{{ settings.root_path }}/js_dist/cell_text_component.mjs"></script>
|
||||||
|
|
|
||||||
87
interim-server/templates/navbar.html
Normal file
87
interim-server/templates/navbar.html
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<link rel="stylesheet" href="{{ root_path }}/css_dist/navbar.css">
|
||||||
|
<nav class="navbar">
|
||||||
|
<button type="button" class="base-switcher">
|
||||||
|
<div>{{ base.name }}</div>
|
||||||
|
{#- TODO: icon #}
|
||||||
|
</button>
|
||||||
|
<section>
|
||||||
|
<h2 class="navbar__heading">Tables</h2>
|
||||||
|
<menu class="navbar__menu">
|
||||||
|
{%- for schema in namespaces %}
|
||||||
|
<li class="navbar__menu-item">
|
||||||
|
<collapsible-menu class="navbar__collapsible-menu" root-path="{{ root_path }}">
|
||||||
|
<h3 slot="summary" class="navbar__heading navbar__heading--entity">
|
||||||
|
{{ schema.name }}
|
||||||
|
</h3>
|
||||||
|
<menu slot="content" class="navbar__menu">
|
||||||
|
{%- for rel in schema.rels %}
|
||||||
|
<li class="navbar__menu-item
|
||||||
|
{%- if current == Some(NavLocation::Rel(rel.class_oid.to_owned(), None)) -%}
|
||||||
|
{# preserve space #} navbar__menu-item--active
|
||||||
|
{%- endif -%}
|
||||||
|
">
|
||||||
|
<collapsible-menu
|
||||||
|
class="navbar__collapsible-menu"
|
||||||
|
root-path="{{ root_path }}"
|
||||||
|
expanded="
|
||||||
|
{%- if let Some(NavLocation::Rel(rel_oid, _)) = current -%}
|
||||||
|
{%- if rel_oid.to_owned() == rel.class_oid -%}
|
||||||
|
true
|
||||||
|
{%- endif -%}
|
||||||
|
{%- endif -%}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<h4 slot="summary" class="navbar__heading navbar__heading--entity">
|
||||||
|
{{ rel.name }}
|
||||||
|
</h4>
|
||||||
|
<menu slot="content" class="navbar__menu">
|
||||||
|
<li class="navbar__menu-item">
|
||||||
|
<a
|
||||||
|
href="{{ root_path }}/d/{{ base.id.simple() }}/r/{{ rel.class_oid.0 }}/rbac"
|
||||||
|
class="navbar__menu-link"
|
||||||
|
>
|
||||||
|
Sharing
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="navbar__menu-item">
|
||||||
|
<collapsible-menu class="navbar__collapsible-menu" root-path="{{ root_path }}">
|
||||||
|
<h5 slot="summary" class="navbar__heading">Interfaces</h5>
|
||||||
|
<menu slot="content" class="navbar__menu">
|
||||||
|
{% for lens in rel.lenses %}
|
||||||
|
<li class="navbar__menu-item
|
||||||
|
">
|
||||||
|
<a
|
||||||
|
href="
|
||||||
|
{{- root_path -}}
|
||||||
|
/d/
|
||||||
|
{{- base.id.simple() -}}
|
||||||
|
/r/
|
||||||
|
{{- rel.class_oid.0 -}}
|
||||||
|
/l/
|
||||||
|
{{- lens.id.simple() -}}
|
||||||
|
"
|
||||||
|
class="navbar__menu-link navbar__menu-link--entity
|
||||||
|
{%- if current == Some(NavLocation::Rel(rel.class_oid.to_owned(), Some(RelLocation::Lens(lens.id.to_owned())))) -%}
|
||||||
|
{# preserve space #} navbar__menu-link--current
|
||||||
|
{%- endif -%}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ lens.name }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</menu>
|
||||||
|
</collapsible-menu>
|
||||||
|
</li>
|
||||||
|
</menu>
|
||||||
|
</collapsible-menu>
|
||||||
|
</li>
|
||||||
|
{% endfor -%}
|
||||||
|
</menu>
|
||||||
|
</collapsible-menu>
|
||||||
|
</li>
|
||||||
|
{% endfor -%}
|
||||||
|
</menu>
|
||||||
|
</section>
|
||||||
|
<script type="module" src="{{ root_path }}/js_dist/collapsible_menu_component.mjs"></script>
|
||||||
|
</nav>
|
||||||
|
|
@ -10,11 +10,13 @@ $popover-border: $default-border;
|
||||||
$popover-shadow: 0 0.5rem 0.5rem #3333;
|
$popover-shadow: 0 0.5rem 0.5rem #3333;
|
||||||
$border-radius-rounded-sm: 0.25rem;
|
$border-radius-rounded-sm: 0.25rem;
|
||||||
$border-radius-rounded: 0.5rem;
|
$border-radius-rounded: 0.5rem;
|
||||||
|
$link-color: #069;
|
||||||
|
|
||||||
@mixin reset-button {
|
@mixin reset-button {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|
|
||||||
20
sass/collapsible_menu.scss
Normal file
20
sass/collapsible_menu.scss
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
@use 'globals';
|
||||||
|
|
||||||
|
.collapsible-menu {
|
||||||
|
&__summary {
|
||||||
|
@include globals.reset-button;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 0;
|
||||||
|
transition: max-height 0.3s ease-in;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
|
||||||
|
&--expanded {
|
||||||
|
// todo: adjust max-height dynamically based on content
|
||||||
|
max-height: 40rem;
|
||||||
|
transition: max-height 0.3s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1 +1,58 @@
|
||||||
|
@use 'globals';
|
||||||
|
@use 'modern-normalize';
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: "Averia Serif Libre", "Open Sans", "Helvetica Neue", Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
button, input[type="submit"] {
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Serif Libre";
|
||||||
|
src: url("../averia_serif_libre/averia_serif_libre_regular.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Serif Libre";
|
||||||
|
src: url("../averia_serif_libre/averia_serif_libre_bold.ttf");
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Averia Serif Libre";
|
||||||
|
src: url("../averia_serif_libre/averia_serif_libre_light.ttf");
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "Funnel Sans";
|
||||||
|
src: url("../funnel_sans/funnel_sans_variable.ttf");
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-grid {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
display: grid;
|
||||||
|
grid-template:
|
||||||
|
'sidebar toolbar' 4rem
|
||||||
|
'sidebar main' 1fr / max-content 1fr;
|
||||||
|
|
||||||
|
&__toolbar {
|
||||||
|
grid-area: toolbar;
|
||||||
|
border-bottom: globals.$default-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__sidebar {
|
||||||
|
grid-area: sidebar;
|
||||||
|
width: 15rem;
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
border-right: globals.$default-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
grid-area: main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
45
sass/navbar.scss
Normal file
45
sass/navbar.scss
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
@use 'globals';
|
||||||
|
|
||||||
|
$background-current-item: #0001;
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
padding: 2rem;
|
||||||
|
|
||||||
|
&__menu {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__heading {
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
&--entity {
|
||||||
|
font-family: globals.$font-family-data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__menu-link {
|
||||||
|
@include globals.rounded-sm;
|
||||||
|
display: block;
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: globals.$link-color;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&--entity {
|
||||||
|
font-family: globals.$font-family-data;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--current {
|
||||||
|
background: $background-current-item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.base-switcher {
|
||||||
|
@include globals.reset-button;
|
||||||
|
font-family: globals.$font-family-data;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
@ -1,38 +1,34 @@
|
||||||
table.viewer {
|
.viewer-table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
height: 1px; /* css hack to make percentage based cell heights work */
|
height: 1px; // css hack to make percentage based cell heights work
|
||||||
}
|
|
||||||
|
|
||||||
table.viewer > thead > tr > th {
|
&__column-header {
|
||||||
border: solid 1px #ccc;
|
border: solid 1px #ccc;
|
||||||
border-top: none;
|
border-top: none;
|
||||||
font-family: "Funnel Sans";
|
font-family: "Funnel Sans";
|
||||||
background: #0001;
|
background: #0001;
|
||||||
height: 100%; /* css hack to make percentage based cell heights work */
|
height: 100%; // css hack to make percentage based cell heights work
|
||||||
padding: 0 0.5rem;
|
padding: 0.5rem;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.column-adder {
|
&__actions-header {
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
table.viewer .padded-cell {
|
&__td {
|
||||||
padding: 0.5rem;
|
border: solid 1px #ccc;
|
||||||
}
|
height: 100%; // css hack to make percentage based cell heights work
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
table.viewer > tbody > tr > td {
|
&:first-child {
|
||||||
border: solid 1px #ccc;
|
border-left: none;
|
||||||
height: 100%; /* css hack to make percentage based cell heights work */
|
}
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-left: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
field-adder {
|
|
||||||
--popover-border: solid 1px #ccc;
|
|
||||||
--popover-shadow: 0 0.5rem 0.5rem #3333;
|
|
||||||
|
|
||||||
height: 100%;
|
|
||||||
|
|
||||||
& button.expander {
|
|
||||||
appearance: none;
|
|
||||||
border: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-weight: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
font-family: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
background: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
html {
|
|
||||||
font-family: "Averia Serif Libre", "Open Sans", "Helvetica Neue", Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
button, input[type="submit"] {
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Averia Serif Libre";
|
|
||||||
src: url("./averia_serif_libre/averia_serif_libre_regular.ttf");
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Averia Serif Libre";
|
|
||||||
src: url("./averia_serif_libre/averia_serif_libre_bold.ttf");
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Averia Serif Libre";
|
|
||||||
src: url("./averia_serif_libre/averia_serif_libre_light.ttf");
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Funnel Sans";
|
|
||||||
src: url("./funnel_sans/funnel_sans_variable.ttf");
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
table.viewer {
|
|
||||||
border-collapse: collapse;
|
|
||||||
height: 1px; /* css hack to make percentage based cell heights work */
|
|
||||||
}
|
|
||||||
|
|
||||||
table.viewer > thead > tr > th {
|
|
||||||
border: solid 1px #ccc;
|
|
||||||
border-top: none;
|
|
||||||
font-family: "Funnel Sans";
|
|
||||||
background: #0001;
|
|
||||||
height: 100%; /* css hack to make percentage based cell heights work */
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
text-align: left;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.column-adder {
|
|
||||||
border: none;
|
|
||||||
background: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
table.viewer .padded-cell {
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.viewer .clickable-header-cell {
|
|
||||||
appearance: none;
|
|
||||||
border: none;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
font-weight: inherit;
|
|
||||||
font-size: inherit;
|
|
||||||
font-family: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
table.viewer > tbody > tr > td {
|
|
||||||
border: solid 1px #ccc;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
85
webc/src/collapsible_menu_component.gleam
Normal file
85
webc/src/collapsible_menu_component.gleam
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
import gleam/dynamic.{type Dynamic}
|
||||||
|
import gleam/dynamic/decode
|
||||||
|
import gleam/json
|
||||||
|
import gleam/regexp
|
||||||
|
import gleam/result
|
||||||
|
import gleam/string
|
||||||
|
import lustre.{type App}
|
||||||
|
import lustre/attribute as attr
|
||||||
|
import lustre/component
|
||||||
|
import lustre/effect.{type Effect}
|
||||||
|
import lustre/element.{type Element}
|
||||||
|
import lustre/element/html
|
||||||
|
import lustre/event
|
||||||
|
|
||||||
|
pub const name: String = "collapsible-menu"
|
||||||
|
|
||||||
|
pub fn component() -> App(Nil, Model, Msg) {
|
||||||
|
lustre.component(init, update, view, [
|
||||||
|
component.on_attribute_change("root-path", fn(value) {
|
||||||
|
ParentChangedRootPath(value) |> Ok
|
||||||
|
}),
|
||||||
|
component.on_attribute_change("expanded", fn(value) {
|
||||||
|
ParentChangedExpanded(value == "true") |> Ok
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Model {
|
||||||
|
Model(root_path: String, expanded: Bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(_) -> #(Model, Effect(Msg)) {
|
||||||
|
#(Model(root_path: "", expanded: True), effect.none())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type Msg {
|
||||||
|
ParentChangedRootPath(String)
|
||||||
|
ParentChangedExpanded(Bool)
|
||||||
|
UserClickedSummary
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(model: Model, msg: Msg) -> #(Model, Effect(Msg)) {
|
||||||
|
case msg {
|
||||||
|
ParentChangedRootPath(root_path) -> #(
|
||||||
|
Model(..model, root_path:),
|
||||||
|
effect.none(),
|
||||||
|
)
|
||||||
|
ParentChangedExpanded(expanded) -> #(
|
||||||
|
Model(..model, expanded:),
|
||||||
|
effect.none(),
|
||||||
|
)
|
||||||
|
UserClickedSummary -> #(
|
||||||
|
Model(..model, expanded: !model.expanded),
|
||||||
|
effect.none(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view(model: Model) -> Element(Msg) {
|
||||||
|
element.fragment([
|
||||||
|
html.link([
|
||||||
|
attr.rel("stylesheet"),
|
||||||
|
attr.href(model.root_path <> "/css_dist/collapsible_menu.css"),
|
||||||
|
]),
|
||||||
|
html.div([attr.class("collapsible-menu")], [
|
||||||
|
html.button(
|
||||||
|
[
|
||||||
|
attr.class("collapsible-menu__summary"),
|
||||||
|
event.on_click(UserClickedSummary),
|
||||||
|
],
|
||||||
|
[component.named_slot("summary", [], [])],
|
||||||
|
),
|
||||||
|
html.div(
|
||||||
|
[
|
||||||
|
attr.class("collapsible-menu__content"),
|
||||||
|
case model.expanded {
|
||||||
|
True -> attr.class("collapsible-menu__content--expanded")
|
||||||
|
False -> attr.none()
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[component.named_slot("content", [], [])],
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
@ -15,7 +15,7 @@ export function overwriteInHoverbar(value) {
|
||||||
|
|
||||||
export function clearSelectedAttrs() {
|
export function clearSelectedAttrs() {
|
||||||
document.querySelectorAll(
|
document.querySelectorAll(
|
||||||
"table.viewer > tbody > tr > td > [selected='true']",
|
".viewer-table__td > [selected='true']",
|
||||||
)
|
)
|
||||||
.forEach((element) => element.setAttribute("selected", ""));
|
.forEach((element) => element.setAttribute("selected", ""));
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +40,7 @@ export function syncCellValueToHoverbar(row, column, fieldType) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function queryCell(row, column) {
|
function queryCell(row, column) {
|
||||||
const tr = document.querySelectorAll("table.viewer > tbody > tr")[row];
|
const tr = document.querySelectorAll(".viewer-table > tbody > tr")[row];
|
||||||
if (tr) {
|
if (tr) {
|
||||||
return [...tr.querySelectorAll(":scope > td > *")][column];
|
return [...tr.querySelectorAll(":scope > td > *")][column];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue