sui_gql_client/
lib.rs

1#![cfg_attr(all(doc, not(doctest)), feature(doc_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;
58pub use sui_gql_schema::{scalars, schema};
59
60pub mod queries;
61mod raw_client;
62pub mod reqwest;
63
64#[deprecated(since = "0.14.8", note = "use the graphql-extract crate")]
65pub mod extract;
66mod paged;
67
68pub use self::paged::{Paged, PagedResponse, PagesDataResult};
69pub use self::raw_client::{Error as RawClientError, RawClient};
70
71/// A generic GraphQL client. Agnostic to the backend used.
72#[trait_variant::make(Send)]
73pub trait GraphQlClient: Sync {
74    type Error: std::error::Error + Send + 'static;
75
76    async fn query_paged<Init>(&self, vars: Init::Input) -> Result<PagedResponse<Init>, Self::Error>
77    where
78        Init: Paged + Send + 'static,
79        Init::SchemaType: QueryRoot,
80        Init::Input: Clone,
81        Init::NextPage:
82            Paged<Input = Init::NextInput, NextInput = Init::NextInput, NextPage = Init::NextPage>,
83        <Init::NextPage as QueryFragment>::SchemaType: QueryRoot,
84        <Init::NextPage as Paged>::Input: Clone,
85    {
86        async {
87            let initial: GraphQlResponse<Init> = self.query(vars.clone()).await?;
88            let mut next_vars = initial.data.as_ref().and_then(|d| d.next_variables(vars));
89            let mut pages = vec![];
90            while let Some(vars) = next_vars {
91                let next_page: GraphQlResponse<Init::NextPage> = self.query(vars.clone()).await?;
92                next_vars = next_page.data.as_ref().and_then(|d| d.next_variables(vars));
93                pages.push(next_page);
94            }
95            Ok(PagedResponse(initial, pages))
96        }
97    }
98
99    async fn query<Query, Variables>(
100        &self,
101        vars: Variables,
102    ) -> Result<GraphQlResponse<Query>, Self::Error>
103    where
104        Variables: QueryVariables + Send + Serialize,
105        Query: DeserializeOwned + QueryFragment<VariablesFields = Variables::Fields> + 'static,
106        Query::SchemaType: QueryRoot,
107    {
108        use cynic::QueryBuilder as _;
109        self.run_graphql(Query::build(vars))
110    }
111
112    async fn mutation<Mutation, Vars>(
113        &self,
114        vars: Vars,
115    ) -> Result<GraphQlResponse<Mutation>, Self::Error>
116    where
117        Vars: QueryVariables + Send + Serialize,
118        Mutation: DeserializeOwned + QueryFragment<VariablesFields = Vars::Fields> + 'static,
119        Mutation::SchemaType: MutationRoot,
120    {
121        use cynic::MutationBuilder as _;
122        self.run_graphql(Mutation::build(vars))
123    }
124
125    async fn run_graphql<Query, Vars>(
126        &self,
127        operation: Operation<Query, Vars>,
128    ) -> Result<GraphQlResponse<Query>, Self::Error>
129    where
130        Vars: Serialize + Send,
131        Query: DeserializeOwned + 'static;
132}
133
134/// Adds [`try_into_data`](GraphQlResponseExt::try_into_data).
135#[extension(pub trait GraphQlResponseExt)]
136impl<T> GraphQlResponse<T> {
137    /// Extract the `data` field from the response, if any, or fail if the `errors` field contains
138    /// any errors.
139    fn try_into_data(self) -> Result<Option<T>, GraphQlErrors> {
140        if let Some(errors) = self.errors {
141            if !errors.is_empty() {
142                return Err(GraphQlErrors { errors, page: None });
143            }
144        }
145
146        let Some(data) = self.data else {
147            return Ok(None);
148        };
149        Ok(Some(data))
150    }
151}
152
153/// Error for [`GraphQlResponseExt::try_into_data`].
154#[derive(thiserror::Error, Clone, Debug, Eq, PartialEq, serde::Deserialize)]
155pub struct GraphQlErrors<Extensions = serde::de::IgnoredAny> {
156    pub errors: Vec<GraphQlError<Extensions>>,
157    pub page: Option<usize>,
158}
159
160impl<Extensions> std::fmt::Display for GraphQlErrors<Extensions> {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        let page_info = self
163            .page
164            .map_or_else(String::new, |page| format!(" at page {page}"));
165        writeln!(
166            f,
167            "Query execution produced the following errors{page_info}:"
168        )?;
169        for error in &self.errors {
170            writeln!(f, "{error}")?;
171        }
172        Ok(())
173    }
174}