···11# Changelog
2233+## 2.1.4
44+55+### Fixed
66+77+- 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.
88+39## 2.1.3
410511### Fixed
+1-1
gleam.toml
···11name = "swell"
22-version = "2.1.3"
22+version = "2.1.4"
33description = "๐ A GraphQL implementation in Gleam"
44licences = ["Apache-2.0"]
55repository = { type = "github", user = "bigmoves", repo = "swell" }
+10-4
src/swell/executor.gleam
···590590 }
591591592592 // Execute nested selections using the resolved type
593593- // Create new context with this object's data
593593+ // Create new context with this object's data, preserving variables
594594 let object_ctx =
595595- schema.context(option.Some(field_value))
595595+ schema.context_with_variables(
596596+ option.Some(field_value),
597597+ ctx.variables,
598598+ )
596599 let selection_set =
597600 parser.SelectionSet(nested_selections)
598601 case
···667670 False -> inner_type
668671 }
669672670670- // Create context with this item's data
673673+ // Create context with this item's data, preserving variables
671674 let item_ctx =
672672- schema.context(option.Some(item))
675675+ schema.context_with_variables(
676676+ option.Some(item),
677677+ ctx.variables,
678678+ )
673679 execute_selection_set(
674680 selection_set,
675681 item_type,
+194
test/executor_test.gleam
···12821282 }
12831283}
1284128412851285+// Test: Variables are preserved in nested object selections
12861286+// This verifies that when traversing into a nested object, variables
12871287+// from the parent context are still accessible
12881288+pub fn execute_variables_preserved_in_nested_object_test() {
12891289+ // Create a nested type structure where the nested resolver needs access to variables
12901290+ let post_type =
12911291+ schema.object_type("Post", "A post", [
12921292+ schema.field("title", schema.string_type(), "Post title", fn(ctx) {
12931293+ case ctx.data {
12941294+ option.Some(value.Object(fields)) -> {
12951295+ case list.key_find(fields, "title") {
12961296+ Ok(title_val) -> Ok(title_val)
12971297+ Error(_) -> Ok(value.Null)
12981298+ }
12991299+ }
13001300+ _ -> Ok(value.Null)
13011301+ }
13021302+ }),
13031303+ // This field uses a variable from the outer context
13041304+ schema.field_with_args(
13051305+ "formattedTitle",
13061306+ schema.string_type(),
13071307+ "Formatted title",
13081308+ [schema.argument("prefix", schema.string_type(), "Prefix to add", None)],
13091309+ fn(ctx) {
13101310+ let prefix = case schema.get_argument(ctx, "prefix") {
13111311+ Some(value.String(p)) -> p
13121312+ _ -> ""
13131313+ }
13141314+ case ctx.data {
13151315+ option.Some(value.Object(fields)) -> {
13161316+ case list.key_find(fields, "title") {
13171317+ Ok(value.String(title)) ->
13181318+ Ok(value.String(prefix <> ": " <> title))
13191319+ _ -> Ok(value.Null)
13201320+ }
13211321+ }
13221322+ _ -> Ok(value.Null)
13231323+ }
13241324+ },
13251325+ ),
13261326+ ])
13271327+13281328+ let query_type =
13291329+ schema.object_type("Query", "Root query type", [
13301330+ schema.field("post", post_type, "Get a post", fn(_ctx) {
13311331+ Ok(value.Object([#("title", value.String("Hello World"))]))
13321332+ }),
13331333+ ])
13341334+13351335+ let test_schema = schema.schema(query_type, None)
13361336+13371337+ // Query using a variable in a nested field
13381338+ let query =
13391339+ "query GetPost($prefix: String!) { post { formattedTitle(prefix: $prefix) } }"
13401340+13411341+ // Create context with variables
13421342+ let variables = dict.from_list([#("prefix", value.String("Article"))])
13431343+ let ctx = schema.context_with_variables(None, variables)
13441344+13451345+ let result = executor.execute(query, test_schema, ctx)
13461346+13471347+ case result {
13481348+ Ok(executor.Response(data: value.Object(fields), errors: _)) -> {
13491349+ case list.key_find(fields, "post") {
13501350+ Ok(value.Object(post_fields)) -> {
13511351+ case list.key_find(post_fields, "formattedTitle") {
13521352+ Ok(value.String("Article: Hello World")) -> should.be_true(True)
13531353+ Ok(other) -> {
13541354+ // Variable was lost - this is the bug we're testing for
13551355+ should.equal(other, value.String("Article: Hello World"))
13561356+ }
13571357+ Error(_) -> should.fail()
13581358+ }
13591359+ }
13601360+ _ -> should.fail()
13611361+ }
13621362+ }
13631363+ Error(err) -> should.equal(err, "")
13641364+ _ -> should.fail()
13651365+ }
13661366+}
13671367+13681368+// Test: Variables are preserved in nested list item selections
13691369+// This verifies that when iterating over list items, variables
13701370+// from the parent context are still accessible to each item's resolvers
13711371+pub fn execute_variables_preserved_in_nested_list_test() {
13721372+ // Create a type structure where list item resolvers need access to variables
13731373+ let item_type =
13741374+ schema.object_type("Item", "An item", [
13751375+ schema.field("name", schema.string_type(), "Item name", fn(ctx) {
13761376+ case ctx.data {
13771377+ option.Some(value.Object(fields)) -> {
13781378+ case list.key_find(fields, "name") {
13791379+ Ok(name_val) -> Ok(name_val)
13801380+ Error(_) -> Ok(value.Null)
13811381+ }
13821382+ }
13831383+ _ -> Ok(value.Null)
13841384+ }
13851385+ }),
13861386+ // This field uses a variable from the outer context
13871387+ schema.field_with_args(
13881388+ "formattedName",
13891389+ schema.string_type(),
13901390+ "Formatted name",
13911391+ [schema.argument("suffix", schema.string_type(), "Suffix to add", None)],
13921392+ fn(ctx) {
13931393+ let suffix = case schema.get_argument(ctx, "suffix") {
13941394+ Some(value.String(s)) -> s
13951395+ _ -> ""
13961396+ }
13971397+ case ctx.data {
13981398+ option.Some(value.Object(fields)) -> {
13991399+ case list.key_find(fields, "name") {
14001400+ Ok(value.String(name)) ->
14011401+ Ok(value.String(name <> " " <> suffix))
14021402+ _ -> Ok(value.Null)
14031403+ }
14041404+ }
14051405+ _ -> Ok(value.Null)
14061406+ }
14071407+ },
14081408+ ),
14091409+ ])
14101410+14111411+ let query_type =
14121412+ schema.object_type("Query", "Root query type", [
14131413+ schema.field("items", schema.list_type(item_type), "Get items", fn(_ctx) {
14141414+ Ok(
14151415+ value.List([
14161416+ value.Object([#("name", value.String("Apple"))]),
14171417+ value.Object([#("name", value.String("Banana"))]),
14181418+ ]),
14191419+ )
14201420+ }),
14211421+ ])
14221422+14231423+ let test_schema = schema.schema(query_type, None)
14241424+14251425+ // Query using a variable in nested list item fields
14261426+ let query =
14271427+ "query GetItems($suffix: String!) { items { formattedName(suffix: $suffix) } }"
14281428+14291429+ // Create context with variables
14301430+ let variables = dict.from_list([#("suffix", value.String("(organic)"))])
14311431+ let ctx = schema.context_with_variables(None, variables)
14321432+14331433+ let result = executor.execute(query, test_schema, ctx)
14341434+14351435+ case result {
14361436+ Ok(executor.Response(data: value.Object(fields), errors: _)) -> {
14371437+ case list.key_find(fields, "items") {
14381438+ Ok(value.List(items)) -> {
14391439+ // Should have 2 items
14401440+ list.length(items) |> should.equal(2)
14411441+14421442+ // First item should have formatted name with suffix
14431443+ case list.first(items) {
14441444+ Ok(value.Object(item_fields)) -> {
14451445+ case list.key_find(item_fields, "formattedName") {
14461446+ Ok(value.String("Apple (organic)")) -> should.be_true(True)
14471447+ Ok(other) -> {
14481448+ // Variable was lost - this is the bug we're testing for
14491449+ should.equal(other, value.String("Apple (organic)"))
14501450+ }
14511451+ Error(_) -> should.fail()
14521452+ }
14531453+ }
14541454+ _ -> should.fail()
14551455+ }
14561456+14571457+ // Second item should also have formatted name with suffix
14581458+ case list.drop(items, 1) {
14591459+ [value.Object(item_fields), ..] -> {
14601460+ case list.key_find(item_fields, "formattedName") {
14611461+ Ok(value.String("Banana (organic)")) -> should.be_true(True)
14621462+ Ok(other) -> {
14631463+ should.equal(other, value.String("Banana (organic)"))
14641464+ }
14651465+ Error(_) -> should.fail()
14661466+ }
14671467+ }
14681468+ _ -> should.fail()
14691469+ }
14701470+ }
14711471+ _ -> should.fail()
14721472+ }
14731473+ }
14741474+ Error(err) -> should.equal(err, "")
14751475+ _ -> should.fail()
14761476+ }
14771477+}
14781478+12851479// Test: Union type wrapped in NonNull resolves correctly
12861480// This tests the fix for fields like `node: NonNull(UnionType)` in connections
12871481// Previously, is_union check failed because it only matched bare UnionType