Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
1/// Integration test for groupBy field enum validation
2///
3/// Verifies that aggregation queries use collection-specific GroupByField enums
4/// instead of plain strings, providing type safety and autocomplete.
5import gleam/dict
6import gleam/list
7import gleam/option
8import gleam/result
9import gleeunit/should
10import lexicon_graphql
11import lexicon_graphql/output/aggregate
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 groupby_field_enum_exists_test() {
20 // Test: Each collection should have its own GroupByField enum
21 // Using introspection query to verify AppBskyFeedPostGroupByField exists
22
23 let lexicons = load_test_lexicons()
24
25 // Create a stub fetcher
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 // Create a stub aggregate fetcher
40 let stub_aggregate_fetcher = fn(
41 _uri: String,
42 _params: database.AggregateParams,
43 ) -> Result(List(aggregate.AggregateResult), String) {
44 Error("Not implemented for test")
45 }
46
47 let assert Ok(graphql_schema) =
48 database.build_schema_with_subscriptions(
49 lexicons,
50 stub_fetcher,
51 option.None,
52 option.None,
53 option.None,
54 option.None,
55 option.None,
56 option.None,
57 option.Some(stub_aggregate_fetcher),
58 option.None,
59 option.None,
60 option.None,
61 option.None,
62 option.None,
63 option.None,
64 )
65
66 // Introspection query to check if AppBskyFeedPostGroupByField enum exists
67 let query =
68 "
69 {
70 __type(name: \"AppBskyFeedPostGroupByField\") {
71 name
72 kind
73 enumValues {
74 name
75 description
76 }
77 }
78 }
79 "
80
81 let ctx = schema.context(option.None)
82 let result = executor.execute(query, graphql_schema, ctx)
83
84 // Should successfully execute
85 result
86 |> should.be_ok()
87
88 // Verify the enum exists and has the expected values
89 case result {
90 Ok(executor.Response(data, errors)) -> {
91 // Should have no errors
92 errors
93 |> should.equal([])
94
95 case data {
96 value.Object(fields) -> {
97 case list.key_find(fields, "__type") {
98 Ok(value.Object(type_fields)) -> {
99 // Verify type name
100 case list.key_find(type_fields, "name") {
101 Ok(value.String(name)) -> {
102 name
103 |> should.equal("AppBskyFeedPostGroupByField")
104 }
105 _ -> should.fail()
106 }
107
108 // Verify it's an ENUM
109 case list.key_find(type_fields, "kind") {
110 Ok(value.String(kind)) -> {
111 kind
112 |> should.equal("ENUM")
113 }
114 _ -> should.fail()
115 }
116
117 // Verify it has enum values including standard fields
118 case list.key_find(type_fields, "enumValues") {
119 Ok(value.List(enum_values)) -> {
120 // Convert to list of names
121 let value_names =
122 list.filter_map(enum_values, fn(v) {
123 case v {
124 value.Object(enum_data) -> {
125 case list.key_find(enum_data, "name") {
126 Ok(value.String(n)) -> Ok(n)
127 _ -> Error(Nil)
128 }
129 }
130 _ -> Error(Nil)
131 }
132 })
133
134 // Should have standard fields
135 value_names
136 |> list.contains("uri")
137 |> should.equal(True)
138
139 value_names
140 |> list.contains("did")
141 |> should.equal(True)
142
143 value_names
144 |> list.contains("indexedAt")
145 |> should.equal(True)
146
147 // Should have actorHandle (computed field)
148 value_names
149 |> list.contains("actorHandle")
150 |> should.equal(True)
151
152 // Should have custom property fields (lang, author from test lexicon)
153 value_names
154 |> list.contains("lang")
155 |> should.equal(True)
156
157 value_names
158 |> list.contains("author")
159 |> should.equal(True)
160 }
161 _ -> should.fail()
162 }
163 }
164 _ -> should.fail()
165 }
166 }
167 _ -> should.fail()
168 }
169 }
170 _ -> should.fail()
171 }
172}
173
174pub fn groupby_input_uses_field_enum_test() {
175 // Test: GroupByFieldInput should use the collection-specific enum for the field parameter
176 // Verify AppBskyFeedPostGroupByFieldInput.field uses AppBskyFeedPostGroupByField
177
178 let lexicons = load_test_lexicons()
179
180 let stub_fetcher = fn(_uri: String, _params: dataloader.PaginationParams) -> Result(
181 #(
182 List(#(value.Value, String)),
183 option.Option(String),
184 Bool,
185 Bool,
186 option.Option(Int),
187 ),
188 String,
189 ) {
190 Error("Not implemented for test")
191 }
192
193 // Create a stub aggregate fetcher
194 let stub_aggregate_fetcher = fn(
195 _uri: String,
196 _params: database.AggregateParams,
197 ) -> Result(List(aggregate.AggregateResult), String) {
198 Error("Not implemented for test")
199 }
200
201 let assert Ok(graphql_schema) =
202 database.build_schema_with_subscriptions(
203 lexicons,
204 stub_fetcher,
205 option.None,
206 option.None,
207 option.None,
208 option.None,
209 option.None,
210 option.None,
211 option.Some(stub_aggregate_fetcher),
212 option.None,
213 option.None,
214 option.None,
215 option.None,
216 option.None,
217 option.None,
218 )
219
220 // Introspection query to check AppBskyFeedPostGroupByFieldInput
221 let query =
222 "
223 {
224 __type(name: \"AppBskyFeedPostGroupByFieldInput\") {
225 name
226 kind
227 inputFields {
228 name
229 type {
230 kind
231 ofType {
232 name
233 kind
234 }
235 }
236 }
237 }
238 }
239 "
240
241 let ctx = schema.context(option.None)
242 let result = executor.execute(query, graphql_schema, ctx)
243
244 result
245 |> should.be_ok()
246
247 // Verify the input type uses the correct enum
248 case result {
249 Ok(executor.Response(data, errors)) -> {
250 errors
251 |> should.equal([])
252
253 case data {
254 value.Object(fields) -> {
255 case list.key_find(fields, "__type") {
256 Ok(value.Object(type_fields)) -> {
257 // Verify it's an INPUT_OBJECT
258 case list.key_find(type_fields, "kind") {
259 Ok(value.String(kind)) -> {
260 kind
261 |> should.equal("INPUT_OBJECT")
262 }
263 _ -> should.fail()
264 }
265
266 // Find the "field" input field
267 case list.key_find(type_fields, "inputFields") {
268 Ok(value.List(input_fields)) -> {
269 let field_input =
270 list.find(input_fields, fn(f) {
271 case f {
272 value.Object(field_data) -> {
273 case list.key_find(field_data, "name") {
274 Ok(value.String("field")) -> True
275 _ -> False
276 }
277 }
278 _ -> False
279 }
280 })
281
282 case field_input {
283 Ok(value.Object(field_data)) -> {
284 // Check the type is AppBskyFeedPostGroupByField (wrapped in NON_NULL)
285 case list.key_find(field_data, "type") {
286 Ok(value.Object(type_data)) -> {
287 // Should be NON_NULL
288 case list.key_find(type_data, "kind") {
289 Ok(value.String("NON_NULL")) -> {
290 // Get the inner type
291 case list.key_find(type_data, "ofType") {
292 Ok(value.Object(inner_type)) -> {
293 case list.key_find(inner_type, "name") {
294 Ok(value.String(enum_name)) -> {
295 enum_name
296 |> should.equal(
297 "AppBskyFeedPostGroupByField",
298 )
299 }
300 _ -> should.fail()
301 }
302
303 // Verify it's an ENUM
304 case list.key_find(inner_type, "kind") {
305 Ok(value.String(kind)) -> {
306 kind
307 |> should.equal("ENUM")
308 }
309 _ -> should.fail()
310 }
311 }
312 _ -> should.fail()
313 }
314 }
315 _ -> should.fail()
316 }
317 }
318 _ -> should.fail()
319 }
320 }
321 _ -> should.fail()
322 }
323 }
324 _ -> should.fail()
325 }
326 }
327 _ -> should.fail()
328 }
329 }
330 _ -> should.fail()
331 }
332 }
333 _ -> should.fail()
334 }
335}
336
337// Helper to load app.bsky.feed.post lexicon for testing
338fn load_test_lexicons() -> List(types.Lexicon) {
339 let post_json =
340 "{
341 \"lexicon\": 1,
342 \"id\": \"app.bsky.feed.post\",
343 \"defs\": {
344 \"main\": {
345 \"type\": \"record\",
346 \"key\": \"tid\",
347 \"record\": {
348 \"type\": \"object\",
349 \"required\": [\"text\"],
350 \"properties\": {
351 \"text\": {\"type\": \"string\"},
352 \"lang\": {\"type\": \"string\"},
353 \"author\": {\"type\": \"string\"},
354 \"likes\": {\"type\": \"integer\"}
355 }
356 }
357 }
358 }
359 }"
360
361 [
362 lexicon_graphql.parse_lexicon(post_json) |> result.unwrap(empty_lexicon()),
363 ]
364}
365
366fn empty_lexicon() -> types.Lexicon {
367 types.Lexicon(
368 id: "empty",
369 defs: types.Defs(main: option.None, others: dict.new()),
370 )
371}