+6
CHANGELOG.md
+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
gleam.toml
+10
-4
src/swell/executor.gleam
+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
+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