···11----
22-version: 1.4.1
33-title: Enum without descriptions
44-file: ./test/sdl_test.gleam
55-test_name: enum_without_descriptions_test
66----
77-enum Color {
88- RED
99- GREEN
1010- BLUE
1111-}
···11----
22-version: 1.4.1
33-title: Execute field with object argument
44-file: ./test/executor_test.gleam
55-test_name: execute_field_receives_object_argument_test
66----
77-Response(Object([#("posts", String("Sorting by date DESC"))]), [])
···11----
22-version: 1.4.1
33-title: Execute union with inline fragment
44-file: ./test/executor_test.gleam
55-test_name: execute_union_with_inline_fragment_test
66----
77-Response(Object([#("search", Object([#("title", String("GraphQL is awesome")), #("content", String("Learn all about GraphQL..."))]))]), [])
···11----
22-version: 1.4.1
33-title: Input object with default values
44-file: ./test/sdl_test.gleam
55-test_name: input_object_with_default_values_test
66----
77-"""Filter options for queries"""
88-input FilterInput {
99- """Maximum number of results"""
1010- limit: Int
1111- """Number of results to skip"""
1212- offset: Int
1313-}
···11----
22-version: 1.4.1
33-title: Simple enum type
44-file: ./test/sdl_test.gleam
55-test_name: simple_enum_test
66----
77-"""Order status"""
88-enum Status {
99- """Order is pending"""
1010- PENDING
1111- """Order is being processed"""
1212- PROCESSING
1313- """Order has been shipped"""
1414- SHIPPED
1515- """Order has been delivered"""
1616- DELIVERED
1717-}
···11----
22-version: 1.4.1
33-title: Type with NonNull and List modifiers
44-file: ./test/sdl_test.gleam
55-test_name: type_with_non_null_and_list_test
66----
77-"""Complex type modifiers"""
88-input ComplexInput {
99- """Required string"""
1010- required: String!
1111- """Optional list of strings"""
1212- optionalList: [String]
1313- """Required list of optional strings"""
1414- requiredList: [String]!
1515- """Optional list of required strings"""
1616- listOfRequired: [String!]
1717- """Required list of required strings"""
1818- requiredListOfRequired: [String!]!
1919-}
-20
graphql/gleam.toml
···11-name = "graphql"
22-version = "1.0.0"
33-44-# Fill out these fields if you intend to generate HTML documentation or publish
55-# your project to the Hex package manager.
66-#
77-# description = ""
88-# licences = ["Apache-2.0"]
99-# repository = { type = "github", user = "", repo = "" }
1010-# links = [{ title = "Website", href = "" }]
1111-#
1212-# For a full reference of all the available options, you can have a look at
1313-# https://gleam.run/writing-gleam/gleam-toml/.
1414-1515-[dependencies]
1616-gleam_stdlib = ">= 0.44.0 and < 2.0.0"
1717-1818-[dev-dependencies]
1919-gleeunit = ">= 1.0.0 and < 2.0.0"
2020-birdie = ">= 1.0.0 and < 2.0.0"
-28
graphql/manifest.toml
···11-# This file was generated by Gleam
22-# You typically do not need to edit this file
33-44-packages = [
55- { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" },
66- { 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" },
77- { name = "edit_distance", version = "3.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "edit_distance", source = "hex", outer_checksum = "7DC465C34695F9E57D79FC65670C53C992CE342BF29E0AA41FF44F61AF62FC56" },
88- { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" },
99- { name = "glance", version = "5.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "7F216D97935465FF4AC46699CD1C3E0FB19CB678B002E4ACAFCE256E96312F14" },
1010- { 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" },
1111- { 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" },
1212- { name = "gleam_json", version = "3.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "874FA3C3BB6E22DD2BB111966BD40B3759E9094E05257899A7C08F5DE77EC049" },
1313- { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" },
1414- { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" },
1515- { name = "gleeunit", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "CD701726CBCE5588B375D157B4391CFD0F2F134CD12D9B6998A395484DE05C58" },
1616- { name = "glexer", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "splitter"], otp_app = "glexer", source = "hex", outer_checksum = "40A1FB0919FA080AD6C5809B4C7DBA545841CAAC8168FACDFA0B0667C22475CC" },
1717- { name = "justin", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "justin", source = "hex", outer_checksum = "7FA0C6DB78640C6DC5FBFD59BF3456009F3F8B485BF6825E97E1EB44E9A1E2CD" },
1818- { name = "rank", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "rank", source = "hex", outer_checksum = "5660E361F0E49CBB714CC57CC4C89C63415D8986F05B2DA0C719D5642FAD91C9" },
1919- { name = "simplifile", version = "2.3.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0A868DAC6063D9E983477981839810DC2E553285AB4588B87E3E9C96A7FB4CB4" },
2020- { name = "splitter", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "05564A381580395DCDEFF4F88A64B021E8DAFA6540AE99B4623962F52976AA9D" },
2121- { name = "term_size", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "term_size", source = "hex", outer_checksum = "D00BD2BC8FB3EBB7E6AE076F3F1FF2AC9D5ED1805F004D0896C784D06C6645F1" },
2222- { name = "trie_again", version = "1.1.4", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "trie_again", source = "hex", outer_checksum = "E3BD66B4E126EF567EA8C4944EAB216413392ADF6C16C36047AF79EE5EF13466" },
2323-]
2424-2525-[requirements]
2626-birdie = { version = ">= 1.0.0 and < 2.0.0" }
2727-gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" }
2828-gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
-318
graphql/src/graphql/connection.gleam
···11-/// GraphQL Connection Types for Relay Cursor Connections
22-///
33-/// Implements the Relay Cursor Connections Specification:
44-/// https://relay.dev/graphql/connections.htm
55-import gleam/list
66-import gleam/option.{type Option, None, Some}
77-import graphql/schema
88-import graphql/value
99-1010-/// PageInfo type for connection pagination metadata
1111-pub type PageInfo {
1212- PageInfo(
1313- has_next_page: Bool,
1414- has_previous_page: Bool,
1515- start_cursor: Option(String),
1616- end_cursor: Option(String),
1717- )
1818-}
1919-2020-/// Edge wrapper containing a node and its cursor
2121-pub type Edge(node_type) {
2222- Edge(node: node_type, cursor: String)
2323-}
2424-2525-/// Connection wrapper containing edges and page info
2626-pub type Connection(node_type) {
2727- Connection(
2828- edges: List(Edge(node_type)),
2929- page_info: PageInfo,
3030- total_count: Option(Int),
3131- )
3232-}
3333-3434-/// Creates the PageInfo GraphQL type
3535-pub fn page_info_type() -> schema.Type {
3636- schema.object_type(
3737- "PageInfo",
3838- "Information about pagination in a connection",
3939- [
4040- schema.field(
4141- "hasNextPage",
4242- schema.non_null(schema.boolean_type()),
4343- "When paginating forwards, are there more items?",
4444- fn(ctx) {
4545- // Extract from context data
4646- case ctx.data {
4747- Some(value.Object(fields)) -> {
4848- case list.key_find(fields, "hasNextPage") {
4949- Ok(val) -> Ok(val)
5050- Error(_) -> Ok(value.Boolean(False))
5151- }
5252- }
5353- _ -> Ok(value.Boolean(False))
5454- }
5555- },
5656- ),
5757- schema.field(
5858- "hasPreviousPage",
5959- schema.non_null(schema.boolean_type()),
6060- "When paginating backwards, are there more items?",
6161- fn(ctx) {
6262- case ctx.data {
6363- Some(value.Object(fields)) -> {
6464- case list.key_find(fields, "hasPreviousPage") {
6565- Ok(val) -> Ok(val)
6666- Error(_) -> Ok(value.Boolean(False))
6767- }
6868- }
6969- _ -> Ok(value.Boolean(False))
7070- }
7171- },
7272- ),
7373- schema.field(
7474- "startCursor",
7575- schema.string_type(),
7676- "Cursor corresponding to the first item in the page",
7777- fn(ctx) {
7878- case ctx.data {
7979- Some(value.Object(fields)) -> {
8080- case list.key_find(fields, "startCursor") {
8181- Ok(val) -> Ok(val)
8282- Error(_) -> Ok(value.Null)
8383- }
8484- }
8585- _ -> Ok(value.Null)
8686- }
8787- },
8888- ),
8989- schema.field(
9090- "endCursor",
9191- schema.string_type(),
9292- "Cursor corresponding to the last item in the page",
9393- fn(ctx) {
9494- case ctx.data {
9595- Some(value.Object(fields)) -> {
9696- case list.key_find(fields, "endCursor") {
9797- Ok(val) -> Ok(val)
9898- Error(_) -> Ok(value.Null)
9999- }
100100- }
101101- _ -> Ok(value.Null)
102102- }
103103- },
104104- ),
105105- ],
106106- )
107107-}
108108-109109-/// Creates an Edge type for a given node type name
110110-pub fn edge_type(node_type_name: String, node_type: schema.Type) -> schema.Type {
111111- let edge_type_name = node_type_name <> "Edge"
112112-113113- schema.object_type(
114114- edge_type_name,
115115- "An edge in a connection for " <> node_type_name,
116116- [
117117- schema.field(
118118- "node",
119119- schema.non_null(node_type),
120120- "The item at the end of the edge",
121121- fn(ctx) {
122122- // Extract node from context data
123123- case ctx.data {
124124- Some(value.Object(fields)) -> {
125125- case list.key_find(fields, "node") {
126126- Ok(val) -> Ok(val)
127127- Error(_) -> Ok(value.Null)
128128- }
129129- }
130130- _ -> Ok(value.Null)
131131- }
132132- },
133133- ),
134134- schema.field(
135135- "cursor",
136136- schema.non_null(schema.string_type()),
137137- "A cursor for use in pagination",
138138- fn(ctx) {
139139- case ctx.data {
140140- Some(value.Object(fields)) -> {
141141- case list.key_find(fields, "cursor") {
142142- Ok(val) -> Ok(val)
143143- Error(_) -> Ok(value.String(""))
144144- }
145145- }
146146- _ -> Ok(value.String(""))
147147- }
148148- },
149149- ),
150150- ],
151151- )
152152-}
153153-154154-/// Creates a Connection type for a given node type name
155155-pub fn connection_type(
156156- node_type_name: String,
157157- edge_type: schema.Type,
158158-) -> schema.Type {
159159- let connection_type_name = node_type_name <> "Connection"
160160-161161- schema.object_type(
162162- connection_type_name,
163163- "A connection to a list of items for " <> node_type_name,
164164- [
165165- schema.field(
166166- "edges",
167167- schema.non_null(schema.list_type(schema.non_null(edge_type))),
168168- "A list of edges",
169169- fn(ctx) {
170170- // Extract edges from context data
171171- case ctx.data {
172172- Some(value.Object(fields)) -> {
173173- case list.key_find(fields, "edges") {
174174- Ok(val) -> Ok(val)
175175- Error(_) -> Ok(value.List([]))
176176- }
177177- }
178178- _ -> Ok(value.List([]))
179179- }
180180- },
181181- ),
182182- schema.field(
183183- "pageInfo",
184184- schema.non_null(page_info_type()),
185185- "Information to aid in pagination",
186186- fn(ctx) {
187187- // Extract pageInfo from context data
188188- case ctx.data {
189189- Some(value.Object(fields)) -> {
190190- case list.key_find(fields, "pageInfo") {
191191- Ok(val) -> Ok(val)
192192- Error(_) ->
193193- Ok(
194194- value.Object([
195195- #("hasNextPage", value.Boolean(False)),
196196- #("hasPreviousPage", value.Boolean(False)),
197197- #("startCursor", value.Null),
198198- #("endCursor", value.Null),
199199- ]),
200200- )
201201- }
202202- }
203203- _ ->
204204- Ok(
205205- value.Object([
206206- #("hasNextPage", value.Boolean(False)),
207207- #("hasPreviousPage", value.Boolean(False)),
208208- #("startCursor", value.Null),
209209- #("endCursor", value.Null),
210210- ]),
211211- )
212212- }
213213- },
214214- ),
215215- schema.field(
216216- "totalCount",
217217- schema.int_type(),
218218- "Total number of items in the connection",
219219- fn(ctx) {
220220- case ctx.data {
221221- Some(value.Object(fields)) -> {
222222- case list.key_find(fields, "totalCount") {
223223- Ok(val) -> Ok(val)
224224- Error(_) -> Ok(value.Null)
225225- }
226226- }
227227- _ -> Ok(value.Null)
228228- }
229229- },
230230- ),
231231- ],
232232- )
233233-}
234234-235235-/// Standard pagination arguments for forward pagination
236236-pub fn forward_pagination_args() -> List(schema.Argument) {
237237- [
238238- schema.argument(
239239- "first",
240240- schema.int_type(),
241241- "Returns the first n items from the list",
242242- None,
243243- ),
244244- schema.argument(
245245- "after",
246246- schema.string_type(),
247247- "Returns items after the given cursor",
248248- None,
249249- ),
250250- ]
251251-}
252252-253253-/// Standard pagination arguments for backward pagination
254254-pub fn backward_pagination_args() -> List(schema.Argument) {
255255- [
256256- schema.argument(
257257- "last",
258258- schema.int_type(),
259259- "Returns the last n items from the list",
260260- None,
261261- ),
262262- schema.argument(
263263- "before",
264264- schema.string_type(),
265265- "Returns items before the given cursor",
266266- None,
267267- ),
268268- ]
269269-}
270270-271271-/// All standard connection arguments (forward + backward)
272272-/// Note: sortBy is not included yet as it requires InputObject type support
273273-pub fn connection_args() -> List(schema.Argument) {
274274- list.flatten([forward_pagination_args(), backward_pagination_args()])
275275-}
276276-277277-/// Converts a PageInfo value to a GraphQL value
278278-pub fn page_info_to_value(page_info: PageInfo) -> value.Value {
279279- value.Object([
280280- #("hasNextPage", value.Boolean(page_info.has_next_page)),
281281- #("hasPreviousPage", value.Boolean(page_info.has_previous_page)),
282282- #("startCursor", case page_info.start_cursor {
283283- Some(cursor) -> value.String(cursor)
284284- None -> value.Null
285285- }),
286286- #("endCursor", case page_info.end_cursor {
287287- Some(cursor) -> value.String(cursor)
288288- None -> value.Null
289289- }),
290290- ])
291291-}
292292-293293-/// Converts an Edge to a GraphQL value
294294-pub fn edge_to_value(edge: Edge(value.Value)) -> value.Value {
295295- value.Object([
296296- #("node", edge.node),
297297- #("cursor", value.String(edge.cursor)),
298298- ])
299299-}
300300-301301-/// Converts a Connection to a GraphQL value
302302-pub fn connection_to_value(connection: Connection(value.Value)) -> value.Value {
303303- let edges_value =
304304- connection.edges
305305- |> list.map(edge_to_value)
306306- |> value.List
307307-308308- let total_count_value = case connection.total_count {
309309- Some(count) -> value.Int(count)
310310- None -> value.Null
311311- }
312312-313313- value.Object([
314314- #("edges", edges_value),
315315- #("pageInfo", page_info_to_value(connection.page_info)),
316316- #("totalCount", total_count_value),
317317- ])
318318-}
-927
graphql/src/graphql/executor.gleam
···11-/// GraphQL Executor
22-///
33-/// Executes GraphQL queries against a schema
44-import gleam/dict.{type Dict}
55-import gleam/list
66-import gleam/option.{None, Some}
77-import gleam/set.{type Set}
88-import graphql/introspection
99-import graphql/parser
1010-import graphql/schema
1111-import graphql/value
1212-1313-/// GraphQL Error
1414-pub type GraphQLError {
1515- GraphQLError(message: String, path: List(String))
1616-}
1717-1818-/// GraphQL Response
1919-pub type Response {
2020- Response(data: value.Value, errors: List(GraphQLError))
2121-}
2222-2323-/// Get the response key for a field (alias if present, otherwise field name)
2424-fn response_key(field_name: String, alias: option.Option(String)) -> String {
2525- case alias {
2626- option.Some(alias_name) -> alias_name
2727- option.None -> field_name
2828- }
2929-}
3030-3131-/// Execute a GraphQL query
3232-pub fn execute(
3333- query: String,
3434- graphql_schema: schema.Schema,
3535- ctx: schema.Context,
3636-) -> Result(Response, String) {
3737- // Parse the query
3838- case parser.parse(query) {
3939- Error(parse_error) ->
4040- Error("Parse error: " <> format_parse_error(parse_error))
4141- Ok(document) -> {
4242- // Execute the document
4343- case execute_document(document, graphql_schema, ctx) {
4444- Ok(#(data, errors)) -> Ok(Response(data, errors))
4545- Error(err) -> Error(err)
4646- }
4747- }
4848- }
4949-}
5050-5151-fn format_parse_error(err: parser.ParseError) -> String {
5252- case err {
5353- parser.UnexpectedToken(_, msg) -> msg
5454- parser.UnexpectedEndOfInput(msg) -> msg
5555- parser.LexerError(_) -> "Lexer error"
5656- }
5757-}
5858-5959-/// Execute a document
6060-fn execute_document(
6161- document: parser.Document,
6262- graphql_schema: schema.Schema,
6363- ctx: schema.Context,
6464-) -> Result(#(value.Value, List(GraphQLError)), String) {
6565- case document {
6666- parser.Document(operations) -> {
6767- // Separate fragments from executable operations
6868- let #(fragments, executable_ops) = partition_operations(operations)
6969-7070- // Build fragments dictionary
7171- let fragments_dict = build_fragments_dict(fragments)
7272-7373- // Execute the first executable operation
7474- case executable_ops {
7575- [operation, ..] ->
7676- execute_operation(operation, graphql_schema, ctx, fragments_dict)
7777- [] -> Error("No executable operations in document")
7878- }
7979- }
8080- }
8181-}
8282-8383-/// Partition operations into fragments and executable operations
8484-fn partition_operations(
8585- operations: List(parser.Operation),
8686-) -> #(List(parser.Operation), List(parser.Operation)) {
8787- list.partition(operations, fn(op) {
8888- case op {
8989- parser.FragmentDefinition(_, _, _) -> True
9090- _ -> False
9191- }
9292- })
9393-}
9494-9595-/// Build a dictionary of fragments keyed by name
9696-fn build_fragments_dict(
9797- fragments: List(parser.Operation),
9898-) -> Dict(String, parser.Operation) {
9999- fragments
100100- |> list.filter_map(fn(frag) {
101101- case frag {
102102- parser.FragmentDefinition(name, _, _) -> Ok(#(name, frag))
103103- _ -> Error(Nil)
104104- }
105105- })
106106- |> dict.from_list
107107-}
108108-109109-/// Execute an operation
110110-fn execute_operation(
111111- operation: parser.Operation,
112112- graphql_schema: schema.Schema,
113113- ctx: schema.Context,
114114- fragments: Dict(String, parser.Operation),
115115-) -> Result(#(value.Value, List(GraphQLError)), String) {
116116- case operation {
117117- parser.Query(selection_set) -> {
118118- let root_type = schema.query_type(graphql_schema)
119119- execute_selection_set(
120120- selection_set,
121121- root_type,
122122- graphql_schema,
123123- ctx,
124124- fragments,
125125- [],
126126- )
127127- }
128128- parser.NamedQuery(_, _, selection_set) -> {
129129- let root_type = schema.query_type(graphql_schema)
130130- execute_selection_set(
131131- selection_set,
132132- root_type,
133133- graphql_schema,
134134- ctx,
135135- fragments,
136136- [],
137137- )
138138- }
139139- parser.Mutation(selection_set) -> {
140140- // Get mutation root type from schema
141141- case schema.get_mutation_type(graphql_schema) {
142142- option.Some(mutation_type) ->
143143- execute_selection_set(
144144- selection_set,
145145- mutation_type,
146146- graphql_schema,
147147- ctx,
148148- fragments,
149149- [],
150150- )
151151- option.None -> Error("Schema does not define a mutation type")
152152- }
153153- }
154154- parser.NamedMutation(_, _, selection_set) -> {
155155- // Get mutation root type from schema
156156- case schema.get_mutation_type(graphql_schema) {
157157- option.Some(mutation_type) ->
158158- execute_selection_set(
159159- selection_set,
160160- mutation_type,
161161- graphql_schema,
162162- ctx,
163163- fragments,
164164- [],
165165- )
166166- option.None -> Error("Schema does not define a mutation type")
167167- }
168168- }
169169- parser.Subscription(selection_set) -> {
170170- // Get subscription root type from schema
171171- case schema.get_subscription_type(graphql_schema) {
172172- option.Some(subscription_type) ->
173173- execute_selection_set(
174174- selection_set,
175175- subscription_type,
176176- graphql_schema,
177177- ctx,
178178- fragments,
179179- [],
180180- )
181181- option.None -> Error("Schema does not define a subscription type")
182182- }
183183- }
184184- parser.NamedSubscription(_, _, selection_set) -> {
185185- // Get subscription root type from schema
186186- case schema.get_subscription_type(graphql_schema) {
187187- option.Some(subscription_type) ->
188188- execute_selection_set(
189189- selection_set,
190190- subscription_type,
191191- graphql_schema,
192192- ctx,
193193- fragments,
194194- [],
195195- )
196196- option.None -> Error("Schema does not define a subscription type")
197197- }
198198- }
199199- parser.FragmentDefinition(_, _, _) ->
200200- Error("Fragment definitions are not executable operations")
201201- }
202202-}
203203-204204-/// Execute a selection set
205205-fn execute_selection_set(
206206- selection_set: parser.SelectionSet,
207207- parent_type: schema.Type,
208208- graphql_schema: schema.Schema,
209209- ctx: schema.Context,
210210- fragments: Dict(String, parser.Operation),
211211- path: List(String),
212212-) -> Result(#(value.Value, List(GraphQLError)), String) {
213213- case selection_set {
214214- parser.SelectionSet(selections) -> {
215215- let results =
216216- list.map(selections, fn(selection) {
217217- execute_selection(
218218- selection,
219219- parent_type,
220220- graphql_schema,
221221- ctx,
222222- fragments,
223223- path,
224224- )
225225- })
226226-227227- // Collect all data and errors, merging fragment fields
228228- let #(data, errors) = collect_and_merge_fields(results)
229229-230230- Ok(#(value.Object(data), errors))
231231- }
232232- }
233233-}
234234-235235-/// Collect and merge fields from selection results, handling fragment fields
236236-fn collect_and_merge_fields(
237237- results: List(Result(#(String, value.Value, List(GraphQLError)), String)),
238238-) -> #(List(#(String, value.Value)), List(GraphQLError)) {
239239- let #(data, errors) =
240240- results
241241- |> list.fold(#([], []), fn(acc, r) {
242242- let #(fields_acc, errors_acc) = acc
243243- case r {
244244- Ok(#("__fragment_fields", value.Object(fragment_fields), errs)) -> {
245245- // Merge fragment fields into parent
246246- #(
247247- list.append(fields_acc, fragment_fields),
248248- list.append(errors_acc, errs),
249249- )
250250- }
251251- Ok(#("__fragment_skip", _, _errs)) -> {
252252- // Skip fragment that didn't match type condition
253253- acc
254254- }
255255- Ok(#(name, val, errs)) -> {
256256- // Regular field
257257- #(
258258- list.append(fields_acc, [#(name, val)]),
259259- list.append(errors_acc, errs),
260260- )
261261- }
262262- Error(_) -> acc
263263- }
264264- })
265265-266266- #(data, errors)
267267-}
268268-269269-/// Execute a selection
270270-fn execute_selection(
271271- selection: parser.Selection,
272272- parent_type: schema.Type,
273273- graphql_schema: schema.Schema,
274274- ctx: schema.Context,
275275- fragments: Dict(String, parser.Operation),
276276- path: List(String),
277277-) -> Result(#(String, value.Value, List(GraphQLError)), String) {
278278- case selection {
279279- parser.FragmentSpread(name) -> {
280280- // Look up the fragment definition
281281- case dict.get(fragments, name) {
282282- Error(_) -> Error("Fragment '" <> name <> "' not found")
283283- Ok(parser.FragmentDefinition(
284284- _fname,
285285- type_condition,
286286- fragment_selection_set,
287287- )) -> {
288288- // Check type condition
289289- let current_type_name = schema.type_name(parent_type)
290290- case type_condition == current_type_name {
291291- False -> {
292292- // Type condition doesn't match, skip this fragment
293293- // Return empty object as a placeholder that will be filtered out
294294- Ok(#("__fragment_skip", value.Null, []))
295295- }
296296- True -> {
297297- // Type condition matches, execute fragment's selections
298298- case
299299- execute_selection_set(
300300- fragment_selection_set,
301301- parent_type,
302302- graphql_schema,
303303- ctx,
304304- fragments,
305305- path,
306306- )
307307- {
308308- Ok(#(value.Object(fields), errs)) -> {
309309- // Fragment selections should be merged into parent
310310- // For now, return as a special marker
311311- Ok(#("__fragment_fields", value.Object(fields), errs))
312312- }
313313- Ok(#(val, errs)) -> Ok(#("__fragment_fields", val, errs))
314314- Error(err) -> Error(err)
315315- }
316316- }
317317- }
318318- }
319319- Ok(_) -> Error("Invalid fragment definition")
320320- }
321321- }
322322- parser.InlineFragment(type_condition_opt, inline_selections) -> {
323323- // Check type condition if present
324324- let current_type_name = schema.type_name(parent_type)
325325- let should_execute = case type_condition_opt {
326326- None -> True
327327- Some(type_condition) -> type_condition == current_type_name
328328- }
329329-330330- case should_execute {
331331- False -> Ok(#("__fragment_skip", value.Null, []))
332332- True -> {
333333- let inline_selection_set = parser.SelectionSet(inline_selections)
334334- case
335335- execute_selection_set(
336336- inline_selection_set,
337337- parent_type,
338338- graphql_schema,
339339- ctx,
340340- fragments,
341341- path,
342342- )
343343- {
344344- Ok(#(value.Object(fields), errs)) ->
345345- Ok(#("__fragment_fields", value.Object(fields), errs))
346346- Ok(#(val, errs)) -> Ok(#("__fragment_fields", val, errs))
347347- Error(err) -> Error(err)
348348- }
349349- }
350350- }
351351- }
352352- parser.Field(name, alias, arguments, nested_selections) -> {
353353- // Convert arguments to dict (with variable resolution from context)
354354- let args_dict = arguments_to_dict(arguments, ctx)
355355-356356- // Determine the response key (use alias if provided, otherwise field name)
357357- let key = response_key(name, alias)
358358-359359- // Handle introspection meta-fields
360360- case name {
361361- "__typename" -> {
362362- let type_name = schema.type_name(parent_type)
363363- Ok(#(key, value.String(type_name), []))
364364- }
365365- "__schema" -> {
366366- let schema_value = introspection.schema_introspection(graphql_schema)
367367- // Handle nested selections on __schema
368368- case nested_selections {
369369- [] -> Ok(#(key, schema_value, []))
370370- _ -> {
371371- let selection_set = parser.SelectionSet(nested_selections)
372372- // We don't have an actual type for __Schema, so we'll handle it specially
373373- // For now, just return the schema value with nested execution
374374- case
375375- execute_introspection_selection_set(
376376- selection_set,
377377- schema_value,
378378- graphql_schema,
379379- ctx,
380380- fragments,
381381- ["__schema", ..path],
382382- set.new(),
383383- )
384384- {
385385- Ok(#(nested_data, nested_errors)) ->
386386- Ok(#(key, nested_data, nested_errors))
387387- Error(err) -> {
388388- let error = GraphQLError(err, ["__schema", ..path])
389389- Ok(#(key, value.Null, [error]))
390390- }
391391- }
392392- }
393393- }
394394- }
395395- "__type" -> {
396396- // Extract the "name" argument
397397- case dict.get(args_dict, "name") {
398398- Ok(value.String(type_name)) -> {
399399- // Look up the type in the schema
400400- case
401401- introspection.type_by_name_introspection(
402402- graphql_schema,
403403- type_name,
404404- )
405405- {
406406- option.Some(type_value) -> {
407407- // Handle nested selections on __type
408408- case nested_selections {
409409- [] -> Ok(#(key, type_value, []))
410410- _ -> {
411411- let selection_set = parser.SelectionSet(nested_selections)
412412- case
413413- execute_introspection_selection_set(
414414- selection_set,
415415- type_value,
416416- graphql_schema,
417417- ctx,
418418- fragments,
419419- ["__type", ..path],
420420- set.new(),
421421- )
422422- {
423423- Ok(#(nested_data, nested_errors)) ->
424424- Ok(#(key, nested_data, nested_errors))
425425- Error(err) -> {
426426- let error = GraphQLError(err, ["__type", ..path])
427427- Ok(#(key, value.Null, [error]))
428428- }
429429- }
430430- }
431431- }
432432- }
433433- option.None -> {
434434- // Type not found, return null (per GraphQL spec)
435435- Ok(#(key, value.Null, []))
436436- }
437437- }
438438- }
439439- Ok(_) -> {
440440- let error =
441441- GraphQLError("__type argument 'name' must be a String", path)
442442- Ok(#(key, value.Null, [error]))
443443- }
444444- Error(_) -> {
445445- let error =
446446- GraphQLError("__type requires a 'name' argument", path)
447447- Ok(#(key, value.Null, [error]))
448448- }
449449- }
450450- }
451451- _ -> {
452452- // Get field from schema
453453- case schema.get_field(parent_type, name) {
454454- None -> {
455455- let error = GraphQLError("Field '" <> name <> "' not found", path)
456456- Ok(#(key, value.Null, [error]))
457457- }
458458- Some(field) -> {
459459- // Get the field's type for nested selections
460460- let field_type_def = schema.field_type(field)
461461-462462- // Create context with arguments (preserve variables from parent context)
463463- let field_ctx = schema.Context(ctx.data, args_dict, ctx.variables)
464464-465465- // Resolve the field
466466- case schema.resolve_field(field, field_ctx) {
467467- Error(err) -> {
468468- let error = GraphQLError(err, [name, ..path])
469469- Ok(#(key, value.Null, [error]))
470470- }
471471- Ok(field_value) -> {
472472- // If there are nested selections, recurse
473473- case nested_selections {
474474- [] -> Ok(#(key, field_value, []))
475475- _ -> {
476476- // Need to resolve nested fields
477477- case field_value {
478478- value.Object(_) -> {
479479- // Check if field_type_def is a union type
480480- // If so, resolve it to the concrete type first
481481- let type_to_use = case
482482- schema.is_union(field_type_def)
483483- {
484484- True -> {
485485- // Create context with the field value for type resolution
486486- let resolve_ctx =
487487- schema.context(option.Some(field_value))
488488- case
489489- schema.resolve_union_type(
490490- field_type_def,
491491- resolve_ctx,
492492- )
493493- {
494494- Ok(concrete_type) -> concrete_type
495495- Error(_) -> field_type_def
496496- // Fallback to union type if resolution fails
497497- }
498498- }
499499- False -> field_type_def
500500- }
501501-502502- // Execute nested selections using the resolved type
503503- // Create new context with this object's data
504504- let object_ctx =
505505- schema.context(option.Some(field_value))
506506- let selection_set =
507507- parser.SelectionSet(nested_selections)
508508- case
509509- execute_selection_set(
510510- selection_set,
511511- type_to_use,
512512- graphql_schema,
513513- object_ctx,
514514- fragments,
515515- [name, ..path],
516516- )
517517- {
518518- Ok(#(nested_data, nested_errors)) ->
519519- Ok(#(key, nested_data, nested_errors))
520520- Error(err) -> {
521521- let error = GraphQLError(err, [name, ..path])
522522- Ok(#(key, value.Null, [error]))
523523- }
524524- }
525525- }
526526- value.List(items) -> {
527527- // Handle list with nested selections
528528- // Get the inner type from the LIST wrapper, unwrapping NonNull if needed
529529- let inner_type = case
530530- schema.inner_type(field_type_def)
531531- {
532532- option.Some(t) -> {
533533- // If the result is still wrapped (NonNull), unwrap it too
534534- case schema.inner_type(t) {
535535- option.Some(unwrapped) -> unwrapped
536536- option.None -> t
537537- }
538538- }
539539- option.None -> field_type_def
540540- }
541541-542542- // Execute nested selections on each item
543543- let selection_set =
544544- parser.SelectionSet(nested_selections)
545545- let results =
546546- list.map(items, fn(item) {
547547- // Check if inner_type is a union and resolve it
548548- let item_type = case schema.is_union(inner_type) {
549549- True -> {
550550- // Create context with the item value for type resolution
551551- let resolve_ctx =
552552- schema.context(option.Some(item))
553553- case
554554- schema.resolve_union_type(
555555- inner_type,
556556- resolve_ctx,
557557- )
558558- {
559559- Ok(concrete_type) -> concrete_type
560560- Error(_) -> inner_type
561561- // Fallback to union type if resolution fails
562562- }
563563- }
564564- False -> inner_type
565565- }
566566-567567- // Create context with this item's data
568568- let item_ctx = schema.context(option.Some(item))
569569- execute_selection_set(
570570- selection_set,
571571- item_type,
572572- graphql_schema,
573573- item_ctx,
574574- fragments,
575575- [name, ..path],
576576- )
577577- })
578578-579579- // Collect results and errors
580580- let processed_items =
581581- results
582582- |> list.filter_map(fn(r) {
583583- case r {
584584- Ok(#(val, _)) -> Ok(val)
585585- Error(_) -> Error(Nil)
586586- }
587587- })
588588-589589- let all_errors =
590590- results
591591- |> list.flat_map(fn(r) {
592592- case r {
593593- Ok(#(_, errs)) -> errs
594594- Error(_) -> []
595595- }
596596- })
597597-598598- Ok(#(key, value.List(processed_items), all_errors))
599599- }
600600- _ -> Ok(#(key, field_value, []))
601601- }
602602- }
603603- }
604604- }
605605- }
606606- }
607607- }
608608- }
609609- }
610610- }
611611- }
612612-}
613613-614614-/// Execute a selection set on an introspection value (like __schema)
615615-/// This directly reads fields from the value.Object rather than using resolvers
616616-fn execute_introspection_selection_set(
617617- selection_set: parser.SelectionSet,
618618- value_obj: value.Value,
619619- graphql_schema: schema.Schema,
620620- ctx: schema.Context,
621621- fragments: Dict(String, parser.Operation),
622622- path: List(String),
623623- visited_types: Set(String),
624624-) -> Result(#(value.Value, List(GraphQLError)), String) {
625625- case selection_set {
626626- parser.SelectionSet(selections) -> {
627627- case value_obj {
628628- value.List(items) -> {
629629- // For lists, execute the selection set on each item
630630- let results =
631631- list.map(items, fn(item) {
632632- execute_introspection_selection_set(
633633- selection_set,
634634- item,
635635- graphql_schema,
636636- ctx,
637637- fragments,
638638- path,
639639- visited_types,
640640- )
641641- })
642642-643643- // Collect the data and errors
644644- let data_items =
645645- results
646646- |> list.filter_map(fn(r) {
647647- case r {
648648- Ok(#(val, _)) -> Ok(val)
649649- Error(_) -> Error(Nil)
650650- }
651651- })
652652-653653- let all_errors =
654654- results
655655- |> list.flat_map(fn(r) {
656656- case r {
657657- Ok(#(_, errs)) -> errs
658658- Error(_) -> []
659659- }
660660- })
661661-662662- Ok(#(value.List(data_items), all_errors))
663663- }
664664- value.Null -> {
665665- // If the value is null, just return null regardless of selections
666666- // This handles cases like mutationType and subscriptionType which are null
667667- Ok(#(value.Null, []))
668668- }
669669- value.Object(fields) -> {
670670- // CYCLE DETECTION: Extract type name from object to detect circular references
671671- let type_name = case list.key_find(fields, "name") {
672672- Ok(value.String(name)) -> option.Some(name)
673673- _ -> option.None
674674- }
675675-676676- // Check if we've already visited this type to prevent infinite loops
677677- let is_cycle = case type_name {
678678- option.Some(name) -> set.contains(visited_types, name)
679679- option.None -> False
680680- }
681681-682682- // If we detected a cycle, return a minimal object to break the loop
683683- case is_cycle {
684684- True -> {
685685- // Return just the type name and kind to break the cycle
686686- let minimal_fields = case type_name {
687687- option.Some(name) -> {
688688- let kind_value = case list.key_find(fields, "kind") {
689689- Ok(kind) -> kind
690690- Error(_) -> value.Null
691691- }
692692- [#("name", value.String(name)), #("kind", kind_value)]
693693- }
694694- option.None -> []
695695- }
696696- Ok(#(value.Object(minimal_fields), []))
697697- }
698698- False -> {
699699- // Add current type to visited set before recursing
700700- let new_visited = case type_name {
701701- option.Some(name) -> set.insert(visited_types, name)
702702- option.None -> visited_types
703703- }
704704-705705- // For each selection, find the corresponding field in the object
706706- let results =
707707- list.map(selections, fn(selection) {
708708- case selection {
709709- parser.FragmentSpread(name) -> {
710710- // Look up the fragment definition
711711- case dict.get(fragments, name) {
712712- Error(_) -> {
713713- // Fragment not found - return error
714714- let error =
715715- GraphQLError(
716716- "Fragment '" <> name <> "' not found",
717717- path,
718718- )
719719- Ok(
720720- #(
721721- "__FRAGMENT_ERROR",
722722- value.String("Fragment not found: " <> name),
723723- [error],
724724- ),
725725- )
726726- }
727727- Ok(parser.FragmentDefinition(
728728- _fname,
729729- _type_condition,
730730- fragment_selection_set,
731731- )) -> {
732732- // For introspection, we don't check type conditions - just execute the fragment
733733- // IMPORTANT: Use visited_types (not new_visited) because we're selecting from
734734- // the SAME object, not recursing into it. The current object was already added
735735- // to new_visited, but the fragment is just selecting different fields.
736736- case
737737- execute_introspection_selection_set(
738738- fragment_selection_set,
739739- value_obj,
740740- graphql_schema,
741741- ctx,
742742- fragments,
743743- path,
744744- visited_types,
745745- )
746746- {
747747- Ok(#(value.Object(fragment_fields), errs)) ->
748748- Ok(#(
749749- "__fragment_fields",
750750- value.Object(fragment_fields),
751751- errs,
752752- ))
753753- Ok(#(val, errs)) ->
754754- Ok(#("__fragment_fields", val, errs))
755755- Error(_err) -> Error(Nil)
756756- }
757757- }
758758- Ok(_) -> Error(Nil)
759759- // Invalid fragment definition
760760- }
761761- }
762762- parser.InlineFragment(
763763- _type_condition_opt,
764764- inline_selections,
765765- ) -> {
766766- // For introspection, inline fragments always execute (no type checking needed)
767767- // Execute the inline fragment's selections on this object
768768- let inline_selection_set =
769769- parser.SelectionSet(inline_selections)
770770- case
771771- execute_introspection_selection_set(
772772- inline_selection_set,
773773- value_obj,
774774- graphql_schema,
775775- ctx,
776776- fragments,
777777- path,
778778- new_visited,
779779- )
780780- {
781781- Ok(#(value.Object(fragment_fields), errs)) ->
782782- // Return fragment fields to be merged
783783- Ok(#(
784784- "__fragment_fields",
785785- value.Object(fragment_fields),
786786- errs,
787787- ))
788788- Ok(#(val, errs)) ->
789789- Ok(#("__fragment_fields", val, errs))
790790- Error(_err) -> Error(Nil)
791791- }
792792- }
793793- parser.Field(name, alias, _arguments, nested_selections) -> {
794794- // Determine the response key (use alias if provided, otherwise field name)
795795- let key = response_key(name, alias)
796796-797797- // Find the field in the object
798798- case list.key_find(fields, name) {
799799- Ok(field_value) -> {
800800- // Handle nested selections
801801- case nested_selections {
802802- [] -> Ok(#(key, field_value, []))
803803- _ -> {
804804- let selection_set =
805805- parser.SelectionSet(nested_selections)
806806- case
807807- execute_introspection_selection_set(
808808- selection_set,
809809- field_value,
810810- graphql_schema,
811811- ctx,
812812- fragments,
813813- [name, ..path],
814814- new_visited,
815815- )
816816- {
817817- Ok(#(nested_data, nested_errors)) ->
818818- Ok(#(key, nested_data, nested_errors))
819819- Error(err) -> {
820820- let error = GraphQLError(err, [name, ..path])
821821- Ok(#(key, value.Null, [error]))
822822- }
823823- }
824824- }
825825- }
826826- }
827827- Error(_) -> {
828828- let error =
829829- GraphQLError(
830830- "Field '" <> name <> "' not found",
831831- path,
832832- )
833833- Ok(#(key, value.Null, [error]))
834834- }
835835- }
836836- }
837837- }
838838- })
839839-840840- // Collect all data and errors, merging fragment fields
841841- let #(data, errors) =
842842- results
843843- |> list.fold(#([], []), fn(acc, r) {
844844- let #(fields_acc, errors_acc) = acc
845845- case r {
846846- Ok(#(
847847- "__fragment_fields",
848848- value.Object(fragment_fields),
849849- errs,
850850- )) -> {
851851- // Merge fragment fields into parent
852852- #(
853853- list.append(fields_acc, fragment_fields),
854854- list.append(errors_acc, errs),
855855- )
856856- }
857857- Ok(#(name, val, errs)) -> {
858858- // Regular field
859859- #(
860860- list.append(fields_acc, [#(name, val)]),
861861- list.append(errors_acc, errs),
862862- )
863863- }
864864- Error(_) -> acc
865865- }
866866- })
867867-868868- Ok(#(value.Object(data), errors))
869869- }
870870- }
871871- }
872872- _ ->
873873- Error(
874874- "Expected object, list, or null for introspection selection set",
875875- )
876876- }
877877- }
878878- }
879879-}
880880-881881-/// Convert parser ArgumentValue to value.Value
882882-fn argument_value_to_value(
883883- arg_value: parser.ArgumentValue,
884884- ctx: schema.Context,
885885-) -> value.Value {
886886- case arg_value {
887887- parser.IntValue(s) -> value.String(s)
888888- parser.FloatValue(s) -> value.String(s)
889889- parser.StringValue(s) -> value.String(s)
890890- parser.BooleanValue(b) -> value.Boolean(b)
891891- parser.NullValue -> value.Null
892892- parser.EnumValue(s) -> value.String(s)
893893- parser.ListValue(items) ->
894894- value.List(
895895- list.map(items, fn(item) { argument_value_to_value(item, ctx) }),
896896- )
897897- parser.ObjectValue(fields) ->
898898- value.Object(
899899- list.map(fields, fn(pair) {
900900- let #(name, val) = pair
901901- #(name, argument_value_to_value(val, ctx))
902902- }),
903903- )
904904- parser.VariableValue(name) -> {
905905- // Look up variable value from context
906906- case schema.get_variable(ctx, name) {
907907- option.Some(val) -> val
908908- option.None -> value.Null
909909- }
910910- }
911911- }
912912-}
913913-914914-/// Convert list of Arguments to a Dict of values
915915-fn arguments_to_dict(
916916- arguments: List(parser.Argument),
917917- ctx: schema.Context,
918918-) -> Dict(String, value.Value) {
919919- list.fold(arguments, dict.new(), fn(acc, arg) {
920920- case arg {
921921- parser.Argument(name, arg_value) -> {
922922- let value = argument_value_to_value(arg_value, ctx)
923923- dict.insert(acc, name, value)
924924- }
925925- }
926926- })
927927-}
-424
graphql/src/graphql/introspection.gleam
···11-/// GraphQL Introspection
22-///
33-/// Implements the GraphQL introspection system per the GraphQL spec.
44-/// Provides __schema, __type, and __typename meta-fields.
55-import gleam/dict
66-import gleam/list
77-import gleam/option
88-import gleam/result
99-import graphql/schema
1010-import graphql/value
1111-1212-/// Build introspection value for __schema
1313-pub fn schema_introspection(graphql_schema: schema.Schema) -> value.Value {
1414- let query_type = schema.query_type(graphql_schema)
1515- let mutation_type_option = schema.get_mutation_type(graphql_schema)
1616- let subscription_type_option = schema.get_subscription_type(graphql_schema)
1717-1818- // Build list of all types in the schema
1919- let all_types = get_all_types(graphql_schema)
2020-2121- // Build mutation type ref if it exists
2222- let mutation_type_value = case mutation_type_option {
2323- option.Some(mutation_type) -> type_ref(mutation_type)
2424- option.None -> value.Null
2525- }
2626-2727- // Build subscription type ref if it exists
2828- let subscription_type_value = case subscription_type_option {
2929- option.Some(subscription_type) -> type_ref(subscription_type)
3030- option.None -> value.Null
3131- }
3232-3333- value.Object([
3434- #("queryType", type_ref(query_type)),
3535- #("mutationType", mutation_type_value),
3636- #("subscriptionType", subscription_type_value),
3737- #("types", value.List(all_types)),
3838- #("directives", value.List([])),
3939- ])
4040-}
4141-4242-/// Build introspection value for __type(name: "TypeName")
4343-/// Returns Some(type_introspection) if the type is found, None otherwise
4444-pub fn type_by_name_introspection(
4545- graphql_schema: schema.Schema,
4646- type_name: String,
4747-) -> option.Option(value.Value) {
4848- let all_types = get_all_schema_types(graphql_schema)
4949-5050- // Find the type with the matching name
5151- let found_type =
5252- list.find(all_types, fn(t) { schema.type_name(t) == type_name })
5353-5454- case found_type {
5555- Ok(t) -> option.Some(type_introspection(t))
5656- Error(_) -> option.None
5757- }
5858-}
5959-6060-/// Get all types from the schema as schema.Type values
6161-/// Useful for testing and documentation generation
6262-pub fn get_all_schema_types(graphql_schema: schema.Schema) -> List(schema.Type) {
6363- let query_type = schema.query_type(graphql_schema)
6464- let mutation_type_option = schema.get_mutation_type(graphql_schema)
6565- let subscription_type_option = schema.get_subscription_type(graphql_schema)
6666-6767- // Collect all types by traversing the query type
6868- let mut_collected_types = collect_types_from_type(query_type, [])
6969-7070- // Also collect types from mutation type if it exists
7171- let mutation_collected_types = case mutation_type_option {
7272- option.Some(mutation_type) ->
7373- collect_types_from_type(mutation_type, mut_collected_types)
7474- option.None -> mut_collected_types
7575- }
7676-7777- // Also collect types from subscription type if it exists
7878- let all_collected_types = case subscription_type_option {
7979- option.Some(subscription_type) ->
8080- collect_types_from_type(subscription_type, mutation_collected_types)
8181- option.None -> mutation_collected_types
8282- }
8383-8484- // Deduplicate by type name, preferring types with more fields
8585- // This ensures we get the "most complete" version of each type
8686- let unique_types = deduplicate_types_by_name(all_collected_types)
8787-8888- // Add any built-in scalars that aren't already in the list
8989- let all_built_ins = [
9090- schema.string_type(),
9191- schema.int_type(),
9292- schema.float_type(),
9393- schema.boolean_type(),
9494- schema.id_type(),
9595- ]
9696-9797- let collected_names = list.map(unique_types, schema.type_name)
9898- let missing_built_ins =
9999- list.filter(all_built_ins, fn(built_in) {
100100- let built_in_name = schema.type_name(built_in)
101101- !list.contains(collected_names, built_in_name)
102102- })
103103-104104- list.append(unique_types, missing_built_ins)
105105-}
106106-107107-/// Get all types from the schema
108108-fn get_all_types(graphql_schema: schema.Schema) -> List(value.Value) {
109109- let all_types = get_all_schema_types(graphql_schema)
110110-111111- // Convert all types to introspection values
112112- list.map(all_types, type_introspection)
113113-}
114114-115115-/// Deduplicate types by name, keeping the version with the most fields
116116-/// This ensures we get the "most complete" version of each type when
117117-/// multiple versions exist (e.g., from different passes in schema building)
118118-fn deduplicate_types_by_name(types: List(schema.Type)) -> List(schema.Type) {
119119- // Group types by name
120120- types
121121- |> list.group(schema.type_name)
122122- |> dict.to_list
123123- |> list.map(fn(pair) {
124124- let #(_name, type_list) = pair
125125- // For each group, find the type with the most content
126126- type_list
127127- |> list.reduce(fn(best, current) {
128128- // Count content: fields for object types, enum values for enums, etc.
129129- let best_content_count = get_type_content_count(best)
130130- let current_content_count = get_type_content_count(current)
131131-132132- // Prefer the type with more content
133133- case current_content_count > best_content_count {
134134- True -> current
135135- False -> best
136136- }
137137- })
138138- |> result.unwrap(
139139- list.first(type_list)
140140- |> result.unwrap(schema.string_type()),
141141- )
142142- })
143143-}
144144-145145-/// Get the "content count" for a type (fields, enum values, input fields, etc.)
146146-/// This helps us pick the most complete version of a type during deduplication
147147-fn get_type_content_count(t: schema.Type) -> Int {
148148- // For object types, count fields
149149- let field_count = list.length(schema.get_fields(t))
150150-151151- // For enum types, count enum values
152152- let enum_value_count = list.length(schema.get_enum_values(t))
153153-154154- // For input object types, count input fields
155155- let input_field_count = list.length(schema.get_input_fields(t))
156156-157157- // Return the maximum (types will only have one of these be non-zero)
158158- [field_count, enum_value_count, input_field_count]
159159- |> list.reduce(fn(a, b) {
160160- case a > b {
161161- True -> a
162162- False -> b
163163- }
164164- })
165165- |> result.unwrap(0)
166166-}
167167-168168-/// Collect all types referenced in a type (recursively)
169169-/// Note: We collect ALL instances of each type (even duplicates by name)
170170-/// because we want to find the "most complete" version during deduplication
171171-fn collect_types_from_type(
172172- t: schema.Type,
173173- acc: List(schema.Type),
174174-) -> List(schema.Type) {
175175- // Always add this type - we'll deduplicate later by choosing the version with most fields
176176- let new_acc = [t, ..acc]
177177-178178- // To prevent infinite recursion, check if we've already traversed this exact type instance
179179- // We use a simple heuristic: if this type name appears multiple times AND this specific
180180- // instance has the same or fewer content than what we've seen, skip traversing its children
181181- let should_traverse_children = case
182182- schema.is_object(t) || schema.is_enum(t) || schema.is_union(t)
183183- {
184184- True -> {
185185- let current_content_count = get_type_content_count(t)
186186- let existing_with_same_name =
187187- list.filter(acc, fn(existing) {
188188- schema.type_name(existing) == schema.type_name(t)
189189- })
190190- let max_existing_content =
191191- existing_with_same_name
192192- |> list.map(get_type_content_count)
193193- |> list.reduce(fn(a, b) {
194194- case a > b {
195195- True -> a
196196- False -> b
197197- }
198198- })
199199- |> result.unwrap(0)
200200-201201- // Only traverse if this instance has more content than we've seen before
202202- current_content_count > max_existing_content
203203- }
204204- False -> True
205205- }
206206-207207- case should_traverse_children {
208208- False -> new_acc
209209- True -> {
210210- // Recursively collect types from fields if this is an object type
211211- case schema.is_object(t) {
212212- True -> {
213213- let fields = schema.get_fields(t)
214214- list.fold(fields, new_acc, fn(acc2, field) {
215215- let field_type = schema.field_type(field)
216216- let acc3 = collect_types_from_type_deep(field_type, acc2)
217217-218218- // Also collect types from field arguments
219219- let arguments = schema.field_arguments(field)
220220- list.fold(arguments, acc3, fn(acc4, arg) {
221221- let arg_type = schema.argument_type(arg)
222222- collect_types_from_type_deep(arg_type, acc4)
223223- })
224224- })
225225- }
226226- False -> {
227227- // Check if it's a union type
228228- case schema.is_union(t) {
229229- True -> {
230230- // Collect types from union's possible_types
231231- let possible_types = schema.get_possible_types(t)
232232- list.fold(possible_types, new_acc, fn(acc2, union_type) {
233233- collect_types_from_type_deep(union_type, acc2)
234234- })
235235- }
236236- False -> {
237237- // Check if it's an InputObjectType
238238- let input_fields = schema.get_input_fields(t)
239239- case list.is_empty(input_fields) {
240240- False -> {
241241- // This is an InputObjectType, collect types from its fields
242242- list.fold(input_fields, new_acc, fn(acc2, input_field) {
243243- let field_type = schema.input_field_type(input_field)
244244- collect_types_from_type_deep(field_type, acc2)
245245- })
246246- }
247247- True -> {
248248- // Check if it's a wrapping type (List or NonNull)
249249- case schema.inner_type(t) {
250250- option.Some(inner) ->
251251- collect_types_from_type_deep(inner, new_acc)
252252- option.None -> new_acc
253253- }
254254- }
255255- }
256256- }
257257- }
258258- }
259259- }
260260- }
261261- }
262262-}
263263-264264-/// Helper to unwrap LIST and NON_NULL and collect the inner type
265265-fn collect_types_from_type_deep(
266266- t: schema.Type,
267267- acc: List(schema.Type),
268268-) -> List(schema.Type) {
269269- // Check if this is a wrapping type (List or NonNull)
270270- case schema.inner_type(t) {
271271- option.Some(inner) -> collect_types_from_type_deep(inner, acc)
272272- option.None -> collect_types_from_type(t, acc)
273273- }
274274-}
275275-276276-/// Build full type introspection value
277277-fn type_introspection(t: schema.Type) -> value.Value {
278278- let kind = schema.type_kind(t)
279279- let type_name = schema.type_name(t)
280280-281281- // Get inner type for LIST and NON_NULL
282282- let of_type = case schema.inner_type(t) {
283283- option.Some(inner) -> type_ref(inner)
284284- option.None -> value.Null
285285- }
286286-287287- // Determine fields based on kind
288288- let fields = case kind {
289289- "OBJECT" -> value.List(get_fields_for_type(t))
290290- _ -> value.Null
291291- }
292292-293293- // Determine inputFields for INPUT_OBJECT types
294294- let input_fields = case kind {
295295- "INPUT_OBJECT" -> value.List(get_input_fields_for_type(t))
296296- _ -> value.Null
297297- }
298298-299299- // Determine enumValues for ENUM types
300300- let enum_values = case kind {
301301- "ENUM" -> value.List(get_enum_values_for_type(t))
302302- _ -> value.Null
303303- }
304304-305305- // Determine possibleTypes for UNION types
306306- let possible_types = case kind {
307307- "UNION" -> {
308308- let types = schema.get_possible_types(t)
309309- value.List(list.map(types, type_ref))
310310- }
311311- _ -> value.Null
312312- }
313313-314314- // Handle wrapping types (LIST/NON_NULL) differently
315315- let name = case kind {
316316- "LIST" -> value.Null
317317- "NON_NULL" -> value.Null
318318- _ -> value.String(type_name)
319319- }
320320-321321- let description = case schema.type_description(t) {
322322- "" -> value.Null
323323- desc -> value.String(desc)
324324- }
325325-326326- value.Object([
327327- #("kind", value.String(kind)),
328328- #("name", name),
329329- #("description", description),
330330- #("fields", fields),
331331- #("interfaces", value.List([])),
332332- #("possibleTypes", possible_types),
333333- #("enumValues", enum_values),
334334- #("inputFields", input_fields),
335335- #("ofType", of_type),
336336- ])
337337-}
338338-339339-/// Get fields for a type (if it's an object type)
340340-fn get_fields_for_type(t: schema.Type) -> List(value.Value) {
341341- let fields = schema.get_fields(t)
342342-343343- list.map(fields, fn(field) {
344344- let field_type_val = schema.field_type(field)
345345- let args = schema.field_arguments(field)
346346-347347- value.Object([
348348- #("name", value.String(schema.field_name(field))),
349349- #("description", value.String(schema.field_description(field))),
350350- #("args", value.List(list.map(args, argument_introspection))),
351351- #("type", type_ref(field_type_val)),
352352- #("isDeprecated", value.Boolean(False)),
353353- #("deprecationReason", value.Null),
354354- ])
355355- })
356356-}
357357-358358-/// Get input fields for a type (if it's an input object type)
359359-fn get_input_fields_for_type(t: schema.Type) -> List(value.Value) {
360360- let input_fields = schema.get_input_fields(t)
361361-362362- list.map(input_fields, fn(input_field) {
363363- let field_type_val = schema.input_field_type(input_field)
364364-365365- value.Object([
366366- #("name", value.String(schema.input_field_name(input_field))),
367367- #(
368368- "description",
369369- value.String(schema.input_field_description(input_field)),
370370- ),
371371- #("type", type_ref(field_type_val)),
372372- #("defaultValue", value.Null),
373373- ])
374374- })
375375-}
376376-377377-/// Get enum values for a type (if it's an enum type)
378378-fn get_enum_values_for_type(t: schema.Type) -> List(value.Value) {
379379- let enum_values = schema.get_enum_values(t)
380380-381381- list.map(enum_values, fn(enum_value) {
382382- value.Object([
383383- #("name", value.String(schema.enum_value_name(enum_value))),
384384- #("description", value.String(schema.enum_value_description(enum_value))),
385385- #("isDeprecated", value.Boolean(False)),
386386- #("deprecationReason", value.Null),
387387- ])
388388- })
389389-}
390390-391391-/// Build introspection for an argument
392392-fn argument_introspection(arg: schema.Argument) -> value.Value {
393393- value.Object([
394394- #("name", value.String(schema.argument_name(arg))),
395395- #("description", value.String(schema.argument_description(arg))),
396396- #("type", type_ref(schema.argument_type(arg))),
397397- #("defaultValue", value.Null),
398398- ])
399399-}
400400-401401-/// Build a type reference (simplified version of type_introspection for field types)
402402-fn type_ref(t: schema.Type) -> value.Value {
403403- let kind = schema.type_kind(t)
404404- let type_name = schema.type_name(t)
405405-406406- // Get inner type for LIST and NON_NULL
407407- let of_type = case schema.inner_type(t) {
408408- option.Some(inner) -> type_ref(inner)
409409- option.None -> value.Null
410410- }
411411-412412- // Handle wrapping types (LIST/NON_NULL) differently
413413- let name = case kind {
414414- "LIST" -> value.Null
415415- "NON_NULL" -> value.Null
416416- _ -> value.String(type_name)
417417- }
418418-419419- value.Object([
420420- #("kind", value.String(kind)),
421421- #("name", name),
422422- #("ofType", of_type),
423423- ])
424424-}
···11-/// GraphQL Value types
22-///
33-/// Per GraphQL spec Section 2 - Language, values can be scalars, enums,
44-/// lists, or objects. This module defines the core Value type used throughout
55-/// the GraphQL implementation.
66-/// A GraphQL value that can be used in queries, responses, and variables
77-pub type Value {
88- /// Represents null/absence of a value
99- Null
1010-1111- /// Integer value (32-bit signed integer per spec)
1212- Int(Int)
1313-1414- /// Floating point value (IEEE 754 double precision per spec)
1515- Float(Float)
1616-1717- /// UTF-8 string value
1818- String(String)
1919-2020- /// Boolean true or false
2121- Boolean(Bool)
2222-2323- /// Enum value represented as a string (e.g., "ACTIVE", "PENDING")
2424- Enum(String)
2525-2626- /// Ordered list of values
2727- List(List(Value))
2828-2929- /// Unordered set of key-value pairs
3030- /// Using list of tuples for simplicity and ordering preservation
3131- Object(List(#(String, Value)))
3232-}
-867
graphql/test/executor_test.gleam
···11-/// Tests for GraphQL Executor
22-///
33-/// Tests query execution combining parser + schema + resolvers
44-import birdie
55-import gleam/dict
66-import gleam/list
77-import gleam/option.{None, Some}
88-import gleam/string
99-import gleeunit/should
1010-import graphql/executor
1111-import graphql/schema
1212-import graphql/value
1313-1414-// Helper to create a simple test schema
1515-fn test_schema() -> schema.Schema {
1616- let query_type =
1717- schema.object_type("Query", "Root query type", [
1818- schema.field("hello", schema.string_type(), "Hello field", fn(_ctx) {
1919- Ok(value.String("world"))
2020- }),
2121- schema.field("number", schema.int_type(), "Number field", fn(_ctx) {
2222- Ok(value.Int(42))
2323- }),
2424- schema.field_with_args(
2525- "greet",
2626- schema.string_type(),
2727- "Greet someone",
2828- [schema.argument("name", schema.string_type(), "Name to greet", None)],
2929- fn(_ctx) { Ok(value.String("Hello, Alice!")) },
3030- ),
3131- ])
3232-3333- schema.schema(query_type, None)
3434-}
3535-3636-// Nested object schema for testing
3737-fn nested_schema() -> schema.Schema {
3838- let user_type =
3939- schema.object_type("User", "A user", [
4040- schema.field("id", schema.id_type(), "User ID", fn(_ctx) {
4141- Ok(value.String("123"))
4242- }),
4343- schema.field("name", schema.string_type(), "User name", fn(_ctx) {
4444- Ok(value.String("Alice"))
4545- }),
4646- ])
4747-4848- let query_type =
4949- schema.object_type("Query", "Root query type", [
5050- schema.field("user", user_type, "Get user", fn(_ctx) {
5151- Ok(
5252- value.Object([
5353- #("id", value.String("123")),
5454- #("name", value.String("Alice")),
5555- ]),
5656- )
5757- }),
5858- ])
5959-6060- schema.schema(query_type, None)
6161-}
6262-6363-pub fn execute_simple_query_test() {
6464- let schema = test_schema()
6565- let query = "{ hello }"
6666-6767- let result = executor.execute(query, schema, schema.context(None))
6868-6969- let response = case result {
7070- Ok(r) -> r
7171- Error(_) -> panic as "Execution failed"
7272- }
7373-7474- birdie.snap(title: "Execute simple query", content: format_response(response))
7575-}
7676-7777-pub fn execute_multiple_fields_test() {
7878- let schema = test_schema()
7979- let query = "{ hello number }"
8080-8181- let result = executor.execute(query, schema, schema.context(None))
8282-8383- should.be_ok(result)
8484-}
8585-8686-pub fn execute_nested_query_test() {
8787- let schema = nested_schema()
8888- let query = "{ user { id name } }"
8989-9090- let result = executor.execute(query, schema, schema.context(None))
9191-9292- should.be_ok(result)
9393-}
9494-9595-// Helper to format response for snapshots
9696-fn format_response(response: executor.Response) -> String {
9797- string.inspect(response)
9898-}
9999-100100-pub fn execute_field_with_arguments_test() {
101101- let schema = test_schema()
102102- let query = "{ greet(name: \"Alice\") }"
103103-104104- let result = executor.execute(query, schema, schema.context(None))
105105-106106- should.be_ok(result)
107107-}
108108-109109-pub fn execute_invalid_query_returns_error_test() {
110110- let schema = test_schema()
111111- let query = "{ invalid }"
112112-113113- let result = executor.execute(query, schema, schema.context(None))
114114-115115- // Should return error since field doesn't exist
116116- case result {
117117- Ok(executor.Response(_, [_, ..])) -> should.be_true(True)
118118- Error(_) -> should.be_true(True)
119119- _ -> should.be_true(False)
120120- }
121121-}
122122-123123-pub fn execute_parse_error_returns_error_test() {
124124- let schema = test_schema()
125125- let query = "{ invalid syntax"
126126-127127- let result = executor.execute(query, schema, schema.context(None))
128128-129129- should.be_error(result)
130130-}
131131-132132-pub fn execute_typename_introspection_test() {
133133- let schema = test_schema()
134134- let query = "{ __typename }"
135135-136136- let result = executor.execute(query, schema, schema.context(None))
137137-138138- let response = case result {
139139- Ok(r) -> r
140140- Error(_) -> panic as "Execution failed"
141141- }
142142-143143- birdie.snap(
144144- title: "Execute __typename introspection",
145145- content: format_response(response),
146146- )
147147-}
148148-149149-pub fn execute_typename_with_regular_fields_test() {
150150- let schema = test_schema()
151151- let query = "{ __typename hello }"
152152-153153- let result = executor.execute(query, schema, schema.context(None))
154154-155155- let response = case result {
156156- Ok(r) -> r
157157- Error(_) -> panic as "Execution failed"
158158- }
159159-160160- birdie.snap(
161161- title: "Execute __typename with regular fields",
162162- content: format_response(response),
163163- )
164164-}
165165-166166-pub fn execute_schema_introspection_query_type_test() {
167167- let schema = test_schema()
168168- let query = "{ __schema { queryType { name } } }"
169169-170170- let result = executor.execute(query, schema, schema.context(None))
171171-172172- let response = case result {
173173- Ok(r) -> r
174174- Error(_) -> panic as "Execution failed"
175175- }
176176-177177- birdie.snap(
178178- title: "Execute __schema introspection",
179179- content: format_response(response),
180180- )
181181-}
182182-183183-// Fragment execution tests
184184-pub fn execute_simple_fragment_spread_test() {
185185- let schema = nested_schema()
186186- let query =
187187- "
188188- fragment UserFields on User {
189189- id
190190- name
191191- }
192192-193193- { user { ...UserFields } }
194194- "
195195-196196- let result = executor.execute(query, schema, schema.context(None))
197197-198198- let response = case result {
199199- Ok(r) -> r
200200- Error(_) -> panic as "Execution failed"
201201- }
202202-203203- birdie.snap(
204204- title: "Execute simple fragment spread",
205205- content: format_response(response),
206206- )
207207-}
208208-209209-// Test for list fields with nested selections
210210-pub fn execute_list_with_nested_selections_test() {
211211- // Create a schema with a list field
212212- let user_type =
213213- schema.object_type("User", "A user", [
214214- schema.field("id", schema.id_type(), "User ID", fn(ctx) {
215215- case ctx.data {
216216- option.Some(value.Object(fields)) -> {
217217- case list.key_find(fields, "id") {
218218- Ok(id_val) -> Ok(id_val)
219219- Error(_) -> Ok(value.Null)
220220- }
221221- }
222222- _ -> Ok(value.Null)
223223- }
224224- }),
225225- schema.field("name", schema.string_type(), "User name", fn(ctx) {
226226- case ctx.data {
227227- option.Some(value.Object(fields)) -> {
228228- case list.key_find(fields, "name") {
229229- Ok(name_val) -> Ok(name_val)
230230- Error(_) -> Ok(value.Null)
231231- }
232232- }
233233- _ -> Ok(value.Null)
234234- }
235235- }),
236236- schema.field("email", schema.string_type(), "User email", fn(ctx) {
237237- case ctx.data {
238238- option.Some(value.Object(fields)) -> {
239239- case list.key_find(fields, "email") {
240240- Ok(email_val) -> Ok(email_val)
241241- Error(_) -> Ok(value.Null)
242242- }
243243- }
244244- _ -> Ok(value.Null)
245245- }
246246- }),
247247- ])
248248-249249- let list_type = schema.list_type(user_type)
250250-251251- let query_type =
252252- schema.object_type("Query", "Root query type", [
253253- schema.field("users", list_type, "Get all users", fn(_ctx) {
254254- // Return a list of user objects
255255- Ok(
256256- value.List([
257257- value.Object([
258258- #("id", value.String("1")),
259259- #("name", value.String("Alice")),
260260- #("email", value.String("alice@example.com")),
261261- ]),
262262- value.Object([
263263- #("id", value.String("2")),
264264- #("name", value.String("Bob")),
265265- #("email", value.String("bob@example.com")),
266266- ]),
267267- ]),
268268- )
269269- }),
270270- ])
271271-272272- let schema = schema.schema(query_type, None)
273273-274274- // Query with nested field selection - only request id and name, not email
275275- let query = "{ users { id name } }"
276276-277277- let result = executor.execute(query, schema, schema.context(None))
278278-279279- let response = case result {
280280- Ok(r) -> r
281281- Error(_) -> panic as "Execution failed"
282282- }
283283-284284- birdie.snap(
285285- title: "Execute list with nested selections",
286286- content: format_response(response),
287287- )
288288-}
289289-290290-// Test that arguments are actually passed to resolvers
291291-pub fn execute_field_receives_string_argument_test() {
292292- let query_type =
293293- schema.object_type("Query", "Root", [
294294- schema.field_with_args(
295295- "echo",
296296- schema.string_type(),
297297- "Echo the input",
298298- [schema.argument("message", schema.string_type(), "Message", None)],
299299- fn(ctx) {
300300- // Extract the argument from context
301301- case schema.get_argument(ctx, "message") {
302302- Some(value.String(msg)) -> Ok(value.String("Echo: " <> msg))
303303- _ -> Ok(value.String("No message"))
304304- }
305305- },
306306- ),
307307- ])
308308-309309- let test_schema = schema.schema(query_type, None)
310310- let query = "{ echo(message: \"hello\") }"
311311-312312- let result = executor.execute(query, test_schema, schema.context(None))
313313-314314- let response = case result {
315315- Ok(r) -> r
316316- Error(_) -> panic as "Execution failed"
317317- }
318318-319319- birdie.snap(
320320- title: "Execute field with string argument",
321321- content: format_response(response),
322322- )
323323-}
324324-325325-// Test list argument
326326-pub fn execute_field_receives_list_argument_test() {
327327- let query_type =
328328- schema.object_type("Query", "Root", [
329329- schema.field_with_args(
330330- "sum",
331331- schema.int_type(),
332332- "Sum numbers",
333333- [
334334- schema.argument(
335335- "numbers",
336336- schema.list_type(schema.int_type()),
337337- "Numbers",
338338- None,
339339- ),
340340- ],
341341- fn(ctx) {
342342- case schema.get_argument(ctx, "numbers") {
343343- Some(value.List(_items)) -> Ok(value.String("got list"))
344344- _ -> Ok(value.String("no list"))
345345- }
346346- },
347347- ),
348348- ])
349349-350350- let test_schema = schema.schema(query_type, None)
351351- let query = "{ sum(numbers: [1, 2, 3]) }"
352352-353353- let result = executor.execute(query, test_schema, schema.context(None))
354354-355355- should.be_ok(result)
356356- |> fn(response) {
357357- case response {
358358- executor.Response(
359359- data: value.Object([#("sum", value.String("got list"))]),
360360- errors: [],
361361- ) -> True
362362- _ -> False
363363- }
364364- }
365365- |> should.be_true
366366-}
367367-368368-// Test object argument (like sortBy)
369369-pub fn execute_field_receives_object_argument_test() {
370370- let query_type =
371371- schema.object_type("Query", "Root", [
372372- schema.field_with_args(
373373- "posts",
374374- schema.list_type(schema.string_type()),
375375- "Get posts",
376376- [
377377- schema.argument(
378378- "sortBy",
379379- schema.list_type(
380380- schema.input_object_type("SortInput", "Sort", [
381381- schema.input_field("field", schema.string_type(), "Field", None),
382382- schema.input_field(
383383- "direction",
384384- schema.enum_type("Direction", "Direction", [
385385- schema.enum_value("ASC", "Ascending"),
386386- schema.enum_value("DESC", "Descending"),
387387- ]),
388388- "Direction",
389389- None,
390390- ),
391391- ]),
392392- ),
393393- "Sort order",
394394- None,
395395- ),
396396- ],
397397- fn(ctx) {
398398- case schema.get_argument(ctx, "sortBy") {
399399- Some(value.List([value.Object(fields), ..])) -> {
400400- case dict.from_list(fields) {
401401- fields_dict -> {
402402- case
403403- dict.get(fields_dict, "field"),
404404- dict.get(fields_dict, "direction")
405405- {
406406- Ok(value.String(field)), Ok(value.String(dir)) ->
407407- Ok(value.String("Sorting by " <> field <> " " <> dir))
408408- _, _ -> Ok(value.String("Invalid sort"))
409409- }
410410- }
411411- }
412412- }
413413- _ -> Ok(value.String("No sort"))
414414- }
415415- },
416416- ),
417417- ])
418418-419419- let test_schema = schema.schema(query_type, None)
420420- let query = "{ posts(sortBy: [{field: \"date\", direction: DESC}]) }"
421421-422422- let result = executor.execute(query, test_schema, schema.context(None))
423423-424424- let response = case result {
425425- Ok(r) -> r
426426- Error(_) -> panic as "Execution failed"
427427- }
428428-429429- birdie.snap(
430430- title: "Execute field with object argument",
431431- content: format_response(response),
432432- )
433433-}
434434-435435-// Variable resolution tests
436436-pub fn execute_query_with_variable_string_test() {
437437- let query_type =
438438- schema.object_type("Query", "Root query type", [
439439- schema.field_with_args(
440440- "greet",
441441- schema.string_type(),
442442- "Greet someone",
443443- [
444444- schema.argument("name", schema.string_type(), "Name to greet", None),
445445- ],
446446- fn(ctx) {
447447- case schema.get_argument(ctx, "name") {
448448- Some(value.String(name)) ->
449449- Ok(value.String("Hello, " <> name <> "!"))
450450- _ -> Ok(value.String("Hello, stranger!"))
451451- }
452452- },
453453- ),
454454- ])
455455-456456- let test_schema = schema.schema(query_type, None)
457457- let query = "query Test($name: String!) { greet(name: $name) }"
458458-459459- // Create context with variables
460460- let variables = dict.from_list([#("name", value.String("Alice"))])
461461- let ctx = schema.context_with_variables(None, variables)
462462-463463- let result = executor.execute(query, test_schema, ctx)
464464-465465- let response = case result {
466466- Ok(r) -> r
467467- Error(_) -> panic as "Execution failed"
468468- }
469469-470470- birdie.snap(
471471- title: "Execute query with string variable",
472472- content: format_response(response),
473473- )
474474-}
475475-476476-pub fn execute_query_with_variable_int_test() {
477477- let query_type =
478478- schema.object_type("Query", "Root query type", [
479479- schema.field_with_args(
480480- "user",
481481- schema.string_type(),
482482- "Get user by ID",
483483- [
484484- schema.argument("id", schema.int_type(), "User ID", None),
485485- ],
486486- fn(ctx) {
487487- case schema.get_argument(ctx, "id") {
488488- Some(value.Int(id)) ->
489489- Ok(value.String("User #" <> string.inspect(id)))
490490- _ -> Ok(value.String("Unknown user"))
491491- }
492492- },
493493- ),
494494- ])
495495-496496- let test_schema = schema.schema(query_type, None)
497497- let query = "query GetUser($userId: Int!) { user(id: $userId) }"
498498-499499- // Create context with variables
500500- let variables = dict.from_list([#("userId", value.Int(42))])
501501- let ctx = schema.context_with_variables(None, variables)
502502-503503- let result = executor.execute(query, test_schema, ctx)
504504-505505- let response = case result {
506506- Ok(r) -> r
507507- Error(_) -> panic as "Execution failed"
508508- }
509509-510510- birdie.snap(
511511- title: "Execute query with int variable",
512512- content: format_response(response),
513513- )
514514-}
515515-516516-pub fn execute_query_with_multiple_variables_test() {
517517- let query_type =
518518- schema.object_type("Query", "Root query type", [
519519- schema.field_with_args(
520520- "search",
521521- schema.string_type(),
522522- "Search for something",
523523- [
524524- schema.argument("query", schema.string_type(), "Search query", None),
525525- schema.argument("limit", schema.int_type(), "Max results", None),
526526- ],
527527- fn(ctx) {
528528- case
529529- schema.get_argument(ctx, "query"),
530530- schema.get_argument(ctx, "limit")
531531- {
532532- Some(value.String(q)), Some(value.Int(l)) ->
533533- Ok(value.String(
534534- "Searching for '"
535535- <> q
536536- <> "' (limit: "
537537- <> string.inspect(l)
538538- <> ")",
539539- ))
540540- _, _ -> Ok(value.String("Invalid search"))
541541- }
542542- },
543543- ),
544544- ])
545545-546546- let test_schema = schema.schema(query_type, None)
547547- let query =
548548- "query Search($q: String!, $max: Int!) { search(query: $q, limit: $max) }"
549549-550550- // Create context with variables
551551- let variables =
552552- dict.from_list([
553553- #("q", value.String("graphql")),
554554- #("max", value.Int(10)),
555555- ])
556556- let ctx = schema.context_with_variables(None, variables)
557557-558558- let result = executor.execute(query, test_schema, ctx)
559559-560560- let response = case result {
561561- Ok(r) -> r
562562- Error(_) -> panic as "Execution failed"
563563- }
564564-565565- birdie.snap(
566566- title: "Execute query with multiple variables",
567567- content: format_response(response),
568568- )
569569-}
570570-571571-// Union type execution tests
572572-pub fn execute_union_with_inline_fragment_test() {
573573- // Create object types that will be part of the union
574574- let post_type =
575575- schema.object_type("Post", "A blog post", [
576576- schema.field("title", schema.string_type(), "Post title", fn(ctx) {
577577- case ctx.data {
578578- option.Some(value.Object(fields)) -> {
579579- case list.key_find(fields, "title") {
580580- Ok(title_val) -> Ok(title_val)
581581- Error(_) -> Ok(value.Null)
582582- }
583583- }
584584- _ -> Ok(value.Null)
585585- }
586586- }),
587587- schema.field("content", schema.string_type(), "Post content", fn(ctx) {
588588- case ctx.data {
589589- option.Some(value.Object(fields)) -> {
590590- case list.key_find(fields, "content") {
591591- Ok(content_val) -> Ok(content_val)
592592- Error(_) -> Ok(value.Null)
593593- }
594594- }
595595- _ -> Ok(value.Null)
596596- }
597597- }),
598598- ])
599599-600600- let comment_type =
601601- schema.object_type("Comment", "A comment", [
602602- schema.field("text", schema.string_type(), "Comment text", fn(ctx) {
603603- case ctx.data {
604604- option.Some(value.Object(fields)) -> {
605605- case list.key_find(fields, "text") {
606606- Ok(text_val) -> Ok(text_val)
607607- Error(_) -> Ok(value.Null)
608608- }
609609- }
610610- _ -> Ok(value.Null)
611611- }
612612- }),
613613- ])
614614-615615- // Type resolver that examines the __typename field
616616- let type_resolver = fn(ctx: schema.Context) -> Result(String, String) {
617617- case ctx.data {
618618- option.Some(value.Object(fields)) -> {
619619- case list.key_find(fields, "__typename") {
620620- Ok(value.String(type_name)) -> Ok(type_name)
621621- _ -> Error("No __typename field found")
622622- }
623623- }
624624- _ -> Error("No data")
625625- }
626626- }
627627-628628- // Create union type
629629- let search_result_union =
630630- schema.union_type(
631631- "SearchResult",
632632- "A search result",
633633- [post_type, comment_type],
634634- type_resolver,
635635- )
636636-637637- // Create query type with a field returning the union
638638- let query_type =
639639- schema.object_type("Query", "Root query type", [
640640- schema.field(
641641- "search",
642642- search_result_union,
643643- "Search for content",
644644- fn(_ctx) {
645645- // Return a Post
646646- Ok(
647647- value.Object([
648648- #("__typename", value.String("Post")),
649649- #("title", value.String("GraphQL is awesome")),
650650- #("content", value.String("Learn all about GraphQL...")),
651651- ]),
652652- )
653653- },
654654- ),
655655- ])
656656-657657- let test_schema = schema.schema(query_type, None)
658658-659659- // Query with inline fragment
660660- let query =
661661- "
662662- {
663663- search {
664664- ... on Post {
665665- title
666666- content
667667- }
668668- ... on Comment {
669669- text
670670- }
671671- }
672672- }
673673- "
674674-675675- let result = executor.execute(query, test_schema, schema.context(None))
676676-677677- let response = case result {
678678- Ok(r) -> r
679679- Error(_) -> panic as "Execution failed"
680680- }
681681-682682- birdie.snap(
683683- title: "Execute union with inline fragment",
684684- content: format_response(response),
685685- )
686686-}
687687-688688-pub fn execute_union_list_with_inline_fragments_test() {
689689- // Create object types
690690- let post_type =
691691- schema.object_type("Post", "A blog post", [
692692- schema.field("title", schema.string_type(), "Post title", fn(ctx) {
693693- case ctx.data {
694694- option.Some(value.Object(fields)) -> {
695695- case list.key_find(fields, "title") {
696696- Ok(title_val) -> Ok(title_val)
697697- Error(_) -> Ok(value.Null)
698698- }
699699- }
700700- _ -> Ok(value.Null)
701701- }
702702- }),
703703- ])
704704-705705- let comment_type =
706706- schema.object_type("Comment", "A comment", [
707707- schema.field("text", schema.string_type(), "Comment text", fn(ctx) {
708708- case ctx.data {
709709- option.Some(value.Object(fields)) -> {
710710- case list.key_find(fields, "text") {
711711- Ok(text_val) -> Ok(text_val)
712712- Error(_) -> Ok(value.Null)
713713- }
714714- }
715715- _ -> Ok(value.Null)
716716- }
717717- }),
718718- ])
719719-720720- // Type resolver
721721- let type_resolver = fn(ctx: schema.Context) -> Result(String, String) {
722722- case ctx.data {
723723- option.Some(value.Object(fields)) -> {
724724- case list.key_find(fields, "__typename") {
725725- Ok(value.String(type_name)) -> Ok(type_name)
726726- _ -> Error("No __typename field found")
727727- }
728728- }
729729- _ -> Error("No data")
730730- }
731731- }
732732-733733- // Create union type
734734- let search_result_union =
735735- schema.union_type(
736736- "SearchResult",
737737- "A search result",
738738- [post_type, comment_type],
739739- type_resolver,
740740- )
741741-742742- // Create query type with a list of unions
743743- let query_type =
744744- schema.object_type("Query", "Root query type", [
745745- schema.field(
746746- "searchAll",
747747- schema.list_type(search_result_union),
748748- "Search for all content",
749749- fn(_ctx) {
750750- // Return a list with mixed types
751751- Ok(
752752- value.List([
753753- value.Object([
754754- #("__typename", value.String("Post")),
755755- #("title", value.String("First Post")),
756756- ]),
757757- value.Object([
758758- #("__typename", value.String("Comment")),
759759- #("text", value.String("Great article!")),
760760- ]),
761761- value.Object([
762762- #("__typename", value.String("Post")),
763763- #("title", value.String("Second Post")),
764764- ]),
765765- ]),
766766- )
767767- },
768768- ),
769769- ])
770770-771771- let test_schema = schema.schema(query_type, None)
772772-773773- // Query with inline fragments on list items
774774- let query =
775775- "
776776- {
777777- searchAll {
778778- ... on Post {
779779- title
780780- }
781781- ... on Comment {
782782- text
783783- }
784784- }
785785- }
786786- "
787787-788788- let result = executor.execute(query, test_schema, schema.context(None))
789789-790790- let response = case result {
791791- Ok(r) -> r
792792- Error(_) -> panic as "Execution failed"
793793- }
794794-795795- birdie.snap(
796796- title: "Execute union list with inline fragments",
797797- content: format_response(response),
798798- )
799799-}
800800-801801-// Test field aliases
802802-pub fn execute_field_with_alias_test() {
803803- let schema = test_schema()
804804- let query = "{ greeting: hello }"
805805-806806- let result = executor.execute(query, schema, schema.context(None))
807807-808808- let response = case result {
809809- Ok(r) -> r
810810- Error(_) -> panic as "Execution failed"
811811- }
812812-813813- // Response should contain "greeting" as the key, not "hello"
814814- case response.data {
815815- value.Object(fields) -> {
816816- case list.key_find(fields, "greeting") {
817817- Ok(_) -> should.be_true(True)
818818- Error(_) -> {
819819- // Check if it incorrectly used "hello" instead
820820- case list.key_find(fields, "hello") {
821821- Ok(_) ->
822822- panic as "Alias not applied - used 'hello' instead of 'greeting'"
823823- Error(_) ->
824824- panic as "Neither 'greeting' nor 'hello' found in response"
825825- }
826826- }
827827- }
828828- }
829829- _ -> panic as "Expected object response"
830830- }
831831-}
832832-833833-// Test multiple aliases
834834-pub fn execute_multiple_fields_with_aliases_test() {
835835- let schema = test_schema()
836836- let query = "{ greeting: hello num: number }"
837837-838838- let result = executor.execute(query, schema, schema.context(None))
839839-840840- let response = case result {
841841- Ok(r) -> r
842842- Error(_) -> panic as "Execution failed"
843843- }
844844-845845- birdie.snap(
846846- title: "Execute multiple fields with aliases",
847847- content: format_response(response),
848848- )
849849-}
850850-851851-// Test mixed aliased and non-aliased fields
852852-pub fn execute_mixed_aliased_fields_test() {
853853- let schema = test_schema()
854854- let query = "{ greeting: hello number }"
855855-856856- let result = executor.execute(query, schema, schema.context(None))
857857-858858- let response = case result {
859859- Ok(r) -> r
860860- Error(_) -> panic as "Execution failed"
861861- }
862862-863863- birdie.snap(
864864- title: "Execute mixed aliased and non-aliased fields",
865865- content: format_response(response),
866866- )
867867-}
···88import gleam/option.{type Option, None, Some}
99import gleam/result
1010import gleam/string
1111-import graphql/value
1211import lexicon_graphql/collection_meta
1312import lexicon_graphql/uri_extractor
1413import lexicon_graphql/where_input.{type WhereClause}
1414+import swell/value
15151616/// Result of a batch query: maps URIs to their records
1717pub type BatchResult =
···77import gleam/list
88import gleam/option
99import gleam/string
1010-import graphql/schema
1111-import graphql/value
1210import lexicon_graphql/lexicon_registry
1311import lexicon_graphql/nsid
1412import lexicon_graphql/type_mapper
1513import lexicon_graphql/types
1414+import swell/schema
1515+import swell/value
16161717/// Build a GraphQL object type from an ObjectDef
1818/// object_types_dict is used to resolve refs to other object types
···66/// Based on the Elixir implementation but adapted for the pure Gleam GraphQL library.
77import gleam/dict.{type Dict}
88import gleam/option.{type Option}
99-import graphql/schema
109import lexicon_graphql/blob_type
1010+import swell/schema
11111212/// Maps a lexicon type string to a GraphQL output Type.
1313///
···55import gleam/dict.{type Dict}
66import gleam/list
77import gleam/option.{type Option, None, Some}
88-import graphql/value
88+import swell/value
991010/// Simple value type that can represent strings, ints, or other primitives
1111pub type WhereValue {