1//! Relay support for [async-graphql](https://github.com/async-graphql/async-graphql).
2//! Check out [the example application](https://github.com/oscartbeaumont/async-graphql-relay/tree/main/example) to get started.
34#![forbid(unsafe_code)]
5#![warn(missing_docs)]
67use std::{any::Any, fmt, marker::PhantomData, str::FromStr};
89use async_graphql::{Error, InputValueError, InputValueResult, Scalar, ScalarType, Value};
1011pub use async_graphql_relay_derive::*;
12use uuid::Uuid;
1314/// RelayNodeInterface is a trait implemented by the GraphQL interface enum to implement the fetch_node method.
15/// You should refer to the 'RelayInterface' macro which is the recommended way to implement this trait.
16pub trait RelayNodeInterface
17where
18Self: Sized,
19{
20/// fetch_node takes in a RelayContext and a generic relay ID and will return a Node interface with the requested object.
21 /// This function is used to implement the 'node' query required by the Relay server specification for easily refetching an entity in the GraphQL schema.
22fn fetch_node(
23 ctx: RelayContext,
24 relay_id: String,
25 ) -> impl std::future::Future<Output = Result<Self, Error>> + Send;
26}
2728/// RelayNodeStruct is a trait implemented by the GraphQL Object to ensure each Object has a globally unique ID.
29/// You should refer to the 'RelayNodeObject' macro which is the recommended way to implement this trait.
30/// You MUST ensure the ID_SUFFIX is unique for each object for issue will occur.
31pub trait RelayNodeStruct {
32/// ID_SUFFIX is the suffix appended to the nodes ID to create the relay ID.
33 /// This MUST be unique for each type in the system.
34const ID_SUFFIX: &'static str;
35}
3637/// RelayNode is a trait implemented on the GraphQL Object to define how it should be fetched.
38/// This is used by the 'node' query so that the object can be refetched.
39pub trait RelayNode: RelayNodeStruct {
40/// TNode is the type of the Node interface. This should point the enum with the 'RelayInterface' macro.
41type TNode: RelayNodeInterface;
4243/// get is a method defines by the user to refetch an object of a particular type.
44 /// The context can be used to share a database connection or other required context to facilitate the refetch.
45fn get(
46 ctx: RelayContext,
47 id: RelayNodeID<Self>,
48 ) -> impl std::future::Future<Output = Result<Option<Self::TNode>, Error>> + Send;
49}
5051/// RelayNodeID is a wrapper around a UUID with the use of the 'RelayNodeStruct' trait to ensure each object has a globally unique ID.
52#[derive(Clone, PartialEq, Eq)]
53pub struct RelayNodeID<T: RelayNode + ?Sized>(Uuid, PhantomData<T>);
5455impl<T: RelayNode> RelayNodeID<T> {
56/// new creates a new RelayNodeID from a UUID string and a type specified as a generic.
57pub fn new(uuid: Uuid) -> Self {
58 RelayNodeID(uuid, PhantomData)
59 }
6061/// new_from_relay_id takes in a generic relay ID and converts it into a RelayNodeID.
62pub fn new_from_relay_id(relay_id: String) -> Result<Self, Error> {
63if relay_id.len() < 32 {
64return Err(Error::new("Invalid id provided to node query!"));
65 }
66let (id, _) = relay_id.split_at(32);
67let uuid = Uuid::parse_str(id)
68 .map_err(|_err| Error::new("Invalid id provided to node query!"))?;
69Ok(RelayNodeID(uuid, PhantomData))
70 }
7172/// new_from_str is a wrapper around 'Uuid::from_str' to create a new RelayNodeID from a UUIDv4 string.
73pub fn new_from_str(uuid: &str) -> Result<Self, uuid::Error> {
74Ok(Self::new(Uuid::from_str(uuid)?))
75 }
7677/// to_uuid will convert the RelayNodeID into a normal UUID for use in DB queries or internal processing.
78 /// The Uuid from this function is NOT globally unique!
79pub fn to_uuid(&self) -> Uuid {
80self.0
81}
82}
8384impl<T: RelayNode> From<&RelayNodeID<T>> for String {
85fn from(id: &RelayNodeID<T>) -> Self {
86format!("{}{}", id.0.simple(), T::ID_SUFFIX)
87 }
88}
8990impl<T: RelayNode> std::fmt::Display for RelayNodeID<T> {
91fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92write!(f, "{}", String::from(self))
93 }
94}
9596impl<T: RelayNode> fmt::Debug for RelayNodeID<T> {
97fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98 f.debug_tuple("RelayNodeID").field(&self.0).finish()
99 }
100}
101102#[Scalar]
103impl<T: RelayNode + Send + Sync> ScalarType for RelayNodeID<T> {
104fn parse(value: Value) -> InputValueResult<Self> {
105match value {
106 Value::String(s) => Ok(RelayNodeID::<T>::new_from_str(&s)?),
107_ => Err(InputValueError::expected_type(value)),
108 }
109 }
110111fn to_value(&self) -> Value {
112 Value::String(String::from(self))
113 }
114}
115116/// RelayContext allows context to be parsed to the `get` handler to facilitate refetching of objects.
117/// This is designed for parsing the Database connection but could be used for any global state.
118pub struct RelayContext(Box<dyn Any + Sync + Send>);
119120impl RelayContext {
121/// Create a new context which stores a piece of data.
122pub fn new<T: Any + Sync + Send>(data: T) -> Self {
123Self(Box::new(data))
124 }
125126/// Create a new empty context. This can be used if you have no data to put in the context.
127pub fn nil() -> Self {
128let nil: Option<()> = None;
129Self(Box::new(nil))
130 }
131132/// Get a pointer to the data stored in the context if it can be found.
133pub fn get<T: Any + Sync + Send>(&self) -> Option<&T> {
134match self.0.downcast_ref::<T>() {
135Some(v) => Some(v),
136_ => None,
137 }
138 }
139}
140141#[cfg(feature = "sea-orm")]
142impl<T: RelayNode> From<RelayNodeID<T>> for sea_orm::Value {
143fn from(source: RelayNodeID<T>) -> Self {
144 sea_orm::Value::Uuid(Some(Box::new(source.to_uuid())))
145 }
146}
147148#[cfg(feature = "sea-orm")]
149impl<T: RelayNode> sea_orm::TryGetable for RelayNodeID<T> {
150fn try_get_by<I: sea_orm::ColIdx>(
151 res: &sea_orm::QueryResult,
152 index: I,
153 ) -> Result<Self, sea_orm::TryGetError> {
154let val: Uuid = res.try_get_by(index).map_err(sea_orm::TryGetError::DbErr)?;
155Ok(RelayNodeID::<T>::new(val))
156 }
157}
158159#[cfg(feature = "sea-orm")]
160impl<T: RelayNode> sea_orm::sea_query::Nullable for RelayNodeID<T> {
161fn null() -> sea_orm::Value {
162 sea_orm::Value::Uuid(None)
163 }
164}
165166#[cfg(feature = "sea-orm")]
167impl<T: RelayNode> sea_orm::sea_query::ValueType for RelayNodeID<T> {
168fn try_from(v: sea_orm::Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
169match v {
170 sea_orm::Value::Uuid(Some(x)) => Ok(RelayNodeID::<T>::new(*x)),
171_ => Err(sea_orm::sea_query::ValueTypeErr),
172 }
173 }
174175fn type_name() -> String {
176stringify!(Uuid).to_owned()
177 }
178179fn column_type() -> sea_orm::sea_query::ColumnType {
180 sea_orm::sea_query::ColumnType::Uuid
181 }
182183fn array_type() -> sea_orm::sea_query::ArrayType {
184 sea_orm::sea_query::ArrayType::Uuid
185 }
186}
187188#[cfg(feature = "sea-orm")]
189impl<T: RelayNode> sea_orm::TryFromU64 for RelayNodeID<T> {
190fn try_from_u64(_: u64) -> Result<Self, sea_orm::DbErr> {
191Err(sea_orm::DbErr::Exec(sea_orm::error::RuntimeErr::Internal(
192format!(
193"{} cannot be converted from u64",
194 std::any::type_name::<T>()
195 ),
196 )))
197 }
198}
199200#[cfg(feature = "serde")]
201impl<T: RelayNode> serde::Serialize for RelayNodeID<T> {
202fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
203where
204S: serde::Serializer,
205 {
206 serializer.serialize_str(&self.to_string())
207 }
208}
209210// TODO: Unit tests