use async_graphql::dynamic::{Field, FieldFuture, Object};
use async_graphql::{Error, Value};
use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
use sea_orm::{ColumnTrait, ColumnType, EntityName, EntityTrait, IdenStatic, Iterable, ModelTrait};
pub struct EntityObjectConfig {
pub type_name: crate::SimpleNamingFn,
pub column_name: crate::ComplexNamingFn,
pub basic_type_suffix: String,
}
impl std::default::Default for EntityObjectConfig {
fn default() -> Self {
Self {
type_name: Box::new(|entity_name: &str| -> String {
entity_name.to_upper_camel_case()
}),
column_name: Box::new(|_entity_name: &str, column_name: &str| -> String {
if cfg!(feature = "field-snake-case") {
column_name.to_snake_case()
} else {
column_name.to_lower_camel_case()
}
}),
basic_type_suffix: "Basic".into(),
}
}
}
use crate::{format_variant, BuilderContext, GuardAction, TypesMapHelper};
pub struct EntityObjectBuilder {
pub context: &'static BuilderContext,
}
impl EntityObjectBuilder {
pub fn type_name<T>(&self) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let name: String = <T as EntityName>::table_name(&T::default()).into();
self.context.entity_object.type_name.as_ref()(&name)
}
pub fn basic_type_name<T>(&self) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let name: String = <T as EntityName>::table_name(&T::default()).into();
format!(
"{}{}",
self.context.entity_object.type_name.as_ref()(&name),
self.context.entity_object.basic_type_suffix
)
}
pub fn column_name<T>(&self, column: &T::Column) -> String
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = self.type_name::<T>();
let column_name: String = column.as_str().into();
self.context.entity_object.column_name.as_ref()(&entity_name, &column_name)
}
pub fn to_object<T>(&self) -> Object
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let object_name = self.type_name::<T>();
self.basic_object::<T>(&object_name)
}
pub fn basic_to_object<T>(&self) -> Object
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let object_name = self.basic_type_name::<T>();
self.basic_object::<T>(&object_name)
}
fn basic_object<T>(&self, object_name: &str) -> Object
where
T: EntityTrait,
<T as EntityTrait>::Model: Sync,
{
let entity_name = self.type_name::<T>();
let types_map_helper = TypesMapHelper {
context: self.context,
};
T::Column::iter().fold(Object::new(object_name), |object, column: T::Column| {
let column_name = self.column_name::<T>(&column);
let column_def = column.def();
let enum_type_name = column.enum_type_name();
let graphql_type = match types_map_helper.sea_orm_column_type_to_graphql_type(
column_def.get_column_type(),
!column_def.is_null(),
enum_type_name,
) {
Some(type_name) => type_name,
None => return object,
};
let is_enum: bool = match column_def.get_column_type() {
ColumnType::Enum { .. } => true,
#[cfg(feature = "with-postgres-array")]
ColumnType::Array(inner) => matches!(inner.as_ref(), ColumnType::Enum { .. }),
_ => false,
};
let guard = self
.context
.guards
.field_guards
.get(&format!("{}.{}", &object_name, &column_name));
let conversion_fn = self
.context
.types
.output_conversions
.get(&format!("{entity_name}.{column_name}"));
let field = Field::new(column_name, graphql_type, move |ctx| {
let guard_flag = if let Some(guard) = guard {
(*guard)(&ctx)
} else {
GuardAction::Allow
};
if let GuardAction::Block(reason) = guard_flag {
return FieldFuture::new(async move {
match reason {
Some(reason) => {
Err::<Option<()>, async_graphql::Error>(Error::new(reason))
}
None => Err::<Option<()>, async_graphql::Error>(Error::new(
"Field guard triggered.",
)),
}
});
}
let object = ctx
.parent_value
.try_downcast_ref::<T::Model>()
.expect("Something went wrong when trying to downcast entity object.");
if let Some(conversion_fn) = conversion_fn {
let result = conversion_fn(&object.get(column));
return FieldFuture::new(async move {
match result {
Ok(value) => Ok(Some(value)),
Err(_) => Ok(None),
}
});
}
FieldFuture::new(async move {
Ok(sea_query_value_to_graphql_value(
object.get(column),
is_enum,
))
})
});
object.field(field)
})
}
}
fn sea_query_value_to_graphql_value(
sea_query_value: sea_orm::sea_query::Value,
is_enum: bool,
) -> Option<Value> {
match sea_query_value {
sea_orm::Value::Bool(value) => value.map(Value::from),
sea_orm::Value::TinyInt(value) => value.map(Value::from),
sea_orm::Value::SmallInt(value) => value.map(Value::from),
sea_orm::Value::Int(value) => value.map(Value::from),
sea_orm::Value::BigInt(value) => value.map(Value::from),
sea_orm::Value::TinyUnsigned(value) => value.map(Value::from),
sea_orm::Value::SmallUnsigned(value) => value.map(Value::from),
sea_orm::Value::Unsigned(value) => value.map(Value::from),
sea_orm::Value::BigUnsigned(value) => value.map(Value::from),
sea_orm::Value::Float(value) => value.map(Value::from),
sea_orm::Value::Double(value) => value.map(Value::from),
sea_orm::Value::String(value) if is_enum => {
value.map(|it| Value::from(format_variant(it.as_str())))
}
sea_orm::Value::String(value) => value.map(|it| Value::from(it.as_str())),
sea_orm::Value::Char(value) => value.map(|it| Value::from(it.to_string())),
#[allow(clippy::box_collection)]
sea_orm::Value::Bytes(value) => value.map(|it| Value::from(String::from_utf8_lossy(&it))),
#[cfg(feature = "with-postgres-array")]
sea_orm::Value::Array(_array_value, value) => value.map(|it| {
Value::List(
it.into_iter()
.map(|item| {
sea_query_value_to_graphql_value(item, is_enum).unwrap_or(Value::Null)
})
.collect(),
)
}),
#[cfg(feature = "with-json")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-json")))]
sea_orm::sea_query::Value::Json(value) => value.map(|it| Value::from(it.to_string())),
#[cfg(feature = "with-chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
sea_orm::sea_query::Value::ChronoDate(value) => value.map(|it| Value::from(it.to_string())),
#[cfg(feature = "with-chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
sea_orm::sea_query::Value::ChronoTime(value) => value.map(|it| Value::from(it.to_string())),
#[cfg(feature = "with-chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
sea_orm::sea_query::Value::ChronoDateTime(value) => {
value.map(|it| Value::from(it.to_string()))
}
#[cfg(feature = "with-chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
sea_orm::sea_query::Value::ChronoDateTimeUtc(value) => {
value.map(|it| Value::from(it.to_string()))
}
#[cfg(feature = "with-chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
sea_orm::sea_query::Value::ChronoDateTimeLocal(value) => {
value.map(|it| Value::from(it.to_string()))
}
#[cfg(feature = "with-chrono")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-chrono")))]
sea_orm::sea_query::Value::ChronoDateTimeWithTimeZone(value) => {
value.map(|it| Value::from(it.to_string()))
}
#[cfg(feature = "with-time")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-time")))]
sea_orm::sea_query::Value::TimeDate(value) => value.map(|it| Value::from(it.to_string())),
#[cfg(feature = "with-time")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-time")))]
sea_orm::sea_query::Value::TimeTime(value) => value.map(|it| Value::from(it.to_string())),
#[cfg(feature = "with-time")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-time")))]
sea_orm::sea_query::Value::TimeDateTime(value) => {
value.map(|it| Value::from(it.to_string()))
}
#[cfg(feature = "with-time")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-time")))]
sea_orm::sea_query::Value::TimeDateTimeWithTimeZone(value) => {
value.map(|it| Value::from(it.to_string()))
}
#[cfg(feature = "with-uuid")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-uuid")))]
sea_orm::sea_query::Value::Uuid(value) => value.map(|it| Value::from(it.to_string())),
#[cfg(feature = "with-decimal")]
sea_orm::sea_query::Value::Decimal(value) => value.map(|it| Value::from(it.to_string())),
#[cfg(feature = "with-bigdecimal")]
#[cfg_attr(docsrs, doc(cfg(feature = "with-bigdecimal")))]
sea_orm::sea_query::Value::BigDecimal(value) => value.map(|it| Value::from(it.to_string())),
#[allow(unreachable_patterns)]
_ => panic!("Cannot convert SeaORM value"),
}
}