sui_gql_client/queries/
latest_objects_version.rs

1use std::collections::HashMap;
2
3use af_sui_types::{Address, ObjectId, Version};
4use graphql_extract::extract;
5
6use super::fragments::PageInfoForward;
7use crate::queries::Error;
8use crate::{missing_data, scalars, schema, GraphQlClient, GraphQlResponseExt};
9
10#[derive(cynic::QueryVariables, Clone, Debug)]
11struct Variables<'a> {
12    filter: Option<ObjectFilter<'a>>,
13    after: Option<String>,
14    first: Option<i32>,
15}
16
17impl Variables<'_> {
18    fn next_variables(mut self, page_info: &PageInfoForward) -> Option<Self> {
19        let PageInfoForward {
20            has_next_page,
21            end_cursor,
22        } = page_info;
23        if *has_next_page {
24            self.after.clone_from(end_cursor);
25            Some(self)
26        } else {
27            None
28        }
29    }
30}
31
32#[derive(cynic::InputObject, Clone, Debug, Default)]
33struct ObjectFilter<'a> {
34    #[cynic(rename = "type")]
35    type_: Option<&'a scalars::TypeTag>,
36    owner: Option<&'a Address>,
37    object_ids: Option<&'a [ObjectId]>,
38    object_keys: Option<&'a [ObjectKey<'a>]>,
39}
40
41#[derive(cynic::InputObject, Clone, Debug)]
42struct ObjectKey<'a> {
43    object_id: &'a ObjectId,
44    version: Version,
45}
46
47pub async fn query<C: GraphQlClient>(
48    client: &C,
49    object_ids: &[ObjectId],
50) -> super::Result<(u64, HashMap<ObjectId, u64>), C> {
51    let vars = Variables {
52        after: None,
53        first: None,
54        filter: Some(ObjectFilter {
55            object_ids: Some(object_ids),
56            ..Default::default()
57        }),
58    };
59    let init = client
60        .query::<Query, _>(vars.clone())
61        .await
62        .map_err(Error::Client)?
63        .try_into_data()?;
64
65    extract!(init => {
66        checkpoint? {
67            sequence_number
68        }
69        objects {
70            nodes
71            page_info
72        }
73    });
74    let ckpt_num = sequence_number;
75    let init_nodes = nodes;
76
77    let mut next_vars = vars.next_variables(&page_info);
78    let mut pages = vec![];
79    while let Some(vars) = next_vars {
80        let Some(next_page) = client
81            .query::<QueryPage, _>(vars.clone())
82            .await
83            .map_err(Error::Client)?
84            .try_into_data()?
85        else {
86            break;
87        };
88        next_vars = vars.next_variables(&next_page.objects.page_info);
89        pages.push(next_page);
90    }
91
92    let mut raw_objs = HashMap::new();
93    let page_nodes = pages.into_iter().flat_map(|q| q.objects.nodes);
94    for object in init_nodes.into_iter().chain(page_nodes) {
95        let object_id = object.object_id;
96        let version = object.version;
97        raw_objs.insert(object_id, version);
98    }
99    // Ensure all input objects have versions
100    for id in object_ids {
101        raw_objs
102            .contains_key(id)
103            .then_some(())
104            .ok_or(missing_data!("Object version for {id}"))?;
105    }
106
107    Ok((ckpt_num, raw_objs))
108}
109
110#[derive(cynic::QueryFragment, Clone, Debug)]
111#[cynic(variables = "Variables")]
112struct Query {
113    checkpoint: Option<Checkpoint>,
114
115    #[arguments(filter: $filter, first: $first, after: $after)]
116    objects: ObjectConnection,
117}
118
119// =============================================================================
120//  Subsequent pages
121// =============================================================================
122
123#[derive(cynic::QueryFragment, Debug)]
124#[cynic(graphql_type = "Query", variables = "Variables")]
125struct QueryPage {
126    #[arguments(filter: $filter, first: $first, after: $after)]
127    objects: ObjectConnection,
128}
129
130// =============================================================================
131//  Inner query fragments
132// =============================================================================
133
134#[derive(cynic::QueryFragment, Debug, Clone)]
135struct Object {
136    version: Version,
137    #[cynic(rename = "address")]
138    object_id: ObjectId,
139}
140
141#[derive(cynic::QueryFragment, Debug, Clone)]
142struct Checkpoint {
143    sequence_number: Version,
144}
145
146/// `ObjectConnection` where the `Object` fragment does take any parameters.
147#[derive(cynic::QueryFragment, Clone, Debug)]
148#[cynic(graphql_type = "ObjectConnection")]
149struct ObjectConnection {
150    nodes: Vec<Object>,
151    page_info: PageInfoForward,
152}
153
154#[cfg(test)]
155#[allow(clippy::unwrap_used)]
156#[test]
157fn gql_output() {
158    use cynic::QueryBuilder as _;
159
160    let ids = vec![
161        "0x4264c07a42f9d002c1244e43a1f0fa21c49e4a25c7202c597b8476ef6bb57113"
162            .parse()
163            .unwrap(),
164        "0x60d1a85f81172a7418206f4b16e1e07e40c91cf58783f63f18a25efc81442dcb"
165            .parse()
166            .unwrap(),
167    ];
168
169    let vars = Variables {
170        filter: Some(ObjectFilter {
171            object_ids: Some(&ids),
172            ..Default::default()
173        }),
174        after: None,
175        first: None,
176    };
177
178    let operation = Query::build(vars);
179    insta::assert_snapshot!(operation.query, @r###"
180    query Query($filter: ObjectFilter, $after: String, $first: Int) {
181      checkpoint {
182        sequenceNumber
183      }
184      objects(filter: $filter, first: $first, after: $after) {
185        nodes {
186          version
187          address
188        }
189        pageInfo {
190          hasNextPage
191          endCursor
192        }
193      }
194    }
195    "###);
196}
197
198#[cfg(test)]
199#[allow(clippy::unwrap_used)]
200#[test]
201fn page_gql_output() {
202    use cynic::QueryBuilder as _;
203    let ids = vec![
204        "0x4264c07a42f9d002c1244e43a1f0fa21c49e4a25c7202c597b8476ef6bb57113"
205            .parse()
206            .unwrap(),
207        "0x60d1a85f81172a7418206f4b16e1e07e40c91cf58783f63f18a25efc81442dcb"
208            .parse()
209            .unwrap(),
210    ];
211    let vars = Variables {
212        filter: Some(ObjectFilter {
213            object_ids: Some(&ids),
214            ..Default::default()
215        }),
216        after: None,
217        first: None,
218    };
219    let operation = QueryPage::build(vars);
220    insta::assert_snapshot!(operation.query, @r###"
221    query QueryPage($filter: ObjectFilter, $after: String, $first: Int) {
222      objects(filter: $filter, first: $first, after: $after) {
223        nodes {
224          version
225          address
226        }
227        pageInfo {
228          hasNextPage
229          endCursor
230        }
231      }
232    }
233    "###);
234}