Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql
at main 371 lines 11 kB view raw
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}