Repository: apollographql/router (Rust)

Issue:
Cannot return null for non-nullable field when using @include directive
**Describe the bug**
It seems `apollo-server` is erroring out on non nullable fields if `@include(if: $variable)` is used and `variable: false`. Interestingly it works if `@include(if: false)` is passed in explicitly.

**To Reproduce**
1. Create a non nullable fields.
2. Create a query with `@include(if: $variable)` and send in `{"variable": false}`
3. The response will fail with a validation error.

Example query:
```graphql
query Example($doInclude: Boolean!) {
  ...QueriedAt @include(if: $doInclude)
}

fragment QueriedAt on Query {
  queriedAt
}
```

Variables:
```json
{
  "doInclude": false
}
```

Using same query on the subgraph works.

**Expected behavior**
Fields can be nullable if `@include` directive is present.




Discussion:
could you provide more context?
- reduced schema
- the response you are seeing (and what response you would expect)
- what the subgraph receives and returns
**1. Query/response when querying the subgraph directly:**

Query:
```graphql
query query Example($doInclude: Boolean!) {
  ...QueriedAt @include(if: $doInclude)
}

fragment QueriedAt on Query {
  queriedAt
}
```

Variables:
```json
{"doInclude": false}
```

Response from subgraph:
```json
{
	"data": {}
}
```




**2. Request response when queried through apollo server:**

Query:
```graphql
query query Example($doInclude: Boolean!) {
  ...QueriedAt @include(if: $doInclude)
}

fragment QueriedAt on Query {
  queriedAt
}
```


Query (received by subgraph):
```graphql
query Example__main__0($doInclude:Boolean!){...on Query@include(if:$doInclude){queriedAt}}
```

Variables (received by subgraph):
```json
{"doInclude": false}
```

Response:
```json
{
	"data": {}
}
```

Response from supergraph:
```json
{
	"data": null,
	"errors": [
		{
			"message": "Cannot return null for non-nullable field Query.queriedAt",
			"path": []
		}
	]
}
```


**3. Minimal schema**

```graphql
schema
  @link(url: "https://specs.apollo.dev/link/v1.0")
  @link(url: "https://specs.apollo.dev/join/v0.2", for: EXECUTION)
{
  query: Query
}

enum link__Purpose {
  """
  `SECURITY` features provide metadata necessary to securely resolve fields.
  """
  SECURITY

  """
  `EXECUTION` features provide metadata necessary for operation execution.
  """
  EXECUTION
}

directive @join__field(graph: join__Graph!, requires: join__FieldSet, provides: join__FieldSet, type: String, external: Boolean, override: String, usedOverridden: Boolean) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION

directive @join__graph(name: String!, url: String!) on ENUM_VALUE

directive @join__implements(graph: join__Graph!, interface: String!) repeatable on OBJECT | INTERFACE

directive @join__type(graph: join__Graph!, key: join__FieldSet, extension: Boolean! = false, resolvable: Boolean! = true) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA

type Query
  @join__type(graph: MAIN)
{
  queriedAt: DateTime!
}

scalar DateTime
  @join__type(graph: MAIN)

scalar join__FieldSet
scalar link__Import

enum join__Graph {
  MAIN @join__graph(name: "main", url: "http://127.0.0.1:8000/graphql/")
}
```


@Geal let me know if you need anything else.

thanks! That's an interesting one, the nullability rules should not have triggered on that field
Were you able to reproduce the issue? Just want to confirm its not something weird on our side.
not yet, I'll look at that tomorrow
Quick update -- the following query works with the same variables `{"doInclude": false}`, so the issue seems to be related to non nullable fields within fragments.

```graphql
query Example($doInclude: Boolean!) {
  queriedAt @include(if: $doInclude)
}
```

If it helps, this diff introduces a failing test case:

```diff
diff --git a/apollo-router/src/spec/query/tests.rs b/apollo-router/src/spec/query/tests.rs
index f2344e43..9626547e 100644
--- a/apollo-router/src/spec/query/tests.rs
+++ b/apollo-router/src/spec/query/tests.rs
@@ -3734,7 +3734,7 @@ fn include() {

     type Product {
         id: String!
-        name: String
+        name: String!
         review: Review
     }

@@ -4002,6 +4002,38 @@ fn include() {
         }})
         .test();

+    FormatTest::builder()
+        .schema(schema)
+        .query(
+            "query Example($shouldInclude: Boolean) {
+            get {
+                id
+            }
+            ...test @include(if: $shouldInclude)
+        }
+
+        fragment test on Query {
+            get {
+                name
+            }
+        }",
+        )
+        .response(json! {{
+            "get": {
+                "id": "a",
+            },
+        }})
+        .operation("Example")
+        .variables(json! {{
+            "shouldInclude": false
+        }})
+        .expected(json! {{
+            "get": {
+                "id": "a",
+            },
+        }})
+        .test();
+
     FormatTest::builder()
         .schema(schema)
         .query(
```

```
running 1 test
test spec::query::tests::include ... FAILED

failures:

---- spec::query::tests::include stdout ----
thread 'spec::query::tests::include' panicked at 'assertion failed: `(left == right)`
  left: `Object({"get": Null})`,
 right: `Object({"get": Object({"id": String("a")})})`', apollo-router/src/spec/query/tests.rs:4035:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    spec::query::tests::include

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 312 filtered out; finished in 0.41s
```

