async_graphql_relay/
lib.rs

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.
3
4#![forbid(unsafe_code)]
5#![warn(missing_docs)]
6
7use std::{any::Any, fmt, marker::PhantomData, str::FromStr};
8
9use async_graphql::{Error, InputValueError, InputValueResult, Scalar, ScalarType, Value};
10
11pub use async_graphql_relay_derive::*;
12use async_trait::async_trait;
13use uuid::Uuid;
14
15#[doc(hidden)]
16pub use async_trait::async_trait as _async_trait;
17
18/// RelayNodeInterface is a trait implemented by the GraphQL interface enum to implement the fetch_node method.
19/// You should refer to the 'RelayInterface' macro which is the recommended way to implement this trait.
20#[async_trait]
21pub trait RelayNodeInterface
22where
23    Self: Sized,
24{
25    /// fetch_node takes in a RelayContext and a generic relay ID and will return a Node interface with the requested object.
26    /// This function is used to implement the 'node' query required by the Relay server specification for easily refetching an entity in the GraphQL schema.
27    async fn fetch_node(ctx: RelayContext, relay_id: String) -> Result<Self, Error>;
28}
29
30/// RelayNodeStruct is a trait implemented by the GraphQL Object to ensure each Object has a globally unique ID.
31/// You should refer to the 'RelayNodeObject' macro which is the recommended way to implement this trait.
32/// You MUST ensure the ID_SUFFIX is unique for each object for issue will occur.
33pub trait RelayNodeStruct {
34    /// ID_SUFFIX is the suffix appended to the nodes ID to create the relay ID.
35    /// This MUST be unique for each type in the system.
36    const ID_SUFFIX: &'static str;
37}
38
39/// RelayNode is a trait implemented on the GraphQL Object to define how it should be fetched.
40/// This is used by the 'node' query so that the object can be refetched.
41#[async_trait]
42pub trait RelayNode: RelayNodeStruct {
43    /// TNode is the type of the Node interface. This should point the enum with the 'RelayInterface' macro.
44    type TNode: RelayNodeInterface;
45
46    /// get is a method defines by the user to refetch an object of a particular type.
47    /// The context can be used to share a database connection or other required context to facilitate the refetch.
48    async fn get(ctx: RelayContext, id: RelayNodeID<Self>) -> Result<Option<Self::TNode>, Error>;
49}
50
51/// 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>);
54
55impl<T: RelayNode> RelayNodeID<T> {
56    /// new creates a new RelayNodeID from a UUID string and a type specified as a generic.
57    pub fn new(uuid: Uuid) -> Self {
58        RelayNodeID(uuid, PhantomData)
59    }
60
61    /// new_from_relay_id takes in a generic relay ID and converts it into a RelayNodeID.
62    pub fn new_from_relay_id(relay_id: String) -> Result<Self, Error> {
63        if relay_id.len() < 32 {
64            return Err(Error::new("Invalid id provided to node query!"));
65        }
66        let (id, _) = relay_id.split_at(32);
67        let uuid = Uuid::parse_str(&id)
68            .map_err(|_err| Error::new("Invalid id provided to node query!"))?;
69        Ok(RelayNodeID(uuid, PhantomData))
70    }
71
72    /// new_from_str is a wrapper around 'Uuid::from_str' to create a new RelayNodeID from a UUIDv4 string.
73    pub fn new_from_str(uuid: &str) -> Result<Self, uuid::Error> {
74        Ok(Self::new(Uuid::from_str(uuid)?))
75    }
76
77    /// 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!
79    pub fn to_uuid(&self) -> Uuid {
80        self.0
81    }
82}
83
84impl<T: RelayNode> From<&RelayNodeID<T>> for String {
85    fn from(id: &RelayNodeID<T>) -> Self {
86        format!("{}{}", id.0.to_simple().to_string(), T::ID_SUFFIX)
87    }
88}
89
90impl<T: RelayNode> ToString for RelayNodeID<T> {
91    fn to_string(&self) -> String {
92        String::from(self)
93    }
94}
95
96impl<T: RelayNode> fmt::Debug for RelayNodeID<T> {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.debug_tuple("RelayNodeID").field(&self.0).finish()
99    }
100}
101
102#[Scalar]
103impl<T: RelayNode + Send + Sync> ScalarType for RelayNodeID<T> {
104    fn parse(value: Value) -> InputValueResult<Self> {
105        match value {
106            Value::String(s) => Ok(RelayNodeID::<T>::new_from_str(&s)?),
107            _ => Err(InputValueError::expected_type(value)),
108        }
109    }
110
111    fn to_value(&self) -> Value {
112        Value::String(String::from(self))
113    }
114}
115
116/// 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>);
119
120impl RelayContext {
121    /// Create a new context which stores a piece of data.
122    pub fn new<T: Any + Sync + Send>(data: T) -> Self {
123        Self(Box::new(data))
124    }
125
126    /// Create a new empty context. This can be used if you have no data to put in the context.
127    pub fn nil() -> Self {
128        let nil: Option<()> = None;
129        Self(Box::new(nil))
130    }
131
132    /// Get a pointer to the data stored in the context if it can be found.
133    pub fn get<T: Any + Sync + Send>(&self) -> Option<&T> {
134        match self.0.downcast_ref::<T>() {
135            Some(v) => Some(v),
136            _ => None,
137        }
138    }
139}
140
141#[cfg(feature = "sea-orm")]
142impl<T: RelayNode> From<RelayNodeID<T>> for sea_orm::Value {
143    fn from(source: RelayNodeID<T>) -> Self {
144        sea_orm::Value::Uuid(Some(Box::new(source.to_uuid())))
145    }
146}
147
148#[cfg(feature = "sea-orm")]
149impl<T: RelayNode> sea_orm::TryGetable for RelayNodeID<T> {
150    fn try_get(
151        res: &sea_orm::QueryResult,
152        pre: &str,
153        col: &str,
154    ) -> Result<Self, sea_orm::TryGetError> {
155        let val: Uuid = res.try_get(pre, col).map_err(sea_orm::TryGetError::DbErr)?;
156        Ok(RelayNodeID::<T>::new(val))
157    }
158}
159
160#[cfg(feature = "sea-orm")]
161impl<T: RelayNode> sea_orm::sea_query::Nullable for RelayNodeID<T> {
162    fn null() -> sea_orm::Value {
163        sea_orm::Value::Uuid(None)
164    }
165}
166
167#[cfg(feature = "sea-orm")]
168impl<T: RelayNode> sea_orm::sea_query::ValueType for RelayNodeID<T> {
169    fn try_from(v: sea_orm::Value) -> Result<Self, sea_orm::sea_query::ValueTypeErr> {
170        match v {
171            sea_orm::Value::Uuid(Some(x)) => Ok(RelayNodeID::<T>::new(*x)),
172            _ => Err(sea_orm::sea_query::ValueTypeErr),
173        }
174    }
175
176    fn type_name() -> String {
177        stringify!(Uuid).to_owned()
178    }
179
180    fn column_type() -> sea_orm::sea_query::ColumnType {
181        sea_orm::sea_query::ColumnType::Uuid
182    }
183}
184
185#[cfg(feature = "sea-orm")]
186impl<T: RelayNode> sea_orm::TryFromU64 for RelayNodeID<T> {
187    fn try_from_u64(_: u64) -> Result<Self, sea_orm::DbErr> {
188        Err(sea_orm::DbErr::Exec(format!(
189            "{} cannot be converted from u64",
190            std::any::type_name::<T>()
191        )))
192    }
193}
194
195#[cfg(feature = "serde")]
196impl<T: RelayNode> serde::Serialize for RelayNodeID<T> {
197    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
198    where
199        S: serde::Serializer,
200    {
201        serializer.serialize_str(&self.to_string())
202    }
203}
204
205// TODO: Unit tests