···1----
2-version: 1.4.1
3-title: Enum without descriptions
4-file: ./test/sdl_test.gleam
5-test_name: enum_without_descriptions_test
6----
7-enum Color {
8- RED
9- GREEN
10- BLUE
11-}
···1----
2-version: 1.4.1
3-title: Execute field with object argument
4-file: ./test/executor_test.gleam
5-test_name: execute_field_receives_object_argument_test
6----
7-Response(Object([#("posts", String("Sorting by date DESC"))]), [])
···1----
2-version: 1.4.1
3-title: Execute union with inline fragment
4-file: ./test/executor_test.gleam
5-test_name: execute_union_with_inline_fragment_test
6----
7-Response(Object([#("search", Object([#("title", String("GraphQL is awesome")), #("content", String("Learn all about GraphQL..."))]))]), [])
···1----
2-version: 1.4.1
3-title: Input object with default values
4-file: ./test/sdl_test.gleam
5-test_name: input_object_with_default_values_test
6----
7-"""Filter options for queries"""
8-input FilterInput {
9- """Maximum number of results"""
10- limit: Int
11- """Number of results to skip"""
12- offset: Int
13-}
···1----
2-version: 1.4.1
3-title: Simple enum type
4-file: ./test/sdl_test.gleam
5-test_name: simple_enum_test
6----
7-"""Order status"""
8-enum Status {
9- """Order is pending"""
10- PENDING
11- """Order is being processed"""
12- PROCESSING
13- """Order has been shipped"""
14- SHIPPED
15- """Order has been delivered"""
16- DELIVERED
17-}
···1----
2-version: 1.4.1
3-title: Type with NonNull and List modifiers
4-file: ./test/sdl_test.gleam
5-test_name: type_with_non_null_and_list_test
6----
7-"""Complex type modifiers"""
8-input ComplexInput {
9- """Required string"""
10- required: String!
11- """Optional list of strings"""
12- optionalList: [String]
13- """Required list of optional strings"""
14- requiredList: [String]!
15- """Optional list of required strings"""
16- listOfRequired: [String!]
17- """Required list of required strings"""
18- requiredListOfRequired: [String!]!
19-}
···0000000000000000000
-20
graphql/gleam.toml
···1-name = "graphql"
2-version = "1.0.0"
3-4-# Fill out these fields if you intend to generate HTML documentation or publish
5-# your project to the Hex package manager.
6-#
7-# description = ""
8-# licences = ["Apache-2.0"]
9-# repository = { type = "github", user = "", repo = "" }
10-# links = [{ title = "Website", href = "" }]
11-#
12-# For a full reference of all the available options, you can have a look at
13-# https://gleam.run/writing-gleam/gleam-toml/.
14-15-[dependencies]
16-gleam_stdlib = ">= 0.44.0 and < 2.0.0"
17-18-[dev-dependencies]
19-gleeunit = ">= 1.0.0 and < 2.0.0"
20-birdie = ">= 1.0.0 and < 2.0.0"
···00000000000000000000
-28
graphql/manifest.toml
···1-# This file was generated by Gleam
2-# You typically do not need to edit this file
3-4-packages = [
5- { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
6- { name = "birdie", version = "1.4.1", build_tools = ["gleam"], requirements = ["argv", "edit_distance", "filepath", "glance", "gleam_community_ansi", "gleam_stdlib", "justin", "rank", "simplifile", "term_size", "trie_again"], otp_app = "birdie", source = "hex", outer_checksum = "18599E478C14BD9EBD2465F0561F96EB9B58A24DB44AF86F103EF81D4B9834BF" },
7- { name = "edit_distance", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "7DC465C34695F9E57D79FC65670C53C992CE342BF29E0AA41FF44F61AF62FC56" },
8- { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
9- { name = "glance", version = "5.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "7F216D97935465FF4AC46699CD1C3E0FB19CB678B002E4ACAFCE256E96312F14" },
10- { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" },
11- { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" },
12- { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
13- { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
14- { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
15- { name = "gleeunit", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "CD701726CBCE5588B375D157B4391CFD0F2F134CD12D9B6998A395484DE05C58" },
16- { name = "glexer", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "40A1FB0919FA080AD6C5809B4C7DBA545841CAAC8168FACDFA0B0667C22475CC" },
17- { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" },
18- { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" },
19- { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
20- { name = "splitter", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "05564A381580395DCDEFF4F88A64B021E8DAFA6540AE99B4623962F52976AA9D" },
21- { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
22- { name = "trie_again", version = "1.1.4", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "E3BD66B4E126EF567EA8C4944EAB216413392ADF6C16C36047AF79EE5EF13466" },
23-]
24-25-[requirements]
26-birdie = { version = ">= 1.0.0 and < 2.0.0" }
27-gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
28-gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
···0000000000000000000000000000
-318
graphql/src/graphql/connection.gleam
···1-/// GraphQL Connection Types for Relay Cursor Connections
2-///
3-/// Implements the Relay Cursor Connections Specification:
4-/// https://relay.dev/graphql/connections.htm
5-import gleam/list
6-import gleam/option.{type Option, None, Some}
7-import graphql/schema
8-import graphql/value
9-10-/// PageInfo type for connection pagination metadata
11-pub type PageInfo {
12- PageInfo(
13- has_next_page: Bool,
14- has_previous_page: Bool,
15- start_cursor: Option(String),
16- end_cursor: Option(String),
17- )
18-}
19-20-/// Edge wrapper containing a node and its cursor
21-pub type Edge(node_type) {
22- Edge(node: node_type, cursor: String)
23-}
24-25-/// Connection wrapper containing edges and page info
26-pub type Connection(node_type) {
27- Connection(
28- edges: List(Edge(node_type)),
29- page_info: PageInfo,
30- total_count: Option(Int),
31- )
32-}
33-34-/// Creates the PageInfo GraphQL type
35-pub fn page_info_type() -> schema.Type {
36- schema.object_type(
37- "PageInfo",
38- "Information about pagination in a connection",
39- [
40- schema.field(
41- "hasNextPage",
42- schema.non_null(schema.boolean_type()),
43- "When paginating forwards, are there more items?",
44- fn(ctx) {
45- // Extract from context data
46- case ctx.data {
47- Some(value.Object(fields)) -> {
48- case list.key_find(fields, "hasNextPage") {
49- Ok(val) -> Ok(val)
50- Error(_) -> Ok(value.Boolean(False))
51- }
52- }
53- _ -> Ok(value.Boolean(False))
54- }
55- },
56- ),
57- schema.field(
58- "hasPreviousPage",
59- schema.non_null(schema.boolean_type()),
60- "When paginating backwards, are there more items?",
61- fn(ctx) {
62- case ctx.data {
63- Some(value.Object(fields)) -> {
64- case list.key_find(fields, "hasPreviousPage") {
65- Ok(val) -> Ok(val)
66- Error(_) -> Ok(value.Boolean(False))
67- }
68- }
69- _ -> Ok(value.Boolean(False))
70- }
71- },
72- ),
73- schema.field(
74- "startCursor",
75- schema.string_type(),
76- "Cursor corresponding to the first item in the page",
77- fn(ctx) {
78- case ctx.data {
79- Some(value.Object(fields)) -> {
80- case list.key_find(fields, "startCursor") {
81- Ok(val) -> Ok(val)
82- Error(_) -> Ok(value.Null)
83- }
84- }
85- _ -> Ok(value.Null)
86- }
87- },
88- ),
89- schema.field(
90- "endCursor",
91- schema.string_type(),
92- "Cursor corresponding to the last item in the page",
93- fn(ctx) {
94- case ctx.data {
95- Some(value.Object(fields)) -> {
96- case list.key_find(fields, "endCursor") {
97- Ok(val) -> Ok(val)
98- Error(_) -> Ok(value.Null)
99- }
100- }
101- _ -> Ok(value.Null)
102- }
103- },
104- ),
105- ],
106- )
107-}
108-109-/// Creates an Edge type for a given node type name
110-pub fn edge_type(node_type_name: String, node_type: schema.Type) -> schema.Type {
111- let edge_type_name = node_type_name <> "Edge"
112-113- schema.object_type(
114- edge_type_name,
115- "An edge in a connection for " <> node_type_name,
116- [
117- schema.field(
118- "node",
119- schema.non_null(node_type),
120- "The item at the end of the edge",
121- fn(ctx) {
122- // Extract node from context data
123- case ctx.data {
124- Some(value.Object(fields)) -> {
125- case list.key_find(fields, "node") {
126- Ok(val) -> Ok(val)
127- Error(_) -> Ok(value.Null)
128- }
129- }
130- _ -> Ok(value.Null)
131- }
132- },
133- ),
134- schema.field(
135- "cursor",
136- schema.non_null(schema.string_type()),
137- "A cursor for use in pagination",
138- fn(ctx) {
139- case ctx.data {
140- Some(value.Object(fields)) -> {
141- case list.key_find(fields, "cursor") {
142- Ok(val) -> Ok(val)
143- Error(_) -> Ok(value.String(""))
144- }
145- }
146- _ -> Ok(value.String(""))
147- }
148- },
149- ),
150- ],
151- )
152-}
153-154-/// Creates a Connection type for a given node type name
155-pub fn connection_type(
156- node_type_name: String,
157- edge_type: schema.Type,
158-) -> schema.Type {
159- let connection_type_name = node_type_name <> "Connection"
160-161- schema.object_type(
162- connection_type_name,
163- "A connection to a list of items for " <> node_type_name,
164- [
165- schema.field(
166- "edges",
167- schema.non_null(schema.list_type(schema.non_null(edge_type))),
168- "A list of edges",
169- fn(ctx) {
170- // Extract edges from context data
171- case ctx.data {
172- Some(value.Object(fields)) -> {
173- case list.key_find(fields, "edges") {
174- Ok(val) -> Ok(val)
175- Error(_) -> Ok(value.List([]))
176- }
177- }
178- _ -> Ok(value.List([]))
179- }
180- },
181- ),
182- schema.field(
183- "pageInfo",
184- schema.non_null(page_info_type()),
185- "Information to aid in pagination",
186- fn(ctx) {
187- // Extract pageInfo from context data
188- case ctx.data {
189- Some(value.Object(fields)) -> {
190- case list.key_find(fields, "pageInfo") {
191- Ok(val) -> Ok(val)
192- Error(_) ->
193- Ok(
194- value.Object([
195- #("hasNextPage", value.Boolean(False)),
196- #("hasPreviousPage", value.Boolean(False)),
197- #("startCursor", value.Null),
198- #("endCursor", value.Null),
199- ]),
200- )
201- }
202- }
203- _ ->
204- Ok(
205- value.Object([
206- #("hasNextPage", value.Boolean(False)),
207- #("hasPreviousPage", value.Boolean(False)),
208- #("startCursor", value.Null),
209- #("endCursor", value.Null),
210- ]),
211- )
212- }
213- },
214- ),
215- schema.field(
216- "totalCount",
217- schema.int_type(),
218- "Total number of items in the connection",
219- fn(ctx) {
220- case ctx.data {
221- Some(value.Object(fields)) -> {
222- case list.key_find(fields, "totalCount") {
223- Ok(val) -> Ok(val)
224- Error(_) -> Ok(value.Null)
225- }
226- }
227- _ -> Ok(value.Null)
228- }
229- },
230- ),
231- ],
232- )
233-}
234-235-/// Standard pagination arguments for forward pagination
236-pub fn forward_pagination_args() -> List(schema.Argument) {
237- [
238- schema.argument(
239- "first",
240- schema.int_type(),
241- "Returns the first n items from the list",
242- None,
243- ),
244- schema.argument(
245- "after",
246- schema.string_type(),
247- "Returns items after the given cursor",
248- None,
249- ),
250- ]
251-}
252-253-/// Standard pagination arguments for backward pagination
254-pub fn backward_pagination_args() -> List(schema.Argument) {
255- [
256- schema.argument(
257- "last",
258- schema.int_type(),
259- "Returns the last n items from the list",
260- None,
261- ),
262- schema.argument(
263- "before",
264- schema.string_type(),
265- "Returns items before the given cursor",
266- None,
267- ),
268- ]
269-}
270-271-/// All standard connection arguments (forward + backward)
272-/// Note: sortBy is not included yet as it requires InputObject type support
273-pub fn connection_args() -> List(schema.Argument) {
274- list.flatten([forward_pagination_args(), backward_pagination_args()])
275-}
276-277-/// Converts a PageInfo value to a GraphQL value
278-pub fn page_info_to_value(page_info: PageInfo) -> value.Value {
279- value.Object([
280- #("hasNextPage", value.Boolean(page_info.has_next_page)),
281- #("hasPreviousPage", value.Boolean(page_info.has_previous_page)),
282- #("startCursor", case page_info.start_cursor {
283- Some(cursor) -> value.String(cursor)
284- None -> value.Null
285- }),
286- #("endCursor", case page_info.end_cursor {
287- Some(cursor) -> value.String(cursor)
288- None -> value.Null
289- }),
290- ])
291-}
292-293-/// Converts an Edge to a GraphQL value
294-pub fn edge_to_value(edge: Edge(value.Value)) -> value.Value {
295- value.Object([
296- #("node", edge.node),
297- #("cursor", value.String(edge.cursor)),
298- ])
299-}
300-301-/// Converts a Connection to a GraphQL value
302-pub fn connection_to_value(connection: Connection(value.Value)) -> value.Value {
303- let edges_value =
304- connection.edges
305- |> list.map(edge_to_value)
306- |> value.List
307-308- let total_count_value = case connection.total_count {
309- Some(count) -> value.Int(count)
310- None -> value.Null
311- }
312-313- value.Object([
314- #("edges", edges_value),
315- #("pageInfo", page_info_to_value(connection.page_info)),
316- #("totalCount", total_count_value),
317- ])
318-}
···1-/// GraphQL Introspection
2-///
3-/// Implements the GraphQL introspection system per the GraphQL spec.
4-/// Provides __schema, __type, and __typename meta-fields.
5-import gleam/dict
6-import gleam/list
7-import gleam/option
8-import gleam/result
9-import graphql/schema
10-import graphql/value
11-12-/// Build introspection value for __schema
13-pub fn schema_introspection(graphql_schema: schema.Schema) -> value.Value {
14- let query_type = schema.query_type(graphql_schema)
15- let mutation_type_option = schema.get_mutation_type(graphql_schema)
16- let subscription_type_option = schema.get_subscription_type(graphql_schema)
17-18- // Build list of all types in the schema
19- let all_types = get_all_types(graphql_schema)
20-21- // Build mutation type ref if it exists
22- let mutation_type_value = case mutation_type_option {
23- option.Some(mutation_type) -> type_ref(mutation_type)
24- option.None -> value.Null
25- }
26-27- // Build subscription type ref if it exists
28- let subscription_type_value = case subscription_type_option {
29- option.Some(subscription_type) -> type_ref(subscription_type)
30- option.None -> value.Null
31- }
32-33- value.Object([
34- #("queryType", type_ref(query_type)),
35- #("mutationType", mutation_type_value),
36- #("subscriptionType", subscription_type_value),
37- #("types", value.List(all_types)),
38- #("directives", value.List([])),
39- ])
40-}
41-42-/// Build introspection value for __type(name: "TypeName")
43-/// Returns Some(type_introspection) if the type is found, None otherwise
44-pub fn type_by_name_introspection(
45- graphql_schema: schema.Schema,
46- type_name: String,
47-) -> option.Option(value.Value) {
48- let all_types = get_all_schema_types(graphql_schema)
49-50- // Find the type with the matching name
51- let found_type =
52- list.find(all_types, fn(t) { schema.type_name(t) == type_name })
53-54- case found_type {
55- Ok(t) -> option.Some(type_introspection(t))
56- Error(_) -> option.None
57- }
58-}
59-60-/// Get all types from the schema as schema.Type values
61-/// Useful for testing and documentation generation
62-pub fn get_all_schema_types(graphql_schema: schema.Schema) -> List(schema.Type) {
63- let query_type = schema.query_type(graphql_schema)
64- let mutation_type_option = schema.get_mutation_type(graphql_schema)
65- let subscription_type_option = schema.get_subscription_type(graphql_schema)
66-67- // Collect all types by traversing the query type
68- let mut_collected_types = collect_types_from_type(query_type, [])
69-70- // Also collect types from mutation type if it exists
71- let mutation_collected_types = case mutation_type_option {
72- option.Some(mutation_type) ->
73- collect_types_from_type(mutation_type, mut_collected_types)
74- option.None -> mut_collected_types
75- }
76-77- // Also collect types from subscription type if it exists
78- let all_collected_types = case subscription_type_option {
79- option.Some(subscription_type) ->
80- collect_types_from_type(subscription_type, mutation_collected_types)
81- option.None -> mutation_collected_types
82- }
83-84- // Deduplicate by type name, preferring types with more fields
85- // This ensures we get the "most complete" version of each type
86- let unique_types = deduplicate_types_by_name(all_collected_types)
87-88- // Add any built-in scalars that aren't already in the list
89- let all_built_ins = [
90- schema.string_type(),
91- schema.int_type(),
92- schema.float_type(),
93- schema.boolean_type(),
94- schema.id_type(),
95- ]
96-97- let collected_names = list.map(unique_types, schema.type_name)
98- let missing_built_ins =
99- list.filter(all_built_ins, fn(built_in) {
100- let built_in_name = schema.type_name(built_in)
101- !list.contains(collected_names, built_in_name)
102- })
103-104- list.append(unique_types, missing_built_ins)
105-}
106-107-/// Get all types from the schema
108-fn get_all_types(graphql_schema: schema.Schema) -> List(value.Value) {
109- let all_types = get_all_schema_types(graphql_schema)
110-111- // Convert all types to introspection values
112- list.map(all_types, type_introspection)
113-}
114-115-/// Deduplicate types by name, keeping the version with the most fields
116-/// This ensures we get the "most complete" version of each type when
117-/// multiple versions exist (e.g., from different passes in schema building)
118-fn deduplicate_types_by_name(types: List(schema.Type)) -> List(schema.Type) {
119- // Group types by name
120- types
121- |> list.group(schema.type_name)
122- |> dict.to_list
123- |> list.map(fn(pair) {
124- let #(_name, type_list) = pair
125- // For each group, find the type with the most content
126- type_list
127- |> list.reduce(fn(best, current) {
128- // Count content: fields for object types, enum values for enums, etc.
129- let best_content_count = get_type_content_count(best)
130- let current_content_count = get_type_content_count(current)
131-132- // Prefer the type with more content
133- case current_content_count > best_content_count {
134- True -> current
135- False -> best
136- }
137- })
138- |> result.unwrap(
139- list.first(type_list)
140- |> result.unwrap(schema.string_type()),
141- )
142- })
143-}
144-145-/// Get the "content count" for a type (fields, enum values, input fields, etc.)
146-/// This helps us pick the most complete version of a type during deduplication
147-fn get_type_content_count(t: schema.Type) -> Int {
148- // For object types, count fields
149- let field_count = list.length(schema.get_fields(t))
150-151- // For enum types, count enum values
152- let enum_value_count = list.length(schema.get_enum_values(t))
153-154- // For input object types, count input fields
155- let input_field_count = list.length(schema.get_input_fields(t))
156-157- // Return the maximum (types will only have one of these be non-zero)
158- [field_count, enum_value_count, input_field_count]
159- |> list.reduce(fn(a, b) {
160- case a > b {
161- True -> a
162- False -> b
163- }
164- })
165- |> result.unwrap(0)
166-}
167-168-/// Collect all types referenced in a type (recursively)
169-/// Note: We collect ALL instances of each type (even duplicates by name)
170-/// because we want to find the "most complete" version during deduplication
171-fn collect_types_from_type(
172- t: schema.Type,
173- acc: List(schema.Type),
174-) -> List(schema.Type) {
175- // Always add this type - we'll deduplicate later by choosing the version with most fields
176- let new_acc = [t, ..acc]
177-178- // To prevent infinite recursion, check if we've already traversed this exact type instance
179- // We use a simple heuristic: if this type name appears multiple times AND this specific
180- // instance has the same or fewer content than what we've seen, skip traversing its children
181- let should_traverse_children = case
182- schema.is_object(t) || schema.is_enum(t) || schema.is_union(t)
183- {
184- True -> {
185- let current_content_count = get_type_content_count(t)
186- let existing_with_same_name =
187- list.filter(acc, fn(existing) {
188- schema.type_name(existing) == schema.type_name(t)
189- })
190- let max_existing_content =
191- existing_with_same_name
192- |> list.map(get_type_content_count)
193- |> list.reduce(fn(a, b) {
194- case a > b {
195- True -> a
196- False -> b
197- }
198- })
199- |> result.unwrap(0)
200-201- // Only traverse if this instance has more content than we've seen before
202- current_content_count > max_existing_content
203- }
204- False -> True
205- }
206-207- case should_traverse_children {
208- False -> new_acc
209- True -> {
210- // Recursively collect types from fields if this is an object type
211- case schema.is_object(t) {
212- True -> {
213- let fields = schema.get_fields(t)
214- list.fold(fields, new_acc, fn(acc2, field) {
215- let field_type = schema.field_type(field)
216- let acc3 = collect_types_from_type_deep(field_type, acc2)
217-218- // Also collect types from field arguments
219- let arguments = schema.field_arguments(field)
220- list.fold(arguments, acc3, fn(acc4, arg) {
221- let arg_type = schema.argument_type(arg)
222- collect_types_from_type_deep(arg_type, acc4)
223- })
224- })
225- }
226- False -> {
227- // Check if it's a union type
228- case schema.is_union(t) {
229- True -> {
230- // Collect types from union's possible_types
231- let possible_types = schema.get_possible_types(t)
232- list.fold(possible_types, new_acc, fn(acc2, union_type) {
233- collect_types_from_type_deep(union_type, acc2)
234- })
235- }
236- False -> {
237- // Check if it's an InputObjectType
238- let input_fields = schema.get_input_fields(t)
239- case list.is_empty(input_fields) {
240- False -> {
241- // This is an InputObjectType, collect types from its fields
242- list.fold(input_fields, new_acc, fn(acc2, input_field) {
243- let field_type = schema.input_field_type(input_field)
244- collect_types_from_type_deep(field_type, acc2)
245- })
246- }
247- True -> {
248- // Check if it's a wrapping type (List or NonNull)
249- case schema.inner_type(t) {
250- option.Some(inner) ->
251- collect_types_from_type_deep(inner, new_acc)
252- option.None -> new_acc
253- }
254- }
255- }
256- }
257- }
258- }
259- }
260- }
261- }
262-}
263-264-/// Helper to unwrap LIST and NON_NULL and collect the inner type
265-fn collect_types_from_type_deep(
266- t: schema.Type,
267- acc: List(schema.Type),
268-) -> List(schema.Type) {
269- // Check if this is a wrapping type (List or NonNull)
270- case schema.inner_type(t) {
271- option.Some(inner) -> collect_types_from_type_deep(inner, acc)
272- option.None -> collect_types_from_type(t, acc)
273- }
274-}
275-276-/// Build full type introspection value
277-fn type_introspection(t: schema.Type) -> value.Value {
278- let kind = schema.type_kind(t)
279- let type_name = schema.type_name(t)
280-281- // Get inner type for LIST and NON_NULL
282- let of_type = case schema.inner_type(t) {
283- option.Some(inner) -> type_ref(inner)
284- option.None -> value.Null
285- }
286-287- // Determine fields based on kind
288- let fields = case kind {
289- "OBJECT" -> value.List(get_fields_for_type(t))
290- _ -> value.Null
291- }
292-293- // Determine inputFields for INPUT_OBJECT types
294- let input_fields = case kind {
295- "INPUT_OBJECT" -> value.List(get_input_fields_for_type(t))
296- _ -> value.Null
297- }
298-299- // Determine enumValues for ENUM types
300- let enum_values = case kind {
301- "ENUM" -> value.List(get_enum_values_for_type(t))
302- _ -> value.Null
303- }
304-305- // Determine possibleTypes for UNION types
306- let possible_types = case kind {
307- "UNION" -> {
308- let types = schema.get_possible_types(t)
309- value.List(list.map(types, type_ref))
310- }
311- _ -> value.Null
312- }
313-314- // Handle wrapping types (LIST/NON_NULL) differently
315- let name = case kind {
316- "LIST" -> value.Null
317- "NON_NULL" -> value.Null
318- _ -> value.String(type_name)
319- }
320-321- let description = case schema.type_description(t) {
322- "" -> value.Null
323- desc -> value.String(desc)
324- }
325-326- value.Object([
327- #("kind", value.String(kind)),
328- #("name", name),
329- #("description", description),
330- #("fields", fields),
331- #("interfaces", value.List([])),
332- #("possibleTypes", possible_types),
333- #("enumValues", enum_values),
334- #("inputFields", input_fields),
335- #("ofType", of_type),
336- ])
337-}
338-339-/// Get fields for a type (if it's an object type)
340-fn get_fields_for_type(t: schema.Type) -> List(value.Value) {
341- let fields = schema.get_fields(t)
342-343- list.map(fields, fn(field) {
344- let field_type_val = schema.field_type(field)
345- let args = schema.field_arguments(field)
346-347- value.Object([
348- #("name", value.String(schema.field_name(field))),
349- #("description", value.String(schema.field_description(field))),
350- #("args", value.List(list.map(args, argument_introspection))),
351- #("type", type_ref(field_type_val)),
352- #("isDeprecated", value.Boolean(False)),
353- #("deprecationReason", value.Null),
354- ])
355- })
356-}
357-358-/// Get input fields for a type (if it's an input object type)
359-fn get_input_fields_for_type(t: schema.Type) -> List(value.Value) {
360- let input_fields = schema.get_input_fields(t)
361-362- list.map(input_fields, fn(input_field) {
363- let field_type_val = schema.input_field_type(input_field)
364-365- value.Object([
366- #("name", value.String(schema.input_field_name(input_field))),
367- #(
368- "description",
369- value.String(schema.input_field_description(input_field)),
370- ),
371- #("type", type_ref(field_type_val)),
372- #("defaultValue", value.Null),
373- ])
374- })
375-}
376-377-/// Get enum values for a type (if it's an enum type)
378-fn get_enum_values_for_type(t: schema.Type) -> List(value.Value) {
379- let enum_values = schema.get_enum_values(t)
380-381- list.map(enum_values, fn(enum_value) {
382- value.Object([
383- #("name", value.String(schema.enum_value_name(enum_value))),
384- #("description", value.String(schema.enum_value_description(enum_value))),
385- #("isDeprecated", value.Boolean(False)),
386- #("deprecationReason", value.Null),
387- ])
388- })
389-}
390-391-/// Build introspection for an argument
392-fn argument_introspection(arg: schema.Argument) -> value.Value {
393- value.Object([
394- #("name", value.String(schema.argument_name(arg))),
395- #("description", value.String(schema.argument_description(arg))),
396- #("type", type_ref(schema.argument_type(arg))),
397- #("defaultValue", value.Null),
398- ])
399-}
400-401-/// Build a type reference (simplified version of type_introspection for field types)
402-fn type_ref(t: schema.Type) -> value.Value {
403- let kind = schema.type_kind(t)
404- let type_name = schema.type_name(t)
405-406- // Get inner type for LIST and NON_NULL
407- let of_type = case schema.inner_type(t) {
408- option.Some(inner) -> type_ref(inner)
409- option.None -> value.Null
410- }
411-412- // Handle wrapping types (LIST/NON_NULL) differently
413- let name = case kind {
414- "LIST" -> value.Null
415- "NON_NULL" -> value.Null
416- _ -> value.String(type_name)
417- }
418-419- value.Object([
420- #("kind", value.String(kind)),
421- #("name", name),
422- #("ofType", of_type),
423- ])
424-}
···1-/// GraphQL Value types
2-///
3-/// Per GraphQL spec Section 2 - Language, values can be scalars, enums,
4-/// lists, or objects. This module defines the core Value type used throughout
5-/// the GraphQL implementation.
6-/// A GraphQL value that can be used in queries, responses, and variables
7-pub type Value {
8- /// Represents null/absence of a value
9- Null
10-11- /// Integer value (32-bit signed integer per spec)
12- Int(Int)
13-14- /// Floating point value (IEEE 754 double precision per spec)
15- Float(Float)
16-17- /// UTF-8 string value
18- String(String)
19-20- /// Boolean true or false
21- Boolean(Bool)
22-23- /// Enum value represented as a string (e.g., "ACTIVE", "PENDING")
24- Enum(String)
25-26- /// Ordered list of values
27- List(List(Value))
28-29- /// Unordered set of key-value pairs
30- /// Using list of tuples for simplicity and ordering preservation
31- Object(List(#(String, Value)))
32-}
···00000000000000000000000000000000
-867
graphql/test/executor_test.gleam
···1-/// Tests for GraphQL Executor
2-///
3-/// Tests query execution combining parser + schema + resolvers
4-import birdie
5-import gleam/dict
6-import gleam/list
7-import gleam/option.{None, Some}
8-import gleam/string
9-import gleeunit/should
10-import graphql/executor
11-import graphql/schema
12-import graphql/value
13-14-// Helper to create a simple test schema
15-fn 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
37-fn 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-63-pub 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-77-pub 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-86-pub 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
96-fn format_response(response: executor.Response) -> String {
97- string.inspect(response)
98-}
99-100-pub 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-109-pub 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-123-pub 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-132-pub 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-149-pub 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-166-pub 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
184-pub 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 list fields with nested selections
210-pub fn execute_list_with_nested_selections_test() {
211- // Create a schema with a list field
212- let user_type =
213- schema.object_type("User", "A user", [
214- schema.field("id", schema.id_type(), "User ID", fn(ctx) {
215- case ctx.data {
216- option.Some(value.Object(fields)) -> {
217- case list.key_find(fields, "id") {
218- Ok(id_val) -> Ok(id_val)
219- Error(_) -> Ok(value.Null)
220- }
221- }
222- _ -> Ok(value.Null)
223- }
224- }),
225- schema.field("name", schema.string_type(), "User name", fn(ctx) {
226- case ctx.data {
227- option.Some(value.Object(fields)) -> {
228- case list.key_find(fields, "name") {
229- Ok(name_val) -> Ok(name_val)
230- Error(_) -> Ok(value.Null)
231- }
232- }
233- _ -> Ok(value.Null)
234- }
235- }),
236- schema.field("email", schema.string_type(), "User email", fn(ctx) {
237- case ctx.data {
238- option.Some(value.Object(fields)) -> {
239- case list.key_find(fields, "email") {
240- Ok(email_val) -> Ok(email_val)
241- Error(_) -> Ok(value.Null)
242- }
243- }
244- _ -> Ok(value.Null)
245- }
246- }),
247- ])
248-249- let list_type = schema.list_type(user_type)
250-251- let query_type =
252- schema.object_type("Query", "Root query type", [
253- schema.field("users", list_type, "Get all users", fn(_ctx) {
254- // Return a list of user objects
255- Ok(
256- value.List([
257- value.Object([
258- #("id", value.String("1")),
259- #("name", value.String("Alice")),
260- #("email", value.String("alice@example.com")),
261- ]),
262- value.Object([
263- #("id", value.String("2")),
264- #("name", value.String("Bob")),
265- #("email", value.String("bob@example.com")),
266- ]),
267- ]),
268- )
269- }),
270- ])
271-272- let schema = schema.schema(query_type, None)
273-274- // Query with nested field selection - only request id and name, not email
275- let query = "{ users { id name } }"
276-277- let result = executor.execute(query, schema, schema.context(None))
278-279- let response = case result {
280- Ok(r) -> r
281- Error(_) -> panic as "Execution failed"
282- }
283-284- birdie.snap(
285- title: "Execute list with nested selections",
286- content: format_response(response),
287- )
288-}
289-290-// Test that arguments are actually passed to resolvers
291-pub fn execute_field_receives_string_argument_test() {
292- let query_type =
293- schema.object_type("Query", "Root", [
294- schema.field_with_args(
295- "echo",
296- schema.string_type(),
297- "Echo the input",
298- [schema.argument("message", schema.string_type(), "Message", None)],
299- fn(ctx) {
300- // Extract the argument from context
301- case schema.get_argument(ctx, "message") {
302- Some(value.String(msg)) -> Ok(value.String("Echo: " <> msg))
303- _ -> Ok(value.String("No message"))
304- }
305- },
306- ),
307- ])
308-309- let test_schema = schema.schema(query_type, None)
310- let query = "{ echo(message: \"hello\") }"
311-312- let result = executor.execute(query, test_schema, schema.context(None))
313-314- let response = case result {
315- Ok(r) -> r
316- Error(_) -> panic as "Execution failed"
317- }
318-319- birdie.snap(
320- title: "Execute field with string argument",
321- content: format_response(response),
322- )
323-}
324-325-// Test list argument
326-pub fn execute_field_receives_list_argument_test() {
327- let query_type =
328- schema.object_type("Query", "Root", [
329- schema.field_with_args(
330- "sum",
331- schema.int_type(),
332- "Sum numbers",
333- [
334- schema.argument(
335- "numbers",
336- schema.list_type(schema.int_type()),
337- "Numbers",
338- None,
339- ),
340- ],
341- fn(ctx) {
342- case schema.get_argument(ctx, "numbers") {
343- Some(value.List(_items)) -> Ok(value.String("got list"))
344- _ -> Ok(value.String("no list"))
345- }
346- },
347- ),
348- ])
349-350- let test_schema = schema.schema(query_type, None)
351- let query = "{ sum(numbers: [1, 2, 3]) }"
352-353- let result = executor.execute(query, test_schema, schema.context(None))
354-355- should.be_ok(result)
356- |> fn(response) {
357- case response {
358- executor.Response(
359- data: value.Object([#("sum", value.String("got list"))]),
360- errors: [],
361- ) -> True
362- _ -> False
363- }
364- }
365- |> should.be_true
366-}
367-368-// Test object argument (like sortBy)
369-pub fn execute_field_receives_object_argument_test() {
370- let query_type =
371- schema.object_type("Query", "Root", [
372- schema.field_with_args(
373- "posts",
374- schema.list_type(schema.string_type()),
375- "Get posts",
376- [
377- schema.argument(
378- "sortBy",
379- schema.list_type(
380- schema.input_object_type("SortInput", "Sort", [
381- schema.input_field("field", schema.string_type(), "Field", None),
382- schema.input_field(
383- "direction",
384- schema.enum_type("Direction", "Direction", [
385- schema.enum_value("ASC", "Ascending"),
386- schema.enum_value("DESC", "Descending"),
387- ]),
388- "Direction",
389- None,
390- ),
391- ]),
392- ),
393- "Sort order",
394- None,
395- ),
396- ],
397- fn(ctx) {
398- case schema.get_argument(ctx, "sortBy") {
399- Some(value.List([value.Object(fields), ..])) -> {
400- case dict.from_list(fields) {
401- fields_dict -> {
402- case
403- dict.get(fields_dict, "field"),
404- dict.get(fields_dict, "direction")
405- {
406- Ok(value.String(field)), Ok(value.String(dir)) ->
407- Ok(value.String("Sorting by " <> field <> " " <> dir))
408- _, _ -> Ok(value.String("Invalid sort"))
409- }
410- }
411- }
412- }
413- _ -> Ok(value.String("No sort"))
414- }
415- },
416- ),
417- ])
418-419- let test_schema = schema.schema(query_type, None)
420- let query = "{ posts(sortBy: [{field: \"date\", direction: DESC}]) }"
421-422- let result = executor.execute(query, test_schema, schema.context(None))
423-424- let response = case result {
425- Ok(r) -> r
426- Error(_) -> panic as "Execution failed"
427- }
428-429- birdie.snap(
430- title: "Execute field with object argument",
431- content: format_response(response),
432- )
433-}
434-435-// Variable resolution tests
436-pub fn execute_query_with_variable_string_test() {
437- let query_type =
438- schema.object_type("Query", "Root query type", [
439- schema.field_with_args(
440- "greet",
441- schema.string_type(),
442- "Greet someone",
443- [
444- schema.argument("name", schema.string_type(), "Name to greet", None),
445- ],
446- fn(ctx) {
447- case schema.get_argument(ctx, "name") {
448- Some(value.String(name)) ->
449- Ok(value.String("Hello, " <> name <> "!"))
450- _ -> Ok(value.String("Hello, stranger!"))
451- }
452- },
453- ),
454- ])
455-456- let test_schema = schema.schema(query_type, None)
457- let query = "query Test($name: String!) { greet(name: $name) }"
458-459- // Create context with variables
460- let variables = dict.from_list([#("name", value.String("Alice"))])
461- let ctx = schema.context_with_variables(None, variables)
462-463- let result = executor.execute(query, test_schema, ctx)
464-465- let response = case result {
466- Ok(r) -> r
467- Error(_) -> panic as "Execution failed"
468- }
469-470- birdie.snap(
471- title: "Execute query with string variable",
472- content: format_response(response),
473- )
474-}
475-476-pub fn execute_query_with_variable_int_test() {
477- let query_type =
478- schema.object_type("Query", "Root query type", [
479- schema.field_with_args(
480- "user",
481- schema.string_type(),
482- "Get user by ID",
483- [
484- schema.argument("id", schema.int_type(), "User ID", None),
485- ],
486- fn(ctx) {
487- case schema.get_argument(ctx, "id") {
488- Some(value.Int(id)) ->
489- Ok(value.String("User #" <> string.inspect(id)))
490- _ -> Ok(value.String("Unknown user"))
491- }
492- },
493- ),
494- ])
495-496- let test_schema = schema.schema(query_type, None)
497- let query = "query GetUser($userId: Int!) { user(id: $userId) }"
498-499- // Create context with variables
500- let variables = dict.from_list([#("userId", value.Int(42))])
501- let ctx = schema.context_with_variables(None, variables)
502-503- let result = executor.execute(query, test_schema, ctx)
504-505- let response = case result {
506- Ok(r) -> r
507- Error(_) -> panic as "Execution failed"
508- }
509-510- birdie.snap(
511- title: "Execute query with int variable",
512- content: format_response(response),
513- )
514-}
515-516-pub fn execute_query_with_multiple_variables_test() {
517- let query_type =
518- schema.object_type("Query", "Root query type", [
519- schema.field_with_args(
520- "search",
521- schema.string_type(),
522- "Search for something",
523- [
524- schema.argument("query", schema.string_type(), "Search query", None),
525- schema.argument("limit", schema.int_type(), "Max results", None),
526- ],
527- fn(ctx) {
528- case
529- schema.get_argument(ctx, "query"),
530- schema.get_argument(ctx, "limit")
531- {
532- Some(value.String(q)), Some(value.Int(l)) ->
533- Ok(value.String(
534- "Searching for '"
535- <> q
536- <> "' (limit: "
537- <> string.inspect(l)
538- <> ")",
539- ))
540- _, _ -> Ok(value.String("Invalid search"))
541- }
542- },
543- ),
544- ])
545-546- let test_schema = schema.schema(query_type, None)
547- let query =
548- "query Search($q: String!, $max: Int!) { search(query: $q, limit: $max) }"
549-550- // Create context with variables
551- let variables =
552- dict.from_list([
553- #("q", value.String("graphql")),
554- #("max", value.Int(10)),
555- ])
556- let ctx = schema.context_with_variables(None, variables)
557-558- let result = executor.execute(query, test_schema, ctx)
559-560- let response = case result {
561- Ok(r) -> r
562- Error(_) -> panic as "Execution failed"
563- }
564-565- birdie.snap(
566- title: "Execute query with multiple variables",
567- content: format_response(response),
568- )
569-}
570-571-// Union type execution tests
572-pub fn execute_union_with_inline_fragment_test() {
573- // Create object types that will be part of the union
574- let post_type =
575- schema.object_type("Post", "A blog post", [
576- schema.field("title", schema.string_type(), "Post title", fn(ctx) {
577- case ctx.data {
578- option.Some(value.Object(fields)) -> {
579- case list.key_find(fields, "title") {
580- Ok(title_val) -> Ok(title_val)
581- Error(_) -> Ok(value.Null)
582- }
583- }
584- _ -> Ok(value.Null)
585- }
586- }),
587- schema.field("content", schema.string_type(), "Post content", fn(ctx) {
588- case ctx.data {
589- option.Some(value.Object(fields)) -> {
590- case list.key_find(fields, "content") {
591- Ok(content_val) -> Ok(content_val)
592- Error(_) -> Ok(value.Null)
593- }
594- }
595- _ -> Ok(value.Null)
596- }
597- }),
598- ])
599-600- let comment_type =
601- schema.object_type("Comment", "A comment", [
602- schema.field("text", schema.string_type(), "Comment text", fn(ctx) {
603- case ctx.data {
604- option.Some(value.Object(fields)) -> {
605- case list.key_find(fields, "text") {
606- Ok(text_val) -> Ok(text_val)
607- Error(_) -> Ok(value.Null)
608- }
609- }
610- _ -> Ok(value.Null)
611- }
612- }),
613- ])
614-615- // Type resolver that examines the __typename field
616- let type_resolver = fn(ctx: schema.Context) -> Result(String, String) {
617- case ctx.data {
618- option.Some(value.Object(fields)) -> {
619- case list.key_find(fields, "__typename") {
620- Ok(value.String(type_name)) -> Ok(type_name)
621- _ -> Error("No __typename field found")
622- }
623- }
624- _ -> Error("No data")
625- }
626- }
627-628- // Create union type
629- let search_result_union =
630- schema.union_type(
631- "SearchResult",
632- "A search result",
633- [post_type, comment_type],
634- type_resolver,
635- )
636-637- // Create query type with a field returning the union
638- let query_type =
639- schema.object_type("Query", "Root query type", [
640- schema.field(
641- "search",
642- search_result_union,
643- "Search for content",
644- fn(_ctx) {
645- // Return a Post
646- Ok(
647- value.Object([
648- #("__typename", value.String("Post")),
649- #("title", value.String("GraphQL is awesome")),
650- #("content", value.String("Learn all about GraphQL...")),
651- ]),
652- )
653- },
654- ),
655- ])
656-657- let test_schema = schema.schema(query_type, None)
658-659- // Query with inline fragment
660- let query =
661- "
662- {
663- search {
664- ... on Post {
665- title
666- content
667- }
668- ... on Comment {
669- text
670- }
671- }
672- }
673- "
674-675- let result = executor.execute(query, test_schema, schema.context(None))
676-677- let response = case result {
678- Ok(r) -> r
679- Error(_) -> panic as "Execution failed"
680- }
681-682- birdie.snap(
683- title: "Execute union with inline fragment",
684- content: format_response(response),
685- )
686-}
687-688-pub fn execute_union_list_with_inline_fragments_test() {
689- // Create object types
690- let post_type =
691- schema.object_type("Post", "A blog post", [
692- schema.field("title", schema.string_type(), "Post title", fn(ctx) {
693- case ctx.data {
694- option.Some(value.Object(fields)) -> {
695- case list.key_find(fields, "title") {
696- Ok(title_val) -> Ok(title_val)
697- Error(_) -> Ok(value.Null)
698- }
699- }
700- _ -> Ok(value.Null)
701- }
702- }),
703- ])
704-705- let comment_type =
706- schema.object_type("Comment", "A comment", [
707- schema.field("text", schema.string_type(), "Comment text", fn(ctx) {
708- case ctx.data {
709- option.Some(value.Object(fields)) -> {
710- case list.key_find(fields, "text") {
711- Ok(text_val) -> Ok(text_val)
712- Error(_) -> Ok(value.Null)
713- }
714- }
715- _ -> Ok(value.Null)
716- }
717- }),
718- ])
719-720- // Type resolver
721- let type_resolver = fn(ctx: schema.Context) -> Result(String, String) {
722- case ctx.data {
723- option.Some(value.Object(fields)) -> {
724- case list.key_find(fields, "__typename") {
725- Ok(value.String(type_name)) -> Ok(type_name)
726- _ -> Error("No __typename field found")
727- }
728- }
729- _ -> Error("No data")
730- }
731- }
732-733- // Create union type
734- let search_result_union =
735- schema.union_type(
736- "SearchResult",
737- "A search result",
738- [post_type, comment_type],
739- type_resolver,
740- )
741-742- // Create query type with a list of unions
743- let query_type =
744- schema.object_type("Query", "Root query type", [
745- schema.field(
746- "searchAll",
747- schema.list_type(search_result_union),
748- "Search for all content",
749- fn(_ctx) {
750- // Return a list with mixed types
751- Ok(
752- value.List([
753- value.Object([
754- #("__typename", value.String("Post")),
755- #("title", value.String("First Post")),
756- ]),
757- value.Object([
758- #("__typename", value.String("Comment")),
759- #("text", value.String("Great article!")),
760- ]),
761- value.Object([
762- #("__typename", value.String("Post")),
763- #("title", value.String("Second Post")),
764- ]),
765- ]),
766- )
767- },
768- ),
769- ])
770-771- let test_schema = schema.schema(query_type, None)
772-773- // Query with inline fragments on list items
774- let query =
775- "
776- {
777- searchAll {
778- ... on Post {
779- title
780- }
781- ... on Comment {
782- text
783- }
784- }
785- }
786- "
787-788- let result = executor.execute(query, test_schema, schema.context(None))
789-790- let response = case result {
791- Ok(r) -> r
792- Error(_) -> panic as "Execution failed"
793- }
794-795- birdie.snap(
796- title: "Execute union list with inline fragments",
797- content: format_response(response),
798- )
799-}
800-801-// Test field aliases
802-pub fn execute_field_with_alias_test() {
803- let schema = test_schema()
804- let query = "{ greeting: hello }"
805-806- let result = executor.execute(query, schema, schema.context(None))
807-808- let response = case result {
809- Ok(r) -> r
810- Error(_) -> panic as "Execution failed"
811- }
812-813- // Response should contain "greeting" as the key, not "hello"
814- case response.data {
815- value.Object(fields) -> {
816- case list.key_find(fields, "greeting") {
817- Ok(_) -> should.be_true(True)
818- Error(_) -> {
819- // Check if it incorrectly used "hello" instead
820- case list.key_find(fields, "hello") {
821- Ok(_) ->
822- panic as "Alias not applied - used 'hello' instead of 'greeting'"
823- Error(_) ->
824- panic as "Neither 'greeting' nor 'hello' found in response"
825- }
826- }
827- }
828- }
829- _ -> panic as "Expected object response"
830- }
831-}
832-833-// Test multiple aliases
834-pub fn execute_multiple_fields_with_aliases_test() {
835- let schema = test_schema()
836- let query = "{ greeting: hello num: number }"
837-838- let result = executor.execute(query, schema, schema.context(None))
839-840- let response = case result {
841- Ok(r) -> r
842- Error(_) -> panic as "Execution failed"
843- }
844-845- birdie.snap(
846- title: "Execute multiple fields with aliases",
847- content: format_response(response),
848- )
849-}
850-851-// Test mixed aliased and non-aliased fields
852-pub fn execute_mixed_aliased_fields_test() {
853- let schema = test_schema()
854- let query = "{ greeting: hello number }"
855-856- let result = executor.execute(query, schema, schema.context(None))
857-858- let response = case result {
859- Ok(r) -> r
860- Error(_) -> panic as "Execution failed"
861- }
862-863- birdie.snap(
864- title: "Execute mixed aliased and non-aliased fields",
865- content: format_response(response),
866- )
867-}
···8import gleam/option.{type Option, None, Some}
9import gleam/result
10import gleam/string
11-import graphql/value
12import lexicon_graphql/collection_meta
13import lexicon_graphql/uri_extractor
14import lexicon_graphql/where_input.{type WhereClause}
01516/// Result of a batch query: maps URIs to their records
17pub type BatchResult =
···8import gleam/option.{type Option, None, Some}
9import gleam/result
10import gleam/string
011import lexicon_graphql/collection_meta
12import lexicon_graphql/uri_extractor
13import lexicon_graphql/where_input.{type WhereClause}
14+import swell/value
1516/// Result of a batch query: maps URIs to their records
17pub type BatchResult =
···8import gleam/dict
9import gleam/list
10import gleam/option
11-import graphql/schema
12-import graphql/value
13import lexicon_graphql/nsid
14import lexicon_graphql/type_mapper
15import lexicon_graphql/types
001617/// Resolver factory function type
18/// Takes collection name and returns a resolver function
···8import gleam/dict
9import gleam/list
10import gleam/option
0011import lexicon_graphql/nsid
12import lexicon_graphql/type_mapper
13import lexicon_graphql/types
14+import swell/schema
15+import swell/value
1617/// Resolver factory function type
18/// Takes collection name and returns a resolver function
···7import gleam/list
8import gleam/option
9import gleam/string
10-import graphql/schema
11-import graphql/value
12import lexicon_graphql/lexicon_registry
13import lexicon_graphql/nsid
14import lexicon_graphql/type_mapper
15import lexicon_graphql/types
001617/// Build a GraphQL object type from an ObjectDef
18/// object_types_dict is used to resolve refs to other object types
···7import gleam/list
8import gleam/option
9import gleam/string
0010import lexicon_graphql/lexicon_registry
11import lexicon_graphql/nsid
12import lexicon_graphql/type_mapper
13import lexicon_graphql/types
14+import swell/schema
15+import swell/value
1617/// Build a GraphQL object type from an ObjectDef
18/// object_types_dict is used to resolve refs to other object types
···6/// Based on the Elixir implementation but adapted for the pure Gleam GraphQL library.
7import gleam/dict.{type Dict}
8import gleam/option.{type Option}
9-import graphql/schema
10import lexicon_graphql/blob_type
01112/// Maps a lexicon type string to a GraphQL output Type.
13///
···6/// Based on the Elixir implementation but adapted for the pure Gleam GraphQL library.
7import gleam/dict.{type Dict}
8import gleam/option.{type Option}
09import lexicon_graphql/blob_type
10+import swell/schema
1112/// Maps a lexicon type string to a GraphQL output Type.
13///
···5import gleam/dict.{type Dict}
6import gleam/list
7import gleam/option.{type Option, None, Some}
8-import graphql/value
910/// Simple value type that can represent strings, ints, or other primitives
11pub type WhereValue {
···5import gleam/dict.{type Dict}
6import gleam/list
7import gleam/option.{type Option, None, Some}
8+import swell/value
910/// Simple value type that can represent strings, ints, or other primitives
11pub type WhereValue {
···225 "
226227 let assert Ok(response_json) =
228- graphql_gleam.execute_query_with_db(db, query, "{}", Error(Nil), "", "https://plc.directory")
0000000229230 // Verify only 1 post is returned
231 string.contains(response_json, "\"edges\"")
···335 "
336337 let assert Ok(response_json) =
338- graphql_gleam.execute_query_with_db(db, query, "{}", Error(Nil), "", "https://plc.directory")
0000000339340 // Count how many post URIs appear (should be 2)
341 let post_count =
···444 "
445446 let assert Ok(response_json) =
447- graphql_gleam.execute_query_with_db(db, query, "{}", Error(Nil), "", "https://plc.directory")
0000000448449 // Count how many like URIs appear (should be 1)
450 let like_count =
···553 "
554555 let assert Ok(response_json) =
556- graphql_gleam.execute_query_with_db(db, query, "{}", Error(Nil), "", "https://plc.directory")
0000000557558 // All 3 posts should be returned (within default limit of 50)
559 let post_count =
···225 "
226227 let assert Ok(response_json) =
228+ graphql_gleam.execute_query_with_db(
229+ db,
230+ query,
231+ "{}",
232+ Error(Nil),
233+ "",
234+ "https://plc.directory",
235+ )
236237 // Verify only 1 post is returned
238 string.contains(response_json, "\"edges\"")
···342 "
343344 let assert Ok(response_json) =
345+ graphql_gleam.execute_query_with_db(
346+ db,
347+ query,
348+ "{}",
349+ Error(Nil),
350+ "",
351+ "https://plc.directory",
352+ )
353354 // Count how many post URIs appear (should be 2)
355 let post_count =
···458 "
459460 let assert Ok(response_json) =
461+ graphql_gleam.execute_query_with_db(
462+ db,
463+ query,
464+ "{}",
465+ Error(Nil),
466+ "",
467+ "https://plc.directory",
468+ )
469470 // Count how many like URIs appear (should be 1)
471 let like_count =
···574 "
575576 let assert Ok(response_json) =
577+ graphql_gleam.execute_query_with_db(
578+ db,
579+ query,
580+ "{}",
581+ Error(Nil),
582+ "",
583+ "https://plc.directory",
584+ )
585586 // All 3 posts should be returned (within default limit of 50)
587 let post_count =