sui_gql_client/queries/
latest_objects_version.rs1use 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 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#[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#[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#[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}