It passes if the fragment is on `Product` in this test. It only fails when the fragment is on the top-level Query.
@garypen do you have thoughts on this? This seems to be a core bug in router, and is currently a blocker for us to adopt it for our project as it basically rejects valid queries that we are using in production today.
we had some changes related to default values handling recently, that's probably related to that bug, I'm looking into it



Patch:
diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md
index d6aab17885..b7240fef20 100644
--- a/NEXT_CHANGELOG.md
+++ b/NEXT_CHANGELOG.md
@@ -63,6 +63,12 @@ the `Accept` header means `*/*` when it is absent.
 
 By [@bnjjj](https://github.com/bnjjj) in https://github.com/apollographql/router/pull/2078
 
+### Missing `@skip` and `@include` implementation for root operations ([Issue #2072](https://github.com/apollographql/router/issues/2072))
+
+`@skip` and `@include` were not implemented for inline fragments and fragment spreads on top level operations.
+
+By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/2096
+
 ## 🛠 Maintenance
 ## 📚 Documentation
 
diff --git a/apollo-router/src/spec/query.rs b/apollo-router/src/spec/query.rs
index d21df6a82f..986f1fed56 100644
--- a/apollo-router/src/spec/query.rs
+++ b/apollo-router/src/spec/query.rs
@@ -735,6 +735,8 @@ impl Query {
                 Selection::InlineFragment {
                     type_condition,
                     selection_set,
+                    skip,
+                    include,
                     ..
                 } => {
                     // top level objects will not provide a __typename field
@@ -744,6 +746,14 @@ impl Query {
                         return Err(InvalidValue);
                     }
 
+                    if skip.should_skip(parameters.variables).unwrap_or(false) {
+                        continue;
+                    }
+
+                    if !include.should_include(parameters.variables).unwrap_or(true) {
+                        continue;
+                    }
+
                     self.apply_selection_set(
                         selection_set,
                         parameters,
@@ -756,10 +766,33 @@ impl Query {
                 Selection::FragmentSpread {
                     name,
                     known_type: _,
-                    skip: _,
-                    include: _,
+                    skip,
+                    include,
                 } => {
+                    if skip.should_skip(parameters.variables).unwrap_or(false) {
+                        continue;
+                    }
+
+                    if !include.should_include(parameters.variables).unwrap_or(true) {
+                        continue;
+                    }
+
                     if let Some(fragment) = self.fragments.get(name) {
+                        if fragment
+                            .skip
+                            .should_skip(parameters.variables)
+                            .unwrap_or(false)
+                        {
+                            continue;
+                        }
+                        if !fragment
+                            .include
+                            .should_include(parameters.variables)
+                            .unwrap_or(true)
+                        {
+                            continue;
+                        }
+
                         let operation_type_name =
                             parameters.schema.root_operation_name(operation.kind);
                         let is_apply = {


Test patch:
diff --git a/apollo-router/src/spec/query/tests.rs b/apollo-router/src/spec/query/tests.rs
index 7bf5aa13fa..3214d7a194 100644
--- a/apollo-router/src/spec/query/tests.rs
+++ b/apollo-router/src/spec/query/tests.rs
@@ -4102,6 +4102,120 @@ fn include() {
             },
         }})
         .test();
+
+    FormatTest::builder()
+        .schema(schema)
+        .query(
+            "query Example($shouldInclude: Boolean) {
+            get {
+                name
+            }
+            ...test @include(if: $shouldInclude)
+        }
+
+        fragment test on Query {
+            get {
+                id
+            }
+        }",
+        )
+        .response(json! {{
+            "get": {
+                "name": "a",
+            },
+        }})
+        .operation("Example")
+        .variables(json! {{
+            "shouldInclude": false
+        }})
+        .expected(json! {{
+            "get": {
+               "name": "a",
+            },
+        }})
+        .test();
+
+    FormatTest::builder()
+        .schema(schema)
+        .query(
+            "query Example($shouldInclude: Boolean) {
+            get {
+                name
+            }
+            ...test @include(if: $shouldInclude)
+        }
+
+        fragment test on Query {
+            get {
+                id
+            }
+        }",
+        )
+        .response(json! {{
+            "get": {
+                "name": "a",
+            },
+        }})
+        .operation("Example")
+        .expected(json! {{
+            "get": null,
+        }})
+        .test();
+
+    FormatTest::builder()
+        .schema(schema)
+        .query(
+            "query Example($shouldInclude: Boolean) {
+            get {
+                name
+            }
+            ... @include(if: $shouldInclude) {
+                get {
+                    id
+                }
+            }
+        }",
+        )
+        .response(json! {{
+            "get": {
+                "name": "a",
+            },
+        }})
+        .operation("Example")
+        .variables(json! {{
+            "shouldInclude": false
+        }})
+        .expected(json! {{
+            "get": {
+               "name": "a",
+            },
+        }})
+        .test();
+
+    FormatTest::builder()
+        .schema(schema)
+        .query(
+            "query Example($shouldInclude: Boolean) {
+            get {
+                name
+            }
+            ... @include(if: $shouldInclude) {
+                get {
+                    id
+                }
+            }
+        }",
+        )
+        .response(json! {{
+            "get": {
+                "name": "a",
+            },
+        }})
+        .operation("Example")
+        .expected(json! {{
+            "get": null,
+        }})
+        .test();
 }
 
 #[test]