๐ŸŒŠ A GraphQL implementation in Gleam

fix: fragment spread and __typename handling for wrapped types

- Add named_type_name function to get base type without List/NonNull wrappers
- Fix fragment spread type condition matching on NonNull/List wrapped types
- Fix inline fragment type condition matching with wrapped types
- Fix __typename to return concrete type name without modifiers

+7
CHANGELOG.md
··· 7 7 - Support for variable default values (`$name: Type = defaultValue`) 8 8 - Default values are applied during execution when variables are not provided 9 9 - Provided variables override default values 10 + - New `schema.named_type_name` function to get the base type name without List/NonNull wrappers 11 + 12 + ### Fixed 13 + 14 + - Fragment spread type condition matching now works correctly when parent type is wrapped in NonNull or List 15 + - Inline fragment type condition matching now works correctly with wrapped types 16 + - `__typename` introspection now returns the concrete type name without modifiers 10 17 11 18 ### Breaking Changes 12 19
+7
birdie_snapshots/execute_fragment_spread_on_non_null_type.accepted
··· 1 + --- 2 + version: 1.4.1 3 + title: Execute fragment spread on NonNull type 4 + file: ./test/executor_test.gleam 5 + test_name: execute_fragment_spread_on_non_null_type_test 6 + --- 7 + Response(Object([#("user", Object([#("id", String("123")), #("name", String("Alice"))]))]), [])
+8 -5
src/swell/executor.gleam
··· 322 322 type_condition, 323 323 fragment_selection_set, 324 324 )) -> { 325 - // Check type condition 326 - let current_type_name = schema.type_name(parent_type) 325 + // Check type condition - use named_type_name to get the base type 326 + // without NonNull/List wrappers, since fragments are defined on named types 327 + let current_type_name = schema.named_type_name(parent_type) 327 328 case type_condition == current_type_name { 328 329 False -> { 329 330 // Type condition doesn't match, skip this fragment ··· 357 358 } 358 359 } 359 360 parser.InlineFragment(type_condition_opt, inline_selections) -> { 360 - // Check type condition if present 361 - let current_type_name = schema.type_name(parent_type) 361 + // Check type condition if present - use named_type_name to get the base type 362 + // without NonNull/List wrappers, since fragments are defined on named types 363 + let current_type_name = schema.named_type_name(parent_type) 362 364 let should_execute = case type_condition_opt { 363 365 None -> True 364 366 Some(type_condition) -> type_condition == current_type_name ··· 396 398 // Handle introspection meta-fields 397 399 case name { 398 400 "__typename" -> { 399 - let type_name = schema.type_name(parent_type) 401 + // Use named_type_name to return the concrete type without modifiers 402 + let type_name = schema.named_type_name(parent_type) 400 403 Ok(#(key, value.String(type_name), [])) 401 404 } 402 405 "__schema" -> {
+15
src/swell/schema.gleam
··· 239 239 } 240 240 } 241 241 242 + /// Get the named (base) type name, unwrapping List and NonNull wrappers. 243 + /// This is used for fragment type condition matching and __typename where 244 + /// we need the concrete type name without modifiers. 245 + pub fn named_type_name(t: Type) -> String { 246 + case t { 247 + ScalarType(name) -> name 248 + ObjectType(name, _, _) -> name 249 + InputObjectType(name, _, _) -> name 250 + EnumType(name, _, _) -> name 251 + UnionType(name, _, _, _) -> name 252 + ListType(inner) -> named_type_name(inner) 253 + NonNullType(inner) -> named_type_name(inner) 254 + } 255 + } 256 + 242 257 pub fn field_name(f: Field) -> String { 243 258 case f { 244 259 Field(name, _, _, _, _) -> name
+52
test/executor_test.gleam
··· 206 206 ) 207 207 } 208 208 209 + // Test for fragment spread on NonNull wrapped type 210 + pub fn execute_fragment_spread_on_non_null_type_test() { 211 + // Create a schema where the user field returns a NonNull type 212 + let user_type = 213 + schema.object_type("User", "A user", [ 214 + schema.field("id", schema.id_type(), "User ID", fn(_ctx) { 215 + Ok(value.String("123")) 216 + }), 217 + schema.field("name", schema.string_type(), "User name", fn(_ctx) { 218 + Ok(value.String("Alice")) 219 + }), 220 + ]) 221 + 222 + let query_type = 223 + schema.object_type("Query", "Root query type", [ 224 + // Wrap user_type in NonNull to test fragment type condition matching 225 + schema.field("user", schema.non_null(user_type), "Get user", fn(_ctx) { 226 + Ok( 227 + value.Object([ 228 + #("id", value.String("123")), 229 + #("name", value.String("Alice")), 230 + ]), 231 + ) 232 + }), 233 + ]) 234 + 235 + let test_schema = schema.schema(query_type, None) 236 + 237 + // Fragment is defined on "User" (not "User!") - this should still work 238 + let query = 239 + " 240 + fragment UserFields on User { 241 + id 242 + name 243 + } 244 + 245 + { user { ...UserFields } } 246 + " 247 + 248 + let result = executor.execute(query, test_schema, schema.context(None)) 249 + 250 + let response = case result { 251 + Ok(r) -> r 252 + Error(_) -> panic as "Execution failed" 253 + } 254 + 255 + birdie.snap( 256 + title: "Execute fragment spread on NonNull type", 257 + content: format_response(response), 258 + ) 259 + } 260 + 209 261 // Test for list fields with nested selections 210 262 pub fn execute_list_with_nested_selections_test() { 211 263 // Create a schema with a list field