Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
1/// Integration test for sorting enum validation
2///
3/// Verifies that each Connection field uses the correct collection-specific
4/// SortFieldInput type with the appropriate enum, fixing the bug where all
5/// fields were sharing a single global enum.
6import gleam/dict
7import gleam/list
8import gleam/option
9import gleam/result
10import gleeunit/should
11import lexicon_graphql
12import lexicon_graphql/query/dataloader
13import lexicon_graphql/schema/database
14import lexicon_graphql/types
15import swell/executor
16import swell/schema
17import swell/value
18
19pub fn sorting_enum_input_types_are_unique_per_collection_test() {
20 // Test: Each collection should have its own SortFieldInput type
21 // Using introspection query to verify SocialGrainGalleryItemSortFieldInput exists
22
23 let lexicons = load_social_grain_lexicons()
24
25 // Create a stub fetcher that won't actually be called
26 let stub_fetcher = fn(_uri: String, _params: dataloader.PaginationParams) -> Result(
27 #(
28 List(#(value.Value, String)),
29 option.Option(String),
30 Bool,
31 Bool,
32 option.Option(Int),
33 ),
34 String,
35 ) {
36 Error("Not implemented for test")
37 }
38
39 let assert Ok(graphql_schema) =
40 database.build_schema_with_fetcher(
41 lexicons,
42 stub_fetcher,
43 option.None,
44 option.None,
45 option.None,
46 option.None,
47 option.None,
48 option.None,
49 )
50
51 // Introspection query to check if SocialGrainGalleryItemSortFieldInput exists
52 let query =
53 "
54 {
55 __type(name: \"SocialGrainGalleryItemSortFieldInput\") {
56 name
57 kind
58 inputFields {
59 name
60 type {
61 name
62 kind
63 ofType {
64 name
65 kind
66 }
67 }
68 }
69 }
70 }
71 "
72
73 let ctx = schema.context(option.None)
74 let result = executor.execute(query, graphql_schema, ctx)
75
76 // Should successfully execute
77 result
78 |> should.be_ok()
79
80 // Verify the type exists and has the correct structure
81 case result {
82 Ok(executor.Response(data, errors)) -> {
83 // Should have no errors
84 errors
85 |> should.equal([])
86
87 case data {
88 value.Object(fields) -> {
89 case list.key_find(fields, "__type") {
90 Ok(value.Object(type_fields)) -> {
91 // Verify type name
92 case list.key_find(type_fields, "name") {
93 Ok(value.String(name)) -> {
94 name
95 |> should.equal("SocialGrainGalleryItemSortFieldInput")
96 }
97 _ -> should.fail()
98 }
99
100 // Verify it's an INPUT_OBJECT
101 case list.key_find(type_fields, "kind") {
102 Ok(value.String(kind)) -> {
103 kind
104 |> should.equal("INPUT_OBJECT")
105 }
106 _ -> should.fail()
107 }
108
109 // Verify it has a "field" input field that uses the correct enum
110 case list.key_find(type_fields, "inputFields") {
111 Ok(value.List(input_fields)) -> {
112 // Find the "field" input field
113 let field_input =
114 list.find(input_fields, fn(f) {
115 case f {
116 value.Object(field_data) -> {
117 case list.key_find(field_data, "name") {
118 Ok(value.String("field")) -> True
119 _ -> False
120 }
121 }
122 _ -> False
123 }
124 })
125
126 case field_input {
127 Ok(value.Object(field_data)) -> {
128 // Check the type is SocialGrainGalleryItemSortField (wrapped in NON_NULL)
129 case list.key_find(field_data, "type") {
130 Ok(value.Object(type_data)) -> {
131 case list.key_find(type_data, "ofType") {
132 Ok(value.Object(inner_type)) -> {
133 case list.key_find(inner_type, "name") {
134 Ok(value.String(enum_name)) -> {
135 enum_name
136 |> should.equal(
137 "SocialGrainGalleryItemSortField",
138 )
139 }
140 _ -> should.fail()
141 }
142 }
143 _ -> should.fail()
144 }
145 }
146 _ -> should.fail()
147 }
148 }
149 _ -> should.fail()
150 }
151 }
152 _ -> should.fail()
153 }
154 }
155 _ -> should.fail()
156 }
157 }
158 _ -> should.fail()
159 }
160 }
161 _ -> should.fail()
162 }
163}
164
165pub fn did_join_uses_correct_sort_enum_test() {
166 // Test: DID join fields should use the SOURCE collection's sort enum
167 // Query SocialGrainGallery.socialGrainGalleryItemByDid's sortBy argument
168
169 let lexicons = load_social_grain_lexicons()
170
171 // Create a stub fetcher that won't actually be called
172 let stub_fetcher = fn(_uri: String, _params: dataloader.PaginationParams) -> Result(
173 #(
174 List(#(value.Value, String)),
175 option.Option(String),
176 Bool,
177 Bool,
178 option.Option(Int),
179 ),
180 String,
181 ) {
182 Error("Not implemented for test")
183 }
184
185 let assert Ok(graphql_schema) =
186 database.build_schema_with_fetcher(
187 lexicons,
188 stub_fetcher,
189 option.None,
190 option.None,
191 option.None,
192 option.None,
193 option.None,
194 option.None,
195 )
196
197 // Introspection query to check socialGrainGalleryItemByDid's sortBy argument
198 let query =
199 "
200 {
201 __type(name: \"SocialGrainGallery\") {
202 fields {
203 name
204 args {
205 name
206 type {
207 kind
208 ofType {
209 kind
210 ofType {
211 name
212 }
213 }
214 }
215 }
216 }
217 }
218 }
219 "
220
221 let ctx = schema.context(option.None)
222 let result = executor.execute(query, graphql_schema, ctx)
223
224 result
225 |> should.be_ok()
226
227 // Find the socialGrainGalleryItemViaGallery field and verify its sortBy arg
228 case result {
229 Ok(executor.Response(data, errors)) -> {
230 errors
231 |> should.equal([])
232
233 case data {
234 value.Object(response_fields) -> {
235 case list.key_find(response_fields, "__type") {
236 Ok(value.Object(type_fields)) -> {
237 case list.key_find(type_fields, "fields") {
238 Ok(value.List(fields)) -> {
239 // Find socialGrainGalleryItemByDid field
240 let did_join_field =
241 list.find(fields, fn(field) {
242 case field {
243 value.Object(field_data) -> {
244 case list.key_find(field_data, "name") {
245 Ok(value.String("socialGrainGalleryItemByDid")) ->
246 True
247 _ -> False
248 }
249 }
250 _ -> False
251 }
252 })
253
254 case did_join_field {
255 Ok(value.Object(field_data)) -> {
256 case list.key_find(field_data, "args") {
257 Ok(value.List(args)) -> {
258 // Find sortBy argument
259 let sortby_arg =
260 list.find(args, fn(arg) {
261 case arg {
262 value.Object(arg_data) -> {
263 case list.key_find(arg_data, "name") {
264 Ok(value.String("sortBy")) -> True
265 _ -> False
266 }
267 }
268 _ -> False
269 }
270 })
271
272 case sortby_arg {
273 Ok(value.Object(arg_data)) -> {
274 // Get the input type name: [SocialGrainGalleryItemSortFieldInput!]
275 case list.key_find(arg_data, "type") {
276 Ok(value.Object(type_data)) -> {
277 case list.key_find(type_data, "ofType") {
278 Ok(value.Object(non_null_data)) -> {
279 case
280 list.key_find(non_null_data, "ofType")
281 {
282 Ok(value.Object(input_type_data)) -> {
283 case
284 list.key_find(
285 input_type_data,
286 "name",
287 )
288 {
289 Ok(value.String(input_type_name)) -> {
290 // Should use GalleryItem's input type, NOT Gallery's or Favorite's
291 input_type_name
292 |> should.equal(
293 "SocialGrainGalleryItemSortFieldInput",
294 )
295 }
296 _ -> should.fail()
297 }
298 }
299 _ -> should.fail()
300 }
301 }
302 _ -> should.fail()
303 }
304 }
305 _ -> should.fail()
306 }
307 }
308 _ -> should.fail()
309 }
310 }
311 _ -> should.fail()
312 }
313 }
314 _ -> should.fail()
315 }
316 }
317 _ -> should.fail()
318 }
319 }
320 _ -> should.fail()
321 }
322 }
323 _ -> should.fail()
324 }
325 }
326 _ -> should.fail()
327 }
328}
329
330// Helper to load social.grain lexicons for testing
331fn load_social_grain_lexicons() -> List(types.Lexicon) {
332 let gallery_json =
333 "{
334 \"lexicon\": 1,
335 \"id\": \"social.grain.gallery\",
336 \"defs\": {
337 \"main\": {
338 \"type\": \"record\",
339 \"key\": \"tid\",
340 \"record\": {
341 \"type\": \"object\",
342 \"required\": [\"title\"],
343 \"properties\": {
344 \"title\": {\"type\": \"string\"},
345 \"description\": {\"type\": \"string\"}
346 }
347 }
348 }
349 }
350 }"
351
352 let gallery_item_json =
353 "{
354 \"lexicon\": 1,
355 \"id\": \"social.grain.gallery.item\",
356 \"defs\": {
357 \"main\": {
358 \"type\": \"record\",
359 \"key\": \"tid\",
360 \"record\": {
361 \"type\": \"object\",
362 \"required\": [\"gallery\", \"item\", \"position\"],
363 \"properties\": {
364 \"gallery\": {\"type\": \"string\"},
365 \"item\": {\"type\": \"string\"},
366 \"position\": {\"type\": \"integer\"}
367 }
368 }
369 }
370 }
371 }"
372
373 let favorite_json =
374 "{
375 \"lexicon\": 1,
376 \"id\": \"social.grain.favorite\",
377 \"defs\": {
378 \"main\": {
379 \"type\": \"record\",
380 \"key\": \"tid\",
381 \"record\": {
382 \"type\": \"object\",
383 \"required\": [\"subject\"],
384 \"properties\": {
385 \"subject\": {\"type\": \"string\"},
386 \"createdAt\": {\"type\": \"string\"}
387 }
388 }
389 }
390 }
391 }"
392
393 [
394 lexicon_graphql.parse_lexicon(gallery_json)
395 |> result.unwrap(empty_lexicon()),
396 lexicon_graphql.parse_lexicon(gallery_item_json)
397 |> result.unwrap(empty_lexicon()),
398 lexicon_graphql.parse_lexicon(favorite_json)
399 |> result.unwrap(empty_lexicon()),
400 ]
401}
402
403fn empty_lexicon() -> types.Lexicon {
404 types.Lexicon(
405 id: "empty",
406 defs: types.Defs(main: option.None, others: dict.new()),
407 )
408}