konobangu/apps/recorder/src/graphql/infra/json.rs
2025-06-16 08:01:02 +08:00

1381 lines
48 KiB
Rust

use async_graphql::{
Error as GraphqlError,
dynamic::{ResolverContext, Scalar, SchemaError},
to_value,
};
use convert_case::Case;
use itertools::Itertools;
use rust_decimal::{Decimal, prelude::FromPrimitive};
use sea_orm::{
Condition, EntityTrait,
sea_query::{ArrayType, Expr, ExprTrait, IntoLikeExpr, SimpleExpr, Value as DbValue},
};
use seaography::{
Builder as SeaographyBuilder, BuilderContext, FilterType, FnFilterCondition, SeaographyError,
};
use serde::{Serialize, de::DeserializeOwned};
use serde_json::Value as JsonValue;
use crate::{
errors::RecorderResult, graphql::infra::util::get_entity_column_key,
utils::json::convert_json_keys,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Copy)]
pub enum JsonbFilterOperation {
Equals,
NotEquals,
GreaterThan,
GreaterThanEquals,
LessThan,
LessThanEquals,
IsIn,
IsNotIn,
IsNull,
IsNotNull,
Contains,
StartsWith,
EndsWith,
Like,
NotLike,
Exists,
NotExists,
Between,
NotBetween,
And,
Or,
JsonbPathQuery,
Match,
Not,
}
impl JsonbFilterOperation {
pub fn is_filter_operation(property_key: &str) -> bool {
property_key.starts_with("$")
}
pub fn parse_str(value: &str) -> Result<Option<Self>, async_graphql::dynamic::SchemaError> {
match value {
"$eq" => Ok(Some(JsonbFilterOperation::Equals)),
"$ne" => Ok(Some(JsonbFilterOperation::NotEquals)),
"$gt" => Ok(Some(JsonbFilterOperation::GreaterThan)),
"$gte" => Ok(Some(JsonbFilterOperation::GreaterThanEquals)),
"$lt" => Ok(Some(JsonbFilterOperation::LessThan)),
"$lte" => Ok(Some(JsonbFilterOperation::LessThanEquals)),
"$is_in" => Ok(Some(JsonbFilterOperation::IsIn)),
"$is_not_in" => Ok(Some(JsonbFilterOperation::IsNotIn)),
"$is_null" => Ok(Some(JsonbFilterOperation::IsNull)),
"$is_not_null" => Ok(Some(JsonbFilterOperation::IsNotNull)),
"$contains" => Ok(Some(JsonbFilterOperation::Contains)),
"$starts_with" => Ok(Some(JsonbFilterOperation::StartsWith)),
"$ends_with" => Ok(Some(JsonbFilterOperation::EndsWith)),
"$like" => Ok(Some(JsonbFilterOperation::Like)),
"$not_like" => Ok(Some(JsonbFilterOperation::NotLike)),
"$between" => Ok(Some(JsonbFilterOperation::Between)),
"$not_between" => Ok(Some(JsonbFilterOperation::NotBetween)),
"$and" => Ok(Some(JsonbFilterOperation::And)),
"$or" => Ok(Some(JsonbFilterOperation::Or)),
"$exists" => Ok(Some(JsonbFilterOperation::Exists)),
"$not_exists" => Ok(Some(JsonbFilterOperation::NotExists)),
"$match" => Ok(Some(JsonbFilterOperation::Match)),
"$not" => Ok(Some(JsonbFilterOperation::Not)),
s if s.starts_with("$query:") && s.len() >= 7 => {
Ok(Some(JsonbFilterOperation::JsonbPathQuery))
}
s if Self::is_filter_operation(s) => Err(async_graphql::dynamic::SchemaError(format!(
"Use reserved but not implemented filter operation: {value}"
))),
_ => Ok(None),
}
}
}
impl AsRef<str> for JsonbFilterOperation {
fn as_ref(&self) -> &str {
match self {
JsonbFilterOperation::Equals => "$eq",
JsonbFilterOperation::NotEquals => "$ne",
JsonbFilterOperation::GreaterThan => "$gt",
JsonbFilterOperation::GreaterThanEquals => "$gte",
JsonbFilterOperation::LessThan => "$lt",
JsonbFilterOperation::LessThanEquals => "$lte",
JsonbFilterOperation::IsIn => "$is_in",
JsonbFilterOperation::IsNotIn => "$is_not_in",
JsonbFilterOperation::IsNull => "$is_null",
JsonbFilterOperation::IsNotNull => "$is_not_null",
JsonbFilterOperation::Contains => "$contains",
JsonbFilterOperation::StartsWith => "$starts_with",
JsonbFilterOperation::EndsWith => "$ends_with",
JsonbFilterOperation::Like => "$like",
JsonbFilterOperation::NotLike => "$not_like",
JsonbFilterOperation::Between => "$between",
JsonbFilterOperation::NotBetween => "$not_between",
JsonbFilterOperation::And => "$and",
JsonbFilterOperation::Or => "$or",
JsonbFilterOperation::Exists => "$exists",
JsonbFilterOperation::NotExists => "$not_exists",
JsonbFilterOperation::JsonbPathQuery => "$query",
JsonbFilterOperation::Match => "$match",
JsonbFilterOperation::Not => "$not",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum JsonPathSegment {
Str(String),
Num(u64),
JsonbPathQuery(String),
Root,
}
impl TryFrom<&str> for JsonPathSegment {
type Error = SchemaError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.is_empty() {
Err(SchemaError("JsonPath segment can not be empty".to_string()))
} else {
Ok(JsonPathSegment::Str(value.to_string()))
}
}
}
impl TryFrom<String> for JsonPathSegment {
type Error = SchemaError;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.is_empty() {
Err(SchemaError("JsonPath segment can not be empty".to_string()))
} else {
Ok(JsonPathSegment::Str(value))
}
}
}
impl TryFrom<u64> for JsonPathSegment {
type Error = SchemaError;
fn try_from(value: u64) -> Result<Self, Self::Error> {
Ok(JsonPathSegment::Num(value))
}
}
pub struct JsonPath(Vec<JsonPathSegment>);
impl Default for JsonPath {
fn default() -> Self {
Self::new()
}
}
impl JsonPath {
pub fn new() -> Self {
Self(vec![JsonPathSegment::Root])
}
pub fn push(&mut self, segment: impl Into<JsonPathSegment>) -> RecorderResult<()> {
let s = segment.into();
match &s {
JsonPathSegment::Root => Err(SchemaError(
"JsonPath can only contain one root segment".to_string(),
))?,
JsonPathSegment::JsonbPathQuery(..) => {
if !self
.0
.last()
.is_some_and(|s| matches!(s, JsonPathSegment::Root))
{
Err(SchemaError(
"JsonPath jsonb_path_query must be the only non-root segment".to_string(),
))?;
}
self.0.push(s);
}
_ => {
if self
.0
.last()
.is_some_and(|s| !matches!(s, JsonPathSegment::JsonbPathQuery(..)))
{
self.0.push(s);
} else {
Err(SchemaError(
"JsonPath jsonb_path_query must be the only non-root segment".to_string(),
))?;
}
}
}
Ok(())
}
fn pop(&mut self) -> Option<JsonPathSegment> {
if self
.0
.last()
.is_none_or(|s| matches!(s, JsonPathSegment::Root))
{
None
} else {
self.0.pop()
}
}
fn join(&self) -> String {
self.0
.iter()
.map(|s| match s {
JsonPathSegment::Str(s) => {
let needs_brackets = s.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_');
if needs_brackets {
let escaped = s
.replace('\\', "\\\\")
.replace('\'', "\\'")
.replace('"', "\\\"");
format!("['{escaped}']")
} else {
format!(".{s}")
}
}
JsonPathSegment::Num(n) => format!("[{n}]"),
JsonPathSegment::JsonbPathQuery(s) => s.into(),
JsonPathSegment::Root => "$".into(),
})
.join("")
}
}
fn jsonb_path_expr(path: &JsonPath) -> SimpleExpr {
Expr::val(path.join()).into()
}
fn jsonb_path_exists_expr(col_expr: impl Into<SimpleExpr>, path: &JsonPath) -> SimpleExpr {
Expr::cust_with_exprs(
"jsonb_path_exists($1, $2)",
[col_expr.into(), jsonb_path_expr(path)],
)
}
fn jsonb_path_query_first_expr(col_expr: impl Into<SimpleExpr>, path: &JsonPath) -> SimpleExpr {
Expr::cust_with_exprs(
"jsonb_path_query_first($1, $2)",
[col_expr.into(), jsonb_path_expr(path)],
)
}
fn jsonb_path_query_first_auto_cast_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
value: &JsonValue,
) -> RecorderResult<SimpleExpr> {
let cast_target = match value {
JsonValue::Number(..) => "numeric",
JsonValue::Bool(..) => "boolean",
JsonValue::String(..) => "text",
_ => {
return Err(SchemaError(
"JsonbFilterInput leaf can not be only be casted to numeric, boolean or text"
.to_string(),
))?;
}
};
Ok(jsonb_path_query_first_expr(col_expr, path).cast_as(cast_target))
}
fn jsonb_path_is_in_values_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
values: Vec<JsonValue>,
) -> SimpleExpr {
Expr::cust_with_exprs(
"$1 = ANY($2)",
[
jsonb_path_query_first_expr(col_expr, path),
Expr::val(DbValue::Array(
ArrayType::Json,
Some(Box::new(
values
.into_iter()
.map(|v| DbValue::Json(Some(Box::new(v))))
.collect(),
)),
))
.into(),
],
)
}
fn jsonb_path_eq_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
value: JsonValue,
) -> SimpleExpr {
jsonb_path_query_first_expr(col_expr, path).eq(value)
}
fn jsonb_path_ne_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
value: JsonValue,
) -> SimpleExpr {
jsonb_path_query_first_expr(col_expr, path).ne(value)
}
fn jsonb_path_type_assert_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
typestr: &str,
) -> SimpleExpr {
Expr::cust_with_exprs(
format!("jsonb_path_exists($1, $2 || ' ? (@.type() = \"{typestr}\")')"),
[col_expr.into(), jsonb_path_expr(path)],
)
}
fn jsonb_path_is_null_expr(col_expr: impl Into<SimpleExpr>, path: &JsonPath) -> SimpleExpr {
Expr::cust_with_exprs(
"jsonb_path_exists($1, $2 || ' ? (@ == null)')",
[col_expr.into(), jsonb_path_expr(path)],
)
}
fn jsonb_path_is_not_null_expr(col_expr: impl Into<SimpleExpr>, path: &JsonPath) -> SimpleExpr {
Expr::cust_with_exprs(
"jsonb_path_exists($1, $2 || ' ? (@ != null)')",
[col_expr.into(), jsonb_path_expr(path)],
)
}
fn convert_jsonb_number_to_db_decimal(jsonb_number: serde_json::Number) -> RecorderResult<Decimal> {
if let Some(f) = jsonb_number.as_f64() {
let decimal = Decimal::from_f64(f).ok_or_else(|| {
SchemaError("JsonbFilterInput leaf value failed to convert to decimal".to_string())
})?;
Ok(decimal)
} else if let Some(i) = jsonb_number.as_i64() {
Ok(Decimal::from(i))
} else if let Some(u) = jsonb_number.as_u64() {
Ok(Decimal::from(u))
} else {
Err(
SchemaError("JsonbFilterInput leaf value failed to convert to a number".to_string())
.into(),
)
}
}
fn jsonb_path_like_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
value: impl IntoLikeExpr,
) -> SimpleExpr {
let col_expr = col_expr.into();
let left = jsonb_path_type_assert_expr(col_expr.clone(), path, "string");
left.and(
jsonb_path_query_first_expr(col_expr, path)
.cast_as("text")
.like(value),
)
}
fn jsonb_path_not_like_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
value: impl IntoLikeExpr,
) -> SimpleExpr {
let col_expr = col_expr.into();
let left = jsonb_path_type_assert_expr(col_expr.clone(), path, "string");
left.and(
jsonb_path_query_first_expr(col_expr, path)
.cast_as("text")
.not_like(value),
)
}
fn jsonb_path_starts_with_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
value: String,
) -> SimpleExpr {
let col_expr = col_expr.into();
let type_assert_expr = jsonb_path_type_assert_expr(col_expr.clone(), path, "string");
let get_value_expr = jsonb_path_query_first_expr(col_expr, path).cast_as("text");
let starts_with_expr = Expr::cust_with_exprs(
"starts_with($1, $2)",
[get_value_expr, Expr::val(value).into()],
);
type_assert_expr.and(starts_with_expr)
}
fn escape_like_expr(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('%', "\\%")
.replace('_', "\\_")
}
fn jsonb_path_ends_with_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
value: String,
) -> SimpleExpr {
jsonb_path_like_expr(col_expr, path, format!("%{}", escape_like_expr(&value)))
}
fn jsonb_path_str_between_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
lhs: String,
rhs: String,
) -> SimpleExpr {
let col_expr = col_expr.into();
let left = jsonb_path_type_assert_expr(col_expr.clone(), path, "string");
let right = jsonb_path_query_first_expr(col_expr, path)
.cast_as("text")
.between(lhs, rhs);
left.and(right)
}
fn jsonb_path_str_not_between_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
lhs: String,
rhs: String,
) -> SimpleExpr {
let col_expr = col_expr.into();
let left = jsonb_path_type_assert_expr(col_expr.clone(), path, "string");
let right = jsonb_path_query_first_expr(col_expr, path)
.cast_as("text")
.not_between(lhs, rhs);
left.and(right)
}
fn jsonb_path_num_between_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
lhs: Decimal,
rhs: Decimal,
) -> SimpleExpr {
let col_expr = col_expr.into();
let left = jsonb_path_type_assert_expr(col_expr.clone(), path, "number");
let right = jsonb_path_query_first_expr(col_expr, path)
.cast_as("numeric")
.between(lhs, rhs);
left.and(right)
}
fn jsonb_path_num_not_between_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
lhs: Decimal,
rhs: Decimal,
) -> SimpleExpr {
let col_expr = col_expr.into();
let left = jsonb_path_type_assert_expr(col_expr.clone(), path, "number");
let right = jsonb_path_query_first_expr(col_expr, path)
.cast_as("numeric")
.not_between(lhs, rhs);
left.and(right)
}
fn jsonb_path_bool_between_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
lhs: bool,
rhs: bool,
) -> SimpleExpr {
let col_expr = col_expr.into();
let left = jsonb_path_type_assert_expr(col_expr.clone(), path, "boolean");
let right = jsonb_path_query_first_expr(col_expr, path)
.cast_as("boolean")
.between(lhs, rhs);
left.and(right)
}
fn jsonb_path_bool_not_between_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
lhs: bool,
rhs: bool,
) -> SimpleExpr {
let col_expr = col_expr.into();
let left = jsonb_path_type_assert_expr(col_expr.clone(), path, "boolean");
let right = jsonb_path_query_first_expr(col_expr, path)
.cast_as("boolean")
.not_between(lhs, rhs);
left.and(right)
}
fn jsonb_path_contains_expr(
col_expr: impl Into<SimpleExpr>,
path: &JsonPath,
value: JsonValue,
) -> SimpleExpr {
let col_expr = col_expr.into();
let jsonb_path_array_contains = Expr::cust_with_exprs(
"jsonb_path_query_first($1, $2) @> $3",
[
col_expr.clone(),
jsonb_path_expr(path),
Expr::val(DbValue::Json(Some(Box::new(JsonValue::Array(vec![
value.clone(),
])))))
.into(),
],
);
let mut case = Expr::case(
Condition::all()
.add(jsonb_path_type_assert_expr(col_expr.clone(), path, "array"))
.add(jsonb_path_array_contains),
Expr::cust("true"),
);
if let JsonValue::String(s) = value {
let jsonb_path_str_contains = jsonb_path_query_first_expr(col_expr.clone(), path)
.cast_as("text")
.like(format!("%{}%", escape_like_expr(&s)));
case = case.case(
Condition::all()
.add(jsonb_path_type_assert_expr(col_expr, path, "string"))
.add(jsonb_path_str_contains),
Expr::cust("true"),
)
};
case.finally(Expr::cust("false")).eq(Expr::cust("true"))
}
fn prepare_jsonb_leaf_condition(
col_expr: impl Into<SimpleExpr>,
op: JsonbFilterOperation,
value: JsonValue,
path: &JsonPath,
) -> RecorderResult<SimpleExpr> {
Ok(match (op, value) {
(op @ (JsonbFilterOperation::Exists | JsonbFilterOperation::NotExists), value) => {
match value {
JsonValue::Bool(exists) => {
let jsonb_exists_expr = jsonb_path_exists_expr(col_expr, path);
if (op == JsonbFilterOperation::Exists && exists)
|| (op == JsonbFilterOperation::NotExists && !exists)
{
jsonb_exists_expr
} else {
jsonb_exists_expr.not()
}
}
_ => Err(SchemaError(
"JsonbFilterInput leaf can not be $exists or $not_exists with a non-boolean \
value"
.to_string(),
))?,
}
}
(
JsonbFilterOperation::And
| JsonbFilterOperation::Or
| JsonbFilterOperation::JsonbPathQuery
| JsonbFilterOperation::Match
| JsonbFilterOperation::Not,
_,
) => {
unreachable!("JsonbFilterInput leaf can not be $and or $or with any value")
}
(JsonbFilterOperation::Equals, value) => jsonb_path_eq_expr(col_expr, path, value),
(JsonbFilterOperation::NotEquals, value) => jsonb_path_ne_expr(col_expr, path, value),
(op @ (JsonbFilterOperation::IsIn | JsonbFilterOperation::IsNotIn), value) => {
if let JsonValue::Array(values) = value {
let expr = jsonb_path_is_in_values_expr(col_expr, path, values.clone());
if op == JsonbFilterOperation::IsIn {
expr
} else {
expr.not()
}
} else {
Err(SchemaError(
"JsonbFilterInput leaf can not be $is_in or $is_not_in with a non-array value"
.to_string(),
))?
}
}
(JsonbFilterOperation::IsNull, value) => match value {
JsonValue::Bool(is) => {
let expr = jsonb_path_is_null_expr(col_expr, path);
if is { expr } else { expr.not() }
}
_ => Err(SchemaError(
"JsonbFilterInput leaf can not be $is_null with a non-boolean value".to_string(),
))?,
},
(JsonbFilterOperation::IsNotNull, value) => match value {
JsonValue::Bool(is) => {
let expr = jsonb_path_is_not_null_expr(col_expr, path);
if is { expr } else { expr.not() }
}
_ => Err(SchemaError(
"JsonbFilterInput leaf can not be $is_not_null with a non-boolean value"
.to_string(),
))?,
},
(JsonbFilterOperation::Contains, value) => jsonb_path_contains_expr(col_expr, path, value),
(
op @ (JsonbFilterOperation::GreaterThan
| JsonbFilterOperation::LessThan
| JsonbFilterOperation::GreaterThanEquals
| JsonbFilterOperation::LessThanEquals),
value,
) => {
let lexpr = jsonb_path_query_first_auto_cast_expr(col_expr, path, &value)?;
let rexpr: SimpleExpr = match value {
JsonValue::Number(n) => Expr::val(DbValue::Decimal(Some(Box::new(
convert_jsonb_number_to_db_decimal(n)?,
))))
.into(),
JsonValue::Bool(b) => Expr::val(b).into(),
JsonValue::String(s) => Expr::val(s).into(),
_ => Err(SchemaError(format!(
"JsonbFilterInput leaf can not be {} with an array, object or null",
op.as_ref()
)))?,
};
match op {
JsonbFilterOperation::GreaterThan => lexpr.gt(rexpr),
JsonbFilterOperation::GreaterThanEquals => lexpr.gte(rexpr),
JsonbFilterOperation::LessThan => lexpr.lt(rexpr),
JsonbFilterOperation::LessThanEquals => lexpr.lte(rexpr),
_ => unreachable!(),
}
}
(JsonbFilterOperation::StartsWith, value) => {
if let JsonValue::String(s) = value {
jsonb_path_starts_with_expr(col_expr, path, s)
} else {
Err(SchemaError(
"JsonbFilterInput leaf can not be $starts_with with a non-string value"
.to_string(),
))?
}
}
(JsonbFilterOperation::EndsWith, value) => {
if let JsonValue::String(s) = value {
jsonb_path_ends_with_expr(col_expr, path, s)
} else {
Err(SchemaError(
"JsonbFilterInput leaf can not be $ends_with with a non-string value"
.to_string(),
))?
}
}
(JsonbFilterOperation::Like, value) => {
if let JsonValue::String(s) = value {
jsonb_path_like_expr(col_expr, path, s)
} else {
Err(SchemaError(
"JsonbFilterInput leaf can not be $like with a non-string value".to_string(),
))?
}
}
(JsonbFilterOperation::NotLike, value) => {
if let JsonValue::String(s) = value {
jsonb_path_not_like_expr(col_expr, path, s)
} else {
Err(SchemaError(
"JsonbFilterInput leaf can not be $not_like with a non-string value"
.to_string(),
))?
}
}
(op @ (JsonbFilterOperation::Between | JsonbFilterOperation::NotBetween), value) => {
if let JsonValue::Array(mut values) = value {
if values.len() != 2 {
return Err(SchemaError(
"JsonbFilterInput leaf can not be $between or $not_between with a \
non-array value"
.to_string(),
)
.into());
} else {
let (rhs, lhs) = (values.pop().unwrap(), values.pop().unwrap());
match (lhs, rhs) {
(JsonValue::Number(lhs), JsonValue::Number(rhs)) => {
let (lhs, rhs) = (
convert_jsonb_number_to_db_decimal(lhs)?,
convert_jsonb_number_to_db_decimal(rhs)?,
);
if op == JsonbFilterOperation::Between {
jsonb_path_num_between_expr(col_expr, path, lhs, rhs)
} else {
jsonb_path_num_not_between_expr(col_expr, path, lhs, rhs)
}
}
(JsonValue::String(lhs), JsonValue::String(rhs)) => {
if op == JsonbFilterOperation::Between {
jsonb_path_str_between_expr(col_expr, path, lhs, rhs)
} else {
jsonb_path_str_not_between_expr(col_expr, path, lhs, rhs)
}
}
(JsonValue::Bool(lhs), JsonValue::Bool(rhs)) => {
if op == JsonbFilterOperation::Between {
jsonb_path_bool_between_expr(col_expr, path, lhs, rhs)
} else {
jsonb_path_bool_not_between_expr(col_expr, path, lhs, rhs)
}
}
_ => Err(SchemaError(
"JsonbFilterInput leaf can not be $between without two same type \
number, string or boolean value"
.to_string(),
))?,
}
}
} else {
Err(SchemaError(
"JsonbFilterInput leaf can not be $between with a non-array value".to_string(),
))?
}
}
})
}
fn recursive_prepare_jsonb_node_condition<E>(
expr: &E,
node: JsonValue,
mut path: JsonPath,
) -> RecorderResult<(Condition, JsonPath)>
where
E: Into<SimpleExpr> + Clone,
{
enum JsonIndex {
Str(String),
Num(u64),
}
impl TryFrom<JsonIndex> for JsonPathSegment {
type Error = SchemaError;
fn try_from(index: JsonIndex) -> Result<Self, Self::Error> {
match index {
JsonIndex::Str(s) => s.try_into(),
JsonIndex::Num(n) => n.try_into(),
}
}
}
let map: Vec<(JsonIndex, JsonValue)> = match node {
JsonValue::Object(object) => object
.into_iter()
.map(|(k, v)| (JsonIndex::Str(k), v))
.collect(),
JsonValue::Array(array) => array
.into_iter()
.enumerate()
.map(|(i, v)| (JsonIndex::Num(i as u64), v))
.collect(),
_ => Err(SchemaError(format!(
"Jsonbfilter input node must be an object or array, but got {node}"
)))?,
};
let mut conditions = Condition::all();
for (key, mut value) in map {
if let JsonIndex::Str(str_key) = &key
&& let Some(operation) = JsonbFilterOperation::parse_str(str_key)?
{
match operation {
JsonbFilterOperation::And => {
let mut condition = Condition::all();
let filters = {
let a = value.as_array_mut().ok_or(SchemaError(
"$and operation must be an array of sub filters".to_string(),
))?;
let mut b = vec![];
std::mem::swap(a, &mut b);
b
};
for filter in filters {
let result = recursive_prepare_jsonb_node_condition(expr, filter, path)?;
condition = condition.add(result.0);
path = result.1;
}
conditions = conditions.add(condition);
}
JsonbFilterOperation::Or => {
let mut condition = Condition::any();
let values = {
let a = value
.as_array_mut()
.and_then(|arr| if arr.len() >= 2 { Some(arr) } else { None })
.ok_or(SchemaError(
"$or operation must be an array of at least two sub filters"
.to_string(),
))?;
let mut b = vec![];
std::mem::swap(a, &mut b);
b
};
for value in values {
let (c, rpath) = recursive_prepare_jsonb_node_condition(expr, value, path)?;
condition = condition.add(c);
path = rpath;
}
conditions = conditions.add(condition);
}
JsonbFilterOperation::JsonbPathQuery => {
path.push(JsonPathSegment::JsonbPathQuery(
str_key.split_at(7).1.to_string(),
))?;
let (condition, rpath) =
recursive_prepare_jsonb_node_condition(expr, value, path)?;
conditions = conditions.add(condition);
path = rpath;
path.pop();
}
JsonbFilterOperation::Match => {
if let JsonValue::String(s) = value {
match s.as_str() {
"$any" => {
continue;
}
_ => {
Err(SchemaError(format!(
"JsonbFilterInput leaf can not be $match with {s} value"
)))?;
}
}
} else {
Err(SchemaError(
"JsonbFilterInput leaf can not be $match with a non-string value"
.to_string(),
))?;
}
}
JsonbFilterOperation::Not => {
let (condition, rpath) =
recursive_prepare_jsonb_node_condition(expr, value, path)?;
conditions = conditions.add(condition.not());
path = rpath;
}
op => {
let condition = prepare_jsonb_leaf_condition(expr.clone(), op, value, &path)?;
conditions = conditions.add(condition);
}
}
} else {
let segment: JsonPathSegment = key.try_into()?;
path.push(segment)?;
let result = recursive_prepare_jsonb_node_condition(expr, value, path)?;
conditions = conditions.add(result.0);
path = result.1;
path.pop();
}
}
Ok((conditions, path))
}
pub fn prepare_jsonb_filter_input<E>(expr: &E, value: JsonValue) -> RecorderResult<Condition>
where
E: Into<SimpleExpr> + Clone,
{
let (condition, _) = recursive_prepare_jsonb_node_condition(expr, value, JsonPath::new())?;
Ok(condition)
}
pub const JSONB_FILTER_NAME: &str = "JsonbFilterInput";
pub fn generate_jsonb_filter_condition_function<T>(
_context: &BuilderContext,
column: &T::Column,
) -> FnFilterCondition
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let column = *column;
Box::new(
move |_resolve_context: &ResolverContext<'_>, condition, filter| {
if let Some(filter) = filter {
let filter_value = to_value(filter.as_index_map()).map_err(|e| {
SeaographyError::AsyncGraphQLError(GraphqlError::new_with_source(e))
})?;
let filter_json: JsonValue = filter_value.into_json().map_err(|e| {
SeaographyError::AsyncGraphQLError(GraphqlError::new(format!("{e:?}")))
})?;
let cond_where = prepare_jsonb_filter_input(&Expr::col(column), filter_json)
.map_err(|e| {
SeaographyError::AsyncGraphQLError(GraphqlError::new_with_source(e))
})?;
let condition = condition.add(cond_where);
Ok(condition)
} else {
Ok(condition)
}
},
)
}
pub fn register_jsonb_input_filter_to_schema_builder(
mut builder: SeaographyBuilder,
) -> SeaographyBuilder {
let json_filter_input_type = Scalar::new(JSONB_FILTER_NAME);
builder.schema = builder.schema.register(json_filter_input_type);
builder
}
pub fn restrict_jsonb_filter_input_for_entity<T>(context: &mut BuilderContext, column: &T::Column)
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_column_key = get_entity_column_key::<T>(context, column);
context.filter_types.overwrites.insert(
entity_column_key.clone(),
Some(FilterType::Custom(JSONB_FILTER_NAME.to_string())),
);
}
pub fn validate_jsonb_input_for_entity<T, S>(context: &mut BuilderContext, column: &T::Column)
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
S: DeserializeOwned + Serialize,
{
let entity_column_key = get_entity_column_key::<T>(context, column);
context.types.input_conversions.insert(
entity_column_key.clone(),
Box::new(move |_resolve_context, accessor| {
let deserialized = accessor.deserialize::<S>().map_err(|err| {
SeaographyError::TypeConversionError(
err.message,
format!("Json - {entity_column_key}"),
)
})?;
let json_value = serde_json::to_value(deserialized).map_err(|err| {
SeaographyError::TypeConversionError(
err.to_string(),
format!("Json - {entity_column_key}"),
)
})?;
Ok(sea_orm::Value::Json(Some(Box::new(json_value))))
}),
);
}
pub fn convert_jsonb_output_case_for_entity<T>(context: &mut BuilderContext, column: &T::Column)
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_column_key = get_entity_column_key::<T>(context, column);
context.types.output_conversions.insert(
entity_column_key.clone(),
Box::new(move |value| {
if let sea_orm::Value::Json(Some(json)) = value {
let result = async_graphql::Value::from_json(convert_json_keys(
json.as_ref().clone(),
Case::Camel,
))
.map_err(|err| {
SeaographyError::TypeConversionError(
err.to_string(),
format!("Json - {entity_column_key}"),
)
})?;
Ok(result)
} else {
Err(SeaographyError::TypeConversionError(
"value should be json".to_string(),
format!("Json - {entity_column_key}"),
))
}
}),
);
}
#[cfg(test)]
mod tests {
use std::assert_matches::assert_matches;
use sea_orm::{
DeriveIden,
sea_query::{IntoCondition, PostgresQueryBuilder, Query, Value, Values},
};
use serde_json::json;
use super::*;
use crate::errors::{RecorderError, RecorderResult};
#[derive(DeriveIden)]
enum TestTable {
Table,
Job,
}
fn build_test_query_sql(condition: impl IntoCondition) -> (String, Vec<Value>) {
let (sql, Values(values)) = Query::select()
.column(TestTable::Job)
.cond_where(condition)
.from(TestTable::Table)
.build(PostgresQueryBuilder);
(sql, values)
}
fn build_test_jsonb_path(path: &[&str]) -> JsonPath {
let mut p = JsonPath::new();
for s in path {
p.push(JsonPathSegment::Str(s.to_string())).unwrap();
}
p
}
#[test]
fn test_jsonb_path_exists_expr() {
let (sql, params) = build_test_query_sql(jsonb_path_exists_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE jsonb_path_exists(\"test_table\".\"job\", \
$1)"
);
assert_eq!(params[0], "$.a.b.c".into());
}
#[test]
fn test_jsonb_path_is_in_expr() -> RecorderResult<()> {
let (sql, params) = build_test_query_sql(jsonb_path_is_in_values_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
vec![json!(1), json!("str"), json!(true)],
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE \
jsonb_path_query_first(\"test_table\".\"job\", $1) = ANY($2)"
);
assert_eq!(params.len(), 2);
assert_eq!(params[0], DbValue::String(Some(Box::new("$.a.b.c".into()))));
assert_matches!(params[1], DbValue::Array(..));
Ok(())
}
#[test]
fn test_jsonb_path_eq_expr() -> RecorderResult<()> {
let (sql, params) = build_test_query_sql(jsonb_path_eq_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
json!("str"),
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE \
(jsonb_path_query_first(\"test_table\".\"job\", $1)) = $2"
);
assert_eq!(params.len(), 2);
assert_eq!(params[0], DbValue::String(Some(Box::new("$.a.b.c".into()))));
assert_eq!(params[1], DbValue::Json(Some(Box::new(json!("str")))));
Ok(())
}
#[test]
fn test_jsonb_path_type_assert_expr() {
let (sql, _) = build_test_query_sql(jsonb_path_type_assert_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
"string",
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE jsonb_path_exists(\"test_table\".\"job\", \
$1 || ' ? (@.type() = \"string\")')"
);
}
#[test]
fn test_jsonb_path_contains_expr() {
{
let (sql, params) = build_test_query_sql(jsonb_path_contains_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
json!(1),
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE (CASE WHEN \
((jsonb_path_exists(\"test_table\".\"job\", $1 || ' ? (@.type() = \"array\")')) \
AND (jsonb_path_query_first(\"test_table\".\"job\", $2) @> $3)) THEN true ELSE \
false END) = (true)"
);
assert_eq!(params.len(), 3);
assert_eq!(params[0], "$.a.b.c".into());
assert_eq!(params[1], "$.a.b.c".into());
assert_eq!(params[2], json!([1]).into());
}
{
let (sql, params) = build_test_query_sql(jsonb_path_contains_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
json!("str"),
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE (CASE WHEN \
((jsonb_path_exists(\"test_table\".\"job\", $1 || ' ? (@.type() = \"array\")')) \
AND (jsonb_path_query_first(\"test_table\".\"job\", $2) @> $3)) THEN true WHEN \
((jsonb_path_exists(\"test_table\".\"job\", $4 || ' ? (@.type() = \"string\")')) \
AND CAST((jsonb_path_query_first(\"test_table\".\"job\", $5)) AS text) LIKE $6) \
THEN true ELSE false END) = (true)"
);
assert_eq!(params.len(), 6);
assert_eq!(params[0], "$.a.b.c".into());
assert_eq!(params[1], "$.a.b.c".into());
assert_eq!(params[2], json!(["str"]).into());
assert_eq!(params[3], "$.a.b.c".into());
assert_eq!(params[4], "$.a.b.c".into());
assert_eq!(params[5], "%str%".into());
}
}
#[test]
fn test_jsonb_path_between_expr() {
{
let (sql, params) = build_test_query_sql(jsonb_path_num_between_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
Decimal::from(1),
Decimal::from(2),
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE \
(jsonb_path_exists(\"test_table\".\"job\", $1 || ' ? (@.type() = \"number\")')) \
AND (CAST((jsonb_path_query_first(\"test_table\".\"job\", $2)) AS numeric) \
BETWEEN $3 AND $4)"
);
assert_eq!(params.len(), 4);
assert_eq!(params[0], "$.a.b.c".into());
assert_eq!(params[1], "$.a.b.c".into());
assert_eq!(params[2], Decimal::from(1).into());
assert_eq!(params[3], Decimal::from(2).into());
}
{
let (sql, params) = build_test_query_sql(jsonb_path_str_between_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
"1".into(),
"2".into(),
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE \
(jsonb_path_exists(\"test_table\".\"job\", $1 || ' ? (@.type() = \"string\")')) \
AND (CAST((jsonb_path_query_first(\"test_table\".\"job\", $2)) AS text) BETWEEN \
$3 AND $4)"
);
assert_eq!(params.len(), 4);
assert_eq!(params[0], "$.a.b.c".into());
assert_eq!(params[1], "$.a.b.c".into());
assert_eq!(params[2], "1".into());
assert_eq!(params[3], "2".into());
}
{
let (sql, params) = build_test_query_sql(jsonb_path_bool_between_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
true,
false,
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE \
(jsonb_path_exists(\"test_table\".\"job\", $1 || ' ? (@.type() = \"boolean\")')) \
AND (CAST((jsonb_path_query_first(\"test_table\".\"job\", $2)) AS boolean) \
BETWEEN $3 AND $4)"
);
assert_eq!(params.len(), 4);
assert_eq!(params[0], "$.a.b.c".into());
assert_eq!(params[1], "$.a.b.c".into());
assert_eq!(params[2], true.into());
assert_eq!(params[3], false.into());
}
}
#[test]
fn test_jsonb_path_ends_with_expr() {
let (sql, params) = build_test_query_sql(jsonb_path_ends_with_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
"str%".into(),
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE (jsonb_path_exists(\"test_table\".\"job\", \
$1 || ' ? (@.type() = \"string\")')) AND \
CAST((jsonb_path_query_first(\"test_table\".\"job\", $2)) AS text) LIKE $3"
);
assert_eq!(params.len(), 3);
assert_eq!(params[0], "$.a.b.c".into());
assert_eq!(params[1], "$.a.b.c".into());
assert_eq!(params[2], "%str\\%".into());
}
#[test]
fn test_jsonb_path_starts_with_expr() {
let (sql, params) = build_test_query_sql(jsonb_path_starts_with_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
"%str%".into(),
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE (jsonb_path_exists(\"test_table\".\"job\", \
$1 || ' ? (@.type() = \"string\")')) AND \
(starts_with(CAST((jsonb_path_query_first(\"test_table\".\"job\", $2)) AS text), $3))"
);
assert_eq!(params.len(), 3);
assert_eq!(params[0], "$.a.b.c".into());
assert_eq!(params[1], "$.a.b.c".into());
assert_eq!(params[2], "%str%".into());
}
#[test]
fn test_jsonb_path_like_expr() {
let (sql, params) = build_test_query_sql(jsonb_path_like_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
"%str%",
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE (jsonb_path_exists(\"test_table\".\"job\", \
$1 || ' ? (@.type() = \"string\")')) AND \
CAST((jsonb_path_query_first(\"test_table\".\"job\", $2)) AS text) LIKE $3"
);
assert_eq!(params.len(), 3);
assert_eq!(params[0], "$.a.b.c".into());
assert_eq!(params[1], "$.a.b.c".into());
assert_eq!(params[2], "%str%".into());
}
#[test]
fn test_jsonb_path_is_not_null_expr() {
let (sql, params) = build_test_query_sql(jsonb_path_is_not_null_expr(
Expr::col((TestTable::Table, TestTable::Job)),
&build_test_jsonb_path(&["a", "b", "c"]),
));
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE jsonb_path_exists(\"test_table\".\"job\", \
$1 || ' ? (@ != null)')"
);
assert_eq!(params.len(), 1);
assert_eq!(params[0], "$.a.b.c".into());
}
#[test]
fn test_convert_jsonb_number_to_db_decimal() {
assert_eq!(
convert_jsonb_number_to_db_decimal(
serde_json::Number::from_f64(1.234_567_890_123_456_7).unwrap()
)
.unwrap(),
Decimal::from_f64(1.234_567_890_123_456_7).unwrap()
);
assert_eq!(
convert_jsonb_number_to_db_decimal(serde_json::Number::from(9007199254740991i64))
.unwrap(),
Decimal::from(9007199254740991i64)
);
}
#[test]
fn test_prepare_jsonb_filter_input() -> RecorderResult<()> {
{
let condition = prepare_jsonb_filter_input(
&Expr::col((TestTable::Table, TestTable::Job)),
json!({ "a": { "b": { "c": 1 } } }),
);
assert_matches!(condition, Err(RecorderError::GraphQLSchemaError { .. }));
}
{
let condition = prepare_jsonb_filter_input(
&Expr::col((TestTable::Table, TestTable::Job)),
json!({ "$and": [
{
"$or": [
{
"a": {
"b": {
"$eq": 1
}
}
},
{
"$not": {
"$query:.c.d.e": {
"$is_in": [1, "haha", true]
}
}
}
]
},
{
"d": [
{
"$match": "$any"
},
{
"$eq": [1, 2, 3]
}
]
}
] }),
)?;
let (sql, params) = build_test_query_sql(condition);
assert_eq!(
sql,
"SELECT \"job\" FROM \"test_table\" WHERE \
((jsonb_path_query_first(\"test_table\".\"job\", $1)) = $2 OR (NOT \
(jsonb_path_query_first(\"test_table\".\"job\", $3) = ANY($4)))) AND (TRUE AND \
(jsonb_path_query_first(\"test_table\".\"job\", $5)) = $6)"
);
assert_eq!(params.len(), 6);
assert_eq!(params[0], "$.a.b".into());
assert_eq!(params[1], json!(1).into());
assert_eq!(params[2], "$.c.d.e".into());
assert_matches!(params[3], DbValue::Array(ArrayType::Json, ..));
assert_eq!(params[4], "$.d[1]".into());
assert_eq!(params[5], json!([1, 2, 3]).into());
}
Ok(())
}
}