๐ŸŒŠ A GraphQL implementation in Gleam

fix(executor): preserve variables in nested object and list contexts

Variables were being lost when traversing into nested contexts during
query execution. Now uses context_with_variables to preserve variables.

Changed files
+211 -5
src
test
+6
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + ## 2.1.4 4 + 5 + ### Fixed 6 + 7 + - Variables are now preserved when executing nested object and list selections. Previously, variables were lost when traversing into nested contexts, causing variable references in nested fields to resolve as empty. 8 + 3 9 ## 2.1.3 4 10 5 11 ### Fixed
+1 -1
gleam.toml
··· 1 1 name = "swell" 2 - version = "2.1.3" 2 + version = "2.1.4" 3 3 description = "๐ŸŒŠ A GraphQL implementation in Gleam" 4 4 licences = ["Apache-2.0"] 5 5 repository = { type = "github", user = "bigmoves", repo = "swell" }
+10 -4
src/swell/executor.gleam
··· 590 590 } 591 591 592 592 // Execute nested selections using the resolved type 593 - // Create new context with this object's data 593 + // Create new context with this object's data, preserving variables 594 594 let object_ctx = 595 - schema.context(option.Some(field_value)) 595 + schema.context_with_variables( 596 + option.Some(field_value), 597 + ctx.variables, 598 + ) 596 599 let selection_set = 597 600 parser.SelectionSet(nested_selections) 598 601 case ··· 667 670 False -> inner_type 668 671 } 669 672 670 - // Create context with this item's data 673 + // Create context with this item's data, preserving variables 671 674 let item_ctx = 672 - schema.context(option.Some(item)) 675 + schema.context_with_variables( 676 + option.Some(item), 677 + ctx.variables, 678 + ) 673 679 execute_selection_set( 674 680 selection_set, 675 681 item_type,
+194
test/executor_test.gleam
··· 1282 1282 } 1283 1283 } 1284 1284 1285 + // Test: Variables are preserved in nested object selections 1286 + // This verifies that when traversing into a nested object, variables 1287 + // from the parent context are still accessible 1288 + pub fn execute_variables_preserved_in_nested_object_test() { 1289 + // Create a nested type structure where the nested resolver needs access to variables 1290 + let post_type = 1291 + schema.object_type("Post", "A post", [ 1292 + schema.field("title", schema.string_type(), "Post title", fn(ctx) { 1293 + case ctx.data { 1294 + option.Some(value.Object(fields)) -> { 1295 + case list.key_find(fields, "title") { 1296 + Ok(title_val) -> Ok(title_val) 1297 + Error(_) -> Ok(value.Null) 1298 + } 1299 + } 1300 + _ -> Ok(value.Null) 1301 + } 1302 + }), 1303 + // This field uses a variable from the outer context 1304 + schema.field_with_args( 1305 + "formattedTitle", 1306 + schema.string_type(), 1307 + "Formatted title", 1308 + [schema.argument("prefix", schema.string_type(), "Prefix to add", None)], 1309 + fn(ctx) { 1310 + let prefix = case schema.get_argument(ctx, "prefix") { 1311 + Some(value.String(p)) -> p 1312 + _ -> "" 1313 + } 1314 + case ctx.data { 1315 + option.Some(value.Object(fields)) -> { 1316 + case list.key_find(fields, "title") { 1317 + Ok(value.String(title)) -> 1318 + Ok(value.String(prefix <> ": " <> title)) 1319 + _ -> Ok(value.Null) 1320 + } 1321 + } 1322 + _ -> Ok(value.Null) 1323 + } 1324 + }, 1325 + ), 1326 + ]) 1327 + 1328 + let query_type = 1329 + schema.object_type("Query", "Root query type", [ 1330 + schema.field("post", post_type, "Get a post", fn(_ctx) { 1331 + Ok(value.Object([#("title", value.String("Hello World"))])) 1332 + }), 1333 + ]) 1334 + 1335 + let test_schema = schema.schema(query_type, None) 1336 + 1337 + // Query using a variable in a nested field 1338 + let query = 1339 + "query GetPost($prefix: String!) { post { formattedTitle(prefix: $prefix) } }" 1340 + 1341 + // Create context with variables 1342 + let variables = dict.from_list([#("prefix", value.String("Article"))]) 1343 + let ctx = schema.context_with_variables(None, variables) 1344 + 1345 + let result = executor.execute(query, test_schema, ctx) 1346 + 1347 + case result { 1348 + Ok(executor.Response(data: value.Object(fields), errors: _)) -> { 1349 + case list.key_find(fields, "post") { 1350 + Ok(value.Object(post_fields)) -> { 1351 + case list.key_find(post_fields, "formattedTitle") { 1352 + Ok(value.String("Article: Hello World")) -> should.be_true(True) 1353 + Ok(other) -> { 1354 + // Variable was lost - this is the bug we're testing for 1355 + should.equal(other, value.String("Article: Hello World")) 1356 + } 1357 + Error(_) -> should.fail() 1358 + } 1359 + } 1360 + _ -> should.fail() 1361 + } 1362 + } 1363 + Error(err) -> should.equal(err, "") 1364 + _ -> should.fail() 1365 + } 1366 + } 1367 + 1368 + // Test: Variables are preserved in nested list item selections 1369 + // This verifies that when iterating over list items, variables 1370 + // from the parent context are still accessible to each item's resolvers 1371 + pub fn execute_variables_preserved_in_nested_list_test() { 1372 + // Create a type structure where list item resolvers need access to variables 1373 + let item_type = 1374 + schema.object_type("Item", "An item", [ 1375 + schema.field("name", schema.string_type(), "Item name", fn(ctx) { 1376 + case ctx.data { 1377 + option.Some(value.Object(fields)) -> { 1378 + case list.key_find(fields, "name") { 1379 + Ok(name_val) -> Ok(name_val) 1380 + Error(_) -> Ok(value.Null) 1381 + } 1382 + } 1383 + _ -> Ok(value.Null) 1384 + } 1385 + }), 1386 + // This field uses a variable from the outer context 1387 + schema.field_with_args( 1388 + "formattedName", 1389 + schema.string_type(), 1390 + "Formatted name", 1391 + [schema.argument("suffix", schema.string_type(), "Suffix to add", None)], 1392 + fn(ctx) { 1393 + let suffix = case schema.get_argument(ctx, "suffix") { 1394 + Some(value.String(s)) -> s 1395 + _ -> "" 1396 + } 1397 + case ctx.data { 1398 + option.Some(value.Object(fields)) -> { 1399 + case list.key_find(fields, "name") { 1400 + Ok(value.String(name)) -> 1401 + Ok(value.String(name <> " " <> suffix)) 1402 + _ -> Ok(value.Null) 1403 + } 1404 + } 1405 + _ -> Ok(value.Null) 1406 + } 1407 + }, 1408 + ), 1409 + ]) 1410 + 1411 + let query_type = 1412 + schema.object_type("Query", "Root query type", [ 1413 + schema.field("items", schema.list_type(item_type), "Get items", fn(_ctx) { 1414 + Ok( 1415 + value.List([ 1416 + value.Object([#("name", value.String("Apple"))]), 1417 + value.Object([#("name", value.String("Banana"))]), 1418 + ]), 1419 + ) 1420 + }), 1421 + ]) 1422 + 1423 + let test_schema = schema.schema(query_type, None) 1424 + 1425 + // Query using a variable in nested list item fields 1426 + let query = 1427 + "query GetItems($suffix: String!) { items { formattedName(suffix: $suffix) } }" 1428 + 1429 + // Create context with variables 1430 + let variables = dict.from_list([#("suffix", value.String("(organic)"))]) 1431 + let ctx = schema.context_with_variables(None, variables) 1432 + 1433 + let result = executor.execute(query, test_schema, ctx) 1434 + 1435 + case result { 1436 + Ok(executor.Response(data: value.Object(fields), errors: _)) -> { 1437 + case list.key_find(fields, "items") { 1438 + Ok(value.List(items)) -> { 1439 + // Should have 2 items 1440 + list.length(items) |> should.equal(2) 1441 + 1442 + // First item should have formatted name with suffix 1443 + case list.first(items) { 1444 + Ok(value.Object(item_fields)) -> { 1445 + case list.key_find(item_fields, "formattedName") { 1446 + Ok(value.String("Apple (organic)")) -> should.be_true(True) 1447 + Ok(other) -> { 1448 + // Variable was lost - this is the bug we're testing for 1449 + should.equal(other, value.String("Apple (organic)")) 1450 + } 1451 + Error(_) -> should.fail() 1452 + } 1453 + } 1454 + _ -> should.fail() 1455 + } 1456 + 1457 + // Second item should also have formatted name with suffix 1458 + case list.drop(items, 1) { 1459 + [value.Object(item_fields), ..] -> { 1460 + case list.key_find(item_fields, "formattedName") { 1461 + Ok(value.String("Banana (organic)")) -> should.be_true(True) 1462 + Ok(other) -> { 1463 + should.equal(other, value.String("Banana (organic)")) 1464 + } 1465 + Error(_) -> should.fail() 1466 + } 1467 + } 1468 + _ -> should.fail() 1469 + } 1470 + } 1471 + _ -> should.fail() 1472 + } 1473 + } 1474 + Error(err) -> should.equal(err, "") 1475 + _ -> should.fail() 1476 + } 1477 + } 1478 + 1285 1479 // Test: Union type wrapped in NonNull resolves correctly 1286 1480 // This tests the fix for fields like `node: NonNull(UnionType)` in connections 1287 1481 // Previously, is_union check failed because it only matched bare UnionType