sui_gql_client/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
#![cfg_attr(all(doc, not(doctest)), feature(doc_auto_cfg))]

//! # Sui GraphQL client
//!
//! First version of Aftermath's Sui GraphQL client using [`cynic`].
//!
//! The main item here is the [`GraphQlClient`](crate::GraphQlClient) trait, defining the common
//! interface for clients interacting with an RPC. See the `reqwest` feature for a pre-made
//! implementation.
//!
//! The queries inclued here (under feature `queries`) were constructed with the help of `cynic`s
//! [generator] and use the scalars defined in [`sui_gql_schema`].
//!
//! ## Custom queries
//!
//! Users building their own queries should first:
//! 1. add [`sui_gql_schema`] as a build dependency
//! 1. register its schema in a `build.rs` file;
//! 1. import the [`schema`](crate::schema) module in any module defining new fragments
//!
//! For steps 1 and 2, you can check this crate's `[build-dependencies]` and `build.rs` for an
//! example of how to do so. Read more about schema crates in <https://cynic-rs.dev/large-apis>.
//!
//! Then, to create query structs, we recommend using the [generator] with Sui's GraphQL
//! [schema][sui_schema] and to try reusing the scalars defined in [`scalars`](crate::scalars)
//! as those automatically convert opaque types to more useful ones like [`af_sui_types`].
//!
//! ## Features
//!
//! - `move-types`: compatibility with `af-move-type` types
//! - `mutations`: enables the `mutations` submodule
//! - `queries`: enables the `queries` submodule with pre-made queries
//! - `reqwest`: enables the `reqwest` submodule with an implementation of
//!   [`GraphQlClient`](crate::GraphQlClient)
//! - `scalars`: re-exports the `scalars` module of [`sui_gql_schema`]
//!
//! ## Handy links:
//!
//! - Query builder: [generator.cynic-rs.dev][generator]. When prompted either
//!   - click the "A URL" button and pass in:
//!     - `https://sui-testnet.mystenlabs.com/graphql` to build queries against the testnet schema
//!     - `https://sui-mainnet.mystenlabs.com/graphql` for the mainnet one
//!   - click the "I'll Paste It" button and paste the [schema][sui_schema]
//! - Cynic's [guide](https://cynic-rs.dev/)
//!
//! [`cynic`]: crate::cynic
//! [`sui_gql_schema`]: https://docs.rs/sui-gql-schema/latest/sui_gql_schema/
//! [generator]: https://generator.cynic-rs.dev/
//! [sui_schema]: https://github.com/MystenLabs/sui/blob/main/crates/sui-graphql-rpc/schema.graphql
//! [`af_sui_types`]: https://docs.rs/af-sui-types/latest/af_sui_types/

pub use cynic;
use cynic::schema::{MutationRoot, QueryRoot};
use cynic::serde::de::DeserializeOwned;
use cynic::serde::Serialize;
use cynic::{GraphQlError, GraphQlResponse, Operation, QueryFragment, QueryVariables};
use extension_traits::extension;
#[cfg(feature = "scalars")]
pub use sui_gql_schema::scalars;
pub use sui_gql_schema::schema;

#[cfg(feature = "mutations")]
pub mod mutations;
#[cfg(feature = "queries")]
pub mod queries;
#[cfg(feature = "raw")]
mod raw_client;
#[cfg(feature = "reqwest")]
pub mod reqwest;

pub mod extract;
mod paged;

pub use self::paged::{Paged, PagedResponse, PagesDataResult};
#[cfg(feature = "raw")]
pub use self::raw_client::{Error as RawClientError, RawClient};

/// A generic GraphQL client. Agnostic to the backend used.
#[trait_variant::make(Send)]
pub trait GraphQlClient: Sync {
    type Error: std::error::Error + Send + 'static;

    async fn query_paged<Init>(&self, vars: Init::Input) -> Result<PagedResponse<Init>, Self::Error>
    where
        Init: Paged + Send + 'static,
        Init::SchemaType: QueryRoot,
        Init::Input: Clone,
        Init::NextPage:
            Paged<Input = Init::NextInput, NextInput = Init::NextInput, NextPage = Init::NextPage>,
        <Init::NextPage as QueryFragment>::SchemaType: QueryRoot,
        <Init::NextPage as Paged>::Input: Clone,
    {
        async {
            let initial: GraphQlResponse<Init> = self.query(vars.clone()).await?;
            let mut next_vars = initial.data.as_ref().and_then(|d| d.next_variables(vars));
            let mut pages = vec![];
            while let Some(vars) = next_vars {
                let next_page: GraphQlResponse<Init::NextPage> = self.query(vars.clone()).await?;
                next_vars = next_page.data.as_ref().and_then(|d| d.next_variables(vars));
                pages.push(next_page);
            }
            Ok(PagedResponse(initial, pages))
        }
    }

    async fn query<Query, Variables>(
        &self,
        vars: Variables,
    ) -> Result<GraphQlResponse<Query>, Self::Error>
    where
        Variables: QueryVariables + Send + Serialize,
        Query: DeserializeOwned + QueryFragment<VariablesFields = Variables::Fields> + 'static,
        Query::SchemaType: QueryRoot,
    {
        use cynic::QueryBuilder as _;
        self.run_graphql(Query::build(vars))
    }

    async fn mutation<Mutation, Vars>(
        &self,
        vars: Vars,
    ) -> Result<GraphQlResponse<Mutation>, Self::Error>
    where
        Vars: QueryVariables + Send + Serialize,
        Mutation: DeserializeOwned + QueryFragment<VariablesFields = Vars::Fields> + 'static,
        Mutation::SchemaType: MutationRoot,
    {
        use cynic::MutationBuilder as _;
        self.run_graphql(Mutation::build(vars))
    }

    async fn run_graphql<Query, Vars>(
        &self,
        operation: Operation<Query, Vars>,
    ) -> Result<GraphQlResponse<Query>, Self::Error>
    where
        Vars: Serialize + Send,
        Query: DeserializeOwned + 'static;
}

/// Adds [`try_into_data`](GraphQlResponseExt::try_into_data).
#[extension(pub trait GraphQlResponseExt)]
impl<T> GraphQlResponse<T> {
    /// Extract the `data` field from the response, if any, or fail if the `errors` field contains
    /// any errors.
    fn try_into_data(self) -> Result<Option<T>, GraphQlErrors> {
        if let Some(errors) = self.errors {
            if !errors.is_empty() {
                return Err(GraphQlErrors { errors, page: None });
            }
        }

        let Some(data) = self.data else {
            return Ok(None);
        };
        Ok(Some(data))
    }
}

/// Error for [`GraphQlResponseExt::try_into_data`].
#[derive(thiserror::Error, Clone, Debug, Eq, PartialEq, serde::Deserialize)]
pub struct GraphQlErrors<Extensions = serde::de::IgnoredAny> {
    pub errors: Vec<GraphQlError<Extensions>>,
    pub page: Option<usize>,
}

impl<Extensions> std::fmt::Display for GraphQlErrors<Extensions> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let page_info = self
            .page
            .map_or_else(String::new, |page| format!(" at page {page}"));
        writeln!(
            f,
            "Query execution produced the following errors{page_info}:"
        )?;
        for error in &self.errors {
            writeln!(f, "{error}")?;
        }
        Ok(())
    }
}