sui_gql_client/
lib.rs

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