move relevant common types to phono-pestgros
This commit is contained in:
parent
17ccd80764
commit
36a0c27ad4
26 changed files with 90 additions and 364 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -2380,6 +2380,7 @@ dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
"nom 8.0.0",
|
"nom 8.0.0",
|
||||||
|
"phono-pestgros",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
|
@ -2397,6 +2398,7 @@ dependencies = [
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
"futures",
|
"futures",
|
||||||
"phono-backends",
|
"phono-backends",
|
||||||
|
"phono-pestgros",
|
||||||
"redact",
|
"redact",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -2456,6 +2458,7 @@ dependencies = [
|
||||||
"phono-backends",
|
"phono-backends",
|
||||||
"phono-models",
|
"phono-models",
|
||||||
"phono-namegen",
|
"phono-namegen",
|
||||||
|
"phono-pestgros",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"redact",
|
"redact",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ version.workspace = true
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
derive_builder = { workspace = true }
|
derive_builder = { workspace = true }
|
||||||
nom = "8.0.0"
|
nom = "8.0.0"
|
||||||
|
phono-pestgros = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
sqlx = { workspace = true }
|
sqlx = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use sqlx::{PgConnection, Postgres, Row as _, pool::PoolConnection, query};
|
use sqlx::{PgConnection, Postgres, Row as _, pool::PoolConnection, query};
|
||||||
|
|
||||||
use crate::escape_identifier;
|
|
||||||
|
|
||||||
/// Newtype to differentiate between workspace and application database
|
/// Newtype to differentiate between workspace and application database
|
||||||
/// connections.
|
/// connections.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,3 @@ pub mod pg_database;
|
||||||
pub mod pg_namespace;
|
pub mod pg_namespace;
|
||||||
pub mod pg_role;
|
pub mod pg_role;
|
||||||
pub mod rolnames;
|
pub mod rolnames;
|
||||||
mod utils;
|
|
||||||
|
|
||||||
pub use utils::escape_identifier;
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use sqlx::{Encode, Postgres, postgres::types::Oid, query_as, query_as_unchecked};
|
use sqlx::{Encode, Postgres, postgres::types::Oid, query_as, query_as_unchecked};
|
||||||
|
|
||||||
use crate::{
|
use crate::{client::WorkspaceClient, pg_acl::PgAclItem, pg_namespace::PgNamespace};
|
||||||
client::WorkspaceClient, escape_identifier, pg_acl::PgAclItem, pg_namespace::PgNamespace,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct PgClass {
|
pub struct PgClass {
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
/// Given a raw identifier (such as a table name, column name, etc.), format it
|
|
||||||
/// so that it may be safely interpolated into a SQL query.
|
|
||||||
pub fn escape_identifier(identifier: &str) -> String {
|
|
||||||
// Escaping identifiers for Postgres is fairly easy, provided that the input is
|
|
||||||
// already known to contain no invalid multi-byte sequences. Backslashes may
|
|
||||||
// remain as-is, and embedded double quotes are escaped simply by doubling
|
|
||||||
// them (`"` becomes `""`). Refer to the PQescapeInternal() function in
|
|
||||||
// libpq (fe-exec.c) and Diesel's PgQueryBuilder::push_identifier().
|
|
||||||
format!("\"{}\"", identifier.replace('"', "\"\""))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_escape_identifier() {
|
|
||||||
assert_eq!(escape_identifier("hello"), r#""hello""#);
|
|
||||||
assert_eq!(escape_identifier("hello world"), r#""hello world""#);
|
|
||||||
assert_eq!(
|
|
||||||
escape_identifier(r#""hello" "world""#),
|
|
||||||
r#""""hello"" ""world""""#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,6 +9,7 @@ chrono = { workspace = true }
|
||||||
derive_builder = { workspace = true }
|
derive_builder = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
phono-backends = { workspace = true }
|
phono-backends = { workspace = true }
|
||||||
|
phono-pestgros = { workspace = true }
|
||||||
redact = { workspace = true }
|
redact = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
use bigdecimal::BigDecimal;
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use sqlx::{Postgres, QueryBuilder};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
|
|
||||||
#[serde(tag = "t", content = "c")]
|
|
||||||
pub enum Datum {
|
|
||||||
// BigDecimal is used because a user may insert a value directly via SQL
|
|
||||||
// which overflows the representational space of `rust_decimal::Decimal`.
|
|
||||||
// Note that by default, [`BigDecimal`] serializes to JSON as a string. This
|
|
||||||
// behavior can be modified, but it's a pain when paired with the [`Option`]
|
|
||||||
// type. String representation should be acceptable for the UI, as [`Datum`]
|
|
||||||
// values should always be parsed through Zod, which can coerce the value to
|
|
||||||
// a number transparently.
|
|
||||||
Numeric(Option<BigDecimal>),
|
|
||||||
Text(Option<String>),
|
|
||||||
Timestamp(Option<DateTime<Utc>>),
|
|
||||||
Uuid(Option<Uuid>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Datum {
|
|
||||||
// TODO: Can something similar be achieved with a generic return type?
|
|
||||||
/// Bind this as a parameter to a sqlx query.
|
|
||||||
pub fn bind_onto<'a>(
|
|
||||||
self,
|
|
||||||
query: sqlx::query::Query<'a, Postgres, <sqlx::Postgres as sqlx::Database>::Arguments<'a>>,
|
|
||||||
) -> sqlx::query::Query<'a, Postgres, <sqlx::Postgres as sqlx::Database>::Arguments<'a>> {
|
|
||||||
match self {
|
|
||||||
Self::Numeric(value) => query.bind(value),
|
|
||||||
Self::Text(value) => query.bind(value),
|
|
||||||
Self::Timestamp(value) => query.bind(value),
|
|
||||||
Self::Uuid(value) => query.bind(value),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push this as a parameter to a [`QueryBuilder`].
|
|
||||||
pub fn push_bind_onto(self, builder: &mut QueryBuilder<'_, Postgres>) {
|
|
||||||
match self {
|
|
||||||
Self::Numeric(value) => builder.push_bind(value),
|
|
||||||
Self::Text(value) => builder.push_bind(value),
|
|
||||||
Self::Timestamp(value) => builder.push_bind(value),
|
|
||||||
Self::Uuid(value) => builder.push_bind(value),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Transform the contained value into a serde_json::Value.
|
|
||||||
pub fn inner_as_value(&self) -> serde_json::Value {
|
|
||||||
let serialized = serde_json::to_value(self).unwrap();
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct Tagged {
|
|
||||||
c: serde_json::Value,
|
|
||||||
}
|
|
||||||
let deserialized: Tagged = serde_json::from_value(serialized).unwrap();
|
|
||||||
deserialized.c
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_none(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::Numeric(None) | Self::Text(None) | Self::Timestamp(None) | Self::Uuid(None) => {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Self::Numeric(_) | Self::Text(_) | Self::Timestamp(_) | Self::Uuid(_) => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
use phono_backends::escape_identifier;
|
use phono_pestgros::{Datum, QueryFragment, escape_identifier};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{datum::Datum, query_builders::QueryFragment};
|
|
||||||
|
|
||||||
/// Building block of a syntax tree for a constrained subset of SQL that can be
|
/// Building block of a syntax tree for a constrained subset of SQL that can be
|
||||||
/// statically analyzed, to validate that user-provided expressions perform only
|
/// statically analyzed, to validate that user-provided expressions perform only
|
||||||
/// operations that are read-only and otherwise safe to execute.
|
/// operations that are read-only and otherwise safe to execute.
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ use bigdecimal::BigDecimal;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use phono_backends::pg_attribute::PgAttribute;
|
use phono_backends::pg_attribute::PgAttribute;
|
||||||
|
use phono_pestgros::Datum;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::Acquire as _;
|
use sqlx::Acquire as _;
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
|
|
@ -11,9 +12,7 @@ use sqlx::{
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::client::AppDbClient;
|
use crate::{client::AppDbClient, presentation::Presentation};
|
||||||
use crate::datum::Datum;
|
|
||||||
use crate::presentation::Presentation;
|
|
||||||
|
|
||||||
/// A materialization of a database column, fit for consumption by an end user.
|
/// A materialization of a database column, fit for consumption by an end user.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
pub mod accessors;
|
pub mod accessors;
|
||||||
pub mod client;
|
pub mod client;
|
||||||
pub mod cluster;
|
pub mod cluster;
|
||||||
pub mod datum;
|
|
||||||
pub mod errors;
|
pub mod errors;
|
||||||
pub mod expression;
|
pub mod expression;
|
||||||
pub mod field;
|
pub mod field;
|
||||||
|
|
@ -25,7 +24,6 @@ pub mod language;
|
||||||
mod macros;
|
mod macros;
|
||||||
pub mod portal;
|
pub mod portal;
|
||||||
pub mod presentation;
|
pub mod presentation;
|
||||||
pub mod query_builders;
|
|
||||||
pub mod service_cred;
|
pub mod service_cred;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
pub mod workspace;
|
pub mod workspace;
|
||||||
|
|
|
||||||
|
|
@ -1,171 +0,0 @@
|
||||||
//! Assorted utilities for dynamically constructing and manipulating [`sqlx`]
|
|
||||||
//! queries.
|
|
||||||
|
|
||||||
use sqlx::{Postgres, QueryBuilder};
|
|
||||||
|
|
||||||
use crate::datum::Datum;
|
|
||||||
|
|
||||||
/// Representation of a partial, parameterized SQL query. Allows callers to
|
|
||||||
/// build queries iteratively and dynamically, handling parameter numbering
|
|
||||||
/// (`$1`, `$2`, `$3`, ...) automatically.
|
|
||||||
///
|
|
||||||
/// This is similar to [`sqlx::QueryBuilder`], except that [`QueryFragment`]
|
|
||||||
/// objects are composable and may be concatenated to each other.
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct QueryFragment {
|
|
||||||
/// SQL string, split wherever there is a query parameter. For example,
|
|
||||||
/// `select * from foo where id = $1 and status = $2` is represented along
|
|
||||||
/// the lines of `["select * from foo where id = ", " and status = ", ""]`.
|
|
||||||
/// `plain_sql` should always have exactly one more element than `params`.
|
|
||||||
plain_sql: Vec<String>,
|
|
||||||
params: Vec<Datum>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl QueryFragment {
|
|
||||||
/// Validate invariants. Should be run immediately before returning any
|
|
||||||
/// useful output.
|
|
||||||
fn gut_checks(&self) {
|
|
||||||
assert!(self.plain_sql.len() == self.params.len() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse from a SQL string with no parameters.
|
|
||||||
pub fn from_sql(sql: &str) -> Self {
|
|
||||||
Self {
|
|
||||||
plain_sql: vec![sql.to_owned()],
|
|
||||||
params: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience function to construct an empty value.
|
|
||||||
pub fn empty() -> Self {
|
|
||||||
Self::from_sql("")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse from a parameter value with no additional SQL. (Renders as `$n`,
|
|
||||||
/// where`n` is the appropriate parameter index.)
|
|
||||||
pub fn from_param(param: Datum) -> Self {
|
|
||||||
Self {
|
|
||||||
plain_sql: vec!["".to_owned(), "".to_owned()],
|
|
||||||
params: vec![param],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Append another query fragment to this one.
|
|
||||||
pub fn push(&mut self, mut other: QueryFragment) {
|
|
||||||
let tail = self
|
|
||||||
.plain_sql
|
|
||||||
.pop()
|
|
||||||
.expect("already asserted that vec contains at least 1 item");
|
|
||||||
let head = other
|
|
||||||
.plain_sql
|
|
||||||
.first()
|
|
||||||
.expect("already asserted that vec contains at least 1 item");
|
|
||||||
self.plain_sql.push(format!("{tail}{head}"));
|
|
||||||
for value in other.plain_sql.drain(1..) {
|
|
||||||
self.plain_sql.push(value);
|
|
||||||
}
|
|
||||||
self.params.append(&mut other.params);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Combine multiple QueryFragments with a separator, similar to
|
|
||||||
/// [`Vec::join`].
|
|
||||||
pub fn join<I: IntoIterator<Item = Self>>(fragments: I, sep: Self) -> Self {
|
|
||||||
let mut acc = QueryFragment::from_sql("");
|
|
||||||
let mut iter = fragments.into_iter();
|
|
||||||
let mut fragment = match iter.next() {
|
|
||||||
Some(value) => value,
|
|
||||||
None => return acc,
|
|
||||||
};
|
|
||||||
for next_fragment in iter {
|
|
||||||
acc.push(fragment);
|
|
||||||
acc.push(sep.clone());
|
|
||||||
fragment = next_fragment;
|
|
||||||
}
|
|
||||||
acc.push(fragment);
|
|
||||||
acc
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convenience method equivalent to:
|
|
||||||
/// `QueryFragment::concat(fragments, QueryFragment::from_sql(""))`
|
|
||||||
pub fn concat<I: IntoIterator<Item = Self>>(fragments: I) -> Self {
|
|
||||||
Self::join(fragments, Self::from_sql(""))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks whether value is empty. A value is considered empty if the
|
|
||||||
/// resulting SQL code is 0 characters long.
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.gut_checks();
|
|
||||||
self.plain_sql.len() == 1
|
|
||||||
&& self
|
|
||||||
.plain_sql
|
|
||||||
.first()
|
|
||||||
.expect("already checked that len == 1")
|
|
||||||
.is_empty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<QueryFragment> for QueryBuilder<'_, Postgres> {
|
|
||||||
fn from(value: QueryFragment) -> Self {
|
|
||||||
value.gut_checks();
|
|
||||||
let mut builder = QueryBuilder::new("");
|
|
||||||
let mut param_iter = value.params.into_iter();
|
|
||||||
for plain_sql in value.plain_sql {
|
|
||||||
builder.push(plain_sql);
|
|
||||||
if let Some(param) = param_iter.next() {
|
|
||||||
param.push_bind_onto(&mut builder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
builder
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Helper type to make it easier to build and reason about multiple related SQL
|
|
||||||
/// queries.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct SelectQuery {
|
|
||||||
/// Query fragment following (not including) "select ".
|
|
||||||
pub selection: QueryFragment,
|
|
||||||
|
|
||||||
/// Query fragment following (not including) "from ".
|
|
||||||
pub source: QueryFragment,
|
|
||||||
|
|
||||||
/// Query fragment following (not including) "where ", or empty if not
|
|
||||||
/// applicable.
|
|
||||||
pub filters: QueryFragment,
|
|
||||||
|
|
||||||
/// Query fragment following (not including) "order by ", or empty if not
|
|
||||||
/// applicable.
|
|
||||||
pub order: QueryFragment,
|
|
||||||
|
|
||||||
/// Query fragment following (not including) "limit ", or empty if not
|
|
||||||
/// applicable.
|
|
||||||
pub limit: QueryFragment,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SelectQuery> for QueryFragment {
|
|
||||||
fn from(value: SelectQuery) -> Self {
|
|
||||||
let mut result = QueryFragment::from_sql("select ");
|
|
||||||
result.push(value.selection);
|
|
||||||
result.push(QueryFragment::from_sql(" from "));
|
|
||||||
result.push(value.source);
|
|
||||||
if !value.filters.is_empty() {
|
|
||||||
result.push(QueryFragment::from_sql(" where "));
|
|
||||||
result.push(value.filters);
|
|
||||||
}
|
|
||||||
if !value.order.is_empty() {
|
|
||||||
result.push(QueryFragment::from_sql(" order by "));
|
|
||||||
result.push(value.order);
|
|
||||||
}
|
|
||||||
if !value.limit.is_empty() {
|
|
||||||
result.push(QueryFragment::from_sql(" limit "));
|
|
||||||
result.push(value.limit);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SelectQuery> for QueryBuilder<'_, Postgres> {
|
|
||||||
fn from(value: SelectQuery) -> Self {
|
|
||||||
QueryFragment::from(value).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -35,7 +35,7 @@ use pest::{
|
||||||
};
|
};
|
||||||
use pest_derive::Parser;
|
use pest_derive::Parser;
|
||||||
|
|
||||||
pub use crate::datum::Datum;
|
pub use crate::{datum::Datum, query_builders::QueryFragment};
|
||||||
|
|
||||||
mod datum;
|
mod datum;
|
||||||
mod query_builders;
|
mod query_builders;
|
||||||
|
|
|
||||||
|
|
@ -220,54 +220,3 @@ impl From<Expr> for QueryBuilder<'_, Postgres> {
|
||||||
Self::from(QueryFragment::from(value))
|
Self::from(QueryFragment::from(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper type to make it easier to build and reason about multiple related SQL
|
|
||||||
/// queries.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct SelectQuery {
|
|
||||||
/// Query fragment following (not including) "select ".
|
|
||||||
pub selection: QueryFragment,
|
|
||||||
|
|
||||||
/// Query fragment following (not including) "from ".
|
|
||||||
pub source: QueryFragment,
|
|
||||||
|
|
||||||
/// Query fragment following (not including) "where ", or empty if not
|
|
||||||
/// applicable.
|
|
||||||
pub filters: QueryFragment,
|
|
||||||
|
|
||||||
/// Query fragment following (not including) "order by ", or empty if not
|
|
||||||
/// applicable.
|
|
||||||
pub order: QueryFragment,
|
|
||||||
|
|
||||||
/// Query fragment following (not including) "limit ", or empty if not
|
|
||||||
/// applicable.
|
|
||||||
pub limit: QueryFragment,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SelectQuery> for QueryFragment {
|
|
||||||
fn from(value: SelectQuery) -> Self {
|
|
||||||
let mut result = QueryFragment::from_sql("select ");
|
|
||||||
result.push(value.selection);
|
|
||||||
result.push(QueryFragment::from_sql(" from "));
|
|
||||||
result.push(value.source);
|
|
||||||
if !value.filters.is_empty() {
|
|
||||||
result.push(QueryFragment::from_sql(" where "));
|
|
||||||
result.push(value.filters);
|
|
||||||
}
|
|
||||||
if !value.order.is_empty() {
|
|
||||||
result.push(QueryFragment::from_sql(" order by "));
|
|
||||||
result.push(value.order);
|
|
||||||
}
|
|
||||||
if !value.limit.is_empty() {
|
|
||||||
result.push(QueryFragment::from_sql(" limit "));
|
|
||||||
result.push(value.limit);
|
|
||||||
}
|
|
||||||
result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SelectQuery> for QueryBuilder<'_, Postgres> {
|
|
||||||
fn from(value: SelectQuery) -> Self {
|
|
||||||
QueryFragment::from(value).into()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ percent-encoding = "2.3.1"
|
||||||
phono-backends = { workspace = true }
|
phono-backends = { workspace = true }
|
||||||
phono-models = { workspace = true }
|
phono-models = { workspace = true }
|
||||||
phono-namegen = { workspace = true }
|
phono-namegen = { workspace = true }
|
||||||
|
phono-pestgros = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
redact = { workspace = true }
|
redact = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ use anyhow::anyhow;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use phono_backends::{
|
use phono_backends::{
|
||||||
client::WorkspaceClient,
|
client::WorkspaceClient,
|
||||||
escape_identifier,
|
|
||||||
pg_acl::{PgAclItem, PgPrivilegeType},
|
pg_acl::{PgAclItem, PgPrivilegeType},
|
||||||
pg_class::PgClass,
|
pg_class::PgClass,
|
||||||
pg_role::RoleTree,
|
pg_role::RoleTree,
|
||||||
|
|
@ -17,6 +16,7 @@ use phono_backends::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use phono_models::{accessors::Actor, service_cred::ServiceCred, user::User};
|
use phono_models::{accessors::Actor, service_cred::ServiceCred, user::User};
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{postgres::types::Oid, prelude::FromRow, query, query_as};
|
use sqlx::{postgres::types::Oid, prelude::FromRow, query, query_as};
|
||||||
use tracing::{Instrument, info_span};
|
use tracing::{Instrument, info_span};
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,13 @@ use axum::{
|
||||||
// [`axum_extra`]'s form extractor is preferred:
|
// [`axum_extra`]'s form extractor is preferred:
|
||||||
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use phono_backends::{escape_identifier, pg_class::PgClass};
|
use phono_backends::pg_class::PgClass;
|
||||||
use phono_models::{
|
use phono_models::{
|
||||||
accessors::{Accessor as _, Actor, portal::PortalAccessor},
|
accessors::{Accessor as _, Actor, portal::PortalAccessor},
|
||||||
field::Field,
|
field::Field,
|
||||||
presentation::Presentation,
|
presentation::Presentation,
|
||||||
};
|
};
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{postgres::types::Oid, query};
|
use sqlx::{postgres::types::Oid, query};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
|
||||||
|
|
@ -5,19 +5,16 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{IntoResponse as _, Response},
|
response::{IntoResponse as _, Response},
|
||||||
};
|
};
|
||||||
use phono_backends::{
|
use phono_backends::{pg_acl::PgPrivilegeType, pg_attribute::PgAttribute, pg_class::PgClass};
|
||||||
escape_identifier, pg_acl::PgPrivilegeType, pg_attribute::PgAttribute, pg_class::PgClass,
|
|
||||||
};
|
|
||||||
use phono_models::{
|
use phono_models::{
|
||||||
accessors::{Accessor, Actor, portal::PortalAccessor},
|
accessors::{Accessor, Actor, portal::PortalAccessor},
|
||||||
datum::Datum,
|
|
||||||
expression::PgExpressionAny,
|
expression::PgExpressionAny,
|
||||||
field::Field,
|
field::Field,
|
||||||
query_builders::{QueryFragment, SelectQuery},
|
|
||||||
};
|
};
|
||||||
|
use phono_pestgros::{Datum, QueryFragment, escape_identifier};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{
|
use sqlx::{
|
||||||
QueryBuilder,
|
Postgres, QueryBuilder,
|
||||||
postgres::{PgRow, types::Oid},
|
postgres::{PgRow, types::Oid},
|
||||||
};
|
};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
@ -47,6 +44,57 @@ pub(super) struct FormBody {
|
||||||
|
|
||||||
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
const FRONTEND_ROW_LIMIT: i64 = 1000;
|
||||||
|
|
||||||
|
/// Helper type to make it easier to build and reason about multiple related SQL
|
||||||
|
/// queries.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SelectQuery {
|
||||||
|
/// Query fragment following (not including) "select ".
|
||||||
|
pub selection: QueryFragment,
|
||||||
|
|
||||||
|
/// Query fragment following (not including) "from ".
|
||||||
|
pub source: QueryFragment,
|
||||||
|
|
||||||
|
/// Query fragment following (not including) "where ", or empty if not
|
||||||
|
/// applicable.
|
||||||
|
pub filters: QueryFragment,
|
||||||
|
|
||||||
|
/// Query fragment following (not including) "order by ", or empty if not
|
||||||
|
/// applicable.
|
||||||
|
pub order: QueryFragment,
|
||||||
|
|
||||||
|
/// Query fragment following (not including) "limit ", or empty if not
|
||||||
|
/// applicable.
|
||||||
|
pub limit: QueryFragment,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SelectQuery> for QueryFragment {
|
||||||
|
fn from(value: SelectQuery) -> Self {
|
||||||
|
let mut result = QueryFragment::from_sql("select ");
|
||||||
|
result.push(value.selection);
|
||||||
|
result.push(QueryFragment::from_sql(" from "));
|
||||||
|
result.push(value.source);
|
||||||
|
if !value.filters.is_empty() {
|
||||||
|
result.push(QueryFragment::from_sql(" where "));
|
||||||
|
result.push(value.filters);
|
||||||
|
}
|
||||||
|
if !value.order.is_empty() {
|
||||||
|
result.push(QueryFragment::from_sql(" order by "));
|
||||||
|
result.push(value.order);
|
||||||
|
}
|
||||||
|
if !value.limit.is_empty() {
|
||||||
|
result.push(QueryFragment::from_sql(" limit "));
|
||||||
|
result.push(value.limit);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SelectQuery> for QueryBuilder<'_, Postgres> {
|
||||||
|
fn from(value: SelectQuery) -> Self {
|
||||||
|
QueryFragment::from(value).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// HTTP GET handler for an API endpoint returning a JSON encoding of portal
|
/// HTTP GET handler for an API endpoint returning a JSON encoding of portal
|
||||||
/// data to display in a table or similar form. If the `subfilter` URL parameter
|
/// data to display in a table or similar form. If the `subfilter` URL parameter
|
||||||
/// is specified, it is `&&`-ed with the portal's stored filter.
|
/// is specified, it is `&&`-ed with the portal's stored filter.
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,9 @@ use axum::{
|
||||||
// [`axum_extra`]'s form extractor is required to support repeated keys:
|
// [`axum_extra`]'s form extractor is required to support repeated keys:
|
||||||
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
// https://docs.rs/axum-extra/0.10.1/axum_extra/extract/struct.Form.html#differences-from-axumextractform
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use phono_backends::{escape_identifier, pg_acl::PgPrivilegeType, pg_class::PgClass};
|
use phono_backends::{pg_acl::PgPrivilegeType, pg_class::PgClass};
|
||||||
use phono_models::{
|
use phono_models::accessors::{Accessor as _, Actor, portal::PortalAccessor};
|
||||||
accessors::{Accessor as _, Actor, portal::PortalAccessor},
|
use phono_pestgros::{Datum, escape_identifier};
|
||||||
datum::Datum,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{postgres::types::Oid, query};
|
use sqlx::{postgres::types::Oid, query};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,12 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use phono_backends::{escape_identifier, pg_class::PgClass};
|
use phono_backends::pg_class::PgClass;
|
||||||
use phono_models::{
|
use phono_models::{
|
||||||
accessors::{Accessor, Actor, portal::PortalAccessor},
|
accessors::{Accessor, Actor, portal::PortalAccessor},
|
||||||
field::Field,
|
field::Field,
|
||||||
};
|
};
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{postgres::types::Oid, query};
|
use sqlx::{postgres::types::Oid, query};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::Response,
|
response::Response,
|
||||||
};
|
};
|
||||||
use phono_backends::{escape_identifier, pg_class::PgClass};
|
use phono_backends::pg_class::PgClass;
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{postgres::types::Oid, query};
|
use sqlx::{postgres::types::Oid, query};
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,9 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::{IntoResponse as _, Response},
|
response::{IntoResponse as _, Response},
|
||||||
};
|
};
|
||||||
use phono_backends::{
|
use phono_backends::{pg_acl::PgPrivilegeType, pg_attribute::PgAttribute, pg_class::PgClass};
|
||||||
escape_identifier, pg_acl::PgPrivilegeType, pg_attribute::PgAttribute, pg_class::PgClass,
|
use phono_models::accessors::{Accessor, Actor, portal::PortalAccessor};
|
||||||
};
|
use phono_pestgros::{Datum, escape_identifier};
|
||||||
use phono_models::{
|
|
||||||
accessors::{Accessor, Actor, portal::PortalAccessor},
|
|
||||||
datum::Datum,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::{Acquire as _, postgres::types::Oid, query};
|
use sqlx::{Acquire as _, postgres::types::Oid, query};
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use axum::{extract::State, response::IntoResponse};
|
use axum::{extract::State, response::IntoResponse};
|
||||||
use phono_backends::{client::WorkspaceClient, escape_identifier, rolnames::ROLE_PREFIX_USER};
|
use phono_backends::{client::WorkspaceClient, rolnames::ROLE_PREFIX_USER};
|
||||||
use phono_models::{
|
use phono_models::{
|
||||||
client::AppDbClient, cluster::Cluster, user::User, workspace::Workspace,
|
client::AppDbClient, cluster::Cluster, user::User, workspace::Workspace,
|
||||||
workspace_user_perm::WorkspaceMembership,
|
workspace_user_perm::WorkspaceMembership,
|
||||||
};
|
};
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use sqlx::{Connection as _, PgConnection, query};
|
use sqlx::{Connection as _, PgConnection, query};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,14 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use phono_backends::{
|
use phono_backends::rolnames::{
|
||||||
escape_identifier,
|
ROLE_PREFIX_SERVICE_CRED, SERVICE_CRED_CONN_LIMIT, SERVICE_CRED_SUFFIX_LEN,
|
||||||
rolnames::{ROLE_PREFIX_SERVICE_CRED, SERVICE_CRED_CONN_LIMIT, SERVICE_CRED_SUFFIX_LEN},
|
|
||||||
};
|
};
|
||||||
use phono_models::{
|
use phono_models::{
|
||||||
accessors::{Accessor as _, Actor, workspace::WorkspaceAccessor},
|
accessors::{Accessor as _, Actor, workspace::WorkspaceAccessor},
|
||||||
service_cred::ServiceCred,
|
service_cred::ServiceCred,
|
||||||
};
|
};
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use rand::distributions::{Alphanumeric, DistString};
|
use rand::distributions::{Alphanumeric, DistString};
|
||||||
use redact::Secret;
|
use redact::Secret;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,11 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use phono_backends::{
|
use phono_backends::rolnames::{
|
||||||
escape_identifier,
|
ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER, ROLE_PREFIX_USER,
|
||||||
rolnames::{
|
|
||||||
ROLE_PREFIX_TABLE_OWNER, ROLE_PREFIX_TABLE_READER, ROLE_PREFIX_TABLE_WRITER,
|
|
||||||
ROLE_PREFIX_USER,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use phono_models::accessors::{Accessor as _, Actor, workspace::WorkspaceAccessor};
|
use phono_models::accessors::{Accessor as _, Actor, workspace::WorkspaceAccessor};
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::{Acquire as _, query};
|
use sqlx::{Acquire as _, query};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,13 @@ use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use phono_backends::{escape_identifier, pg_database::PgDatabase, rolnames::ROLE_PREFIX_USER};
|
use phono_backends::{pg_database::PgDatabase, rolnames::ROLE_PREFIX_USER};
|
||||||
use phono_models::{
|
use phono_models::{
|
||||||
accessors::{Accessor as _, Actor, workspace::WorkspaceAccessor},
|
accessors::{Accessor as _, Actor, workspace::WorkspaceAccessor},
|
||||||
user::User,
|
user::User,
|
||||||
workspace_user_perm::WorkspaceMembership,
|
workspace_user_perm::WorkspaceMembership,
|
||||||
};
|
};
|
||||||
|
use phono_pestgros::escape_identifier;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sqlx::query;
|
use sqlx::query;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue