Repository: mirumee/ariadne

Issue:
Federated schemas should not require at least one query to be implemented
In a Federated environment, the Gateway instantiates the Query type by default. This means that an implementing services should _not_ be required to implement or extend a query. 

# Ideal Scenario
This is an example scenario of what is valid in Node and Java implementations. For example, it should be valid to expose a service that exposes no root queries, but only the federated query fields, like below.

Produced Query type:


**Example**: This is what the schemas would look like for two federated services:
## Product Service
product/schema.gql
```gql
extend type Query {
   products: [Product]
}

type Product {
  id: ID!
  name: String
  reviews: [ProductReview]
}

extend type ProductReview @key(fields: "id") {
  id: ID! @external
}
```
**Output**:

```
products: [Product]
_entities(representations: [_Any]): [_Entity]
_service: _Service
```

## Review Service
review/schema.gql
```gql
# Notice how we don't have to extend the Query type
type ProductReview @key(fields: "id") {
   id: ID!
   comment: String!
}
```

**Output**:
This should be valid.
```
_entities(representations: [_Any]): [_Entity]
_service: _Service
```


# Breaking Scenario
When attempting to implement the `ProductReview` service (see example above) without extending the Query type, Ariadne will fail to [generate a federated schema](https://github.com/mirumee/ariadne/blob/master/ariadne/contrib/federation/schema.py#L57). This is because `make_executable_schema` attempts to generate a federated schema by [extending a Query type](https://github.com/mirumee/ariadne/blob/master/ariadne/contrib/federation/schema.py#L24) with the assumption that a Query type has been defined, which technically it isn't. 




Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mirumee/ariadne/pull/565?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) Report
> Merging [#565](https://codecov.io/gh/mirumee/ariadne/pull/565?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) (d8555f3) into [master](https://codecov.io/gh/mirumee/ariadne/commit/d6ef0dc3204e976d71a032829bd63a969f2be6ab?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) (d6ef0dc) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/mirumee/ariadne/pull/565/graphs/tree.svg?width=650&height=150&src=pr&token=8GSRgVxHd7&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee)](https://codecov.io/gh/mirumee/ariadne/pull/565?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee)

```diff
@@           Coverage Diff           @@
##           master     #565   +/-   ##
=======================================
  Coverage   98.21%   98.22%           
=======================================
  Files         104      104           
  Lines        5332     5347   +15     
=======================================
+ Hits         5237     5252   +15     
  Misses         95       95           
```


| [Impacted Files](https://codecov.io/gh/mirumee/ariadne/pull/565?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee) | Coverage Δ | |
|---|---|---|
| [ariadne/contrib/federation/schema.py](https://codecov.io/gh/mirumee/ariadne/pull/565/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee#diff-YXJpYWRuZS9jb250cmliL2ZlZGVyYXRpb24vc2NoZW1hLnB5) | `100.00% <100.00%> (ø)` | |
| [tests/federation/test\_schema.py](https://codecov.io/gh/mirumee/ariadne/pull/565/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee#diff-dGVzdHMvZmVkZXJhdGlvbi90ZXN0X3NjaGVtYS5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mirumee/ariadne/pull/565?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mirumee/ariadne/pull/565?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee). Last update [d6ef0dc...d8555f3](https://codecov.io/gh/mirumee/ariadne/pull/565?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=mirumee).


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4198b89..14971ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
 - Added `on_connect` and `on_disconnect` options to `ariadne.asgi.GraphQL`, enabling developers to run additional initialization and cleanup for websocket connections.
 - Updated Starlette dependency to 0.15.
 - Added support for multiple keys for GraphQL federations.
-
+- Made `Query` type optional in federated schemas.
 
 ## 0.13.0 (2021-03-17)
 
diff --git a/ariadne/contrib/federation/schema.py b/ariadne/contrib/federation/schema.py
index 686cabb..9d5b302 100644
--- a/ariadne/contrib/federation/schema.py
+++ b/ariadne/contrib/federation/schema.py
@@ -2,6 +2,7 @@ from typing import Dict, List, Type, Union, cast
 
 from graphql import extend_schema, parse
 from graphql.language import DocumentNode
+from graphql.language.ast import ObjectTypeDefinitionNode
 from graphql.type import (
     GraphQLObjectType,
     GraphQLSchema,
@@ -17,13 +18,13 @@ from .utils import get_entity_types, purge_schema_directives, resolve_entities
 federation_service_type_defs = """
     scalar _Any
 
-    type _Service {
+    type _Service {{
         sdl: String
-    }
+    }}
 
-    extend type Query {
+    {type_token} Query {{
         _service: _Service!
-    }
+    }}
 
     directive @external on FIELD_DEFINITION
     directive @requires(fields: String!) on FIELD_DEFINITION
@@ -41,6 +42,17 @@ federation_entity_type_defs = """
 """
 
 
+def has_query_type(type_defs: str) -> bool:
+    ast_document = parse(type_defs)
+    for definition in ast_document.definitions:
+        if (
+            isinstance(definition, ObjectTypeDefinitionNode)
+            and definition.name.value == "Query"
+        ):
+            return True
+    return False
+
+
 def make_federated_schema(
     type_defs: Union[str, List[str]],
     *bindables: Union[SchemaBindable, List[SchemaBindable]],
@@ -52,8 +64,10 @@ def make_federated_schema(
     # Remove custom schema directives (to avoid apollo-gateway crashes).
     # NOTE: This does NOT interfere with ariadne's directives support.
     sdl = purge_schema_directives(type_defs)
+    type_token = "extend type" if has_query_type(sdl) else "type"
+    federation_service_type = federation_service_type_defs.format(type_token=type_token)
 
-    type_defs = join_type_defs([type_defs, federation_service_type_defs])
+    type_defs = join_type_defs([type_defs, federation_service_type])
     schema = make_executable_schema(
         type_defs,
         *bindables,
@@ -66,10 +80,7 @@ def make_federated_schema(
 
     # Add the federation type definitions.
     if has_entities:
-        schema = extend_federated_schema(
-            schema,
-            parse(federation_entity_type_defs),
-        )
+        schema = extend_federated_schema(schema, parse(federation_entity_type_defs))
 
         # Add _entities query.
         entity_type = schema.get_type("_Entity")


Test patch:
diff --git a/tests/federation/test_schema.py b/tests/federation/test_schema.py
index 5ff8b5d..99e0e31 100644
--- a/tests/federation/test_schema.py
+++ b/tests/federation/test_schema.py
@@ -810,3 +810,29 @@ def test_federated_schema_query_service_ignore_custom_directives():
             }
         """
     )
+
+
+def test_federated_schema_without_query_is_valid():
+    type_defs = """
+    type Product @key(fields: "upc") {
+        upc: String!
+        name: String
+        price: Int
+        weight: Int
+    }
+    """
+
+    schema = make_federated_schema(type_defs)
+    result = graphql_sync(
+        schema,
+        """
+            query GetServiceDetails {
+                _service {
+                    sdl
+                }
+            }
+        """,
+    )
+
+    assert result.errors is None
+    assert sic(result.data["_service"]["sdl"]) == sic(type_defs)