馃寠 A GraphQL implementation in Gleam
at v2.1.4 48 kB view raw
1/// Tests for GraphQL Executor 2/// 3/// Tests query execution combining parser + schema + resolvers 4import birdie 5import gleam/dict 6import gleam/list 7import gleam/option.{None, Some} 8import gleam/string 9import gleeunit/should 10import swell/executor 11import swell/schema 12import swell/value 13 14// Helper to create a simple test schema 15fn test_schema() -> schema.Schema { 16 let query_type = 17 schema.object_type("Query", "Root query type", [ 18 schema.field("hello", schema.string_type(), "Hello field", fn(_ctx) { 19 Ok(value.String("world")) 20 }), 21 schema.field("number", schema.int_type(), "Number field", fn(_ctx) { 22 Ok(value.Int(42)) 23 }), 24 schema.field_with_args( 25 "greet", 26 schema.string_type(), 27 "Greet someone", 28 [schema.argument("name", schema.string_type(), "Name to greet", None)], 29 fn(_ctx) { Ok(value.String("Hello, Alice!")) }, 30 ), 31 ]) 32 33 schema.schema(query_type, None) 34} 35 36// Nested object schema for testing 37fn nested_schema() -> schema.Schema { 38 let user_type = 39 schema.object_type("User", "A user", [ 40 schema.field("id", schema.id_type(), "User ID", fn(_ctx) { 41 Ok(value.String("123")) 42 }), 43 schema.field("name", schema.string_type(), "User name", fn(_ctx) { 44 Ok(value.String("Alice")) 45 }), 46 ]) 47 48 let query_type = 49 schema.object_type("Query", "Root query type", [ 50 schema.field("user", user_type, "Get user", fn(_ctx) { 51 Ok( 52 value.Object([ 53 #("id", value.String("123")), 54 #("name", value.String("Alice")), 55 ]), 56 ) 57 }), 58 ]) 59 60 schema.schema(query_type, None) 61} 62 63pub fn execute_simple_query_test() { 64 let schema = test_schema() 65 let query = "{ hello }" 66 67 let result = executor.execute(query, schema, schema.context(None)) 68 69 let response = case result { 70 Ok(r) -> r 71 Error(_) -> panic as "Execution failed" 72 } 73 74 birdie.snap(title: "Execute simple query", content: format_response(response)) 75} 76 77pub fn execute_multiple_fields_test() { 78 let schema = test_schema() 79 let query = "{ hello number }" 80 81 let result = executor.execute(query, schema, schema.context(None)) 82 83 should.be_ok(result) 84} 85 86pub fn execute_nested_query_test() { 87 let schema = nested_schema() 88 let query = "{ user { id name } }" 89 90 let result = executor.execute(query, schema, schema.context(None)) 91 92 should.be_ok(result) 93} 94 95// Helper to format response for snapshots 96fn format_response(response: executor.Response) -> String { 97 string.inspect(response) 98} 99 100pub fn execute_field_with_arguments_test() { 101 let schema = test_schema() 102 let query = "{ greet(name: \"Alice\") }" 103 104 let result = executor.execute(query, schema, schema.context(None)) 105 106 should.be_ok(result) 107} 108 109pub fn execute_invalid_query_returns_error_test() { 110 let schema = test_schema() 111 let query = "{ invalid }" 112 113 let result = executor.execute(query, schema, schema.context(None)) 114 115 // Should return error since field doesn't exist 116 case result { 117 Ok(executor.Response(_, [_, ..])) -> should.be_true(True) 118 Error(_) -> should.be_true(True) 119 _ -> should.be_true(False) 120 } 121} 122 123pub fn execute_parse_error_returns_error_test() { 124 let schema = test_schema() 125 let query = "{ invalid syntax" 126 127 let result = executor.execute(query, schema, schema.context(None)) 128 129 should.be_error(result) 130} 131 132pub fn execute_typename_introspection_test() { 133 let schema = test_schema() 134 let query = "{ __typename }" 135 136 let result = executor.execute(query, schema, schema.context(None)) 137 138 let response = case result { 139 Ok(r) -> r 140 Error(_) -> panic as "Execution failed" 141 } 142 143 birdie.snap( 144 title: "Execute __typename introspection", 145 content: format_response(response), 146 ) 147} 148 149pub fn execute_typename_with_regular_fields_test() { 150 let schema = test_schema() 151 let query = "{ __typename hello }" 152 153 let result = executor.execute(query, schema, schema.context(None)) 154 155 let response = case result { 156 Ok(r) -> r 157 Error(_) -> panic as "Execution failed" 158 } 159 160 birdie.snap( 161 title: "Execute __typename with regular fields", 162 content: format_response(response), 163 ) 164} 165 166pub fn execute_schema_introspection_query_type_test() { 167 let schema = test_schema() 168 let query = "{ __schema { queryType { name } } }" 169 170 let result = executor.execute(query, schema, schema.context(None)) 171 172 let response = case result { 173 Ok(r) -> r 174 Error(_) -> panic as "Execution failed" 175 } 176 177 birdie.snap( 178 title: "Execute __schema introspection", 179 content: format_response(response), 180 ) 181} 182 183// Fragment execution tests 184pub fn execute_simple_fragment_spread_test() { 185 let schema = nested_schema() 186 let query = 187 " 188 fragment UserFields on User { 189 id 190 name 191 } 192 193 { user { ...UserFields } } 194 " 195 196 let result = executor.execute(query, schema, schema.context(None)) 197 198 let response = case result { 199 Ok(r) -> r 200 Error(_) -> panic as "Execution failed" 201 } 202 203 birdie.snap( 204 title: "Execute simple fragment spread", 205 content: format_response(response), 206 ) 207} 208 209// Test for fragment spread on NonNull wrapped type 210pub fn execute_fragment_spread_on_non_null_type_test() { 211 // Create a schema where the user field returns a NonNull type 212 let user_type = 213 schema.object_type("User", "A user", [ 214 schema.field("id", schema.id_type(), "User ID", fn(_ctx) { 215 Ok(value.String("123")) 216 }), 217 schema.field("name", schema.string_type(), "User name", fn(_ctx) { 218 Ok(value.String("Alice")) 219 }), 220 ]) 221 222 let query_type = 223 schema.object_type("Query", "Root query type", [ 224 // Wrap user_type in NonNull to test fragment type condition matching 225 schema.field("user", schema.non_null(user_type), "Get user", fn(_ctx) { 226 Ok( 227 value.Object([ 228 #("id", value.String("123")), 229 #("name", value.String("Alice")), 230 ]), 231 ) 232 }), 233 ]) 234 235 let test_schema = schema.schema(query_type, None) 236 237 // Fragment is defined on "User" (not "User!") - this should still work 238 let query = 239 " 240 fragment UserFields on User { 241 id 242 name 243 } 244 245 { user { ...UserFields } } 246 " 247 248 let result = executor.execute(query, test_schema, schema.context(None)) 249 250 let response = case result { 251 Ok(r) -> r 252 Error(_) -> panic as "Execution failed" 253 } 254 255 birdie.snap( 256 title: "Execute fragment spread on NonNull type", 257 content: format_response(response), 258 ) 259} 260 261// Test for list fields with nested selections 262pub fn execute_list_with_nested_selections_test() { 263 // Create a schema with a list field 264 let user_type = 265 schema.object_type("User", "A user", [ 266 schema.field("id", schema.id_type(), "User ID", fn(ctx) { 267 case ctx.data { 268 option.Some(value.Object(fields)) -> { 269 case list.key_find(fields, "id") { 270 Ok(id_val) -> Ok(id_val) 271 Error(_) -> Ok(value.Null) 272 } 273 } 274 _ -> Ok(value.Null) 275 } 276 }), 277 schema.field("name", schema.string_type(), "User name", fn(ctx) { 278 case ctx.data { 279 option.Some(value.Object(fields)) -> { 280 case list.key_find(fields, "name") { 281 Ok(name_val) -> Ok(name_val) 282 Error(_) -> Ok(value.Null) 283 } 284 } 285 _ -> Ok(value.Null) 286 } 287 }), 288 schema.field("email", schema.string_type(), "User email", fn(ctx) { 289 case ctx.data { 290 option.Some(value.Object(fields)) -> { 291 case list.key_find(fields, "email") { 292 Ok(email_val) -> Ok(email_val) 293 Error(_) -> Ok(value.Null) 294 } 295 } 296 _ -> Ok(value.Null) 297 } 298 }), 299 ]) 300 301 let list_type = schema.list_type(user_type) 302 303 let query_type = 304 schema.object_type("Query", "Root query type", [ 305 schema.field("users", list_type, "Get all users", fn(_ctx) { 306 // Return a list of user objects 307 Ok( 308 value.List([ 309 value.Object([ 310 #("id", value.String("1")), 311 #("name", value.String("Alice")), 312 #("email", value.String("alice@example.com")), 313 ]), 314 value.Object([ 315 #("id", value.String("2")), 316 #("name", value.String("Bob")), 317 #("email", value.String("bob@example.com")), 318 ]), 319 ]), 320 ) 321 }), 322 ]) 323 324 let schema = schema.schema(query_type, None) 325 326 // Query with nested field selection - only request id and name, not email 327 let query = "{ users { id name } }" 328 329 let result = executor.execute(query, schema, schema.context(None)) 330 331 let response = case result { 332 Ok(r) -> r 333 Error(_) -> panic as "Execution failed" 334 } 335 336 birdie.snap( 337 title: "Execute list with nested selections", 338 content: format_response(response), 339 ) 340} 341 342// Test that arguments are actually passed to resolvers 343pub fn execute_field_receives_string_argument_test() { 344 let query_type = 345 schema.object_type("Query", "Root", [ 346 schema.field_with_args( 347 "echo", 348 schema.string_type(), 349 "Echo the input", 350 [schema.argument("message", schema.string_type(), "Message", None)], 351 fn(ctx) { 352 // Extract the argument from context 353 case schema.get_argument(ctx, "message") { 354 Some(value.String(msg)) -> Ok(value.String("Echo: " <> msg)) 355 _ -> Ok(value.String("No message")) 356 } 357 }, 358 ), 359 ]) 360 361 let test_schema = schema.schema(query_type, None) 362 let query = "{ echo(message: \"hello\") }" 363 364 let result = executor.execute(query, test_schema, schema.context(None)) 365 366 let response = case result { 367 Ok(r) -> r 368 Error(_) -> panic as "Execution failed" 369 } 370 371 birdie.snap( 372 title: "Execute field with string argument", 373 content: format_response(response), 374 ) 375} 376 377// Test list argument 378pub fn execute_field_receives_list_argument_test() { 379 let query_type = 380 schema.object_type("Query", "Root", [ 381 schema.field_with_args( 382 "sum", 383 schema.int_type(), 384 "Sum numbers", 385 [ 386 schema.argument( 387 "numbers", 388 schema.list_type(schema.int_type()), 389 "Numbers", 390 None, 391 ), 392 ], 393 fn(ctx) { 394 case schema.get_argument(ctx, "numbers") { 395 Some(value.List(_items)) -> Ok(value.String("got list")) 396 _ -> Ok(value.String("no list")) 397 } 398 }, 399 ), 400 ]) 401 402 let test_schema = schema.schema(query_type, None) 403 let query = "{ sum(numbers: [1, 2, 3]) }" 404 405 let result = executor.execute(query, test_schema, schema.context(None)) 406 407 should.be_ok(result) 408 |> fn(response) { 409 case response { 410 executor.Response( 411 data: value.Object([#("sum", value.String("got list"))]), 412 errors: [], 413 ) -> True 414 _ -> False 415 } 416 } 417 |> should.be_true 418} 419 420// Test object argument (like sortBy) 421pub fn execute_field_receives_object_argument_test() { 422 let query_type = 423 schema.object_type("Query", "Root", [ 424 schema.field_with_args( 425 "posts", 426 schema.list_type(schema.string_type()), 427 "Get posts", 428 [ 429 schema.argument( 430 "sortBy", 431 schema.list_type( 432 schema.input_object_type("SortInput", "Sort", [ 433 schema.input_field("field", schema.string_type(), "Field", None), 434 schema.input_field( 435 "direction", 436 schema.enum_type("Direction", "Direction", [ 437 schema.enum_value("ASC", "Ascending"), 438 schema.enum_value("DESC", "Descending"), 439 ]), 440 "Direction", 441 None, 442 ), 443 ]), 444 ), 445 "Sort order", 446 None, 447 ), 448 ], 449 fn(ctx) { 450 case schema.get_argument(ctx, "sortBy") { 451 Some(value.List([value.Object(fields), ..])) -> { 452 case dict.from_list(fields) { 453 fields_dict -> { 454 case 455 dict.get(fields_dict, "field"), 456 dict.get(fields_dict, "direction") 457 { 458 Ok(value.String(field)), Ok(value.String(dir)) -> 459 Ok(value.String("Sorting by " <> field <> " " <> dir)) 460 _, _ -> Ok(value.String("Invalid sort")) 461 } 462 } 463 } 464 } 465 _ -> Ok(value.String("No sort")) 466 } 467 }, 468 ), 469 ]) 470 471 let test_schema = schema.schema(query_type, None) 472 let query = "{ posts(sortBy: [{field: \"date\", direction: DESC}]) }" 473 474 let result = executor.execute(query, test_schema, schema.context(None)) 475 476 let response = case result { 477 Ok(r) -> r 478 Error(_) -> panic as "Execution failed" 479 } 480 481 birdie.snap( 482 title: "Execute field with object argument", 483 content: format_response(response), 484 ) 485} 486 487// Variable resolution tests 488pub fn execute_query_with_variable_string_test() { 489 let query_type = 490 schema.object_type("Query", "Root query type", [ 491 schema.field_with_args( 492 "greet", 493 schema.string_type(), 494 "Greet someone", 495 [ 496 schema.argument("name", schema.string_type(), "Name to greet", None), 497 ], 498 fn(ctx) { 499 case schema.get_argument(ctx, "name") { 500 Some(value.String(name)) -> 501 Ok(value.String("Hello, " <> name <> "!")) 502 _ -> Ok(value.String("Hello, stranger!")) 503 } 504 }, 505 ), 506 ]) 507 508 let test_schema = schema.schema(query_type, None) 509 let query = "query Test($name: String!) { greet(name: $name) }" 510 511 // Create context with variables 512 let variables = dict.from_list([#("name", value.String("Alice"))]) 513 let ctx = schema.context_with_variables(None, variables) 514 515 let result = executor.execute(query, test_schema, ctx) 516 517 let response = case result { 518 Ok(r) -> r 519 Error(_) -> panic as "Execution failed" 520 } 521 522 birdie.snap( 523 title: "Execute query with string variable", 524 content: format_response(response), 525 ) 526} 527 528pub fn execute_query_with_variable_int_test() { 529 let query_type = 530 schema.object_type("Query", "Root query type", [ 531 schema.field_with_args( 532 "user", 533 schema.string_type(), 534 "Get user by ID", 535 [ 536 schema.argument("id", schema.int_type(), "User ID", None), 537 ], 538 fn(ctx) { 539 case schema.get_argument(ctx, "id") { 540 Some(value.Int(id)) -> 541 Ok(value.String("User #" <> string.inspect(id))) 542 _ -> Ok(value.String("Unknown user")) 543 } 544 }, 545 ), 546 ]) 547 548 let test_schema = schema.schema(query_type, None) 549 let query = "query GetUser($userId: Int!) { user(id: $userId) }" 550 551 // Create context with variables 552 let variables = dict.from_list([#("userId", value.Int(42))]) 553 let ctx = schema.context_with_variables(None, variables) 554 555 let result = executor.execute(query, test_schema, ctx) 556 557 let response = case result { 558 Ok(r) -> r 559 Error(_) -> panic as "Execution failed" 560 } 561 562 birdie.snap( 563 title: "Execute query with int variable", 564 content: format_response(response), 565 ) 566} 567 568pub fn execute_query_with_multiple_variables_test() { 569 let query_type = 570 schema.object_type("Query", "Root query type", [ 571 schema.field_with_args( 572 "search", 573 schema.string_type(), 574 "Search for something", 575 [ 576 schema.argument("query", schema.string_type(), "Search query", None), 577 schema.argument("limit", schema.int_type(), "Max results", None), 578 ], 579 fn(ctx) { 580 case 581 schema.get_argument(ctx, "query"), 582 schema.get_argument(ctx, "limit") 583 { 584 Some(value.String(q)), Some(value.Int(l)) -> 585 Ok(value.String( 586 "Searching for '" 587 <> q 588 <> "' (limit: " 589 <> string.inspect(l) 590 <> ")", 591 )) 592 _, _ -> Ok(value.String("Invalid search")) 593 } 594 }, 595 ), 596 ]) 597 598 let test_schema = schema.schema(query_type, None) 599 let query = 600 "query Search($q: String!, $max: Int!) { search(query: $q, limit: $max) }" 601 602 // Create context with variables 603 let variables = 604 dict.from_list([ 605 #("q", value.String("graphql")), 606 #("max", value.Int(10)), 607 ]) 608 let ctx = schema.context_with_variables(None, variables) 609 610 let result = executor.execute(query, test_schema, ctx) 611 612 let response = case result { 613 Ok(r) -> r 614 Error(_) -> panic as "Execution failed" 615 } 616 617 birdie.snap( 618 title: "Execute query with multiple variables", 619 content: format_response(response), 620 ) 621} 622 623// Union type execution tests 624pub fn execute_union_with_inline_fragment_test() { 625 // Create object types that will be part of the union 626 let post_type = 627 schema.object_type("Post", "A blog post", [ 628 schema.field("title", schema.string_type(), "Post title", fn(ctx) { 629 case ctx.data { 630 option.Some(value.Object(fields)) -> { 631 case list.key_find(fields, "title") { 632 Ok(title_val) -> Ok(title_val) 633 Error(_) -> Ok(value.Null) 634 } 635 } 636 _ -> Ok(value.Null) 637 } 638 }), 639 schema.field("content", schema.string_type(), "Post content", fn(ctx) { 640 case ctx.data { 641 option.Some(value.Object(fields)) -> { 642 case list.key_find(fields, "content") { 643 Ok(content_val) -> Ok(content_val) 644 Error(_) -> Ok(value.Null) 645 } 646 } 647 _ -> Ok(value.Null) 648 } 649 }), 650 ]) 651 652 let comment_type = 653 schema.object_type("Comment", "A comment", [ 654 schema.field("text", schema.string_type(), "Comment text", fn(ctx) { 655 case ctx.data { 656 option.Some(value.Object(fields)) -> { 657 case list.key_find(fields, "text") { 658 Ok(text_val) -> Ok(text_val) 659 Error(_) -> Ok(value.Null) 660 } 661 } 662 _ -> Ok(value.Null) 663 } 664 }), 665 ]) 666 667 // Type resolver that examines the __typename field 668 let type_resolver = fn(ctx: schema.Context) -> Result(String, String) { 669 case ctx.data { 670 option.Some(value.Object(fields)) -> { 671 case list.key_find(fields, "__typename") { 672 Ok(value.String(type_name)) -> Ok(type_name) 673 _ -> Error("No __typename field found") 674 } 675 } 676 _ -> Error("No data") 677 } 678 } 679 680 // Create union type 681 let search_result_union = 682 schema.union_type( 683 "SearchResult", 684 "A search result", 685 [post_type, comment_type], 686 type_resolver, 687 ) 688 689 // Create query type with a field returning the union 690 let query_type = 691 schema.object_type("Query", "Root query type", [ 692 schema.field( 693 "search", 694 search_result_union, 695 "Search for content", 696 fn(_ctx) { 697 // Return a Post 698 Ok( 699 value.Object([ 700 #("__typename", value.String("Post")), 701 #("title", value.String("GraphQL is awesome")), 702 #("content", value.String("Learn all about GraphQL...")), 703 ]), 704 ) 705 }, 706 ), 707 ]) 708 709 let test_schema = schema.schema(query_type, None) 710 711 // Query with inline fragment 712 let query = 713 " 714 { 715 search { 716 ... on Post { 717 title 718 content 719 } 720 ... on Comment { 721 text 722 } 723 } 724 } 725 " 726 727 let result = executor.execute(query, test_schema, schema.context(None)) 728 729 let response = case result { 730 Ok(r) -> r 731 Error(_) -> panic as "Execution failed" 732 } 733 734 birdie.snap( 735 title: "Execute union with inline fragment", 736 content: format_response(response), 737 ) 738} 739 740pub fn execute_union_list_with_inline_fragments_test() { 741 // Create object types 742 let post_type = 743 schema.object_type("Post", "A blog post", [ 744 schema.field("title", schema.string_type(), "Post title", fn(ctx) { 745 case ctx.data { 746 option.Some(value.Object(fields)) -> { 747 case list.key_find(fields, "title") { 748 Ok(title_val) -> Ok(title_val) 749 Error(_) -> Ok(value.Null) 750 } 751 } 752 _ -> Ok(value.Null) 753 } 754 }), 755 ]) 756 757 let comment_type = 758 schema.object_type("Comment", "A comment", [ 759 schema.field("text", schema.string_type(), "Comment text", fn(ctx) { 760 case ctx.data { 761 option.Some(value.Object(fields)) -> { 762 case list.key_find(fields, "text") { 763 Ok(text_val) -> Ok(text_val) 764 Error(_) -> Ok(value.Null) 765 } 766 } 767 _ -> Ok(value.Null) 768 } 769 }), 770 ]) 771 772 // Type resolver 773 let type_resolver = fn(ctx: schema.Context) -> Result(String, String) { 774 case ctx.data { 775 option.Some(value.Object(fields)) -> { 776 case list.key_find(fields, "__typename") { 777 Ok(value.String(type_name)) -> Ok(type_name) 778 _ -> Error("No __typename field found") 779 } 780 } 781 _ -> Error("No data") 782 } 783 } 784 785 // Create union type 786 let search_result_union = 787 schema.union_type( 788 "SearchResult", 789 "A search result", 790 [post_type, comment_type], 791 type_resolver, 792 ) 793 794 // Create query type with a list of unions 795 let query_type = 796 schema.object_type("Query", "Root query type", [ 797 schema.field( 798 "searchAll", 799 schema.list_type(search_result_union), 800 "Search for all content", 801 fn(_ctx) { 802 // Return a list with mixed types 803 Ok( 804 value.List([ 805 value.Object([ 806 #("__typename", value.String("Post")), 807 #("title", value.String("First Post")), 808 ]), 809 value.Object([ 810 #("__typename", value.String("Comment")), 811 #("text", value.String("Great article!")), 812 ]), 813 value.Object([ 814 #("__typename", value.String("Post")), 815 #("title", value.String("Second Post")), 816 ]), 817 ]), 818 ) 819 }, 820 ), 821 ]) 822 823 let test_schema = schema.schema(query_type, None) 824 825 // Query with inline fragments on list items 826 let query = 827 " 828 { 829 searchAll { 830 ... on Post { 831 title 832 } 833 ... on Comment { 834 text 835 } 836 } 837 } 838 " 839 840 let result = executor.execute(query, test_schema, schema.context(None)) 841 842 let response = case result { 843 Ok(r) -> r 844 Error(_) -> panic as "Execution failed" 845 } 846 847 birdie.snap( 848 title: "Execute union list with inline fragments", 849 content: format_response(response), 850 ) 851} 852 853// Test field aliases 854pub fn execute_field_with_alias_test() { 855 let schema = test_schema() 856 let query = "{ greeting: hello }" 857 858 let result = executor.execute(query, schema, schema.context(None)) 859 860 let response = case result { 861 Ok(r) -> r 862 Error(_) -> panic as "Execution failed" 863 } 864 865 // Response should contain "greeting" as the key, not "hello" 866 case response.data { 867 value.Object(fields) -> { 868 case list.key_find(fields, "greeting") { 869 Ok(_) -> should.be_true(True) 870 Error(_) -> { 871 // Check if it incorrectly used "hello" instead 872 case list.key_find(fields, "hello") { 873 Ok(_) -> 874 panic as "Alias not applied - used 'hello' instead of 'greeting'" 875 Error(_) -> 876 panic as "Neither 'greeting' nor 'hello' found in response" 877 } 878 } 879 } 880 } 881 _ -> panic as "Expected object response" 882 } 883} 884 885// Test multiple aliases 886pub fn execute_multiple_fields_with_aliases_test() { 887 let schema = test_schema() 888 let query = "{ greeting: hello num: number }" 889 890 let result = executor.execute(query, schema, schema.context(None)) 891 892 let response = case result { 893 Ok(r) -> r 894 Error(_) -> panic as "Execution failed" 895 } 896 897 birdie.snap( 898 title: "Execute multiple fields with aliases", 899 content: format_response(response), 900 ) 901} 902 903// Test mixed aliased and non-aliased fields 904pub fn execute_mixed_aliased_fields_test() { 905 let schema = test_schema() 906 let query = "{ greeting: hello number }" 907 908 let result = executor.execute(query, schema, schema.context(None)) 909 910 let response = case result { 911 Ok(r) -> r 912 Error(_) -> panic as "Execution failed" 913 } 914 915 birdie.snap( 916 title: "Execute mixed aliased and non-aliased fields", 917 content: format_response(response), 918 ) 919} 920 921pub fn execute_query_with_variable_default_value_test() { 922 let query_type = 923 schema.object_type("Query", "Root query type", [ 924 schema.field_with_args( 925 "greet", 926 schema.string_type(), 927 "Greet someone", 928 [schema.argument("name", schema.string_type(), "Name to greet", None)], 929 fn(ctx) { 930 case schema.get_argument(ctx, "name") { 931 option.Some(value.String(name)) -> 932 Ok(value.String("Hello, " <> name <> "!")) 933 _ -> Ok(value.String("Hello, stranger!")) 934 } 935 }, 936 ), 937 ]) 938 939 let test_schema = schema.schema(query_type, None) 940 let query = "query Test($name: String = \"World\") { greet(name: $name) }" 941 942 // No variables provided - should use default 943 let ctx = schema.context_with_variables(None, dict.new()) 944 945 let result = executor.execute(query, test_schema, ctx) 946 947 // Debug: print the actual result 948 let assert Ok(response) = result 949 let assert executor.Response(data: value.Object(fields), errors: _) = response 950 let assert Ok(greet_value) = list.key_find(fields, "greet") 951 952 // Should use default value "World" since no variable provided 953 greet_value 954 |> should.equal(value.String("Hello, World!")) 955} 956 957pub fn execute_query_with_variable_overriding_default_test() { 958 let query_type = 959 schema.object_type("Query", "Root query type", [ 960 schema.field_with_args( 961 "greet", 962 schema.string_type(), 963 "Greet someone", 964 [schema.argument("name", schema.string_type(), "Name to greet", None)], 965 fn(ctx) { 966 case schema.get_argument(ctx, "name") { 967 option.Some(value.String(name)) -> 968 Ok(value.String("Hello, " <> name <> "!")) 969 _ -> Ok(value.String("Hello, stranger!")) 970 } 971 }, 972 ), 973 ]) 974 975 let test_schema = schema.schema(query_type, None) 976 let query = "query Test($name: String = \"World\") { greet(name: $name) }" 977 978 // Provide variable - should override default 979 let variables = dict.from_list([#("name", value.String("Alice"))]) 980 let ctx = schema.context_with_variables(None, variables) 981 982 let result = executor.execute(query, test_schema, ctx) 983 result 984 |> should.be_ok 985 |> fn(response) { 986 case response { 987 executor.Response(data: value.Object(fields), errors: _) -> { 988 case list.key_find(fields, "greet") { 989 Ok(value.String("Hello, Alice!")) -> True 990 _ -> False 991 } 992 } 993 _ -> False 994 } 995 } 996 |> should.be_true 997} 998 999// Test: List argument rejects object value 1000pub fn list_argument_rejects_object_test() { 1001 // Create a schema with a field that has a list argument 1002 let list_arg_field = 1003 schema.field_with_args( 1004 "items", 1005 schema.string_type(), 1006 "Test field with list arg", 1007 [ 1008 schema.argument( 1009 "ids", 1010 schema.list_type(schema.string_type()), 1011 "List of IDs", 1012 None, 1013 ), 1014 ], 1015 fn(_ctx) { Ok(value.String("test")) }, 1016 ) 1017 1018 let query_type = schema.object_type("Query", "Root", [list_arg_field]) 1019 let s = schema.schema(query_type, None) 1020 1021 // Query with object instead of list should produce an error 1022 let query = "{ items(ids: {foo: \"bar\"}) }" 1023 let result = executor.execute(query, s, schema.context(None)) 1024 1025 case result { 1026 Ok(executor.Response(_, errors)) -> { 1027 // Should have exactly one error 1028 list.length(errors) 1029 |> should.equal(1) 1030 1031 // Check error message mentions list vs object 1032 case list.first(errors) { 1033 Ok(executor.GraphQLError(message, _)) -> { 1034 string.contains(message, "expects a list") 1035 |> should.be_true 1036 string.contains(message, "not an object") 1037 |> should.be_true 1038 } 1039 Error(_) -> should.fail() 1040 } 1041 } 1042 Error(_) -> should.fail() 1043 } 1044} 1045 1046// Test: List argument accepts list value (sanity check) 1047pub fn list_argument_accepts_list_test() { 1048 // Create a schema with a field that has a list argument 1049 let list_arg_field = 1050 schema.field_with_args( 1051 "items", 1052 schema.string_type(), 1053 "Test field with list arg", 1054 [ 1055 schema.argument( 1056 "ids", 1057 schema.list_type(schema.string_type()), 1058 "List of IDs", 1059 None, 1060 ), 1061 ], 1062 fn(_ctx) { Ok(value.String("success")) }, 1063 ) 1064 1065 let query_type = schema.object_type("Query", "Root", [list_arg_field]) 1066 let s = schema.schema(query_type, None) 1067 1068 // Query with proper list should work 1069 let query = "{ items(ids: [\"a\", \"b\"]) }" 1070 let result = executor.execute(query, s, schema.context(None)) 1071 1072 case result { 1073 Ok(executor.Response(value.Object(fields), errors)) -> { 1074 // Should have no errors 1075 list.length(errors) 1076 |> should.equal(0) 1077 1078 // Should return the value 1079 case list.key_find(fields, "items") { 1080 Ok(value.String("success")) -> should.be_true(True) 1081 _ -> should.fail() 1082 } 1083 } 1084 _ -> should.fail() 1085 } 1086} 1087 1088// Test: Union resolution uses canonical type registry 1089// This verifies that when resolving a union type, all fields from the 1090// canonical type definition are accessible, not just those from the 1091// union's internal possible_types copy 1092pub fn execute_union_with_all_fields_via_registry_test() { 1093 // Create a type with multiple fields to verify complete resolution 1094 let article_type = 1095 schema.object_type("Article", "An article", [ 1096 schema.field("id", schema.id_type(), "Article ID", fn(ctx) { 1097 case ctx.data { 1098 option.Some(value.Object(fields)) -> { 1099 case list.key_find(fields, "id") { 1100 Ok(id_val) -> Ok(id_val) 1101 Error(_) -> Ok(value.Null) 1102 } 1103 } 1104 _ -> Ok(value.Null) 1105 } 1106 }), 1107 schema.field("title", schema.string_type(), "Article title", fn(ctx) { 1108 case ctx.data { 1109 option.Some(value.Object(fields)) -> { 1110 case list.key_find(fields, "title") { 1111 Ok(title_val) -> Ok(title_val) 1112 Error(_) -> Ok(value.Null) 1113 } 1114 } 1115 _ -> Ok(value.Null) 1116 } 1117 }), 1118 schema.field("body", schema.string_type(), "Article body", fn(ctx) { 1119 case ctx.data { 1120 option.Some(value.Object(fields)) -> { 1121 case list.key_find(fields, "body") { 1122 Ok(body_val) -> Ok(body_val) 1123 Error(_) -> Ok(value.Null) 1124 } 1125 } 1126 _ -> Ok(value.Null) 1127 } 1128 }), 1129 schema.field("author", schema.string_type(), "Article author", fn(ctx) { 1130 case ctx.data { 1131 option.Some(value.Object(fields)) -> { 1132 case list.key_find(fields, "author") { 1133 Ok(author_val) -> Ok(author_val) 1134 Error(_) -> Ok(value.Null) 1135 } 1136 } 1137 _ -> Ok(value.Null) 1138 } 1139 }), 1140 ]) 1141 1142 let video_type = 1143 schema.object_type("Video", "A video", [ 1144 schema.field("id", schema.id_type(), "Video ID", fn(ctx) { 1145 case ctx.data { 1146 option.Some(value.Object(fields)) -> { 1147 case list.key_find(fields, "id") { 1148 Ok(id_val) -> Ok(id_val) 1149 Error(_) -> Ok(value.Null) 1150 } 1151 } 1152 _ -> Ok(value.Null) 1153 } 1154 }), 1155 schema.field("title", schema.string_type(), "Video title", fn(ctx) { 1156 case ctx.data { 1157 option.Some(value.Object(fields)) -> { 1158 case list.key_find(fields, "title") { 1159 Ok(title_val) -> Ok(title_val) 1160 Error(_) -> Ok(value.Null) 1161 } 1162 } 1163 _ -> Ok(value.Null) 1164 } 1165 }), 1166 schema.field("duration", schema.int_type(), "Video duration", fn(ctx) { 1167 case ctx.data { 1168 option.Some(value.Object(fields)) -> { 1169 case list.key_find(fields, "duration") { 1170 Ok(duration_val) -> Ok(duration_val) 1171 Error(_) -> Ok(value.Null) 1172 } 1173 } 1174 _ -> Ok(value.Null) 1175 } 1176 }), 1177 ]) 1178 1179 // Type resolver that examines the __typename field 1180 let type_resolver = fn(ctx: schema.Context) -> Result(String, String) { 1181 case ctx.data { 1182 option.Some(value.Object(fields)) -> { 1183 case list.key_find(fields, "__typename") { 1184 Ok(value.String(type_name)) -> Ok(type_name) 1185 _ -> Error("No __typename field found") 1186 } 1187 } 1188 _ -> Error("No data") 1189 } 1190 } 1191 1192 // Create union type 1193 let content_union = 1194 schema.union_type( 1195 "Content", 1196 "Content union", 1197 [article_type, video_type], 1198 type_resolver, 1199 ) 1200 1201 // Create query type with a field returning the union 1202 let query_type = 1203 schema.object_type("Query", "Root query type", [ 1204 schema.field("content", content_union, "Get content", fn(_ctx) { 1205 // Return an Article with all fields populated 1206 Ok( 1207 value.Object([ 1208 #("__typename", value.String("Article")), 1209 #("id", value.String("article-1")), 1210 #("title", value.String("GraphQL Best Practices")), 1211 #("body", value.String("Here are some tips...")), 1212 #("author", value.String("Jane Doe")), 1213 ]), 1214 ) 1215 }), 1216 ]) 1217 1218 let test_schema = schema.schema(query_type, None) 1219 1220 // Query requesting ALL fields from the Article type 1221 // If the type registry works correctly, all 4 fields should be returned 1222 let query = 1223 " 1224 { 1225 content { 1226 ... on Article { 1227 id 1228 title 1229 body 1230 author 1231 } 1232 ... on Video { 1233 id 1234 title 1235 duration 1236 } 1237 } 1238 } 1239 " 1240 1241 let result = executor.execute(query, test_schema, schema.context(None)) 1242 1243 case result { 1244 Ok(executor.Response(value.Object(fields), errors)) -> { 1245 // Should have no errors 1246 list.length(errors) 1247 |> should.equal(0) 1248 1249 // Check the content field 1250 case list.key_find(fields, "content") { 1251 Ok(value.Object(content_fields)) -> { 1252 // Should have all 4 Article fields 1253 list.length(content_fields) 1254 |> should.equal(4) 1255 1256 // Verify each field is present with correct value 1257 case list.key_find(content_fields, "id") { 1258 Ok(value.String("article-1")) -> should.be_true(True) 1259 _ -> should.fail() 1260 } 1261 case list.key_find(content_fields, "title") { 1262 Ok(value.String("GraphQL Best Practices")) -> should.be_true(True) 1263 _ -> should.fail() 1264 } 1265 case list.key_find(content_fields, "body") { 1266 Ok(value.String("Here are some tips...")) -> should.be_true(True) 1267 _ -> should.fail() 1268 } 1269 case list.key_find(content_fields, "author") { 1270 Ok(value.String("Jane Doe")) -> should.be_true(True) 1271 _ -> should.fail() 1272 } 1273 } 1274 _ -> should.fail() 1275 } 1276 } 1277 Error(err) -> { 1278 // Print error for debugging 1279 should.equal(err, "") 1280 } 1281 _ -> should.fail() 1282 } 1283} 1284 1285// Test: Variables are preserved in nested object selections 1286// This verifies that when traversing into a nested object, variables 1287// from the parent context are still accessible 1288pub fn execute_variables_preserved_in_nested_object_test() { 1289 // Create a nested type structure where the nested resolver needs access to variables 1290 let post_type = 1291 schema.object_type("Post", "A post", [ 1292 schema.field("title", schema.string_type(), "Post title", fn(ctx) { 1293 case ctx.data { 1294 option.Some(value.Object(fields)) -> { 1295 case list.key_find(fields, "title") { 1296 Ok(title_val) -> Ok(title_val) 1297 Error(_) -> Ok(value.Null) 1298 } 1299 } 1300 _ -> Ok(value.Null) 1301 } 1302 }), 1303 // This field uses a variable from the outer context 1304 schema.field_with_args( 1305 "formattedTitle", 1306 schema.string_type(), 1307 "Formatted title", 1308 [schema.argument("prefix", schema.string_type(), "Prefix to add", None)], 1309 fn(ctx) { 1310 let prefix = case schema.get_argument(ctx, "prefix") { 1311 Some(value.String(p)) -> p 1312 _ -> "" 1313 } 1314 case ctx.data { 1315 option.Some(value.Object(fields)) -> { 1316 case list.key_find(fields, "title") { 1317 Ok(value.String(title)) -> 1318 Ok(value.String(prefix <> ": " <> title)) 1319 _ -> Ok(value.Null) 1320 } 1321 } 1322 _ -> Ok(value.Null) 1323 } 1324 }, 1325 ), 1326 ]) 1327 1328 let query_type = 1329 schema.object_type("Query", "Root query type", [ 1330 schema.field("post", post_type, "Get a post", fn(_ctx) { 1331 Ok(value.Object([#("title", value.String("Hello World"))])) 1332 }), 1333 ]) 1334 1335 let test_schema = schema.schema(query_type, None) 1336 1337 // Query using a variable in a nested field 1338 let query = 1339 "query GetPost($prefix: String!) { post { formattedTitle(prefix: $prefix) } }" 1340 1341 // Create context with variables 1342 let variables = dict.from_list([#("prefix", value.String("Article"))]) 1343 let ctx = schema.context_with_variables(None, variables) 1344 1345 let result = executor.execute(query, test_schema, ctx) 1346 1347 case result { 1348 Ok(executor.Response(data: value.Object(fields), errors: _)) -> { 1349 case list.key_find(fields, "post") { 1350 Ok(value.Object(post_fields)) -> { 1351 case list.key_find(post_fields, "formattedTitle") { 1352 Ok(value.String("Article: Hello World")) -> should.be_true(True) 1353 Ok(other) -> { 1354 // Variable was lost - this is the bug we're testing for 1355 should.equal(other, value.String("Article: Hello World")) 1356 } 1357 Error(_) -> should.fail() 1358 } 1359 } 1360 _ -> should.fail() 1361 } 1362 } 1363 Error(err) -> should.equal(err, "") 1364 _ -> should.fail() 1365 } 1366} 1367 1368// Test: Variables are preserved in nested list item selections 1369// This verifies that when iterating over list items, variables 1370// from the parent context are still accessible to each item's resolvers 1371pub fn execute_variables_preserved_in_nested_list_test() { 1372 // Create a type structure where list item resolvers need access to variables 1373 let item_type = 1374 schema.object_type("Item", "An item", [ 1375 schema.field("name", schema.string_type(), "Item name", fn(ctx) { 1376 case ctx.data { 1377 option.Some(value.Object(fields)) -> { 1378 case list.key_find(fields, "name") { 1379 Ok(name_val) -> Ok(name_val) 1380 Error(_) -> Ok(value.Null) 1381 } 1382 } 1383 _ -> Ok(value.Null) 1384 } 1385 }), 1386 // This field uses a variable from the outer context 1387 schema.field_with_args( 1388 "formattedName", 1389 schema.string_type(), 1390 "Formatted name", 1391 [schema.argument("suffix", schema.string_type(), "Suffix to add", None)], 1392 fn(ctx) { 1393 let suffix = case schema.get_argument(ctx, "suffix") { 1394 Some(value.String(s)) -> s 1395 _ -> "" 1396 } 1397 case ctx.data { 1398 option.Some(value.Object(fields)) -> { 1399 case list.key_find(fields, "name") { 1400 Ok(value.String(name)) -> 1401 Ok(value.String(name <> " " <> suffix)) 1402 _ -> Ok(value.Null) 1403 } 1404 } 1405 _ -> Ok(value.Null) 1406 } 1407 }, 1408 ), 1409 ]) 1410 1411 let query_type = 1412 schema.object_type("Query", "Root query type", [ 1413 schema.field("items", schema.list_type(item_type), "Get items", fn(_ctx) { 1414 Ok( 1415 value.List([ 1416 value.Object([#("name", value.String("Apple"))]), 1417 value.Object([#("name", value.String("Banana"))]), 1418 ]), 1419 ) 1420 }), 1421 ]) 1422 1423 let test_schema = schema.schema(query_type, None) 1424 1425 // Query using a variable in nested list item fields 1426 let query = 1427 "query GetItems($suffix: String!) { items { formattedName(suffix: $suffix) } }" 1428 1429 // Create context with variables 1430 let variables = dict.from_list([#("suffix", value.String("(organic)"))]) 1431 let ctx = schema.context_with_variables(None, variables) 1432 1433 let result = executor.execute(query, test_schema, ctx) 1434 1435 case result { 1436 Ok(executor.Response(data: value.Object(fields), errors: _)) -> { 1437 case list.key_find(fields, "items") { 1438 Ok(value.List(items)) -> { 1439 // Should have 2 items 1440 list.length(items) |> should.equal(2) 1441 1442 // First item should have formatted name with suffix 1443 case list.first(items) { 1444 Ok(value.Object(item_fields)) -> { 1445 case list.key_find(item_fields, "formattedName") { 1446 Ok(value.String("Apple (organic)")) -> should.be_true(True) 1447 Ok(other) -> { 1448 // Variable was lost - this is the bug we're testing for 1449 should.equal(other, value.String("Apple (organic)")) 1450 } 1451 Error(_) -> should.fail() 1452 } 1453 } 1454 _ -> should.fail() 1455 } 1456 1457 // Second item should also have formatted name with suffix 1458 case list.drop(items, 1) { 1459 [value.Object(item_fields), ..] -> { 1460 case list.key_find(item_fields, "formattedName") { 1461 Ok(value.String("Banana (organic)")) -> should.be_true(True) 1462 Ok(other) -> { 1463 should.equal(other, value.String("Banana (organic)")) 1464 } 1465 Error(_) -> should.fail() 1466 } 1467 } 1468 _ -> should.fail() 1469 } 1470 } 1471 _ -> should.fail() 1472 } 1473 } 1474 Error(err) -> should.equal(err, "") 1475 _ -> should.fail() 1476 } 1477} 1478 1479// Test: Union type wrapped in NonNull resolves correctly 1480// This tests the fix for fields like `node: NonNull(UnionType)` in connections 1481// Previously, is_union check failed because it only matched bare UnionType 1482pub fn execute_non_null_union_resolves_correctly_test() { 1483 // Create object types that will be part of the union 1484 let like_type = 1485 schema.object_type("Like", "A like record", [ 1486 schema.field("uri", schema.string_type(), "Like URI", fn(ctx) { 1487 case ctx.data { 1488 option.Some(value.Object(fields)) -> { 1489 case list.key_find(fields, "uri") { 1490 Ok(uri_val) -> Ok(uri_val) 1491 Error(_) -> Ok(value.Null) 1492 } 1493 } 1494 _ -> Ok(value.Null) 1495 } 1496 }), 1497 ]) 1498 1499 let follow_type = 1500 schema.object_type("Follow", "A follow record", [ 1501 schema.field("uri", schema.string_type(), "Follow URI", fn(ctx) { 1502 case ctx.data { 1503 option.Some(value.Object(fields)) -> { 1504 case list.key_find(fields, "uri") { 1505 Ok(uri_val) -> Ok(uri_val) 1506 Error(_) -> Ok(value.Null) 1507 } 1508 } 1509 _ -> Ok(value.Null) 1510 } 1511 }), 1512 ]) 1513 1514 // Type resolver that examines the "type" field 1515 let type_resolver = fn(ctx: schema.Context) -> Result(String, String) { 1516 case ctx.data { 1517 option.Some(value.Object(fields)) -> { 1518 case list.key_find(fields, "type") { 1519 Ok(value.String(type_name)) -> Ok(type_name) 1520 _ -> Error("No type field found") 1521 } 1522 } 1523 _ -> Error("No data") 1524 } 1525 } 1526 1527 // Create union type 1528 let notification_union = 1529 schema.union_type( 1530 "NotificationRecord", 1531 "A notification record", 1532 [like_type, follow_type], 1533 type_resolver, 1534 ) 1535 1536 // Create edge type with node wrapped in NonNull - this is the key scenario 1537 let edge_type = 1538 schema.object_type("NotificationEdge", "An edge in the connection", [ 1539 schema.field( 1540 "node", 1541 schema.non_null(notification_union), 1542 // NonNull wrapping union 1543 "The notification record", 1544 fn(ctx) { 1545 case ctx.data { 1546 option.Some(value.Object(fields)) -> { 1547 case list.key_find(fields, "node") { 1548 Ok(node_val) -> Ok(node_val) 1549 Error(_) -> Ok(value.Null) 1550 } 1551 } 1552 _ -> Ok(value.Null) 1553 } 1554 }, 1555 ), 1556 schema.field("cursor", schema.string_type(), "Cursor", fn(ctx) { 1557 case ctx.data { 1558 option.Some(value.Object(fields)) -> { 1559 case list.key_find(fields, "cursor") { 1560 Ok(cursor_val) -> Ok(cursor_val) 1561 Error(_) -> Ok(value.Null) 1562 } 1563 } 1564 _ -> Ok(value.Null) 1565 } 1566 }), 1567 ]) 1568 1569 // Create query type returning a list of edges 1570 let query_type = 1571 schema.object_type("Query", "Root query type", [ 1572 schema.field( 1573 "notifications", 1574 schema.list_type(edge_type), 1575 "Get notifications", 1576 fn(_ctx) { 1577 Ok( 1578 value.List([ 1579 value.Object([ 1580 #( 1581 "node", 1582 value.Object([ 1583 #("type", value.String("Like")), 1584 #("uri", value.String("at://user/like/1")), 1585 ]), 1586 ), 1587 #("cursor", value.String("cursor1")), 1588 ]), 1589 value.Object([ 1590 #( 1591 "node", 1592 value.Object([ 1593 #("type", value.String("Follow")), 1594 #("uri", value.String("at://user/follow/1")), 1595 ]), 1596 ), 1597 #("cursor", value.String("cursor2")), 1598 ]), 1599 ]), 1600 ) 1601 }, 1602 ), 1603 ]) 1604 1605 let test_schema = schema.schema(query_type, None) 1606 1607 // Query with inline fragments on the NonNull-wrapped union 1608 let query = 1609 " 1610 { 1611 notifications { 1612 cursor 1613 node { 1614 __typename 1615 ... on Like { 1616 uri 1617 } 1618 ... on Follow { 1619 uri 1620 } 1621 } 1622 } 1623 } 1624 " 1625 1626 let result = executor.execute(query, test_schema, schema.context(None)) 1627 1628 case result { 1629 Ok(response) -> { 1630 case response.data { 1631 value.Object(fields) -> { 1632 case list.key_find(fields, "notifications") { 1633 Ok(value.List(edges)) -> { 1634 // Should have 2 edges 1635 list.length(edges) |> should.equal(2) 1636 1637 // First edge should be a Like with resolved fields 1638 case list.first(edges) { 1639 Ok(value.Object(edge_fields)) -> { 1640 case list.key_find(edge_fields, "node") { 1641 Ok(value.Object(node_fields)) -> { 1642 // __typename should be "Like" (resolved from union) 1643 case list.key_find(node_fields, "__typename") { 1644 Ok(value.String("Like")) -> should.be_true(True) 1645 Ok(value.String(other)) -> should.equal(other, "Like") 1646 _ -> should.fail() 1647 } 1648 // uri should be resolved from inline fragment 1649 case list.key_find(node_fields, "uri") { 1650 Ok(value.String("at://user/like/1")) -> 1651 should.be_true(True) 1652 _ -> should.fail() 1653 } 1654 } 1655 _ -> should.fail() 1656 } 1657 } 1658 _ -> should.fail() 1659 } 1660 } 1661 _ -> should.fail() 1662 } 1663 } 1664 _ -> should.fail() 1665 } 1666 } 1667 Error(err) -> { 1668 should.equal(err, "") 1669 } 1670 } 1671}