2025-09-14 16:19:44 -04:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
|
|
|
|
use axum::{
|
|
|
|
|
Json,
|
|
|
|
|
extract::{Path, State},
|
|
|
|
|
response::{IntoResponse as _, Response},
|
|
|
|
|
};
|
2025-11-19 01:45:58 +00:00
|
|
|
use phono_backends::{
|
|
|
|
|
escape_identifier, pg_acl::PgPrivilegeType, pg_attribute::PgAttribute, pg_class::PgClass,
|
|
|
|
|
};
|
|
|
|
|
use phono_models::{
|
2025-11-19 01:31:09 +00:00
|
|
|
accessors::{Accessor, Actor, portal::PortalAccessor},
|
|
|
|
|
datum::Datum,
|
2026-01-13 18:10:44 +00:00
|
|
|
expression::QueryFragment,
|
2025-11-19 01:31:09 +00:00
|
|
|
field::Field,
|
|
|
|
|
};
|
2025-09-14 16:19:44 -04:00
|
|
|
use serde::{Deserialize, Serialize};
|
2025-11-19 01:31:09 +00:00
|
|
|
use sqlx::{
|
|
|
|
|
postgres::{PgRow, types::Oid},
|
|
|
|
|
query,
|
|
|
|
|
};
|
2025-09-14 16:19:44 -04:00
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
use crate::{
|
2025-09-23 13:08:51 -07:00
|
|
|
app::AppDbConn,
|
|
|
|
|
errors::AppError,
|
2025-10-01 22:36:19 -07:00
|
|
|
field_info::TableFieldInfo,
|
2025-09-14 16:19:44 -04:00
|
|
|
user::CurrentUser,
|
2025-09-23 13:08:51 -07:00
|
|
|
workspace_pooler::{RoleAssignment, WorkspacePooler},
|
2025-09-14 16:19:44 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug, Deserialize)]
|
|
|
|
|
pub(super) struct PathParams {
|
|
|
|
|
portal_id: Uuid,
|
2025-11-19 01:31:09 +00:00
|
|
|
rel_oid: u32,
|
|
|
|
|
workspace_id: Uuid,
|
2025-09-14 16:19:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
|
|
|
|
|
|
|
|
|
/// HTTP GET handler for an API endpoint returning a JSON encoding of portal
|
|
|
|
|
/// data to display in a table or similar form.
|
|
|
|
|
///
|
|
|
|
|
/// Only queries up to the first [`FRONTEND_ROW_LIMIT`] rows.
|
|
|
|
|
pub(super) async fn get(
|
|
|
|
|
State(mut workspace_pooler): State<WorkspacePooler>,
|
|
|
|
|
AppDbConn(mut app_db): AppDbConn,
|
2025-11-19 01:31:09 +00:00
|
|
|
CurrentUser(user): CurrentUser,
|
|
|
|
|
Path(PathParams {
|
|
|
|
|
portal_id,
|
|
|
|
|
rel_oid,
|
|
|
|
|
workspace_id,
|
|
|
|
|
}): Path<PathParams>,
|
2025-09-14 16:19:44 -04:00
|
|
|
) -> Result<Response, AppError> {
|
|
|
|
|
let mut workspace_client = workspace_pooler
|
2025-11-19 01:31:09 +00:00
|
|
|
.acquire_for(workspace_id, RoleAssignment::User(user.id))
|
2025-09-14 16:19:44 -04:00
|
|
|
.await?;
|
2025-11-19 01:31:09 +00:00
|
|
|
|
|
|
|
|
let rel = PgClass::with_oid(Oid(rel_oid))
|
2025-09-14 16:19:44 -04:00
|
|
|
.fetch_one(&mut workspace_client)
|
|
|
|
|
.await?;
|
|
|
|
|
|
2025-11-19 01:31:09 +00:00
|
|
|
let portal = PortalAccessor::default()
|
|
|
|
|
.id(portal_id)
|
|
|
|
|
.as_actor(Actor::User(user.id))
|
|
|
|
|
.verify_workspace_id(workspace_id)
|
|
|
|
|
.verify_rel_oid(Oid(rel_oid))
|
|
|
|
|
.verify_rel_permissions([PgPrivilegeType::Select])
|
|
|
|
|
.using_rel(&rel)
|
|
|
|
|
.using_workspace_client(&mut workspace_client)
|
|
|
|
|
.using_app_db(&mut app_db)
|
|
|
|
|
.fetch_one()
|
|
|
|
|
.await?;
|
|
|
|
|
|
2025-09-14 16:19:44 -04:00
|
|
|
let attrs = PgAttribute::all_for_rel(portal.class_oid)
|
|
|
|
|
.fetch_all(&mut workspace_client)
|
|
|
|
|
.await?;
|
|
|
|
|
let pkey_attrs = PgAttribute::pkeys_for_rel(portal.class_oid)
|
|
|
|
|
.fetch_all(&mut workspace_client)
|
|
|
|
|
.await?;
|
|
|
|
|
|
2025-10-01 22:36:19 -07:00
|
|
|
let fields: Vec<TableFieldInfo> = {
|
2025-09-14 16:19:44 -04:00
|
|
|
let fields: Vec<Field> = Field::belonging_to_portal(portal.id)
|
|
|
|
|
.fetch_all(&mut app_db)
|
|
|
|
|
.await?;
|
2025-10-01 22:36:19 -07:00
|
|
|
let mut field_info: Vec<TableFieldInfo> = Vec::with_capacity(fields.len());
|
2025-09-14 16:19:44 -04:00
|
|
|
for field in fields {
|
|
|
|
|
if let Some(attr) = attrs.iter().find(|attr| attr.attname == field.name) {
|
2025-10-01 22:36:19 -07:00
|
|
|
field_info.push(TableFieldInfo {
|
2025-09-14 16:19:44 -04:00
|
|
|
field,
|
2025-10-01 22:36:19 -07:00
|
|
|
column_present: true,
|
2025-09-14 16:19:44 -04:00
|
|
|
has_default: attr.atthasdef,
|
|
|
|
|
not_null: attr.attnotnull.unwrap_or_default(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
field_info
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-13 18:10:44 +00:00
|
|
|
let sql_fragment = {
|
|
|
|
|
// Defensive programming: Make `sql_fragment` immutable once built.
|
|
|
|
|
let mut sql_fragment = QueryFragment::from_sql(&format!(
|
|
|
|
|
"select {0} from {1}",
|
|
|
|
|
pkey_attrs
|
|
|
|
|
.iter()
|
|
|
|
|
.chain(attrs.iter())
|
|
|
|
|
.map(|attr| escape_identifier(&attr.attname))
|
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
|
.join(", "),
|
|
|
|
|
rel.get_identifier(),
|
|
|
|
|
));
|
|
|
|
|
if let Some(filter_expr) = portal.table_filter.0 {
|
|
|
|
|
sql_fragment.push(QueryFragment::from_sql(" where "));
|
|
|
|
|
sql_fragment.push(filter_expr.into_query_fragment());
|
2025-09-14 16:19:44 -04:00
|
|
|
}
|
2026-01-13 18:10:44 +00:00
|
|
|
sql_fragment.push(QueryFragment::from_sql(" order by _id limit "));
|
|
|
|
|
sql_fragment.push(QueryFragment::from_param(Datum::Numeric(Some(
|
|
|
|
|
FRONTEND_ROW_LIMIT.into(),
|
|
|
|
|
))));
|
|
|
|
|
sql_fragment
|
2025-09-14 16:19:44 -04:00
|
|
|
};
|
|
|
|
|
|
2026-01-13 18:10:44 +00:00
|
|
|
let sql_raw = sql_fragment.to_sql(1);
|
|
|
|
|
let mut q = query(&sql_raw);
|
|
|
|
|
for param in sql_fragment.to_params() {
|
|
|
|
|
q = param.bind_onto(q);
|
|
|
|
|
}
|
|
|
|
|
q = q.bind(FRONTEND_ROW_LIMIT);
|
|
|
|
|
let rows: Vec<PgRow> = q.fetch_all(workspace_client.get_conn()).await?;
|
|
|
|
|
|
2025-09-14 16:19:44 -04:00
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct DataRow {
|
|
|
|
|
pkey: String,
|
2025-09-23 13:08:51 -07:00
|
|
|
data: Vec<Datum>,
|
2025-09-14 16:19:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let mut data_rows: Vec<DataRow> = vec![];
|
|
|
|
|
let mut pkeys: Vec<String> = vec![];
|
|
|
|
|
for row in rows.iter() {
|
2025-09-23 13:08:51 -07:00
|
|
|
let mut pkey_values: HashMap<String, Datum> = HashMap::new();
|
2025-09-14 16:19:44 -04:00
|
|
|
for attr in pkey_attrs.clone() {
|
|
|
|
|
let field = Field::default_from_attr(&attr)
|
|
|
|
|
.ok_or(anyhow::anyhow!("unsupported primary key column type"))?;
|
2025-09-23 13:08:51 -07:00
|
|
|
pkey_values.insert(field.name.clone(), field.get_datum(row)?);
|
2025-09-14 16:19:44 -04:00
|
|
|
}
|
|
|
|
|
let pkey = serde_json::to_string(&pkey_values)?;
|
|
|
|
|
pkeys.push(pkey.clone());
|
2025-09-23 13:08:51 -07:00
|
|
|
let mut row_data: Vec<Datum> = vec![];
|
2025-09-14 16:19:44 -04:00
|
|
|
for field in fields.iter() {
|
2025-09-23 13:08:51 -07:00
|
|
|
row_data.push(field.field.get_datum(row)?);
|
2025-09-14 16:19:44 -04:00
|
|
|
}
|
|
|
|
|
data_rows.push(DataRow {
|
|
|
|
|
pkey,
|
|
|
|
|
data: row_data,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Serialize)]
|
|
|
|
|
struct ResponseBody {
|
|
|
|
|
rows: Vec<DataRow>,
|
2025-10-01 22:36:19 -07:00
|
|
|
fields: Vec<TableFieldInfo>,
|
2025-09-14 16:19:44 -04:00
|
|
|
pkeys: Vec<String>,
|
|
|
|
|
}
|
|
|
|
|
Ok(Json(ResponseBody {
|
|
|
|
|
rows: data_rows,
|
|
|
|
|
fields,
|
|
|
|
|
pkeys,
|
|
|
|
|
})
|
|
|
|
|
.into_response())
|
|
|
|
|
}
|