馃寠 A GraphQL implementation in Gleam
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}