Skip to main content

sui_gql_client/
lib.rs

1//! # Sui GraphQL client
2//!
3//! First version of Aftermath's Sui GraphQL client using [`cynic`].
4//!
5//! The main item here is the [`GraphQlClient`](crate::GraphQlClient) trait, defining the common
6//! interface for clients interacting with an RPC. See the `reqwest` feature for a pre-made
7//! implementation.
8//!
9//! The queries inclued here (under feature `queries`) were constructed with the help of `cynic`s
10//! [generator] and use the scalars defined in [`sui_gql_schema`].
11//!
12//! ## Custom queries
13//!
14//! Users building their own queries should first:
15//! 1. add [`sui_gql_schema`] as a build dependency
16//! 1. register its schema in a `build.rs` file;
17//! 1. import the [`schema`](crate::schema) module in any module defining new fragments
18//!
19//! For steps 1 and 2, you can check this crate's `[build-dependencies]` and `build.rs` for an
20//! example of how to do so. Read more about schema crates in <https://cynic-rs.dev/large-apis>.
21//!
22//! Then, to create query structs, we recommend using the [generator] with Sui's GraphQL
23//! [schema][sui_schema] and to try reusing the scalars defined in [`scalars`](crate::scalars)
24//! as those automatically convert opaque types to more useful ones like [`af_sui_types`].
25//!
26//! ## Features
27//!
28//! - `move-types`: compatibility with `af-move-type` types
29//! - `mutations`: enables the `mutations` submodule
30//! - `queries`: enables the `queries` submodule with pre-made queries
31//! - `reqwest`: enables the `reqwest` submodule with an implementation of
32//!   [`GraphQlClient`](crate::GraphQlClient)
33//! - `scalars`: re-exports the `scalars` module of [`sui_gql_schema`]
34//!
35//! ## Handy links:
36//!
37//! - Query builder: [generator.cynic-rs.dev][generator]. When prompted either
38//!   - click the "A URL" button and pass in:
39//!     - `https://sui-testnet.mystenlabs.com/graphql` to build queries against the testnet schema
40//!     - `https://sui-mainnet.mystenlabs.com/graphql` for the mainnet one
41//!   - click the "I'll Paste It" button and paste the [schema][sui_schema]
42//! - Cynic's [guide](https://cynic-rs.dev/)
43//!
44//! [`cynic`]: crate::cynic
45//! [`sui_gql_schema`]: https://docs.rs/sui-gql-schema/latest/sui_gql_schema/
46//! [generator]: https://generator.cynic-rs.dev/
47//! [sui_schema]: https://github.com/MystenLabs/sui/blob/main/crates/sui-graphql-rpc/schema.graphql
48//! [`af_sui_types`]: https://docs.rs/af-sui-types/latest/af_sui_types/
49
50pub use cynic;
51use cynic::schema::{MutationRoot, QueryRoot};
52use cynic::serde::Serialize;
53use cynic::serde::de::DeserializeOwned;
54use cynic::{GraphQlError, GraphQlResponse, Operation, QueryFragment, QueryVariables};
55use extension_traits::extension;
56pub use sui_gql_schema::{scalars, schema};
57
58pub mod queries;
59mod raw_client;
60pub mod reqwest;
61
62#[deprecated(since = "0.14.8", note = "use the graphql-extract crate")]
63pub mod extract;
64mod paged;
65
66pub use self::paged::{Paged, PagedResponse, PagesDataResult};
67pub use self::raw_client::{Error as RawClientError, RawClient};
68
69/// A generic GraphQL client. Agnostic to the backend used.
70#[trait_variant::make(Send)]
71pub trait GraphQlClient: Sync {
72    type Error: std::error::Error + Send + 'static;
73
74    async fn query_paged<Init>(&self, vars: Init::Input) -> Result<PagedResponse<Init>, Self::Error>
75    where
76        Init: Paged + Send + 'static,
77        Init::SchemaType: QueryRoot,
78        Init::Input: Clone,
79        Init::NextPage:
80            Paged<Input = Init::NextInput, NextInput = Init::NextInput, NextPage = Init::NextPage>,
81        <Init::NextPage as QueryFragment>::SchemaType: QueryRoot,
82        <Init::NextPage as Paged>::Input: Clone,
83    {
84        async {
85            let initial: GraphQlResponse<Init> = self.query(vars.clone()).await?;
86            let mut next_vars = initial.data.as_ref().and_then(|d| d.next_variables(vars));
87            let mut pages = vec![];
88            while let Some(vars) = next_vars {
89                let next_page: GraphQlResponse<Init::NextPage> = self.query(vars.clone()).await?;
90                next_vars = next_page.data.as_ref().and_then(|d| d.next_variables(vars));
91                pages.push(next_page);
92            }
93            Ok(PagedResponse(initial, pages))
94        }
95    }
96
97    async fn query<Query, Variables>(
98        &self,
99        vars: Variables,
100    ) -> Result<GraphQlResponse<Query>, Self::Error>
101    where
102        Variables: QueryVariables + Send + Serialize,
103        Query: DeserializeOwned + QueryFragment<VariablesFields = Variables::Fields> + 'static,
104        Query::SchemaType: QueryRoot,
105    {
106        use cynic::QueryBuilder as _;
107        self.run_graphql(Query::build(vars))
108    }
109
110    async fn mutation<Mutation, Vars>(
111        &self,
112        vars: Vars,
113    ) -> Result<GraphQlResponse<Mutation>, Self::Error>
114    where
115        Vars: QueryVariables + Send + Serialize,
116        Mutation: DeserializeOwned + QueryFragment<VariablesFields = Vars::Fields> + 'static,
117        Mutation::SchemaType: MutationRoot,
118    {
119        use cynic::MutationBuilder as _;
120        self.run_graphql(Mutation::build(vars))
121    }
122
123    async fn run_graphql<Query, Vars>(
124        &self,
125        operation: Operation<Query, Vars>,
126    ) -> Result<GraphQlResponse<Query>, Self::Error>
127    where
128        Vars: Serialize + Send,
129        Query: DeserializeOwned + 'static;
130}
131
132/// Adds [`try_into_data`](GraphQlResponseExt::try_into_data).
133#[extension(pub trait GraphQlResponseExt)]
134impl<T> GraphQlResponse<T> {
135    /// Extract the `data` field from the response, if any, or fail if the `errors` field contains
136    /// any errors.
137    fn try_into_data(self) -> Result<Option<T>, GraphQlErrors> {
138        if let Some(errors) = self.errors
139            && !errors.is_empty()
140        {
141            return Err(GraphQlErrors { errors, page: None });
142        }
143
144        let Some(data) = self.data else {
145            return Ok(None);
146        };
147        Ok(Some(data))
148    }
149}
150
151/// Error for [`GraphQlResponseExt::try_into_data`].
152#[derive(thiserror::Error, Clone, Debug, Eq, PartialEq, serde::Deserialize)]
153pub struct GraphQlErrors<Extensions = serde::de::IgnoredAny> {
154    pub errors: Vec<GraphQlError<Extensions>>,
155    pub page: Option<usize>,
156}
157
158impl<Extensions> std::fmt::Display for GraphQlErrors<Extensions> {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        let page_info = self
161            .page
162            .map_or_else(String::new, |page| format!(" at page {page}"));
163        writeln!(
164            f,
165            "Query execution produced the following errors{page_info}:"
166        )?;
167        for error in &self.errors {
168            writeln!(f, "{error}")?;
169        }
170        Ok(())
171    }
172}