@@ -840,7 +840,7 @@ Federation Library: Apo
Plugin based GraphQL schema builder that makes building graphql schemas with TypeScript easy, fast and enjoyable.
-Github: hayes/pothos
+GitHub: hayes/pothos
Type: Code first
Stars: 2.2k ⭐
@@ -897,7 +897,7 @@ Core Library: GraphQL.js
A framework for serving GraphQL from Laravel
-Github: nuwave/lighthouse
+GitHub: nuwave/lighthouse
Type: SDL first
Stars: 3.3k ⭐
@@ -940,7 +940,7 @@ Core Library: webonyx/graphql-p
|
PHP implementation of the GraphQL specification based on the reference implementation in JavaScript
-Github: webonyx/graphql-php
+GitHub: webonyx/graphql-php
Type: Code first
Stars: 4.6k ⭐
@@ -997,7 +997,7 @@ Federation Library: mirumee/ariadne
+GitHub: mirumee/ariadne
Type: SDL first
Stars: 2.1k ⭐
@@ -1040,7 +1040,7 @@ Core Library: GraphQL-c
|
GraphQL framework for Python
-Github: graphql-python/graphene
+GitHub: graphql-python/graphene
Type: Code first
Stars: 8.0k ⭐
@@ -1084,7 +1084,7 @@ Federation Library: strawberry-graphql/strawberry
+GitHub: strawberry-graphql/strawberry
Type: Code first
Stars: 3.8k ⭐
@@ -1141,7 +1141,7 @@ Core Library: GraphQL-c
|
Ruby implementation of GraphQL
-Github: rmosolgo/graphql-ruby
+GitHub: rmosolgo/graphql-ruby
Type: Code first
Stars: 5.3k ⭐
@@ -1198,7 +1198,7 @@ Federation Library: G
|
A GraphQL server library implemented in Rust
-Github: async-graphql/async-graphql
+GitHub: async-graphql/async-graphql
Type: Code first
Stars: 3.2k ⭐
@@ -1254,7 +1254,7 @@ Last Release: 2022-11-28
|
Functional GraphQL library for Scala
-Github: ghostdogpr/caliban
+GitHub: ghostdogpr/caliban
Type: Code first
Stars: 939 ⭐
@@ -1296,7 +1296,7 @@ Last Release: 2024-04-16
|
Scala GraphQL implementation
-Github: sangria-graphql/sangria
+GitHub: sangria-graphql/sangria
Type: Code first
Stars: 2.0k ⭐
@@ -1353,7 +1353,7 @@ Federation Library: GraphQLSwift/Graphiti
+GitHub: GraphQLSwift/Graphiti
Type: SDL first
Stars: 523 ⭐
@@ -1483,7 +1483,7 @@ Last Release: 2023-11-15
|
The GraphQL platform
-Github: grafbase/grafbase
+GitHub: grafbase/grafbase
Type: Code first | SDL first
Stars: 934 ⭐
@@ -1525,7 +1525,7 @@ Last Release: 2024-02-23
|
Executable GraphQL schema from multiple data sources, query anything, run anywhere.
-Github: Urigo/graphql-mesh
+GitHub: Urigo/graphql-mesh
Stars: 3.2k ⭐
@@ -1567,14 +1567,14 @@ Last Release: 2024-04-30
|
A GraphQL to Cypher query execution layer for Neo4j and JavaScript GraphQL implementations.
-Github: neo4j/graphql
+GitHub: neo4j/graphql
Type: Code first | SDL first
Stars: 485 ⭐
Last Release: 2024-04-30
Core Library: GraphQL.js
-Federation Library: Apollo Subgraph
+Federation Library: Apollo Subgraph
|
diff --git a/docs/source/reference/federation/composition-rules.mdx b/docs/source/reference/federation/composition-rules.mdx
new file mode 100644
index 000000000..9d852077a
--- /dev/null
+++ b/docs/source/reference/federation/composition-rules.mdx
@@ -0,0 +1,470 @@
+---
+title: Composition Rules
+subtitle: Learn what rules subgraph schemas must follow to successfully compose
+description: Learn what rules subgraph schemas must follow to successfully compose in a federated GraphQL architecture.
+redirectFrom:
+ - /federation/federated-types/composition/#rules-of-composition
+ - /federation/federated-schemas/composition/#rules-of-composition
+---
+
+In Federation 2, your subgraph schemas must follow all of these rules to successfully compose into a supergraph schema:
+
+- Multiple subgraphs can't define the same field on an object type, unless that field is [shareable](/graphos/schema-design/federated-schemas/sharing-types#using-shareable).
+- A shared field must have both a compatible return type and compatible argument types across each defining subgraph.
+ - For examples of compatible and incompatible differences between subgraphs, see [Differing shared fields](/graphos/schema-design/federated-schemas/sharing-types#differing-shared-fields).
+- If multiple subgraphs define the same type, each field of that type must be resolvable by every valid GraphQL operation that includes it.
+ - This rule is the most complex and the most essential to Federation 2. [Let's look at it more closely.](#unresolvable-field-example)
+
+### Unresolvable field example
+
+This example presents a field of a shared type that is not always resolvable (and therefore [breaks composition](#breaking-composition)).
+
+Consider these subgraph schemas:
+
+❌
+
+
+
+```graphql title="Subgraph A"
+type Query {
+ positionA: Position!
+}
+
+type Position @shareable {
+ x: Int!
+ y: Int!
+}
+```
+
+```graphql title="Subgraph B"
+type Query {
+ positionB: Position!
+}
+
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int!
+}
+```
+
+
+
+
+Note the following about these two subgraphs:
+
+- They both define a shared `Position` type.
+- They both define a top-level `Query` field that returns a `Position`.
+- Subgraph B's `Position` includes a `z` field, whereas Subgraph A's definition only includes shared `x` and `y` fields.
+
+Individually, these subgraph schemas are perfectly valid. However, if they're combined, they break composition. Why?
+
+The composition process attempts to merge inconsistent type definitions into a single definition for the supergraph schema. In this case, the resulting definition for `Position` exactly matches Subgraph B's definition:
+
+❌
+
+```graphql title="Hypothetical supergraph schema"
+type Query {
+ # From A
+ positionA: Position!
+ # From B
+ positionB: Position!
+}
+
+type Position {
+ # From A+B
+ x: Int!
+ y: Int!
+ # From B
+ z: Int!
+}
+```
+
+Based on this hypothetical supergraph schema, the following query should be valid:
+
+```graphql {5}
+query GetPosition {
+ positionA {
+ x
+ y
+ z # ⚠️ Can't be resolved! ⚠️
+ }
+}
+```
+
+Here's our problem. Only Subgraph A can resolve `Query.positionA`, because Subgraph B doesn't define the field. But Subgraph A doesn't define `Position.z`!
+
+If the router sent this query to Subgraph A, it would return an error. And without extra configuration, Subgraph B can't resolve a `z` value for a `Position` in Subgraph A. Therefore, `Position.z` is unresolvable for this query.
+
+Composition recognizes this potential issue, and it fails. The hypothetical supergraph schema above would never actually be generated.
+
+`Position.z` is an example of a field that is not always resolvable. Refer to the following section for solutions.
+
+### Solutions for unresolvable fields
+
+There are multiple solutions for making sure that a field of a shared type is always resolvable. Choose a solution based on your use case:
+
+#### Define the field in every subgraph that defines the type
+
+If every subgraph that defines a type could resolve every field of that type without introducing complexity, a straightforward solution is to define and resolve all fields in all of those subgraphs:
+
+✅
+
+
+
+```graphql {4} title="Subgraph A"
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int
+}
+```
+
+```graphql title="Subgraph B"
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int!
+}
+```
+
+
+
+In this case, if Subgraph A only cares about the `x` and `y` fields, its resolver for `z` can always return `null`.
+
+This is a useful solution for shared types that encapsulate simple scalar data.
+
+
+
+You can use the `@inaccessible` directive to incrementally add a value type field to multiple subgraphs without breaking composition. [Learn more.](/graphos/schema-design/federated-schemas/sharing-types#adding-new-shared-fields)
+
+
+
+#### Make the shared type an entity
+
+✅
+
+
+
+```graphql title="Subgraph A"
+type User @key(fields: "id") {
+ id: ID!
+ name: String!
+}
+```
+
+```graphql title="Subgraph B"
+type User @key(fields: "id") {
+ id: ID!
+ age: Int!
+}
+```
+
+
+
+If you make a shared type an [entity](/graphos/schema-design/federated-schemas/entities/intro), different subgraphs can define any number of different fields for that type, as long as they all define key fields for it.
+
+This is a useful solution when a type corresponds closely to an entry in a data store that one or more of your subgraphs has access to (for example, a `Users` database).
+
+## Merging types from multiple subgraphs
+
+If a particular GraphQL type is defined differently by different subgraphs, composition uses one of two strategies to merge those definitions: _union_ or _intersection_.
+
+- **Union**: The supergraph schema includes all parts of all subgraph definitions for the type.
+- **Intersection**: The supergraph schema includes only the parts of the type that are present in every subgraph that defines the type.
+
+The merging strategy that composition uses for a particular type depends on the type, as described below.
+
+### Object, union, and interface types
+
+Composition always uses the union strategy to merge object, union, and interface types.
+
+Consider the following subgraph schemas:
+
+
+
+```graphql title="Subgraph A"
+type User @key(fields: "id") {
+ id: ID!
+ name: String!
+ email: String!
+}
+
+union Media = Book | Movie
+
+interface BookDetails {
+ title: String!
+ author: String!
+}
+```
+
+```graphql title="Subgraph B"
+type User @key(fields: "id") {
+ id: ID!
+ age: Int!
+}
+
+union Media = Book | Podcast
+
+interface BookDetails {
+ title: String!
+ numPages: Int
+}
+```
+
+
+
+When these subgraph schemas are composed, the composition process merges the three corresponding types by union. This results in the following type definitions in the supergraph schema:
+
+```graphql title="Supergraph schema"
+type User {
+ id: ID!
+ age: Int!
+ name: String!
+ email: String!
+}
+
+union Media = Book | Movie | Podcast
+
+interface BookDetails {
+ title: String!
+ author: String!
+ numPages: Int
+}
+```
+
+Because composition uses the union strategy for these types, subgraphs can contribute distinct parts and guarantee that those parts will appear in the composed supergraph schema.
+
+
+
+If different subgraphs contribute different fields to an interface type, any object types that implement that interface must define all contributed fields from all subgraphs. Otherwise, composition fails.
+
+
+
+### Input types and field arguments
+
+Composition always uses the intersection strategy to merge input types and field arguments. This ensures that the router never passes an argument to a subgraph that doesn't define that argument.
+
+Consider the following subgraph schemas:
+
+
+
+```graphql title="Subgraph A"
+input UserInput {
+ name: String!
+ age: Int
+}
+
+type Library @shareable {
+ book(title: String, author: String): Book
+}
+```
+
+```graphql title="Subgraph B"
+input UserInput {
+ name: String!
+ email: String
+}
+
+type Library @shareable {
+ book(title: String, section: String): Book
+}
+```
+
+
+
+These subgraphs define different fields for the `UserInput` input type, and they define different arguments for the `Library.book` field. After composition merges using intersection, the supergraph schema definitions look like this:
+
+```graphql title="Supergraph schema"
+input UserInput {
+ name: String!
+}
+
+type Library {
+ book(title: String): Book
+}
+```
+
+As you can see, the supergraph schema includes only the input fields and arguments that both subgraphs define.
+
+
+
+If the intersection strategy would omit an input field or argument that is non-nullable, composition fails. This is because at least one subgraph requires that field or argument, and the router can't provide it if it's omitted from the supergraph schema.
+
+When defining input types and field arguments in multiple subgraphs, make sure that every non-nullable field and argument is consistent in every subgraph. For examples, see [Arguments](/graphos/schema-design/federated-schemas/sharing-types#arguments).
+
+
+
+### Enums
+
+If an enum definition differs between subgraphs, the [composition strategy](#merging-types-from-multiple-subgraphs) depends on how the enum is used:
+
+| Scenario | Strategy |
+|----------|----------|
+| The enum is used as the return type for at least one object or interface field. | [Union](#union) |
+| The enum is used as the type for at least one field argument or input type field. | [Intersection](#intersection) |
+| Both of the above are true. | All definitions must [match exactly](#exact-match) |
+
+Examples of these scenarios are provided below.
+
+#### Enum composition examples
+
+##### Union
+
+Consider these subgraph schemas:
+
+
+
+```graphql title="Subgraph A"
+enum Color {
+ RED
+ GREEN
+ BLUE
+}
+
+type Query {
+ favoriteColor: Color
+}
+```
+
+```graphql title="Subgraph B"
+enum Color {
+ RED
+ GREEN
+ YELLOW
+}
+
+type Query {
+ currentColor: Color
+}
+```
+
+
+
+In this case, the `Color` enum is used as the return type of at least one object field. Therefore, composition merges the `Color` enum by union, so that all possible subgraph return values are valid.
+
+This results in the following type definition in the supergraph schema:
+
+```graphql title="Supergraph schema"
+enum Color {
+ RED
+ GREEN
+ BLUE
+ YELLOW
+}
+```
+
+##### Intersection
+
+Consider these subgraph schemas:
+
+
+
+```graphql title="Subgraph A"
+enum Color {
+ RED
+ GREEN
+ BLUE
+}
+
+type Query {
+ products(color: Color): [Product]
+}
+```
+
+```graphql title="Subgraph B"
+enum Color {
+ RED
+ GREEN
+ YELLOW
+}
+
+type Query {
+ images(color: Color): [Image]
+}
+```
+
+
+
+In this case, the `Color` enum is used as the type of at least one field argument (or input type field). Therefore, composition merges the `Color` enum by intersection, so that subgraphs never receive a client-provided enum value that they don't support.
+
+This results in the following type definition in the supergraph schema:
+
+```graphql title="Supergraph schema"
+# BLUE and YELLOW are removed via intersection
+enum Color {
+ RED
+ GREEN
+}
+```
+
+##### Exact match
+
+Consider these subgraph schemas:
+
+❌
+
+
+
+```graphql title="Subgraph A"
+enum Color {
+ RED
+ GREEN
+ BLUE
+}
+
+type Query {
+ favoriteColor: Color
+}
+```
+
+```graphql title="Subgraph B"
+enum Color {
+ RED
+ GREEN
+ YELLOW
+}
+
+type Query {
+ images(color: Color): [Image]
+}
+```
+
+
+
+In this case, the `Color` enum is used as both:
+
+- The return type of at least one object field
+- The type of at least one field argument (or input type field)
+
+Therefore, the definition of the `Color` enum must match exactly in every subgraph that defines it. An exact match is the only scenario that enables union and intersection to produce the same result.
+
+The subgraph schemas above do not compose, because their definitions of the `Color` enum differ.
+
+## Directives
+
+Composition handles a directive differently depending on whether it's an "executable" directive or a "type system" directive.
+
+### Executable directives
+
+Executable directives are intended to be used by clients in their queries. They are applied to one or more of the [executable directive locations](http://spec.graphql.org/June2018/#ExecutableDirectiveLocation). For example, you might have a directive definition of `directive @lowercase on FIELD`, which a client could use in their query like so:
+
+```graphql
+query {
+ getSomeData {
+ someField @lowercase
+ }
+}
+```
+
+An executable directive is composed into the supergraph schema only if all of the following conditions are met:
+
+- The directive is defined in all subgraphs.
+- The directive is defined identically in all subgraphs.
+- The directive is not included in any [`@composeDirective`](/graphos/reference/federation/directives#composedirective) directives.
+
+### Type system directives
+
+Type system directives help define the structure of the schema and are not intended for use by clients. They are applied to one or more of the [type system directive locations](http://spec.graphql.org/June2018/#TypeSystemDirectiveLocation).
+
+These directives are not composed into the supergraph schema, but they can still provide information to the router via the [`@composeDirective`](/graphos/reference/federation/directives#composedirective) directive.
diff --git a/docs/source/federated-schemas/federated-directives.mdx b/docs/source/reference/federation/directives.mdx
similarity index 99%
rename from docs/source/federated-schemas/federated-directives.mdx
rename to docs/source/reference/federation/directives.mdx
index 763eabe69..ba4d4e6a5 100644
--- a/docs/source/federated-schemas/federated-directives.mdx
+++ b/docs/source/reference/federation/directives.mdx
@@ -4,8 +4,9 @@ subtitle: Reference for Apollo Federation specific GraphQL directives
description: Reference for GraphQL federation directives including @key, @extends, @sharable, @override, @requires and more.
---
-import ProgressiveOverrideEnterprise from '../../shared/progressive-override-enterprise.mdx';
-import EnterpriseDirective from '../../shared/enterprise-directive.mdx';
+import ProgressiveOverrideEnterprise from '../../../shared/progressive-override-enterprise.mdx';
+import EnterpriseDirective from '../../../shared/enterprise-directive.mdx';
+import LinkDirective from '../../../shared/link-directive.mdx';
Apollo Federation defines a collection of directives that you use in your subgraph schemas to enable certain features.
@@ -73,6 +74,8 @@ type Book @fed__shareable {
As shown, custom namespace prefixes also end in two underscores.
+## Managing schemas
+
### The `@link` directive
@@ -90,6 +93,8 @@ directive @link(
This directive links definitions from an external specification to this schema. Every Federation 2 subgraph uses the `@link` directive to import the other federation-specific directives described in this article (see the syntax in [Importing directives](#importing-directives)).
+
+
For more information on `@link`, see the [official spec](https://specs.apollo.dev/link/v1.0/).
## Managing types
diff --git a/docs/source/errors.mdx b/docs/source/reference/federation/errors.mdx
similarity index 100%
rename from docs/source/errors.mdx
rename to docs/source/reference/federation/errors.mdx
diff --git a/docs/source/hints.mdx b/docs/source/reference/federation/hints.mdx
similarity index 100%
rename from docs/source/hints.mdx
rename to docs/source/reference/federation/hints.mdx
diff --git a/docs/source/query-plans.mdx b/docs/source/reference/federation/query-plans.mdx
similarity index 100%
rename from docs/source/query-plans.mdx
rename to docs/source/reference/federation/query-plans.mdx
diff --git a/docs/source/subgraph-spec.mdx b/docs/source/reference/federation/subgraph-spec.mdx
similarity index 99%
rename from docs/source/subgraph-spec.mdx
rename to docs/source/reference/federation/subgraph-spec.mdx
index f0c590ac1..4d0d853a1 100644
--- a/docs/source/subgraph-spec.mdx
+++ b/docs/source/reference/federation/subgraph-spec.mdx
@@ -4,6 +4,8 @@ subtitle: Subgraph specification reference for server library developers
description: Learn about Apollo Federation 2 subgraph specifications, enhanced introspection, and entity field resolution for GraphQL server libraries.
---
+import LinkDirective from '../../../shared/link-directive.mdx';
+
This content is provided for developers adding federated subgraph support to a GraphQL server library, and for anyone curious about the inner workings of federation. You do not need to read this if you're building a supergraph with existing [subgraph-compatible libraries](/graphos/reference/federation/compatible-subgraphs), such as Apollo Server.
Servers that are partially or fully compatible with this specification are tracked in Apollo's [subgraph compatibility repository](https://github.com/apollographql/apollo-federation-subgraph-compatibility).
@@ -13,6 +15,7 @@ For a GraphQL service to operate as an Apollo Federation 2 subgraph, it must do
- Automatically extend its schema with all definitions listed in [Subgraph schema additions](#subgraph-schema-additions)
- Correctly resolve the `Query._service` [enhanced introspection field](#fetch-service-capabilities)
- Provide a mechanism for subgraph developers to resolve entity fields via the [`Query._entities` field](#resolving-entity-fields-with-query_entities)
+- Apply the [`@link` directive to the `schema` type](#apply-link-directive)
Each of these requirements is described in the sections below.
@@ -408,6 +411,10 @@ For this reference resolver, the developer calls a `fetchProductByUPC` function,
Your subgraph library does not need to use this reference resolver pattern. It just needs to provide and document some pattern for defining entity-fetching logic.
+## Apply @link directive
+
+
+
## Glossary of schema additions
This section describes type and field definitions that a valid subgraph service must automatically add to its schema. These definitions are all listed above in [Subgraph schema additions](#subgraph-schema-additions).
diff --git a/docs/source/building-supergraphs/subgraph-specific-fields.mdx b/docs/source/reference/federation/subgraph-specific-fields.mdx
similarity index 100%
rename from docs/source/building-supergraphs/subgraph-specific-fields.mdx
rename to docs/source/reference/federation/subgraph-specific-fields.mdx
diff --git a/docs/source/federation-versions.mdx b/docs/source/reference/federation/versions.mdx
similarity index 98%
rename from docs/source/federation-versions.mdx
rename to docs/source/reference/federation/versions.mdx
index 36537d041..615788713 100644
--- a/docs/source/federation-versions.mdx
+++ b/docs/source/reference/federation/versions.mdx
@@ -981,6 +981,12 @@ Value types
## v1.1
+
+
+Apollo Router Core and GraphOS Router v1.60 and later don't support Federation v1.x supergraphs.
+
+
+
#### Directive changes
@@ -1018,6 +1024,12 @@ directive @tag(name: String!) repeatable on
## v1.0
+
+
+Apollo Router Core and GraphOS Router v1.60 and later don't support Federation v1.x supergraphs.
+
+
+
#### Directive changes
For details on these directives as defined in Federation 1, see the [Federation 1 subgraph spec](/federation/v1/federation-spec).
diff --git a/docs/source/federation-2/backward-compatibility.mdx b/docs/source/reference/migration/backward-compatibility.mdx
similarity index 100%
rename from docs/source/federation-2/backward-compatibility.mdx
rename to docs/source/reference/migration/backward-compatibility.mdx
diff --git a/docs/source/reference/migration/from-monolith.mdx b/docs/source/reference/migration/from-monolith.mdx
new file mode 100644
index 000000000..d82b12b74
--- /dev/null
+++ b/docs/source/reference/migration/from-monolith.mdx
@@ -0,0 +1,281 @@
+---
+title: Moving a GraphQL Monolith to Apollo Federation
+subtitle: Steps for migrating from a GraphQL monolith to a federated supergraph
+description: A step-by-step guide for migrating from a GraphQL monolith to a federated supergraph with Apollo Federation.
+published: 2022-09-06
+id: TN0013
+tags: [federation, server]
+redirectFrom:
+ - /technotes/TN0013-monolith-to-federation/
+---
+
+
+
+For a complete, step-by-step tutorial, check out [Voyage II: Federating the monolith](https://www.apollographql.com/tutorials/voyage-part2).
+
+
+
+As with any monolithic service, teams can struggle to change and maintain their GraphQL API as it grows larger and receives contributions from more developers.
+
+Breaking up the monolith into smaller GraphQL APIs might be the right strategy for your team. With Apollo Federation, you can break up a monolith without sacrificing the unified API that your client applications depend on. Each _subgraph_ can be independently updated, deployed, and scaled while contributing to a single unified schema.
+
+Here are the steps we recommend to convert a monolithic GraphQL API into a federated GraphQL API.
+
+## Planning and preparation
+
+### 1. Put a router in front of your existing API
+
+Start the process by making a "federated graph of one." Your existing monolith can act as a subgraph without any schema changes.
+
+1. If you're not already publishing your schema to GraphOS Studio, create a new graph in your Studio organization. Choose "Supergraph" for your graph architecture.
+1. Publish your monolith schema to GraphOS Studio as a subgraph with the following command (modify your `--routing-url` and `--schema` path as needed):
+
+ ```sh
+ rover subgraph publish --name monolith \
+ --schema ./schema.graphql \
+ --routing-url http://monolith.prod.svc.cluster.local/graphql \
+ --convert # necessary if you're publishing to an existing variant
+ ```
+
+1. Deploy an instance of the GraphOS Router to your environment.
+
+
+ Self-hosting the GraphOS Router is limited to [GraphOS Enterprise plans](https://www.apollographql.com/pricing). Other plan types use [managed cloud routing with GraphOS](/graphos/cloud-routing). Check out the [pricing page](https://www.apollographql.com/pricing/) to learn more.
+
+
+1. Set up [header propagation](/router/configuration/header-propagation/) so that the monolith receives any necessary headers from the router.
+1. Set up internal routing rules to redirect client requests to the router instead of your monolith.
+1. Enable usage metrics reporting to GraphOS Studio.
+1. Add [subgraph checks](/graphos/platform/schema-management/checks/) to your monolith's CI pipeline.
+
+At this point, client requests go to your new router instead of the monolith, but it's serving the same schema so clients won't know the difference.
+
+Not only are you prepared to federate your schema, you now have field-level visibility into graph usage and breaking change detection.
+
+### 2. Identify entities
+
+Next, look through all the types in your schema and identify possible [entities](/graphos/schema-design/federated-schemas/entities/intro#entity-overview). Entities are types that form the foundation of your data model, and they must include fields that can uniquely identify each instance of them.
+
+Consider this schema for a travel booking site:
+
+
+
+```graphql
+type Account {
+ id: ID!
+ username: String
+ user: User
+ profile: Profile
+}
+
+type Address {
+ line1: String
+ line2: String
+ city: String
+ state: String
+ zip: String
+}
+
+type Airline {
+ id: ID!
+ name: String
+}
+
+type Airplane {
+ id: ID!
+ class: String
+}
+
+type Airport {
+ code: AirportCode!
+ airlines: [Airline]
+}
+
+type Amenity {
+ name: String
+ description: String
+ photoUrl: String
+}
+
+type Bed {
+ id: ID!
+ size: BedSize
+ room: Room
+}
+
+type CancellationPolicy {
+ text: String
+ updatedAt: DateTime
+}
+
+type Flight {
+ number: FlightNumber!
+ airplane: Airplane
+ origin: Airport
+ destination: Airport
+ scheduledTakeoff: DateTime
+}
+
+type Hotel {
+ id: ID!
+ name: String
+ address: Address
+}
+
+type Profile {
+ name: String
+ address: Address
+ phone: String
+ email: String
+}
+
+type Reservation {
+ account: Account
+ flights: [Flight]
+ hotels: [Hotel]
+}
+
+type Room {
+ number: ID!
+ floor: Int
+ hotel: Hotel
+}
+
+type Seat {
+ number: ID!
+ airplane: Airplane
+}
+
+type User {
+ id: ID!
+ account: Account
+ username: String
+ reservations: [Reservation]
+}
+
+type Query {
+ me: User
+ searchHotels(input: SearchHotelInput!): [Hotel]
+ searchFlights(input: SearchFlightInput!): [Flight]
+}
+```
+
+
+
+Types such as `User`, `Reservation`, `Flight`, and `Hotel` are uniquely identifiable, whereas `Profile`, ` CancellationPolicy`, and `Amenity` are basically groups of attributes attached to those entities.
+
+### 3. Group entities
+
+After you've identified your entities, group them by their logical domain or concern. These groups usually correspond to product boundaries, but they might also be team boundaries. This is how you'll determine how many subgraphs you'll end up with.
+
+| Accounts domain | Flights domain | Hotels domain |
+| --------------- | -------------- | ------------- |
+| `Account` | `Airplane` | `Bed` |
+| `User` | `Airline` | `Hotel` |
+| | `Flight` | `Reservation` |
+| | `Reservation` | `Room` |
+| | `Seat` | |
+
+### 4. Rank entities by ease of migration
+
+When you're deciding how to start migrating types to other subgraphs, it's helpful to consider a few things first:
+
+#### How many related types will you have to migrate at the same time?
+
+Count the number of value types associated with an entity. You'll need to copy all those types over to the new subgraph when you migrate the entity. Entities with fewer related scalars, enums, and non-entity object types will be a bit easier to migrate.
+
+You won't need to move related entities at the same time as long as you can return an [entity reference](/graphos/schema-design/federated-schemas/entities/contribute-fields#referencing-an-entity-without-contributing-fields). For example, you can move the `Room` type if you have access to the `Hotel` foreign key:
+
+```graphql title="Hotels subgraph"
+type Room @key(fields: "number") {
+ number: ID!
+ floor: Int
+ hotel: Hotel
+}
+
+type Hotel @key(fields: "id", resolvable: false) {
+ id: ID! # comes from rooms.hotel_id in the database
+}
+```
+
+It might be safer and easier to move the entire `Room` type but only a "stub" of the `Hotel` type. The query planner can fetch the rest of the `Hotel` fields from the monolith until you move that type as well.
+
+#### How complex will your query plans become during the migration?
+
+If you start by moving a type that's deeply interconnected with other types, you might introduce unnecessary complexity to your router's query plans. For example, consider this query:
+
+```graphql
+query MyFlights {
+ me {
+ reservations {
+ flights {
+ ...FlightDetails
+ }
+ }
+ }
+}
+```
+
+This query returns a list of `Reservation` objects belonging to a particular `User`, and each `Reservation` contains a list of `Flight`s. If you start by moving the `Reservation` type to another subgraph, this query results in an "A→B→A" query plan (fetching the `User`, then the `Reservation`, then the `Flight` in three serial subgraph fetches):
+
+```mermaid
+flowchart TD
+ A(monolith) --> B(subgraph)
+ B --> C(monolith)
+```
+
+A better choice at this stage might be to move the `Flight` type so that the query plan is much more efficient, fetching both the `User` and `Reservation` together before fetching the `Flight`:
+
+```mermaid
+flowchart TD
+ A(monolith) --> B(subgraph)
+```
+
+When you move a type to another subgraph, you should also move all root-level fields that return that type (such as `Query.flight(id:)`. This way, objects of that type can be returned with only a single subgraph operation in the best case. And in the general case, the query plan can fetch any additional data with fewer total subgraph operations:
+
+```mermaid
+flowchart TD
+ A(subgraph) --> B(monolith)
+```
+
+Inevitably, some query plans become more complex while you're migrating types between subgraphs. By ranking your entities and moving the lowest-impact ones first, you can minimize this increase.
+
+## Implementation
+
+### 1. Make your monolith a real subgraph
+
+Now that you have a migration plan, you can start making schema and code changes. The first change is to add the [Apollo Federation subgraph specification](/federation/subgraph-spec/) to the monolith. The steps involved depend on which [Apollo-Federation-compatible library](/graphos/reference/federation/compatible-subgraphs) you use with your monolith's language and framework.
+
+The most important functionality to add is defining your entities (by adding `@key` directives) and adding their [reference resolvers](/graphos/schema-design/federated-schemas/entities/intro#defining-an-entity).
+
+### 2. Deploy your new subgraph
+
+Start with an empty subgraph to quickly set up your deployment and continuous integration pipelines. You can use this stub subgraph schema, which won't affect the client-facing schema:
+
+```graphql
+extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@shareable", "@inaccessible"])
+
+type Query {
+ _todo: String @shareable @inaccessible
+}
+```
+
+After your new subgraph is deployed, set up [schema checks and publishes](/rover/commands/subgraphs#publishing-a-subgraph-schema-to-graphos) so that you can catch composition errors quickly and start contributing to the supergraph.
+
+### 3. Move an entity along with related types and relevant root fields
+
+1. Start by marking all the value types (enums and non-entity object types) you're going to move to the subgraph as `@shareable` in the monolith.
+2. Copy the types and fields over to the subgraph schema and port their resolvers from the monolith.
+3. Deploy the subgraph and test it by making calls to it directly. Use the [`_entities` root field](/federation/building-supergraphs/subgraphs-overview/#query_entities) to test joins between entities.
+
+When you're satisfied with the behavior and performance of your new subgraph, you can start moving all traffic to it and cleaning up the monolith.
+
+1. Use the [`@override`](/graphos/reference/federation/directives/#override) directive to mark fields in the subgraph with `@override(from: "monolith")`, telling the query planner to prefer the new subgraph over the monolith.
+2. Remove types and fields from the monolith schema.
+3. Delete unneeded resolvers from the monolith.
+4. Remove `@override` directives from the subgraph.
+5. Remove `@shareable` from types and fields in the subgraph when appropriate.
+
+### 4. Migrate additional functionality out of the monolith as desired
+
+Towards the end of the migration, you can decide whether to leave the monolith in place to handle the few entities that sit in the middle of your domain (such as `User`), or completely deconstruct the monolith into new services and decommission it. Either way, your newly federated GraphQL API is well-positioned to scale even larger in the future.
diff --git a/docs/source/migrating-from-stitching.mdx b/docs/source/reference/migration/migrating-from-stitching.mdx
similarity index 100%
rename from docs/source/migrating-from-stitching.mdx
rename to docs/source/reference/migration/migrating-from-stitching.mdx
diff --git a/docs/source/federation-2/moving-to-federation-2.mdx b/docs/source/reference/migration/moving-to-federation-2.mdx
similarity index 99%
rename from docs/source/federation-2/moving-to-federation-2.mdx
rename to docs/source/reference/migration/moving-to-federation-2.mdx
index 42f0cc7ff..0875a417b 100644
--- a/docs/source/federation-2/moving-to-federation-2.mdx
+++ b/docs/source/reference/migration/moving-to-federation-2.mdx
@@ -88,13 +88,13 @@ Open the Settings page for the variant you want to move to Federation 2, then se
Click **Edit Configuration**. The following dialog appears:
-
+
In the Federation Version dropdown, select **Federation 2** and click **Save**.
diff --git a/docs/source/schema-design/federated-schemas/composition.mdx b/docs/source/schema-design/federated-schemas/composition.mdx
new file mode 100644
index 000000000..112bd7614
--- /dev/null
+++ b/docs/source/schema-design/federated-schemas/composition.mdx
@@ -0,0 +1,104 @@
+---
+title: Schema Composition
+subtitle: Learn how GraphOS combines subgraph schemas into a supergraph schema
+description: Learn about schema composition in a federated GraphQL architecture. Explore strategies for handling conflicting types and directives.
+redirectFrom:
+ - /federation/federated-types/composition
+ - /federation/federated-schemas/composition
+---
+
+In Apollo Federation, _composition_ is the process of combining a set of subgraph schemas into a supergraph schema:
+
+```mermaid
+graph TB;
+ serviceA[Subgraph schema A];
+ serviceB[Subgraph schema B];
+ serviceC[Subgraph schema C];
+ composition[["🛠 Composition "]];
+ supergraph{{"Supergraph schema (A + B + C + routing machinery)"}};
+ serviceA & serviceB & serviceC --> composition;
+ composition -- "(Composition succeeds)" --> supergraph;
+ class composition tertiary;
+```
+
+The supergraph schema includes all of the type and field definitions from your subgraph schemas. It also includes metadata that enables your router to intelligently route incoming GraphQL operations across all of your different subgraphs.
+
+## Supported methods
+
+You can perform schema composition with any of the following methods:
+
+### Automatically with GraphOS
+
+Apollo GraphOS performs composition automatically whenever you publish a subgraph schema.
+This enables your running router to dynamically fetch an updated supergraph schema from Apollo as soon as it's available:
+
+```mermaid
+graph LR;
+ subgraph "Your infrastructure"
+ serviceA[Products subgraph];
+ serviceB[Reviews subgraph];
+ gateway([Router]);
+ end
+ subgraph "GraphOS"
+ registry{{Schema Registry}};
+ uplink{{Apollo Uplink}}
+ end
+ serviceA & serviceB -->|Publishes schema| registry;
+ registry -->|Updates config| uplink;
+ gateway -->|Polls for config changes| uplink;
+ class registry secondary;
+ class uplink secondary;
+```
+
+
+
+GraphOS also provides a [schema linter](/graphos/platform/schema-management/linting) with [composition specific rules](/graphos/platform/schema-management/linting/rules#composition-rules) to help you follow best practices. You can set up schema checks for your graph in GraphOS Studio or perform one-off linting with the Rover CLI. Check out the [schema linting](/graphos/platform/schema-management/linting) docs to learn more.
+
+
+
+### Manually with the Rover CLI
+
+The [Rover CLI](https://www.apollographql.com/docs/rover/) supports a `supergraph compose` command that you can use to compose a supergraph schema from a collection of subgraph schemas:
+
+```bash showLineNumbers=false
+rover supergraph compose --config ./supergraph-config.yaml
+```
+
+To learn how to install Rover and use this command, see the [Rover docs](/rover/).
+
+## Breaking composition
+
+Sometimes, your subgraph schemas might conflict in a way that causes composition to fail. This is called _breaking composition_.
+
+For example, take a look at these two subgraph schemas:
+
+❌
+
+
+```graphql {2} title="Subgraph A"
+type Event @shareable {
+ timestamp: String!
+}
+```
+
+```graphql {2} title="Subgraph B"
+type Event @shareable {
+ timestamp: Int!
+}
+```
+
+
+
+One subgraph defines `Event.timestamp` as a `String`, and the other defines it as an `Int`. Composition doesn't know which type to use, so it fails.
+
+
+
+For examples of valid inconsistencies in field return types, see [Differing shared field return types](/graphos/schema-design/federated-schemas/sharing-types/#return-types).
+
+
+
+Breaking composition is a helpful feature of federation! Whenever a team modifies their subgraph schema, those changes might conflict with another subgraph. But that conflict won't affect your router, because composition fails to generate a new supergraph schema. It's like a compiler error that prevents you from running invalid code. Refer to the [Composition Rules Reference](/graphos/reference/federation/composition-rules) for details.
+
+## Next steps
+
+Ready to compose your first supergraph? [Get started with GraphOS!](/graphos/get-started/guides/quickstart)
diff --git a/docs/source/schema-design/federated-schemas/entities/best-practices.mdx b/docs/source/schema-design/federated-schemas/entities/best-practices.mdx
new file mode 100644
index 000000000..e00b7a61c
--- /dev/null
+++ b/docs/source/schema-design/federated-schemas/entities/best-practices.mdx
@@ -0,0 +1,84 @@
+---
+title: Thinking in Entities
+subtitle: Best practices for designing your schema with entities
+description: Schema design best practices for entities, including when to define, reference and extend entities.
+published: 2023-01-09
+id: TN0026
+tags: [federation, schema-design]
+redirectFrom:
+ - /technotes/TN0026-thinking-in-entities/
+---
+
+
+
+If you're an enterprise customer looking for more material on this topic, try the [Enterprise best practices: Schema design](https://www.apollographql.com/tutorials/schema-design-best-practices) course on Odyssey.
+
+Not an enterprise customer? [Learn about GraphOS for Enterprise.](https://www.apollographql.com/pricing)
+
+
+
+Entities are the core building blocks of a federated graph, so the adoption of any schema design best practice must be approached with the unique role of entities in mind. While there's no requirement for subgraphs to define any entities at all with Apollo Federation, the federated schema design process often begins by thinking about what the initial entity types will be and how they will be referenced and extended throughout the graph to help preserve the separation of concerns between subgraphs both today and as the graph evolves in the future.
+
+## Define, reference, and extend entities as needed
+
+The [Apollo Federation specification](/graphos/reference/federation/subgraph-spec/) indicates that an Object or Interface type can be made into an entity by adding the `@key` directive to its definition in a subgraph schema. The `@key` directive defines a unique key for the entity and its `fields` argument will contain one or more of the type's fields. In the following example, the `Product` entity's primary key would be its `upc` field:
+
+```graphql title="Products Subgraph"
+type Product @key(fields: "upc") {
+ upc: String!
+ name: String!
+ description: String
+}
+```
+
+Setting the `upc` field as the key means that other subgraphs that want to use this entity will need to know at least that value for any product. The keys we define should be values that uniquely identify a resource. This is because we want to avoid scenarios where they are used to arbitrarily pass dynamic field data around query execution between subgraphs.
+
+After defining an entity in a schema, other subgraphs can reference that entity in their schemas. In order for the referencing subgraph's schema to be valid, it must define a stub of the entity in its schema. For example, we can reference a `Product` type defined in the products subgraph as the return type corresponding to a `product` field on a `Review` type defined in reviews subgraph:
+
+```graphql title="Reviews Subgraph"
+type Review {
+ rating: Int
+ product: Product
+}
+
+type Product @key(fields: "upc") {
+ upc: String!
+}
+```
+
+The `@key` directive indicates that the reviews subgraph will be able to identify a product by its UPC value and therefore be able to connect to a product based on its `upc` primary key field, but the reviews subgraph does not need to be aware of any other details about a given product.
+
+Referencing entities is a key feature of federation, but it's only half of the story. While an entity will be owned by a single subgraph, other subgraphs might wish to add additional fields to the entity's type to provide a more holistic representation of the entity in the graph. Doing so is a simple as adding the additional field to the extended type in a non-originating subgraph. For example, a reviews subgraph's schema might add a `reviews` field to the extended `Product` type that was originally defined in the products subgraph:
+
+```graphql title="Reviews Subgraph"
+type Review {
+ rating: Int
+ product: Product
+}
+
+type Product @key(fields: "upc") {
+ upc: String!
+ reviews: [Review]
+}
+```
+
+When extending entities, it's important to keep in mind that _the entity's originating subgraph will not be aware of the added fields_. Additionally, each field in an entity must only be defined once or the gateway will encounter schema composition errors.
+
+## Work from the entities outward
+
+When migrating from a client-only or monolithic GraphQL pattern, that work begins by identifying what entities will be exposed in the first subgraph extracted from the larger schema. When migrating from an architecture consisting of BFF-based GraphQL APIs or any other architecture of multiple overlapping graphs, the work of identifying entities (and determining new subgraph boundaries, in general) might be a bit more complex and involve some degree of negotiation with respect to type ownership, as well as a migration process to help account for any breaking changes that might result for clients.
+
+Whatever your architectural starting point, Apollo Federation was designed to allow the work of identifying entities and defining subgraph boundaries to be done in an incremental, non-disruptive fashion. Beginning to identify these entities is also the essential prerequisite for adopting the other schema design best practices that will follow.
+
+## `@defer` and entities
+
+Entities aren't just useful for connecting data across subgraphs. You can also use entities to enable the new [`@defer` directive for client-controlled prioritization of response data](https://github.com/graphql/graphql-wg/blob/main/rfcs/DeferStream.md). The GraphOS Router can [defer resolution of fields in entities](/graphos/routing/operations/defer) (and root fields) and handle sending data back to the client in prioritized chunks. By defining types as entities within your graph, clients can improve perceived user experience by adding a directive to their operations.
+
+## Next steps
+
+If you haven't already, follow the [Write Federated Schemas](/graphos/schema-design/federated-schemas/entities/intro) guide to learn how to define your first entity.
+
+To learn about more advanced ways of using entities, check out these guides:
+
+- [Define Advanced Keys](/graphos/schema-design/federated-schemas/entities/define-keys), including compound and nested key fields
+- [Contribute and Reference Entity Fields](/graphos/schema-design/federated-schemas/entities/contribute-fields), including computed fields
diff --git a/docs/source/entities/contribute-fields.mdx b/docs/source/schema-design/federated-schemas/entities/contribute-fields.mdx
similarity index 100%
rename from docs/source/entities/contribute-fields.mdx
rename to docs/source/schema-design/federated-schemas/entities/contribute-fields.mdx
diff --git a/docs/source/entities/define-advanced-keys.mdx b/docs/source/schema-design/federated-schemas/entities/define-advanced-keys.mdx
similarity index 100%
rename from docs/source/entities/define-advanced-keys.mdx
rename to docs/source/schema-design/federated-schemas/entities/define-advanced-keys.mdx
diff --git a/docs/source/schema-design/federated-schemas/entities/enforce-ownership.mdx b/docs/source/schema-design/federated-schemas/entities/enforce-ownership.mdx
new file mode 100644
index 000000000..bcbb897df
--- /dev/null
+++ b/docs/source/schema-design/federated-schemas/entities/enforce-ownership.mdx
@@ -0,0 +1,279 @@
+---
+title: Enforcing Entity Ownership in Apollo Federation
+subtitle: Designating entity ownership in Apollo Federation 2
+description: Learn how to designate entity ownership and make "entity extension" a first-class concept in your Apollo Federation 2 supergraph.
+published: 2023-02-16
+id: TN0036
+tags: [federation]
+redirectFrom:
+ - /technotes/TN0036-owner-pattern/
+---
+
+In Federation 2, the notion of "extending" an entity type is strictly conceptual. All definitions of a type in different subgraphs are merged according to the "shareability" of fields. In the following example, neither subgraph really owns or extends the `Product` entity. Instead, they both contribute fields to it.
+
+
+
+```graphql title="subgraph-a.graphql"
+type Product @key(fields: "id") {
+ id: ID!
+ name: String
+}
+```
+
+```graphql title="subgraph-b.graphql"
+type Product @key(fields: "id") {
+ id: ID!
+ reviews: [Review]
+}
+```
+
+
+
+Federation 1 required that one of these definitions used the `extend` keyword or `@extends` directive. Federation 2 drops this requirement to improve the flexibility of composition and reduce the possibility of hard composition errors.
+
+However, in some situations you still might want to designate an "owner" of an entity and make "entity extension" a first-class concept in your supergraph.
+
+One example is the ability assert which subgraph is responsible for documenting an entity. If two subgraphs add different descriptions to a type, composition selects one of those descriptions and emits a hint informing you of the inconsistency:
+
+```
+HINT: [INCONSISTENT_DESCRIPTION]: Element "Product" has inconsistent
+descriptions across subgraphs. The supergraph will use description
+(from subgraph "one"):
+ """
+ The Product type.
+ """
+In subgraph "two", the description is:
+ """
+ This is my description of the Product type.
+ """
+```
+
+
+
+When a description is inconsistent across subgraphs, composition selects the description from the first subgraph alphabetically by name.
+
+
+
+A mechanism for deciding the "owner" of the type allows tools such as linters to catch these inconsistencies early in the development process.
+
+## Creating an `@owner` directive
+
+You can add an `@owner` directive to your supergraph using the [`@composeDirective`](/graphos/reference/federation/directives#composedirective) functionality introduced in Federation 2.2.
+
+```graphql title="subgraph-a.graphql"
+extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@composeDirective"])
+ @link(url: "https://graphql.mycompany.dev/owner/v1.0", import: ["@owner"])
+ @composeDirective(name: "@owner")
+
+directive @owner(team: String!) on OBJECT
+
+type Product @key(fields: "id") @owner(team: "subgraph-a") {
+ id: ID!
+ name: String
+}
+```
+
+The `@owner` directive now appears in the supergraph. Because we did not define the directive as `repeatable`, subgraphs cannot define it with different arguments.
+
+
+
+```graphql title="supergraph.graphql" {69}
+schema
+ @link(url: "https://specs.apollo.dev/link/v1.0")
+ @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
+ @link(url: "https://graphql.mycompany.dev/owner/v1.0", import: ["@owner"]) {
+ query: Query
+}
+
+directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
+
+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
+ isInterfaceObject: Boolean! = false
+) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
+
+directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
+
+directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
+
+directive @owner(team: String!) on OBJECT
+
+scalar join__FieldSet
+
+enum join__Graph {
+ ONE @join__graph(name: "one", url: "http://localhost:4001")
+}
+
+scalar link__Import
+
+enum link__Purpose {
+ """
+ `SECURITY` features provide metadata necessary to securely resolve fields.
+ """
+ SECURITY
+
+ """
+ `EXECUTION` features provide metadata necessary for operation execution.
+ """
+ EXECUTION
+}
+
+type Product @join__type(graph: ONE, key: "id") @owner(team: "subgraph-a") {
+ id: ID!
+ name: String
+}
+
+type Query @join__type(graph: ONE) {
+ products: [Product]
+}
+```
+
+
+
+## Writing a lint rule using the `@owner` directive
+
+Here's an example of a [`@graphql-eslint`](https://the-guild.dev/graphql/eslint/docs) rule for subgraph schemas that uses the `@owner` directive to determine if a description is required:
+
+
+
+```js
+const { getDirective } = require("@graphql-tools/utils");
+const { buildSchema } = require("graphql");
+
+module.exports = {
+ rules: {
+ /** @type {import("@graphql-eslint/eslint-plugin").GraphQLESLintRule} */
+ "subgraph-owned-type-has-description": {
+ create(context) {
+ const schema = buildSchema(context.getSourceCode().text, {
+ assumeValidSDL: true, // subgraph schemas may not be valid on their own
+ });
+
+ return {
+ /**
+ * For each object type defintion, look for an `@owner` directive.
+ * If it exist, require a description.
+ * If it doesn't, disallow a description.
+ */
+ ObjectTypeDefinition(node) {
+ const type = schema.getType(node.name.value);
+ const owner = getDirective(schema, type, "owner");
+
+ if (owner && !node.description) {
+ context.report({
+ node,
+ message: "Description is required on owned types",
+ });
+ }
+
+ if (!owner && node.description) {
+ context.report({
+ node,
+ message: "Description not allowed on unowned types",
+ });
+ }
+ },
+ };
+ },
+ },
+ },
+};
+```
+
+
+
+## Using `@owner` to determine required approvers
+
+Another use case for the `@owner` directive is to determine required reviewers when a schema change affects a type owned by another team.
+
+The exact process depends on your source control and continuous integration systems. The following example steps assume you're using GitHub for both.
+
+1. Add a `pull_request` workflow:
+
+ ```yaml title=".github/workflows/add-reviewers.yaml"
+ name: Add required reviewers for owned GraphQL types
+ on: [pull_request]
+ ```
+
+2. Determine the affected types in the schema change:
+
+ ```js
+ import { diff } from "@graphql-inspector/core";
+ import { buildSchema } from "graphql";
+
+ const differences = diff(
+ buildSchema(schemaFromBaseRef, { assumeValidSDL: false }),
+ buildSchema(schemaFromCurrentRef, { assumeValidSDL: false })
+ );
+
+ /* Derive a list of affected types from the result:
+ [
+ {
+ "criticality": {
+ "level": "NON_BREAKING"
+ },
+ "type": "FIELD_ADDED",
+ "message": "Field 'newField' was added to object type 'Product'",
+ "path": "Product.newField"
+ }
+ ]
+ */
+ ```
+
+3. Obtain the supergraph schema.
+
+ You can use [`rover supergraph fetch`](https://www.apollographql.com/docs/rover/commands/supergraphs#supergraph-fetch) or retrieve it using the [Apollo Platform API](https://studio.apollographql.com/public/apollo-platform/explorer?explorerURLState=N4IgJg9gxgrgtgUwHYBcQC4QEcYIE4CeABAOIIoDKMADvgOZ4CG1AFgBQAkeCAZukQEkAIgEIAlEWAAdJESIA3RngCWjVG258iXXhOmy5RAPpGUBWkkaIZhogDoHRCLJJNWANSWrUkm7aIANowoCADOKACC1NR4EPIIYAAyjDBIUCy%2BBv5EAEYwygFgmdm23KEwASjFJbYmZhZWCH41cg52TrIAQvmFVFBQYaHVLYZQENwU6QhwjMMjo%2BMIQtDwyCjN8wC%2BGzXbWSV7uztEh4ane5sgm0A&variant=main).
+
+4. Extract the owners for the affected types:
+
+ ```js
+ import { getDirective } from "@graphql-tools/utils";
+
+ const supergraphSchema = buildSchema(supergraphSdl);
+ const affectedTeams = [];
+
+ for (const typeName of affectedTypes) {
+ const type = supergraphSchema.getType(typeName);
+
+ const owner = getDirective(schema, type, "owner")?.[0];
+
+ if (owner) {
+ affectedTeams.push(owner.team);
+ }
+ }
+ ```
+
+5. Add the team as reviewers on the pull request:
+
+ ```js
+ import { Octokit } from "@octokit/action";
+
+ const octokit = new Octokit();
+
+ const [owner, repo] = process.env.GITHUB_REPOSITORY.split("/");
+
+ await octokit.pulls.requestReviewers({
+ owner,
+ repo,
+ pull_number: pullNumber, // ${{ github.event.number }}
+ team_reviewers: affectedTeams,
+ });
+ ```
diff --git a/docs/source/entities/interfaces.mdx b/docs/source/schema-design/federated-schemas/entities/interfaces.mdx
similarity index 100%
rename from docs/source/entities/interfaces.mdx
rename to docs/source/schema-design/federated-schemas/entities/interfaces.mdx
diff --git a/docs/source/schema-design/federated-schemas/entities/intro.mdx b/docs/source/schema-design/federated-schemas/entities/intro.mdx
new file mode 100644
index 000000000..2ee4c1de1
--- /dev/null
+++ b/docs/source/schema-design/federated-schemas/entities/intro.mdx
@@ -0,0 +1,181 @@
+---
+title: Introduction to Entities
+subtitle: Fundamental keyed object type of Apollo Federation
+description: Learn to define, contribute to, and reference entities, the fundamental object types of Apollo Federation that resolve their fields across one or more subgraphs.
+redirectFrom:
+ - /federation/entities
+---
+
+
+
+Before getting started with entities, you may want to check out the [Introduction to Apollo Federation](/graphos/schema-design/federated-schemas/federation) for a conceptual overview.
+
+
+
+## Entity overview
+
+In Apollo Federation, federated data objects are represented as _entities_. Entities are objects that can be fetched with one or more unique key fields. Like a row in a database table, an entity contains fields of various types, and it can be uniquely identified by a key field or set of fields.
+
+Entities are defined in subgraph schemas. Each subgraph can contribute different fields to an entity it defines and is responsible for _resolving_ it—returning only the fields that it contributes. This enables subgraphs to adhere to the separation of concerns principle.
+
+An _entity type_ is an object type that has been [defined as an entity](#defining-an-entity). Because an entity is keyed, an entity type's definition must have a `@key` directive. For example, this `Product` entity's fields are defined and resolved across two subgraphs:
+
+
+
+```graphql title="Products subgraph"
+type Product @key(fields: "upc") {
+ upc: ID!
+ name: String!
+ price: Int
+}
+```
+
+```graphql title="Reviews subgraph"
+type Product @key(fields: "productUpc") {
+ productUpc: ID!
+ rating: Int!
+}
+```
+
+
+
+
+
+Only object types can be entities.
+
+
+
+The rest of this guide goes over how to define entities in your subgraph schemas and code.
+
+## Defining an entity
+
+To define an entity within a particular subgraph, you do the following:
+
+1. Apply the [`@key` directive](#1-define-a-key) to an object type.
+2. Define the object type's [reference resolver](#2-define-a-reference-resolver).
+
+
+
+With Apollo Connectors, you add [connector directives](/graphos/schema-design/connectors/directives) instead of writing reference resolver code.
+
+You can [set `entity: true`](/graphos/schema-design/connectors/directives#rules-for-entity-true) for the `@connect` directive to provide an entity resolver for its fields.
+
+
+
+
+
+
+
+
+
+### 1. Define a `@key`
+
+In a subgraph schema, you can designate any object type as an entity by adding the `@key` directive to its definition, like so:
+
+```graphql {1} title="Products subgraph"
+type Product @key(fields: "upc") {
+ upc: ID!
+ name: String!
+ price: Int
+}
+```
+
+The `@key` directive defines an entity's _unique key_, which consists of one or more of the type's `fields`.
+In the previous example, the `Product` entity's unique key is its `upc` field.
+Every instance of an entity must be uniquely identifiable by its `@key` field(s).
+Key fields' uniqueness enable your router to associate fields from different subgraphs with the same entity instance.
+
+In most cases, the `@key` field(s) for the same entity will be the same across subgraphs.
+For example, if one subgraph uses `upc` as the `@key` field for the `Product` entity, other subgraphs should likely do the same.
+However, this [isn't strictly required](/graphos/schema-design/federated-schemas/entities/define-keys/#differing-keys-across-subgraphs).
+
+If coming from a database context, it can be helpful to think of a `@key` as an entity's [primary key](https://en.wikipedia.org/wiki/Primary_key).
+This term isn't completely accurate for entities since a single entity can have [multiple `@key`s](/graphos/schema-design/federated-schemas/entities/define-keys/#multiple-keys). The field(s) you select for an entity's `@key` must, however, uniquely identify the entity.
+In that way, `@key`s are similar to [candidate keys](https://en.wikipedia.org/wiki/Candidate_key).
+
+
+
+```graphql title="Products subgraph"
+type Product @key(fields: "upc") {
+ upc: ID!
+ name: String!
+ price: Int
+}
+```
+
+```graphql title="Reviews subgraph"
+type Product @key(fields: "productUpc") {
+ productUpc: ID!
+ inStock: Boolean!
+}
+```
+
+
+
+For more information on advanced key options, like defining [multiple keys](/graphos/schema-design/federated-schemas/entities/define-keys/#multiple-keys) or [compound keys](/graphos/schema-design/federated-schemas/entities/define-keys/#compound-keys), see the guide on [Defining keys](/graphos/schema-design/federated-schemas/entities/define-keys).
+
+#### Key field limitations
+
+An entity's `@key` cannot include:
+
+- Fields that return a union or interface
+- Fields that take arguments
+
+Though not strictly required, it's best to use non-nullable fields for keys. If you use fields that return `null` values, GraphOS may encounter issues resolving the entity.
+
+### 2. Define a reference resolver
+
+The `@key` directive effectively tells the router, "This subgraph can resolve an instance of this entity if you provide its unique key." For this to be true, the subgraph must have a _reference resolver_ for the entity.
+
+
+
+This section describes how to create reference resolvers in Apollo Server.
+
+- If you're using Apollo Connectors, the [connectors directives](/graphos/schema-design/connectors/directives) declare which REST endpoints to use to resolve entity fields, so you don't write any reference resolvers.
+
+- If you're using another [subgraph-compatible library](/graphos/reference/federation/compatible-subgraphs), see its documentation for creating reference resolvers or the equivalent functionality.
+
+
+
+For the `Product` entity defined [above](#1-define-a-key), the reference resolver might look like this:
+
+```js {4-6} title="resolvers.js"
+// Products subgraph
+const resolvers = {
+ Product: {
+ __resolveReference(productRepresentation) {
+ return fetchProductByID(productRepresentation.upc);
+ }
+ },
+ // ...other resolvers...
+}
+```
+
+Let's break this example down:
+
+- You declare an entity's reference resolver in your resolver map, as a member of the entity's corresponding object.
+- A reference resolver's name is always `__resolveReference`.
+- A reference resolver's first parameter is a representation of the entity being resolved.
+ - An entity representation is an object that contains the entity's `@key` fields, plus its `__typename` field. These values are automatically provided to your subgraph by your router.
+- A reference resolver is responsible for returning all of the entity fields that this subgraph defines.
+ - In this example, the hypothetical `fetchProductByID` function fetches a particular `Product`'s field data based on its `upc`.
+
+
+
+A particular reference resolver might be called many times to resolve a single query. It's crucial that reference resolvers account for "N+1" issues (typically via [data loaders](https://github.com/graphql/dataloader)). For details, see [Handling the N+1 problem](/graphos/schema-design/guides/handling-n-plus-one).
+
+
+
+Every subgraph that contributes at least one unique field to an entity must define a reference resolver for that entity.
+
+To learn more about `__resolveReference` in Apollo Server, see the [API docs](/apollo-server/using-federation/api/apollo-subgraph/#__resolvereference).
+
+## Next steps
+
+Once you [add your subgraphs](/graphos/platform/graph-management/add-subgraphs) to your supergraph, GraphOS composes them into a supergraph schema.
+Clients querying your supergraph can interact with entity fields without needing to know the details of which subgraphs contribute which fields.
+
+To learn about more advanced ways of using entities, check out these guides:
+
+- [Define Advanced Keys](/graphos/schema-design/federated-schemas/entities/define-keys), including compound and nested key fields
+- [Contribute and Reference Entity Fields](/graphos/schema-design/federated-schemas/entities/contribute-fields), including computed fields
diff --git a/docs/source/entities/migrate-fields.mdx b/docs/source/schema-design/federated-schemas/entities/migrate-fields.mdx
similarity index 98%
rename from docs/source/entities/migrate-fields.mdx
rename to docs/source/schema-design/federated-schemas/entities/migrate-fields.mdx
index 849be1547..781521084 100644
--- a/docs/source/entities/migrate-fields.mdx
+++ b/docs/source/schema-design/federated-schemas/entities/migrate-fields.mdx
@@ -4,7 +4,7 @@ subtitle: Transfer entity fields from one subgraph to another
description: Learn how to safely move parts of one subgraph to another subgraph in a federated GraphQL architecture using the @override directive.
---
-import ProgressiveOverrideEnterprise from '../../shared/progressive-override-enterprise.mdx';
+import ProgressiveOverrideEnterprise from '../../../../shared/progressive-override-enterprise.mdx';
As your supergraph grows, you might want to move parts of one subgraph to another subgraph.
For example, suppose your Payments subgraph defines a `Bill` entity:
@@ -286,10 +286,10 @@ A few strategies to mitigate this concern:
### Customizing progressive `@override` behavior with a feature flag service
-Out of the box, the router supports the `percent(x)` syntax for resolving labels based on a given percentage. Unfortunately, updating this number requires a subgraph publish and router redeploy.
+Out of the box, the router supports the `percent(x)` syntax for resolving labels based on a given percentage. Updating this number requires publishing the subgraph.
To avoid this, you can use a feature flag service to dynamically update the label value.
-The router provides an interface for coprocessors and rhai scripts to resolve arbitrary labels.
+The router provides an interface for coprocessors and Rhai scripts to resolve arbitrary labels.
This lets you dial up or disable a label's rollout status without requiring a subgraph publish.
A coprocessor or Rhai script that implements this should take the following steps:
diff --git a/docs/source/entities/resolve-another-subgraphs-fields.mdx b/docs/source/schema-design/federated-schemas/entities/resolve-another-subgraphs-fields.mdx
similarity index 100%
rename from docs/source/entities/resolve-another-subgraphs-fields.mdx
rename to docs/source/schema-design/federated-schemas/entities/resolve-another-subgraphs-fields.mdx
diff --git a/docs/source/entities/use-contexts.mdx b/docs/source/schema-design/federated-schemas/entities/use-contexts.mdx
similarity index 100%
rename from docs/source/entities/use-contexts.mdx
rename to docs/source/schema-design/federated-schemas/entities/use-contexts.mdx
diff --git a/docs/source/schema-design/federated-schemas/federation.mdx b/docs/source/schema-design/federated-schemas/federation.mdx
new file mode 100644
index 000000000..2593b615f
--- /dev/null
+++ b/docs/source/schema-design/federated-schemas/federation.mdx
@@ -0,0 +1,93 @@
+---
+title: Introduction to Apollo Federation
+subtitle: Learn how federation orchestrates your APIs into a unified supergraph
+description: Learn how Apollo Federation helps you declaratively orchestrate your APIs and services into a unified, federated GraphQL API using a microservices architecture.
+redirectFrom:
+ - /federation
+---
+
+Apollo Federation enables you to declaratively combine multiple APIs into a single federated GraphQL API. Federation serves as an API orchestration layer, where clients make a single GraphQL request and it coordinates multiple API calls to return a unified response.
+
+Clients makes requests to the federated GraphQL API's single entry point called the _router_. The router intelligently orchestrates and distributes the request across your APIs and returns a unified response. For a client, the request and response cycle of querying the router looks the same as querying any GraphQL API.
+
+
+
+
+Your federated GraphQL API, or _graph_, can be made of GraphQL APIs, REST APIs, and other data sources.
+
+
+
+
+
+
+
+## Benefits of federation
+
+### Microservices architecture
+
+Apollo Federation lets API teams operate in a [microservices architecture](https://www.atlassian.com/microservices/microservices-architecture/microservices-vs-monolith) while exposing a unified GraphQL API to clients. Understanding these concepts can help you get the most out of federation.
+
+- Learn more about the [considerations and benefits of GraphQL](https://graphql.com/learn/what-is-graphql/).
+- Learn more about the [considerations and benefits of microservices architecture](https://aws.amazon.com/compare/the-difference-between-monolithic-and-microservices-architecture/).
+
+### Preserve client simplicity and performance
+
+A client may need to make multiple requests when interacting with multiple non-federated GraphQL APIs. This can happen when an organization adopting GraphQL has multiple teams developing APIs independently. Each team sets up a GraphQL API that provides the data used by that team. For example, a travel app may have separate GraphQL APIs for users, flights, and hotels:
+
+
+
+
+
+With a single federated graph, you preserve a powerful advantage of GraphQL over traditional REST APIs: the ability to fetch all the data you need in a single request.
+
+
+
+
+
+The router intelligently calls all the APIs it needs to complete requests rather than simply forwarding them.
+For performance and security reasons, clients should only query the router, and only the router should query the constituent APIs.
+No client-side configuration is required.
+
+### Design schemas at scale
+
+Some alternative approaches to combining GraphQL APIs impose limits on your schema, like adding namespaces or representing relationships with IDs instead of types. With these approaches, your individual GraphQL API schemas may look unchanged—but the resulting federated schema that clients interact with is more complex. Subsequently, it requires you to make frontend as well as backend changes.
+
+With Apollo Federation, clients can interact with the federated schema as if it were a monolith. Consumers of your API shouldn't know or care that it's implemented as microservices.
+
+### Maintain a single API
+
+With federation, every team contributes directly to the overall federated GraphQL schema. Each team can work independently without needing to maintain multiple API layers. This frees your platform team to focus on the quality of your API rather than keeping it up to date.
+
+### Connect APIs declaratively
+
+Apollo Federation is the foundation of Apollo Connectors, which allows you to integrate REST APIs into your federated graph by defining them declaratively in your GraphQL schema.
+
+## Next steps
+
+Before continuing, it's helpful to know some terminology:
+
+- When combining multiple GraphQL APIs, the single, federated graph is called a _supergraph_.
+- In a supergraph, the constituent APIs are called _subgraphs_.
+
+
+
+
+Different subgraphs in the same supergraph can use different server implementations and even different programming languages as long as they are [federation-compatible](../../reference/federation/compatible-subgraphs).
+
+Ready to get started?
+
+- Connect REST APIs to your graph using Apollo Connectors with the [REST quickstart](/graphos/get-started/guides/rest-quickstart).
+
+- Create and run a federated graph with the [Quickstart](/graphos/get-started/guides/quickstart).
+
+### Additional resources
+
+If you're new to federated architecture, this [overview article](https://graphql.com/learn/federated-architecture/) can introduce the concepts.
+
+
+
+- To integrate existing APIs into a federated graph, this [interactive course](https://www.apollographql.com/tutorials/connectors-intro-rest) teaches you how to bring an existing REST API into a GraphQL API using Apollo Connectors.
+
+- To federate a GraphQL backend, this [interactive course](https://www.apollographql.com/tutorials/voyage-part1) teaches you how to build an example supergraph using Apollo Federation.
+
+
diff --git a/docs/source/schema-design/federated-schemas/ide-support.mdx b/docs/source/schema-design/federated-schemas/ide-support.mdx
new file mode 100644
index 000000000..777a50f36
--- /dev/null
+++ b/docs/source/schema-design/federated-schemas/ide-support.mdx
@@ -0,0 +1,35 @@
+---
+title: IDE Support for Apollo Federation
+subtitle: Streamline federated GraphQL development
+description: Enhance your development workflow with federation-specific features in VSCode and IntelliJ-based IDEs.
+redirectFrom:
+ - /graphos/reference/federation/jetbrains-ide-support
+ - /graphos/reference/federation/ide-support
+---
+
+Many IDEs provide features to streamline federated GraphQL development, such as federation-aware syntax highlighting, inline performance information, and autocomplete for fields, types, and federation directives. Learn how to enable federation-specific features in tools like VS Code and JetBrains IDEs.
+
+## Visual Studio Code
+
+Apollo's [VS Code Extension](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) provides an all-in-one tooling experience for developing apps with Apollo. See the [dedicated documentation page](/vs-code-extension) for configuration details.
+
+## JetBrains
+
+Apollo's [JetBrains Plugin](https://plugins.jetbrains.com/plugin/20645-apollo-graphql) provides federation-specific development features, such as autocomplete for federation directives. This plugin supports all IntelliJ-based IDEs, including:
+
+- IntelliJ IDEA
+- PyCharm
+- PhpStorm
+- WebStorm
+- CLion
+- RubyMine
+- Rider
+- GoLand
+
+You must enable the Rover integration after installing the plugin. Otherwise, your IDE might display unexpected errors while you're working with a subgraph schema. See this [the dedicated documentation page](/graphos/schema-design/connectors/jetbrains#configuration) for configuration details.
+
+## Additional resources
+
+If your graph uses the Apollo Router Core or GraphOS Router, make sure to enable [router configuration awareness](/graphos/reference/router/configuration#configuration-awareness-in-your-text-editor) in your editor.
+
+If you're developing with Apollo Connectors, refer to the connectors-specific [VS Code Extension](/graphos/schema-design/connectors/vs-code), [JetBrains IDEs](/graphos/schema-design/connectors/jetbrains), and [Vim/NeoVim](/graphos/schema-design/connectors/vim) pages.
diff --git a/docs/source/schema-design/federated-schemas/schema-types.mdx b/docs/source/schema-design/federated-schemas/schema-types.mdx
new file mode 100644
index 000000000..6ccb51db1
--- /dev/null
+++ b/docs/source/schema-design/federated-schemas/schema-types.mdx
@@ -0,0 +1,235 @@
+---
+title: Federated Schemas
+subtitle: Learn about the different types of GraphQL schemas
+description: Learn about subgraph, supergraph, and API schemas in federated GraphQL architectures.
+redirectFrom:
+ - /federation/federated-schemas/
+ - /federation/federated-types/
+---
+
+A federated supergraph uses multiple "types" of GraphQL schemas:
+
+```mermaid
+graph TB;
+ serviceA[Subgraph schema A];
+ serviceB[Subgraph schema B];
+ serviceC[Subgraph schema C];
+ composition[["🛠 Composition "]];
+ supergraph{{"Supergraph schema (A + B + C + routing machinery)"}};
+ api(["API schema (A + B + C)"]);
+ serviceA & serviceB & serviceC --> composition;
+ composition -- "(Composition succeeds)" --> supergraph;
+ supergraph -- "(Remove routing machinery)" --> api;
+ class composition tertiary;
+```
+
+* **Subgraph schemas.** Each subgraph has a distinct schema that indicates which types and fields of your composed supergraph it can resolve.
+ * These are the only schemas that your teams define manually.
+* **Supergraph schema.** This schema combines all of the types and fields from your subgraph schemas, plus some federation-specific information that tells your router which subgraphs can resolve which fields.
+ * This schema is the result of performing [composition](/graphos/schema-design/federated-schemas/composition/) on your collection of subgraph schemas.
+* **API schema.** This schema is similar to the supergraph schema, but it omits federation-specific types, fields, and directives that are considered "machinery" and are not part of your public API.
+ * This is the schema that your router exposes to clients, which don't need to know internal implementation details about your graph.
+
+Let's look at an example!
+
+## Subgraph schemas
+
+Below are example schemas for three subgraphs in an e-commerce company's supergraph. Each subgraph is implemented as a separate GraphQL API:
+
+
+
+```graphql title="Users"
+type Query {
+ me: User
+}
+
+type User @key(fields: "id") {
+ id: ID!
+ username: String! @shareable
+}
+
+# (Subgraph schemas include
+# this to opt in to
+# Federation 2 features.)
+extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.3",
+ import: ["@key", "@shareable"])
+```
+
+```graphql title="Products"
+type Query {
+ topProducts(first: Int = 5): [Product]
+}
+
+type Product @key(fields: "upc") {
+ upc: String!
+ name: String!
+ price: Int
+}
+
+extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.3",
+ import: ["@key", "@shareable"])
+```
+
+
+
+
+
+```graphql title="Reviews"
+type Review {
+ body: String
+ author: User @provides(fields: "username")
+ product: Product
+}
+
+type User @key(fields: "id") {
+ id: ID!
+ username: String! @external
+ reviews: [Review]
+}
+
+type Product @key(fields: "upc") {
+ upc: String!
+ reviews: [Review]
+}
+
+# (This subgraph uses additional
+# federated directives)
+extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.3",
+ import: ["@key", "@shareable", "@provides", "@external"])
+```
+
+
+
+As these schemas show, multiple subgraphs can contribute unique fields to a single type. For example, the Products subgraph and the Reviews subgraph both contribute fields to the `Product` type.
+
+## Supergraph schema
+
+The supergraph schema is the output of [schema composition](/graphos/schema-design/federated-schemas/composition/). It serves the following purposes:
+
+* It provides your router with the name and endpoint URL for each of your subgraphs.
+* It includes all types and fields defined by all of your subgraphs.
+* It tells your router which of your subgraphs can resolve which GraphQL fields.
+
+Here's the supergraph schema composed with [the subgraph schemas above](#subgraph-schemas):
+
+
+
+```graphql
+schema
+ @link(url: "https://specs.apollo.dev/link/v1.0")
+ @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)
+{
+ query: Query
+}
+
+directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE
+
+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, isInterfaceObject: Boolean! = false) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
+
+directive @join__unionMember(graph: join__Graph!, member: String!) repeatable on UNION
+
+directive @link(url: String, as: String, for: link__Purpose, import: [link__Import]) repeatable on SCHEMA
+
+scalar join__FieldSet
+
+enum join__Graph {
+ PRODUCTS @join__graph(name: "products", url: "http://localhost:4002/graphql")
+ REVIEWS @join__graph(name: "reviews", url: "http://localhost:4003/graphql")
+ USERS @join__graph(name: "users", url: "http://localhost:4001/graphql")
+}
+
+scalar link__Import
+
+enum link__Purpose {
+ """
+ `SECURITY` features provide metadata necessary to securely resolve fields.
+ """
+ SECURITY
+
+ """
+ `EXECUTION` features provide metadata necessary for operation execution.
+ """
+ EXECUTION
+}
+
+type Product
+ @join__type(graph: PRODUCTS, key: "upc")
+ @join__type(graph: REVIEWS, key: "upc")
+{
+ upc: String!
+ name: String! @join__field(graph: PRODUCTS)
+ price: Int @join__field(graph: PRODUCTS)
+ reviews: [Review] @join__field(graph: REVIEWS)
+}
+
+type Query
+ @join__type(graph: PRODUCTS)
+ @join__type(graph: REVIEWS)
+ @join__type(graph: USERS)
+{
+ topProducts(first: Int = 5): [Product] @join__field(graph: PRODUCTS)
+ me: User @join__field(graph: USERS)
+}
+
+type Review
+ @join__type(graph: REVIEWS)
+{
+ body: String
+ author: User @join__field(graph: REVIEWS, provides: "username")
+ product: Product
+}
+
+type User
+ @join__type(graph: REVIEWS, key: "id")
+ @join__type(graph: USERS, key: "id")
+{
+ id: ID!
+ username: String! @join__field(graph: REVIEWS, external: true) @join__field(graph: USERS)
+ reviews: [Review] @join__field(graph: REVIEWS)
+}
+```
+
+
+
+As you can see, the supergraph schema includes a lot of Federation-specific additions! These additions are used only by the router, and you'll never need to add them manually.
+
+## API schema
+
+The router uses its [supergraph schema](#supergraph-schema) to produce an **API schema**, which it exposes to clients as your actual GraphQL API. This schema cleanly and logically represents the combination of your [subgraph schemas](#subgraph-schemas):
+
+```graphql
+type Product {
+ name: String!
+ price: Int
+ reviews: [Review]
+ upc: String!
+}
+
+type Query {
+ me: User
+ topProducts(first: Int = 5): [Product]
+}
+
+type Review {
+ author: User
+ body: String
+ product: Product
+}
+
+type User {
+ id: ID!
+ reviews: [Review]
+ username: String!
+}
+```
+
+Unlike the supergraph schema, this schema hides the fact that your GraphQL API is composed of multiple distinct GraphQL APIs.
diff --git a/docs/source/schema-design/federated-schemas/sharing-types.mdx b/docs/source/schema-design/federated-schemas/sharing-types.mdx
new file mode 100644
index 000000000..3a886fe35
--- /dev/null
+++ b/docs/source/schema-design/federated-schemas/sharing-types.mdx
@@ -0,0 +1,637 @@
+---
+title: Value Types in Apollo Federation
+subtitle: Share types and fields across multiple subgraphs
+description: Learn how to share GraphQL types and fields across subgraphs with Apollo Federation.
+redirectFrom:
+ - /federation/federated-types/sharing-types
+ - /federation/federated-schemas/sharing-types
+---
+
+In a federated graph, it's common to want to reuse a GraphQL type across multiple subgraphs.
+
+For example, suppose you want to define and reuse a generic `Position` type in different subgraphs:
+
+```graphql
+type Position {
+ x: Int!
+ y: Int!
+}
+```
+
+Types like this are called _value types_. This article describes how to share value types and their fields in federated graph, enabling multiple subgraphs to define and resolve them.
+
+## Sharing object types
+
+By default, in Federation 2 subgraphs, a single object field can't be defined or resolved by more than one subgraph schema.
+
+Consider the following `Position` example:
+
+❌
+
+
+
+```graphql {2-3} title="Subgraph A"
+type Position {
+ x: Int!
+ y: Int!
+}
+```
+
+```graphql {2-3} title="Subgraph B"
+type Position {
+ x: Int!
+ y: Int!
+}
+```
+
+
+
+Attempting to compose these two subgraph schemas together will [break composition](/graphos/schema-design/federated-schemas/composition/#breaking-composition). The router doesn't know which subgraph is responsible for resolving `Position.x` and `Position.y`. To enable multiple subgraphs to resolve these fields, you must first mark that field as [`@shareable`](#using-shareable).
+
+
+
+As an alternative, if you want Subgraphs A and B to resolve different fields of `Position`, you can designate the `Position` type as an [entity](/graphos/schema-design/federated-schemas/entities/intro).
+
+
+
+### Using `@shareable`
+
+The `@shareable` directive enables multiple subgraphs to resolve a particular object field (or set of object fields).
+
+To use `@shareable` in a subgraph schema, you first need to add the following snippet to that schema to [opt in to Federation 2](/graphos/reference/migration/to-federation-version-2/#opt-in-to-federation-2):
+
+```graphql
+extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.3",
+ import: ["@key", "@shareable"])
+```
+
+Then you can apply the `@shareable` directive to an object type, or to individual fields of that type:
+
+✅
+
+
+
+```graphql {1} title="Subgraph A"
+type Position @shareable {
+ x: Int!
+ y: Int!
+}
+```
+
+```graphql {2-3} title="Subgraph B"
+type Position {
+ x: Int! @shareable
+ y: Int! @shareable
+}
+```
+
+
+
+
+Marking a type as `@shareable` is equivalent to marking all of its fields as `@shareable`, so the two subgraph definitions above are equivalent.
+
+
+
+Both subgraphs A and B can now resolve the `x` and `y` fields for the `Position` type, and our subgraph schemas will successfully compose into a supergraph schema.
+
+#### ⚠️ Important considerations for `@shareable`
+
+* If a type or field is marked `@shareable` in any subgraph, it must be marked either `@shareable` or [`@external`](/graphos/reference/federation/directives/#external) in every subgraph that defines it. Otherwise, composition fails.
+* If multiple subgraphs can resolve a field, make sure each subgraph's resolver for that field behaves identically. Otherwise, queries might return inconsistent results depending on which subgraph resolves the field.
+
+#### Using `@shareable` with `extend`
+
+If you apply `@shareable` to an object type declaration, it only applies to the fields within that exact declaration. It does not apply to other declarations for that same type:
+
+```graphql title="Subgraph A"
+type Position @shareable {
+ x: Int! # shareable
+ y: Int! # shareable
+}
+
+extend type Position {
+ # highlight-start
+ z: Int! # ⚠️ NOT shareable!
+ # highlight-end
+}
+```
+
+Using the `extend` keyword, the schema above includes two different declarations for `Position`. Because only the first declaration is marked `@shareable`, `Position.z` is not considered shareable.
+
+To make `Position.z` shareable, you can do one of the following:
+
+- Mark the individual field `z` with `@shareable`.
+
+ ```graphql
+ extend type Position {
+ # highlight-start
+ z: Int! @shareable
+ # highlight-end
+ }
+ ```
+
+- Mark the entire `extend` declaration with `@shareable`.
+
+ - This strategy requires targeting v2.2 or later of the Apollo Federation specification in your subgraph schema. Earlier versions do not support applying `@shareable` to the same object type multiple times.
+
+ ```graphql
+ extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@shareable"]) #highlight-line
+
+ extend type Position @shareable { #highlight-line
+ z: Int!
+ }
+ ```
+
+## Differing shared fields
+
+Shared fields can only differ in their [return types](#return-types) and [arguments](#arguments) in specific ways.
+If fields you want to share between subgraphs differ more than is permitted, use [entities](/graphos/schema-design/federated-schemas/entities/intro) instead of shareable value types.
+
+
+### Return types
+
+Let's say two subgraphs both define an `Event` object type with a `timestamp` field:
+
+❌
+
+
+
+```graphql {2} title="Subgraph A"
+type Event @shareable {
+ timestamp: Int!
+}
+```
+
+```graphql {2} title="Subgraph B"
+type Event @shareable {
+ timestamp: String!
+}
+```
+
+
+
+Subgraph A's `timestamp` returns an `Int`, and Subgraph B's returns a `String`. This is invalid. When composition attempts to generate an `Event` type for the supergraph schema, it fails due to an unresolvable conflict between the two `timestamp` field definitions.
+
+Next, look at these varying definitions for the `Position` object type:
+
+✅
+
+
+
+```graphql {2-3} title="Subgraph A"
+type Position @shareable {
+ x: Int!
+ y: Int!
+}
+```
+
+```graphql {2-3} title="Subgraph B"
+type Position @shareable {
+ x: Int
+ y: Int
+}
+```
+
+
+
+The `x` and `y` fields are non-nullable in Subgraph A, but they're nullable in Subgraph B. This is valid. Composition recognizes that it can use the following definition for `Position` in the supergraph schema:
+
+
+```graphql title="Supergraph schema"
+type Position {
+ x: Int
+ y: Int
+}
+```
+
+This definition works for querying Subgraph A, because Subgraph A's definition is more restrictive than this (a non-nullable value is always valid for a nullable field). In this case, composition coerces Subgraph A's `Position` fields to satisfy the reduced restrictiveness of Subgraph B.
+
+
+
+Subgraph A's actual subgraph schema is not modified. Within Subgraph A, `x` and `y` remain non-nullable.
+
+
+
+### Arguments
+
+Arguments for a shared field can differ between subgraphs in certain ways:
+
+* If an argument is required in at least one subgraph, it can be optional in other subgraphs. It cannot be omitted.
+* If an argument is optional in every subgraph where it's defined, it is technically valid to omit it in other subgraphs. However:
+ * ⚠️ If a field argument is omitted from any subgraph, that argument is omitted from the supergraph schema entirely. This means that clients can't provide the argument for that field.
+
+✅
+
+
+
+```graphql {3} title="Subgraph A"
+type Building @shareable {
+ # Argument is required
+ height(units: String!): Int!
+}
+```
+
+```graphql {3} title="Subgraph B"
+type Building @shareable {
+ # Argument can be optional
+ height(units: String): Int!
+}
+```
+
+
+
+❌
+
+
+
+```graphql {3} title="Subgraph A"
+type Building @shareable {
+ # Argument is required
+ height(units: String!): Int!
+}
+```
+
+```graphql {3} title="Subgraph B"
+type Building @shareable {
+ # ⚠️ Argument can't be omitted! ⚠️
+ height: Int!
+}
+```
+
+
+
+⚠️
+
+
+
+```graphql {3} title="Subgraph A"
+type Building @shareable {
+ # Argument is optional
+ height(units: String): Int!
+}
+```
+
+```graphql {5} title="Subgraph B"
+type Building @shareable {
+ # Argument can be omitted, BUT
+ # it doesn't appear in the
+ # supergraph schema!
+ height: Int!
+}
+```
+
+
+
+For more information, see [Input types and field arguments](/graphos/schema-design/federated-schemas/composition#input-types-and-field-arguments).
+
+### Omitting fields
+
+Look at these two definitions of a `Position` object type:
+
+⚠️
+
+
+
+```graphql title="Subgraph A"
+type Position @shareable {
+ x: Int!
+ y: Int!
+}
+```
+
+```graphql title="Subgraph B"
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int!
+}
+```
+
+
+
+Subgraph B defines a `z` field, but Subgraph A doesn't. In this case, when composition generates the `Position` type for the supergraph schema, it includes all three fields:
+
+
+
+```graphql title="Supergraph schema"
+type Position {
+ x: Int!
+ y: Int!
+ z: Int!
+}
+```
+
+
+
+This definition works for Subgraph B, but it presents a problem for Subgraph A. Let's say Subgraph A defines the following `Query` type:
+
+
+
+```graphql title="Subgraph A"
+type Query {
+ currentPosition: Position!
+}
+```
+
+
+
+According to the hypothetical supergraph schema, the following query is valid against the supergraph:
+
+❌
+
+
+
+```graphql
+query GetCurrentPosition {
+ currentPosition {
+ x
+ y
+ z # ⚠️ Unresolvable! ⚠️
+ }
+}
+```
+
+
+
+And here's the problem: if Subgraph B doesn't define `Query.currentPosition`, this query must be executed on Subgraph A. But Subgraph A is missing the `Position.z` field, so that field is unresolvable!
+
+Composition recognizes this potential problem, and it fails with an error. To learn how to fix it, check out [Solutions for unresolvable fields](/graphos/reference/federation/composition-rules#solutions-for-unresolvable-fields).
+
+## Adding new shared fields
+
+Adding a new field to a value type can cause composition issues, because it's challenging to add the field to all defining subgraphs at the same time.
+
+Let's say we're adding a `z` field to our `Position` value type, and we start with Subgraph A:
+
+⚠️
+
+
+
+```graphql {4} title="Subgraph A"
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int!
+}
+```
+
+```graphql title="Subgraph B"
+type Position @shareable {
+ x: Int!
+ y: Int!
+}
+```
+
+
+
+It's likely that when we attempt to compose these two schemas, composition will fail, because Subgraph B can't resolve `Position.z`.
+
+To incrementally add the field to all of our subgraphs without breaking composition, we can use the [`@inaccessible` directive](#using-inaccessible).
+
+
+### Using `@inaccessible`
+
+If you apply the `@inaccessible` directive to a field, composition omits that field from your router's API schema. This helps you incrementally add a field to multiple subgraphs without breaking composition.
+
+To use `@inaccessible` in a subgraph, first make sure you include it in the `import` array of your Federation 2 opt-in declaration:
+
+```graphql {3} title="Subgraph A"
+extend schema
+ @link(url: "https://specs.apollo.dev/federation/v2.3",
+ import: ["@key", "@shareable", "@inaccessible"])
+```
+
+Then, whenever you add a new field to a value type, apply `@inaccessible` to that field if it isn't yet present in every subgraph that defines the value type:
+
+
+
+```graphql {4} title="Subgraph A"
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int! @inaccessible
+}
+```
+
+```graphql title="Subgraph B"
+type Position @shareable {
+ x: Int!
+ y: Int!
+}
+```
+
+
+
+Even if `Position.z` is defined in multiple subgraphs, you only need to apply `@inaccessible` in one subgraph to omit it. In fact, you might want to apply it in only one subgraph to simplify removing it later.
+
+With the syntax above, composition omits `Position.z` from the generated API schema, and the resulting `Position` type includes only `x` and `y` fields.
+
+
+
+Notice that `Position.z` does appear in the supergraph schema, but the API schema enforces which fields clients can include in operations. [Learn more about federated schemas.](/graphos/schema-design/federated-schemas/schema-types)
+
+
+
+Whenever you're ready, you can now add `Position.z` to Subgraph B:
+
+
+
+```graphql title="Subgraph A"
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int! @inaccessible
+}
+```
+
+```graphql {4} title="Subgraph B"
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int!
+}
+```
+
+
+
+At this point, `Position.z` is still `@inaccessible`, so composition continues to ignore it.
+
+Finally, when you've added `Position.z` to every subgraph that defines `Position`, you can remove `@inaccessible` from Subgraph A:
+
+
+
+```graphql {4} title="Subgraph A"
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int!
+}
+```
+
+```graphql title="Subgraph B"
+type Position @shareable {
+ x: Int!
+ y: Int!
+ z: Int!
+}
+```
+
+
+
+Composition now successfully includes `Position.z` in the supergraph schema!
+
+## Unions and interfaces
+
+In Federation 2, `union` and `interface` type definitions can be shared between subgraphs by default, and those definitions can differ:
+
+
+```graphql {1, 3, 9} title="Subgraph A"
+union Media = Book | Movie
+
+interface User {
+ name: String!
+}
+```
+
+```graphql {1, 3, 9} title="Subgraph B"
+union Media = Book | Podcast
+
+interface User {
+ name: String!
+ age: Int!
+}
+```
+
+
+
+[Compositional logic](/graphos/schema-design/federated-schemas/composition#merging-types-from-multiple-subgraphs) merges these definitions in your supergraph schema:
+
+```graphql title="Supergraph schema"
+union Media = Book | Movie | Podcast
+
+# The object types that implement this interface are
+# responsible for resolving these fields.
+interface User {
+ name: String!
+ age: Int!
+}
+```
+
+This can be useful when different subgraphs are responsible for different subsets of a particular set of related types or values.
+
+
+
+You can also use the `enum` type across multiple subgraphs. For details, see [Merging types from multiple subgraphs](composition#enums).
+
+
+
+### Challenges with shared interfaces
+
+Sharing an interface type across subgraphs introduces maintenance challenges whenever that interface changes. Consider these subgraphs:
+
+
+
+```graphql title="Subgraph A"
+interface Media {
+ id: ID!
+ title: String!
+}
+
+type Book implements Media {
+ id: ID!
+ title: String!
+}
+```
+
+```graphql title="Subgraph B"
+interface Media {
+ id: ID!
+ title: String!
+}
+
+type Podcast implements Media {
+ id: ID!
+ title: String!
+}
+```
+
+
+
+Now, let's say Subgraph B adds a `creator` field to the `Media` interface:
+
+❌
+
+
+
+```graphql title="Subgraph A"
+interface Media {
+ id: ID!
+ title: String!
+}
+
+type Book implements Media {
+ id: ID!
+ title: String!
+ # ❌ Doesn't define creator!
+}
+```
+
+```graphql title="Subgraph B"
+interface Media {
+ id: ID!
+ title: String!
+ creator: String! #highlight-line
+}
+
+type Podcast implements Media {
+ id: ID!
+ title: String!
+ creator: String! #highlight-line
+}
+```
+
+
+
+This breaks composition, because `Book` also implements `Media` but doesn't define the new `creator` field.
+
+To prevent this error, all implementing types across all subgraphs need to be updated to include all fields of `Media`. This becomes more and more challenging to do as your number of subgraphs and teams grows. Fortunately, there's a [solution](#solution-entity-interfaces).
+
+#### Solution: Entity interfaces
+
+Apollo Federation 2.3 introduces a powerful abstraction mechanism for interfaces, enabling you to add interface fields across subgraphs without needing to update every single implementing type.
+
+[Learn more about entity interfaces.](/graphos/schema-design/federated-schemas/entities/interfaces/)
+
+## Input types
+
+Subgraphs can share `input` type definitions, but composition merges their fields using an intersection strategy. When `input` types are composed across multiple subgraphs, only mutual fields are preserved in the supergraph schema:
+
+
+
+```graphql title="Subgraph A"
+input UserInput {
+ name: String!
+ age: Int # Not in Subgraph B
+}
+```
+
+```graphql title="Subgraph B"
+input UserInput {
+ name: String!
+ email: String # Not in Subgraph A
+}
+```
+
+
+
+Compositional logic merges only the fields that all `input` types have in common. To learn more, see [Merging input types and field arguments](/graphos/schema-design/federated-schemas/composition#input-types-and-field-arguments).
+
+```graphql title="Supergraph schema"
+input UserInput {
+ name: String!
+}
+```
+
+To learn more about how composition merges different schema types under the hood, see [Merging types during composition](/graphos/schema-design/federated-schemas/composition#merging-types-from-multiple-subgraphs).
diff --git a/federation-integration-testsuite-js/package.json b/federation-integration-testsuite-js/package.json
index 75b817824..44b7b6c9b 100644
--- a/federation-integration-testsuite-js/package.json
+++ b/federation-integration-testsuite-js/package.json
@@ -1,7 +1,7 @@
{
"name": "apollo-federation-integration-testsuite",
"private": true,
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"description": "Apollo Federation Integrations / Test Fixtures",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/gateway-js/package.json b/gateway-js/package.json
index 262209797..23848e502 100644
--- a/gateway-js/package.json
+++ b/gateway-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@apollo/gateway",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"description": "Apollo Gateway",
"author": "Apollo ",
"main": "dist/index.js",
@@ -25,9 +25,9 @@
"access": "public"
},
"dependencies": {
- "@apollo/composition": "2.9.3",
- "@apollo/federation-internals": "2.9.3",
- "@apollo/query-planner": "2.9.3",
+ "@apollo/composition": "2.10.0-alpha.4",
+ "@apollo/federation-internals": "2.10.0-alpha.4",
+ "@apollo/query-planner": "2.10.0-alpha.4",
"@apollo/server-gateway-interface": "^1.1.0",
"@apollo/usage-reporting-protobuf": "^4.1.0",
"@apollo/utils.createhash": "^2.0.0",
diff --git a/gateway-js/src/__generated__/graphqlTypes.ts b/gateway-js/src/__generated__/graphqlTypes.ts
index b6055a227..6bcc9114d 100644
--- a/gateway-js/src/__generated__/graphqlTypes.ts
+++ b/gateway-js/src/__generated__/graphqlTypes.ts
@@ -10,6 +10,7 @@ export type Scalars = {
Boolean: boolean;
Int: number;
Float: number;
+ Long: any;
Timestamp: any;
};
@@ -98,6 +99,12 @@ export type QueryRouterEntitlementsArgs = {
ref: Scalars['String'];
};
+export type RateLimit = {
+ __typename?: 'RateLimit';
+ count: Scalars['Long'];
+ durationMs: Scalars['Long'];
+};
+
export type RouterConfigResponse = FetchError | RouterConfigResult | Unchanged;
export type RouterConfigResult = {
@@ -121,6 +128,8 @@ export type RouterEntitlement = {
/** RFC 8037 Ed25519 JWT signed representation of sibling fields. */
jwt: Scalars['String'];
subject: Scalars['String'];
+ /** Router should service requests only till the throughput limits specified in this map. */
+ throughputLimit?: Maybe;
/** Router should warn users after this time if commercial features are in use. */
warnAt?: Maybe;
};
diff --git a/gateway-js/src/datasources/RemoteGraphQLDataSource.ts b/gateway-js/src/datasources/RemoteGraphQLDataSource.ts
index f52005e31..962fd1196 100644
--- a/gateway-js/src/datasources/RemoteGraphQLDataSource.ts
+++ b/gateway-js/src/datasources/RemoteGraphQLDataSource.ts
@@ -220,7 +220,7 @@ export class RemoteGraphQLDataSource<
http: fetchResponse,
};
} catch (error) {
- this.didEncounterError(error, fetchRequest, fetchResponse, context);
+ this.didEncounterError(error, fetchRequest, fetchResponse, context, request);
throw error;
}
}
@@ -284,6 +284,7 @@ export class RemoteGraphQLDataSource<
_fetchRequest: NodeFetchRequest,
_fetchResponse?: FetcherResponse,
_context?: TContext,
+ _request?: GatewayGraphQLRequest,
) {
throw error;
}
diff --git a/internals-js/package.json b/internals-js/package.json
index 93e1b973a..20cff0326 100644
--- a/internals-js/package.json
+++ b/internals-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@apollo/federation-internals",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"description": "Apollo Federation internal utilities",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/internals-js/src/__tests__/schemaUpgrader.test.ts b/internals-js/src/__tests__/schemaUpgrader.test.ts
index 47bed9958..02bca30dc 100644
--- a/internals-js/src/__tests__/schemaUpgrader.test.ts
+++ b/internals-js/src/__tests__/schemaUpgrader.test.ts
@@ -310,14 +310,14 @@ test('handles the addition of @shareable when an @external is used on a type', (
// 1. the @external on type `T` in s2 should be removed, as @external on types were no-ops in fed1 (but not in fed2 anymore, hence the removal)
// 2. field `T.x` in s1 must be marked @shareable since it is resolved by s2 (since again, it's @external annotation is ignored).
- const s2Upgraded = res.subgraphs?.get('s2')!;
- expect(s2Upgraded.schema.type('T')?.hasAppliedDirective('external')).toBe(
+ const s2Upgraded = res.subgraphs?.get('s2');
+ expect(s2Upgraded?.schema.type('T')?.hasAppliedDirective('external')).toBe(
false,
);
- const s1Upgraded = res.subgraphs?.get('s1')!;
+ const s1Upgraded = res.subgraphs?.get('s1');
expect(
- (s1Upgraded.schema.type('T') as ObjectType)
+ (s1Upgraded?.schema.type('T') as ObjectType)
.field('x')
?.hasAppliedDirective('shareable'),
).toBe(true);
@@ -372,8 +372,8 @@ test("don't add @shareable to subscriptions", () => {
type Query {
hello: String
}
-
- type Subscription {
+
+ type Subscription {
update: String!
}
`,
@@ -386,8 +386,8 @@ test("don't add @shareable to subscriptions", () => {
type Query {
hello: String
}
-
- type Subscription {
+
+ type Subscription {
update: String!
}
`,
diff --git a/internals-js/src/error.ts b/internals-js/src/error.ts
index 79b6a1f53..f658b2acc 100644
--- a/internals-js/src/error.ts
+++ b/internals-js/src/error.ts
@@ -591,123 +591,10 @@ const INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE = makeCodeDefinition(
{ addedIn: '2.3.0' },
)
-const SOURCE_FEDERATION_VERSION_REQUIRED = makeCodeDefinition(
- 'SOURCE_FEDERATION_VERSION_REQUIRED',
- 'Schemas using `@source{API,Type,Field}` directives must @link-import v2.7 or later of federation',
- { addedIn: '2.7.1' },
-);
-
-const SOURCE_API_NAME_INVALID = makeCodeDefinition(
- 'SOURCE_API_NAME_INVALID',
- 'Each `@sourceAPI` directive must take a unique and valid name as an argument',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_API_PROTOCOL_INVALID = makeCodeDefinition(
- 'SOURCE_API_PROTOCOL_INVALID',
- 'Each `@sourceAPI` directive must specify exactly one of the known protocols',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_API_HTTP_BASE_URL_INVALID = makeCodeDefinition(
- 'SOURCE_API_HTTP_BASE_URL_INVALID',
- 'The `@sourceAPI` directive must specify a valid http.baseURL',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_HTTP_HEADERS_INVALID = makeCodeDefinition(
- 'SOURCE_HTTP_HEADERS_INVALID',
- 'The `http.headers` argument of `@source*` directives must specify valid HTTP headers',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_TYPE_API_ERROR = makeCodeDefinition(
- 'SOURCE_TYPE_API_ERROR',
- 'The `api` argument of the `@sourceType` directive must match a valid `@sourceAPI` name',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_TYPE_PROTOCOL_INVALID = makeCodeDefinition(
- 'SOURCE_TYPE_PROTOCOL_INVALID',
- 'The `@sourceType` directive must specify the same protocol as its corresponding `@sourceAPI`',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_TYPE_HTTP_METHOD_INVALID = makeCodeDefinition(
- 'SOURCE_TYPE_HTTP_METHOD_INVALID',
- 'The `@sourceType` directive must specify exactly one of `http.GET` or `http.POST`',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_TYPE_HTTP_PATH_INVALID = makeCodeDefinition(
- 'SOURCE_TYPE_HTTP_PATH_INVALID',
- 'The `@sourceType` directive must specify a valid URL template for `http.GET` or `http.POST`',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_TYPE_HTTP_BODY_INVALID = makeCodeDefinition(
- 'SOURCE_TYPE_HTTP_BODY_INVALID',
- 'If the `@sourceType` specifies `http.body`, it must be a valid `JSONSelection`',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY = makeCodeDefinition(
- 'SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY',
- 'The `@sourceType` directive must be applied to an object or interface type that also has `@key`',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_TYPE_SELECTION_INVALID = makeCodeDefinition(
- 'SOURCE_TYPE_SELECTION_INVALID',
- 'The `selection` argument of the `@sourceType` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type',
-);
-
-const SOURCE_FIELD_API_ERROR = makeCodeDefinition(
- 'SOURCE_FIELD_API_ERROR',
- 'The `api` argument of the `@sourceField` directive must match a valid `@sourceAPI` name',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_FIELD_PROTOCOL_INVALID = makeCodeDefinition(
- 'SOURCE_FIELD_PROTOCOL_INVALID',
- 'If `@sourceField` specifies a protocol, it must match the corresponding `@sourceAPI` protocol',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_FIELD_HTTP_METHOD_INVALID = makeCodeDefinition(
- 'SOURCE_FIELD_HTTP_METHOD_INVALID',
- 'The `@sourceField` directive must specify at most one of `http.{GET,POST,PUT,PATCH,DELETE}`',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_FIELD_HTTP_PATH_INVALID = makeCodeDefinition(
- 'SOURCE_FIELD_HTTP_PATH_INVALID',
- 'The `@sourceField` directive must specify a valid URL template for `http.{GET,POST,PUT,PATCH,DELETE}`',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_FIELD_HTTP_BODY_INVALID = makeCodeDefinition(
- 'SOURCE_FIELD_HTTP_BODY_INVALID',
- 'If `@sourceField` specifies http.body, it must be a valid `JSONSelection` matching available arguments and fields',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_FIELD_SELECTION_INVALID = makeCodeDefinition(
- 'SOURCE_FIELD_SELECTION_INVALID',
- 'The `selection` argument of the `@sourceField` directive must be a valid `JSONSelection` that outputs fields of the GraphQL type',
- { addedIn: '2.7.0' },
-);
-
-const SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD = makeCodeDefinition(
- 'SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD',
- 'The `@sourceField` directive must be applied to a field of the `Query` or `Mutation` types, or of an entity type',
- { addedIn: '2.7.0' },
-);
-
const CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS = makeCodeDefinition(
- 'CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS',
- 'Argument on field is marked contextual in only some subgraphs',
- { addedIn: '2.7.0' },
+ 'CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS',
+ 'Argument on field is marked contextual in only some subgraphs',
+ { addedIn: '2.7.0' },
);
const COST_APPLIED_TO_INTERFACE_FIELD = makeCodeDefinition(
@@ -833,26 +720,6 @@ export const ERRORS = {
INTERFACE_OBJECT_USAGE_ERROR,
INTERFACE_KEY_NOT_ON_IMPLEMENTATION,
INTERFACE_KEY_MISSING_IMPLEMENTATION_TYPE,
- // Errors related to @sourceAPI, @sourceType, and/or @sourceField
- SOURCE_FEDERATION_VERSION_REQUIRED,
- SOURCE_API_NAME_INVALID,
- SOURCE_API_PROTOCOL_INVALID,
- SOURCE_API_HTTP_BASE_URL_INVALID,
- SOURCE_HTTP_HEADERS_INVALID,
- SOURCE_TYPE_API_ERROR,
- SOURCE_TYPE_PROTOCOL_INVALID,
- SOURCE_TYPE_HTTP_METHOD_INVALID,
- SOURCE_TYPE_HTTP_PATH_INVALID,
- SOURCE_TYPE_HTTP_BODY_INVALID,
- SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY,
- SOURCE_TYPE_SELECTION_INVALID,
- SOURCE_FIELD_API_ERROR,
- SOURCE_FIELD_PROTOCOL_INVALID,
- SOURCE_FIELD_HTTP_METHOD_INVALID,
- SOURCE_FIELD_HTTP_PATH_INVALID,
- SOURCE_FIELD_HTTP_BODY_INVALID,
- SOURCE_FIELD_SELECTION_INVALID,
- SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD,
CONTEXTUAL_ARGUMENT_NOT_CONTEXTUAL_IN_ALL_SUBGRAPHS,
// Errors related to demand control
COST_APPLIED_TO_INTERFACE_FIELD,
diff --git a/internals-js/src/federation.ts b/internals-js/src/federation.ts
index 8faa98082..3e18dd5c4 100644
--- a/internals-js/src/federation.ts
+++ b/internals-js/src/federation.ts
@@ -95,13 +95,8 @@ import {
import { defaultPrintOptions, PrintOptions as PrintOptions, printSchema } from "./print";
import { createObjectTypeSpecification, createScalarTypeSpecification, createUnionTypeSpecification } from "./directiveAndTypeSpecification";
import { didYouMean, suggestionList } from "./suggestions";
-import { coreFeatureDefinitionIfKnown, validateKnownFeatures } from "./knownCoreFeatures";
+import { coreFeatureDefinitionIfKnown } from "./knownCoreFeatures";
import { joinIdentity } from "./specs/joinSpec";
-import {
- SourceAPIDirectiveArgs,
- SourceFieldDirectiveArgs,
- SourceTypeDirectiveArgs,
-} from "./specs/sourceSpec";
import { COST_VERSIONS, CostDirectiveArguments, ListSizeDirectiveArguments, costIdentity } from "./specs/costSpec";
const linkSpec = LINK_VERSIONS.latest();
@@ -400,10 +395,10 @@ const validateFieldValueType = ({
fromContextParent: ArgumentDefinition>,
}): { resolvedType: InputType | undefined } => {
const selections = selectionSet.selections();
-
+
// ensure that type is not an interfaceObject
const interfaceObjectDirective = metadata.interfaceObjectDirective();
- if (currentType.kind === 'ObjectType' && isFederationDirectiveDefinedInSchema(interfaceObjectDirective) && (currentType.appliedDirectivesOf(interfaceObjectDirective).length > 0)) {
+ if (currentType.kind === 'ObjectType' && isFederationDirectiveDefinedInSchema(interfaceObjectDirective) && (currentType.appliedDirectivesOf(interfaceObjectDirective).length > 0)) {
errorCollector.push(ERRORS.CONTEXT_INVALID_SELECTION.err(
`Context "is used in "${fromContextParent.coordinate}" but the selection is invalid: One of the types in the selection is an interfaceObject: "${currentType.name}"`,
{ nodes: sourceASTs(fromContextParent) }
@@ -597,7 +592,7 @@ function validateFieldValue({
if (selectionType === 'error') {
return;
}
-
+
const usedTypeConditions = new Set;
for (const location of setContextLocations) {
// for each location, we need to validate that the selection will result in exactly one field being selected
@@ -624,7 +619,7 @@ function validateFieldValue({
{ nodes: sourceASTs(fromContextParent) }
));
}
-
+
if (selectionType === 'field') {
const { resolvedType } = validateFieldValueType({
currentType: location,
@@ -797,13 +792,13 @@ export function collectUsedFields(metadata: FederationMetadata): Set(
metadata,
usedFields,
);
-
+
// Collects all fields used to satisfy an interface constraint
for (const itfType of metadata.schema.interfaceTypes()) {
const runtimeTypes = itfType.possibleRuntimeTypes();
@@ -826,12 +821,12 @@ function collectUsedFieldsForFromContext
) {
const fromContextDirective = metadata.fromContextDirective();
const contextDirective = metadata.contextDirective();
-
+
// if one of the directives is not defined, there's nothing to validate
if (!isFederationDirectiveDefinedInSchema(fromContextDirective) || !isFederationDirectiveDefinedInSchema(contextDirective)) {
- return;
+ return;
}
-
+
// build the list of context entry points
const entryPoints = new Map>();
for (const application of contextDirective.applications()) {
@@ -844,9 +839,9 @@ function collectUsedFieldsForFromContext
if (!entryPoints.has(context)) {
entryPoints.set(context, new Set());
}
- entryPoints.get(context)!.add(type as CompositeType);
+ entryPoints.get(context)!.add(type as CompositeType);
}
-
+
for (const application of fromContextDirective.applications()) {
const type = application.parent as TParent;
if (!type) {
@@ -856,20 +851,20 @@ function collectUsedFieldsForFromContext
const fieldValue = application.arguments().field;
const { context, selection } = parseContext(fieldValue);
-
+
if (!context) {
continue;
}
-
+
// now we need to collect all the fields used for every type that they could be used for
const contextTypes = entryPoints.get(context);
if (!contextTypes) {
continue;
}
-
+
for (const contextType of contextTypes) {
try {
- // helper function
+ // helper function
const fieldAccessor = (t: CompositeType, f: string) => {
const field = t.field(f);
if (field) {
@@ -885,7 +880,7 @@ function collectUsedFieldsForFromContext
}
return field;
};
-
+
parseSelectionSet({ parentType: contextType, source: selection, fieldAccessor });
} catch (e) {
// ignore the error, it will be caught later
@@ -1091,7 +1086,7 @@ function validateAssumedSizeNotNegative(
) {
const { assumedSize } = application.arguments();
// Validate assumed size, but we differ from https://ibm.github.io/graphql-specs/cost-spec.html#sec-Valid-Assumed-Size.
- // Assumed size is used as a backup for slicing arguments in the event they are both specified.
+ // Assumed size is used as a backup for slicing arguments in the event they are both specified.
// The spec aims to rule out cases when the assumed size will never be used because there is always
// a slicing argument. Two applications which are compliant with that validation rule can be merged
// into an application which is not compliant, thus we need to handle this case gracefully at runtime regardless.
@@ -1375,18 +1370,6 @@ export class FederationMetadata {
return this.getPost20FederationDirective(FederationDirectiveName.POLICY);
}
- sourceAPIDirective(): Post20FederationDirectiveDefinition {
- return this.getPost20FederationDirective(FederationDirectiveName.SOURCE_API);
- }
-
- sourceTypeDirective(): Post20FederationDirectiveDefinition {
- return this.getPost20FederationDirective(FederationDirectiveName.SOURCE_TYPE);
- }
-
- sourceFieldDirective(): Post20FederationDirectiveDefinition {
- return this.getPost20FederationDirective(FederationDirectiveName.SOURCE_FIELD);
- }
-
fromContextDirective(): Post20FederationDirectiveDefinition<{ field: string }> {
return this.getPost20FederationDirective(FederationDirectiveName.FROM_CONTEXT);
}
@@ -1443,19 +1426,6 @@ export class FederationMetadata {
baseDirectives.push(policyDirective);
}
- const sourceAPIDirective = this.sourceAPIDirective();
- if (isFederationDirectiveDefinedInSchema(sourceAPIDirective)) {
- baseDirectives.push(sourceAPIDirective);
- }
- const sourceTypeDirective = this.sourceTypeDirective();
- if (isFederationDirectiveDefinedInSchema(sourceTypeDirective)) {
- baseDirectives.push(sourceTypeDirective);
- }
- const sourceFieldDirective = this.sourceFieldDirective();
- if (isFederationDirectiveDefinedInSchema(sourceFieldDirective)) {
- baseDirectives.push(sourceFieldDirective);
- }
-
const contextDirective = this.contextDirective();
if (isFederationDirectiveDefinedInSchema(contextDirective)) {
baseDirectives.push(contextDirective);
@@ -1714,7 +1684,6 @@ export class FederationBlueprint extends SchemaBlueprint {
for (const application of contextDirective.applications()) {
const parent = application.parent;
const name = application.arguments().name as string;
-
const match = name.match(/^([A-Za-z]\w*)$/);
if (name.includes('_')) {
errorCollector.push(ERRORS.CONTEXT_NAME_INVALID.err(
@@ -1740,7 +1709,7 @@ export class FederationBlueprint extends SchemaBlueprint {
for (const application of fromContextDirective.applications()) {
const { field } = application.arguments();
const { context, selection } = parseContext(field);
-
+
// error if parent's parent is a directive definition
if (application.parent.parent.kind === 'DirectiveDefinition') {
errorCollector.push(ERRORS.CONTEXT_NOT_SET.err(
@@ -1772,14 +1741,14 @@ export class FederationBlueprint extends SchemaBlueprint {
));
}
}
-
+
if (parent.defaultValue !== undefined) {
errorCollector.push(ERRORS.CONTEXT_NOT_SET.err(
`@fromContext arguments may not have a default value: "${parent.coordinate}".`,
{ nodes: sourceASTs(application) }
- ));
+ ));
}
-
+
if (!context || !selection) {
errorCollector.push(ERRORS.NO_CONTEXT_IN_SELECTION.err(
`@fromContext argument does not reference a context "${field}".`,
@@ -1802,7 +1771,7 @@ export class FederationBlueprint extends SchemaBlueprint {
metadata,
});
}
-
+
// validate that there is at least one resolvable key on the type
const keyDirective = metadata.keyDirective();
const keyApplications = objectType.appliedDirectivesOf(keyDirective);
@@ -1820,10 +1789,6 @@ export class FederationBlueprint extends SchemaBlueprint {
validateKeyOnInterfacesAreAlsoOnAllImplementations(metadata, errorCollector);
validateInterfaceObjectsAreOnEntities(metadata, errorCollector);
- // FeatureDefinition objects passed to registerKnownFeature can register
- // validation functions for subgraph schemas by overriding the
- // validateSubgraphSchema method.
- validateKnownFeatures(schema, errorCollector);
// If tag is redefined by the user, make sure the definition is compatible with what we expect
const tagDirective = metadata.tagDirective();
if (tagDirective) {
@@ -1989,9 +1954,9 @@ export function setSchemaAsFed2Subgraph(schema: Schema, useLatest: boolean = fal
// This is the full @link declaration as added by `asFed2SubgraphDocument`. It's here primarily for uses by tests that print and match
// subgraph schema to avoid having to update 20+ tests every time we use a new directive or the order of import changes ...
-export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@sourceAPI", "@sourceType", "@sourceField", "@context", "@fromContext", "@cost", "@listSize"])';
+export const FEDERATION2_LINK_WITH_FULL_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.10", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject", "@authenticated", "@requiresScopes", "@policy", "@context", "@fromContext", "@cost", "@listSize"])';
// This is the full @link declaration that is added when upgrading fed v1 subgraphs to v2 version. It should only be used by tests.
-export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.9", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
+export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS = '@link(url: "https://specs.apollo.dev/federation/v2.10", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
// This is the federation @link for tests that go through the SchemaUpgrader.
export const FEDERATION2_LINK_WITH_AUTO_EXPANDED_IMPORTS_UPGRADED = '@link(url: "https://specs.apollo.dev/federation/v2.4", import: ["@key", "@requires", "@provides", "@external", "@tag", "@extends", "@shareable", "@inaccessible", "@override", "@composeDirective", "@interfaceObject"])';
diff --git a/internals-js/src/index.ts b/internals-js/src/index.ts
index 9400a73ab..d0f282966 100644
--- a/internals-js/src/index.ts
+++ b/internals-js/src/index.ts
@@ -24,5 +24,5 @@ export * from './argumentCompositionStrategies';
export * from './specs/authenticatedSpec';
export * from './specs/requiresScopesSpec';
export * from './specs/policySpec';
-export * from './specs/sourceSpec';
+export * from './specs/connectSpec';
export * from './specs/costSpec';
diff --git a/internals-js/src/knownCoreFeatures.ts b/internals-js/src/knownCoreFeatures.ts
index 83bb3ceaf..aebb62263 100644
--- a/internals-js/src/knownCoreFeatures.ts
+++ b/internals-js/src/knownCoreFeatures.ts
@@ -1,5 +1,3 @@
-import { GraphQLError } from "graphql";
-import { Schema } from "./definitions";
import { FeatureDefinition, FeatureDefinitions, FeatureUrl } from "./specs/coreSpec";
const registeredFeatures = new Map();
@@ -14,19 +12,6 @@ export function coreFeatureDefinitionIfKnown(url: FeatureUrl): FeatureDefinition
return registeredFeatures.get(url.identity)?.find(url.version);
}
-export function validateKnownFeatures(
- schema: Schema,
- errorCollector: GraphQLError[] = [],
-): GraphQLError[] {
- registeredFeatures.forEach(definitions => {
- const feature = definitions.latest();
- if (feature.validateSubgraphSchema !== FeatureDefinition.prototype.validateSubgraphSchema) {
- errorCollector.push(...feature.validateSubgraphSchema(schema));
- }
- });
- return errorCollector;
-}
-
/**
* Removes a feature from the set of known features.
*
diff --git a/internals-js/src/specs/connectSpec.ts b/internals-js/src/specs/connectSpec.ts
new file mode 100644
index 000000000..213325061
--- /dev/null
+++ b/internals-js/src/specs/connectSpec.ts
@@ -0,0 +1,148 @@
+import {DirectiveLocation, GraphQLError} from 'graphql';
+import { CorePurpose, FeatureDefinition, FeatureDefinitions, FeatureUrl, FeatureVersion } from "./coreSpec";
+import {
+ Schema,
+ NonNullType,
+ InputObjectType,
+ InputFieldDefinition,
+ ListType,
+} from '../definitions';
+import { registerKnownFeature } from '../knownCoreFeatures';
+import { createDirectiveSpecification, createScalarTypeSpecification } from '../directiveAndTypeSpecification';
+
+export const connectIdentity = 'https://specs.apollo.dev/connect';
+
+const CONNECT = "connect";
+const SOURCE = "source";
+const URL_PATH_TEMPLATE = "URLPathTemplate";
+const JSON_SELECTION = "JSONSelection";
+const CONNECT_HTTP = "ConnectHTTP";
+const SOURCE_HTTP = "SourceHTTP";
+const HTTP_HEADER_MAPPING = "HTTPHeaderMapping";
+
+export class ConnectSpecDefinition extends FeatureDefinition {
+ constructor(version: FeatureVersion, readonly minimumFederationVersion: FeatureVersion) {
+ super(new FeatureUrl(connectIdentity, CONNECT, version), minimumFederationVersion);
+
+ this.registerDirective(createDirectiveSpecification({
+ name: CONNECT,
+ locations: [DirectiveLocation.FIELD_DEFINITION],
+ repeatable: true,
+ // We "compose" these directives using the `@join__directive` mechanism,
+ // so they do not need to be composed in the way passing `composes: true`
+ // here implies.
+ composes: false,
+ }));
+
+ this.registerDirective(createDirectiveSpecification({
+ name: SOURCE,
+ locations: [DirectiveLocation.SCHEMA],
+ repeatable: true,
+ composes: false,
+ }));
+
+ this.registerType(createScalarTypeSpecification({ name: URL_PATH_TEMPLATE }));
+ this.registerType(createScalarTypeSpecification({ name: JSON_SELECTION }));
+ this.registerType({ name: CONNECT_HTTP, checkOrAdd: () => [] });
+ this.registerType({ name: SOURCE_HTTP, checkOrAdd: () => [] });
+ this.registerType({ name: HTTP_HEADER_MAPPING, checkOrAdd: () => [] });
+ }
+
+ addElementsToSchema(schema: Schema): GraphQLError[] {
+ /* scalar URLPathTemplate */
+ const URLPathTemplate = this.addScalarType(schema, URL_PATH_TEMPLATE);
+
+ /* scalar JSONSelection */
+ const JSONSelection = this.addScalarType(schema, JSON_SELECTION);
+
+ /*
+ directive @connect(
+ source: String
+ http: ConnectHTTP
+ selection: JSONSelection!
+ entity: Boolean = false
+ ) repeatable on FIELD_DEFINITION
+ */
+ const connect = this.addDirective(schema, CONNECT).addLocations(DirectiveLocation.FIELD_DEFINITION);
+ connect.repeatable = true;
+
+ connect.addArgument(SOURCE, schema.stringType());
+
+ /*
+ input HTTPHeaderMapping {
+ name: String!
+ from: String
+ value: String
+ }
+ */
+ const HTTPHeaderMapping = schema.addType(new InputObjectType(this.typeNameInSchema(schema, HTTP_HEADER_MAPPING)!));
+ HTTPHeaderMapping.addField(new InputFieldDefinition('name')).type =
+ new NonNullType(schema.stringType());
+ HTTPHeaderMapping.addField(new InputFieldDefinition('from')).type =
+ schema.stringType();
+ HTTPHeaderMapping.addField(new InputFieldDefinition('value')).type =
+ schema.stringType();
+
+ /*
+ input ConnectHTTP {
+ GET: URLPathTemplate
+ POST: URLPathTemplate
+ PUT: URLPathTemplate
+ PATCH: URLPathTemplate
+ DELETE: URLPathTemplate
+ body: JSONSelection
+ headers: [HTTPHeaderMapping!]
+ }
+ */
+ const ConnectHTTP = schema.addType(new InputObjectType(this.typeNameInSchema(schema, CONNECT_HTTP)!));
+ ConnectHTTP.addField(new InputFieldDefinition('GET')).type = URLPathTemplate;
+ ConnectHTTP.addField(new InputFieldDefinition('POST')).type = URLPathTemplate;
+ ConnectHTTP.addField(new InputFieldDefinition('PUT')).type = URLPathTemplate;
+ ConnectHTTP.addField(new InputFieldDefinition('PATCH')).type = URLPathTemplate;
+ ConnectHTTP.addField(new InputFieldDefinition('DELETE')).type = URLPathTemplate;
+ ConnectHTTP.addField(new InputFieldDefinition('body')).type = JSONSelection;
+ ConnectHTTP.addField(new InputFieldDefinition('headers')).type =
+ new ListType(new NonNullType(HTTPHeaderMapping));
+ connect.addArgument('http', new NonNullType(ConnectHTTP));
+
+ connect.addArgument('selection', new NonNullType(JSONSelection));
+ connect.addArgument('entity', schema.booleanType(), false);
+
+ /*
+ directive @source(
+ name: String!
+ http: ConnectHTTP
+ ) repeatable on SCHEMA
+ */
+ const source = this.addDirective(schema, SOURCE).addLocations(
+ DirectiveLocation.SCHEMA,
+ );
+ source.repeatable = true;
+ source.addArgument('name', new NonNullType(schema.stringType()));
+
+ /*
+ input SourceHTTP {
+ baseURL: String!
+ headers: [HTTPHeaderMapping!]
+ }
+ */
+ const SourceHTTP = schema.addType(new InputObjectType(this.typeNameInSchema(schema, SOURCE_HTTP)!));
+ SourceHTTP.addField(new InputFieldDefinition('baseURL')).type =
+ new NonNullType(schema.stringType());
+ SourceHTTP.addField(new InputFieldDefinition('headers')).type =
+ new ListType(new NonNullType(HTTPHeaderMapping));
+
+ source.addArgument('http', new NonNullType(SourceHTTP));
+
+ return [];
+ }
+
+ get defaultCorePurpose(): CorePurpose {
+ return 'EXECUTION';
+ }
+}
+
+export const CONNECT_VERSIONS = new FeatureDefinitions(connectIdentity)
+ .add(new ConnectSpecDefinition(new FeatureVersion(0, 1), new FeatureVersion(2, 10)));
+
+registerKnownFeature(CONNECT_VERSIONS);
diff --git a/internals-js/src/specs/coreSpec.ts b/internals-js/src/specs/coreSpec.ts
index aaeb155cb..12fd1989d 100644
--- a/internals-js/src/specs/coreSpec.ts
+++ b/internals-js/src/specs/coreSpec.ts
@@ -117,11 +117,6 @@ export abstract class FeatureDefinition {
.concat(this.typeSpecs().map((spec) => spec.name));
}
- // No-op implementation that can be overridden by subclasses.
- validateSubgraphSchema(_schema: Schema): GraphQLError[] {
- return [];
- }
-
protected nameInSchema(schema: Schema): string | undefined {
const feature = this.featureInSchema(schema);
return feature?.nameInSchema;
@@ -624,7 +619,7 @@ export class FeatureDefinitions
// this._definitions is already sorted with the most recent first
// get the first definition that is compatible with the federation version
// if the minimum version is not present, assume that we won't look for an older version
- const def = this._definitions.find(def => def.minimumFederationVersion ? fedVersion >= def.minimumFederationVersion : true);
+ const def = this._definitions.find(def => def.minimumFederationVersion ? fedVersion.gte(def.minimumFederationVersion) : true);
assert(def, `No compatible definition exists for federation version ${fedVersion}`);
// note that it's necessary that we can only get versions that have the same major version as the latest,
@@ -676,7 +671,7 @@ export class FeatureVersion {
let max: FeatureVersion | undefined;
for (const version of versions) {
- if (!max || version > max) {
+ if (!max || version.gt(max)) {
max = version;
}
}
diff --git a/internals-js/src/specs/federationSpec.ts b/internals-js/src/specs/federationSpec.ts
index 0b8c52542..37084b057 100644
--- a/internals-js/src/specs/federationSpec.ts
+++ b/internals-js/src/specs/federationSpec.ts
@@ -18,7 +18,6 @@ import { INACCESSIBLE_VERSIONS } from "./inaccessibleSpec";
import { AUTHENTICATED_VERSIONS } from "./authenticatedSpec";
import { REQUIRES_SCOPES_VERSIONS } from "./requiresScopesSpec";
import { POLICY_VERSIONS } from './policySpec';
-import { SOURCE_VERSIONS } from './sourceSpec';
import { CONTEXT_VERSIONS } from './contextSpec';
import { COST_VERSIONS } from "./costSpec";
@@ -44,9 +43,6 @@ export enum FederationDirectiveName {
AUTHENTICATED = 'authenticated',
REQUIRES_SCOPES = 'requiresScopes',
POLICY = 'policy',
- SOURCE_API = 'sourceAPI',
- SOURCE_TYPE = 'sourceType',
- SOURCE_FIELD = 'sourceField',
CONTEXT = 'context',
FROM_CONTEXT = 'fromContext',
COST = 'cost',
@@ -135,7 +131,7 @@ export class FederationSpecDefinition extends FeatureDefinition {
this.registerSubFeature(INACCESSIBLE_VERSIONS.getMinimumRequiredVersion(version));
- if (version >= (new FeatureVersion(2, 7))) {
+ if (version.gte(new FeatureVersion(2, 7))) {
this.registerDirective(createDirectiveSpecification({
name: FederationDirectiveName.OVERRIDE,
locations: [DirectiveLocation.FIELD_DEFINITION],
@@ -178,10 +174,6 @@ export class FederationSpecDefinition extends FeatureDefinition {
this.registerSubFeature(POLICY_VERSIONS.find(new FeatureVersion(0, 1))!);
}
- if (version.gte(new FeatureVersion(2, 7))) {
- this.registerSubFeature(SOURCE_VERSIONS.find(new FeatureVersion(0, 1))!);
- }
-
if (version.gte(new FeatureVersion(2, 8))) {
this.registerSubFeature(CONTEXT_VERSIONS.find(new FeatureVersion(0, 1))!);
}
@@ -202,6 +194,7 @@ export const FEDERATION_VERSIONS = new FeatureDefinitions(schema, 'sourceAPI')!;
- }
-
- sourceTypeDirective(schema: Schema) {
- return this.directive(schema, 'sourceType')!;
- }
-
- sourceFieldDirective(schema: Schema) {
- return this.directive(schema, 'sourceField')!;
- }
-
- private getSourceDirectives(schema: Schema) {
- const result: {
- sourceAPI?: DirectiveDefinition;
- sourceType?: DirectiveDefinition;
- sourceField?: DirectiveDefinition;
- } = {};
-
- schema.schemaDefinition.appliedDirectivesOf('link')
- .forEach(linkDirective => {
- const { url, import: imports } = linkDirective.arguments();
- const featureUrl = FeatureUrl.maybeParse(url);
- if (imports && featureUrl && featureUrl.identity === sourceIdentity) {
- imports.forEach(nameOrRename => {
- const originalName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.name;
- const importedName = typeof nameOrRename === 'string' ? nameOrRename : nameOrRename.as || originalName;
- const importedNameWithoutAt = importedName.replace(/^@/, '');
-
- if (originalName === '@sourceAPI') {
- result.sourceAPI = schema.directive(importedNameWithoutAt) as DirectiveDefinition;
- } else if (originalName === '@sourceType') {
- result.sourceType = schema.directive(importedNameWithoutAt) as DirectiveDefinition;
- } else if (originalName === '@sourceField') {
- result.sourceField = schema.directive(importedNameWithoutAt) as DirectiveDefinition;
- }
- });
- }
- });
-
- return result;
- }
-
- override validateSubgraphSchema(schema: Schema): GraphQLError[] {
- const errors = super.validateSubgraphSchema(schema);
- const {
- sourceAPI,
- sourceType,
- sourceField,
- } = this.getSourceDirectives(schema);
-
- if (!(sourceAPI || sourceType || sourceField)) {
- // If none of the @source* directives are present, nothing needs
- // validating.
- return [];
- }
-
- const apiNameToProtocol = new Map();
-
- if (sourceAPI) {
- this.validateSourceAPI(sourceAPI, apiNameToProtocol, errors);
- }
-
- if (sourceType) {
- this.validateSourceType(sourceType, apiNameToProtocol, errors);
- }
-
- if (sourceField) {
- this.validateSourceField(sourceField, apiNameToProtocol, errors);
- }
-
- return errors;
- }
-
- private validateSourceAPI(
- sourceAPI: DirectiveDefinition,
- apiNameToProtocol: Map,
- errors: GraphQLError[],
- ) {
- sourceAPI.applications().forEach(application => {
- const { name, ...rest } = application.arguments();
-
- if (!isValidSourceAPIName(name)) {
- errors.push(ERRORS.SOURCE_API_NAME_INVALID.err(
- `${sourceAPI}(name: ${
- JSON.stringify(name)
- }) must specify name using only [a-zA-Z0-9-_] characters`,
- { nodes: application.sourceAST },
- ));
- }
-
- if (apiNameToProtocol.has(name)) {
- errors.push(ERRORS.SOURCE_API_NAME_INVALID.err(
- `${sourceAPI} must specify unique name (${JSON.stringify(name)} reused)`,
- { nodes: application.sourceAST },
- ));
- }
-
- let protocol: ProtocolName | undefined;
- KNOWN_SOURCE_PROTOCOLS.forEach(knownProtocol => {
- if (rest[knownProtocol]) {
- if (protocol) {
- errors.push(ERRORS.SOURCE_API_PROTOCOL_INVALID.err(
- `${sourceAPI} must specify only one of ${
- KNOWN_SOURCE_PROTOCOLS.join(', ')
- } but specified both ${protocol} and ${knownProtocol}`,
- { nodes: application.sourceAST },
- ));
- }
- protocol = knownProtocol;
- }
- });
-
- if (protocol) {
- apiNameToProtocol.set(name, protocol);
-
- const protocolValue = rest[protocol];
- if (protocolValue && protocol === HTTP_PROTOCOL) {
- const { baseURL, headers } = protocolValue as HTTPSourceAPI;
-
- try {
- new URL(baseURL);
- } catch (e) {
- errors.push(ERRORS.SOURCE_API_HTTP_BASE_URL_INVALID.err(
- `${sourceAPI} http.baseURL ${JSON.stringify(baseURL)} must be valid URL (error: ${e.message})`,
- { nodes: application.sourceAST },
- ));
- }
-
- validateHTTPHeaders(headers, errors, sourceAPI.name);
- }
- } else {
- errors.push(ERRORS.SOURCE_API_PROTOCOL_INVALID.err(
- `${sourceAPI} must specify one protocol from the set {${KNOWN_SOURCE_PROTOCOLS.join(',')}}`,
- { nodes: application.sourceAST },
- ));
- }
- });
- }
-
- private validateSourceType(
- sourceType: DirectiveDefinition,
- apiNameToProtocol: Map,
- errors: GraphQLError[],
- ) {
- sourceType.applications().forEach(application => {
- const { api, selection, ...rest } = application.arguments();
- if (!api || !apiNameToProtocol.has(api)) {
- errors.push(ERRORS.SOURCE_TYPE_API_ERROR.err(
- `${sourceType} specifies unknown api ${api}`,
- { nodes: application.sourceAST },
- ));
- }
-
- const expectedProtocol = apiNameToProtocol.get(api) || HTTP_PROTOCOL;
- const protocolValue = expectedProtocol && rest[expectedProtocol];
- if (expectedProtocol && !protocolValue) {
- errors.push(ERRORS.SOURCE_TYPE_PROTOCOL_INVALID.err(
- `${sourceType} must specify same ${
- expectedProtocol
- } argument as corresponding @sourceAPI for api ${api}`,
- { nodes: application.sourceAST },
- ));
- }
-
- if (protocolValue && expectedProtocol === HTTP_PROTOCOL) {
- const { GET, POST, headers, body } = protocolValue as HTTPSourceType;
-
- if ([GET, POST].filter(Boolean).length !== 1) {
- errors.push(ERRORS.SOURCE_TYPE_HTTP_METHOD_INVALID.err(
- `${sourceType} must specify exactly one of http.GET or http.POST`,
- { nodes: application.sourceAST },
- ));
- } else {
- const urlPathTemplate = (GET || POST)!;
- try {
- // TODO Validate URL path template uses only available @key fields
- // of the type.
- parseURLPathTemplate(urlPathTemplate);
- } catch (e) {
- errors.push(ERRORS.SOURCE_TYPE_HTTP_PATH_INVALID.err(
- `${sourceType} http.GET or http.POST must be valid URL path template (error: ${e.message})`
- ));
- }
- }
-
- validateHTTPHeaders(headers, errors, sourceType.name);
-
- if (body) {
- if (GET) {
- errors.push(ERRORS.SOURCE_TYPE_HTTP_BODY_INVALID.err(
- `${sourceType} http.GET cannot specify http.body`,
- { nodes: application.sourceAST },
- ));
- }
-
- try {
- parseJSONSelection(body);
- // TODO Validate body selection matches the available fields.
- } catch (e) {
- errors.push(ERRORS.SOURCE_TYPE_HTTP_BODY_INVALID.err(
- `${sourceType} http.body not valid JSONSelection (error: ${e.message})`,
- { nodes: application.sourceAST },
- ));
- }
- }
- }
-
- const ast = application.parent.sourceAST;
- switch (ast?.kind) {
- case "ObjectTypeDefinition":
- case "InterfaceTypeDefinition":
- if (!ast.directives?.some(directive => directive.name.value === "key")) {
- errors.push(ERRORS.SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY.err(
- `${sourceType} must be applied to an entity type that also has a @key directive`,
- { nodes: application.sourceAST },
- ));
- }
- try {
- parseJSONSelection(selection);
- // TODO Validate selection is valid JSONSelection for type.
- } catch (e) {
- errors.push(ERRORS.SOURCE_TYPE_SELECTION_INVALID.err(
- `${sourceType} selection not valid JSONSelection (error: ${e.message})`,
- { nodes: application.sourceAST },
- ));
- }
- break;
- default:
- errors.push(ERRORS.SOURCE_TYPE_ON_NON_OBJECT_OR_NON_ENTITY.err(
- `${sourceType} must be applied to object or interface type`,
- { nodes: application.sourceAST },
- ));
- }
- });
- }
-
- private validateSourceField(
- sourceField: DirectiveDefinition,
- apiNameToProtocol: Map,
- errors: GraphQLError[],
- ) {
- sourceField.applications().forEach(application => {
- const { api, selection, ...rest } = application.arguments();
- if (!api || !apiNameToProtocol.has(api)) {
- errors.push(ERRORS.SOURCE_FIELD_API_ERROR.err(
- `${sourceField} specifies unknown api ${api}`,
- { nodes: application.sourceAST },
- ));
- }
-
- const expectedProtocol = apiNameToProtocol.get(api) || HTTP_PROTOCOL;
- const protocolValue = expectedProtocol && rest[expectedProtocol];
- if (protocolValue && expectedProtocol === HTTP_PROTOCOL) {
- const {
- GET, POST, PUT, PATCH, DELETE,
- headers,
- body,
- } = protocolValue as HTTPSourceField;
-
- const usedMethods = [GET, POST, PUT, PATCH, DELETE].filter(Boolean);
- if (usedMethods.length > 1) {
- errors.push(ERRORS.SOURCE_FIELD_HTTP_METHOD_INVALID.err(
- `${sourceField} allows at most one of http.{GET,POST,PUT,PATCH,DELETE}`,
- ));
- } else if (usedMethods.length === 1) {
- const urlPathTemplate = usedMethods[0]!;
- try {
- // TODO Validate URL path template uses only available fields of
- // the type and/or argument names of the field.
- parseURLPathTemplate(urlPathTemplate);
- } catch (e) {
- errors.push(ERRORS.SOURCE_FIELD_HTTP_PATH_INVALID.err(
- `${sourceField} http.{GET,POST,PUT,PATCH,DELETE} must be valid URL path template (error: ${e.message})`
- ));
- }
- }
-
- validateHTTPHeaders(headers, errors, sourceField.name);
-
- if (body) {
- if (GET) {
- errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err(
- `${sourceField} http.GET cannot specify http.body`,
- { nodes: application.sourceAST },
- ));
- } else if (DELETE) {
- errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err(
- `${sourceField} http.DELETE cannot specify http.body`,
- { nodes: application.sourceAST },
- ));
- }
-
- try {
- parseJSONSelection(body);
- // TODO Validate body string matches the available fields of the
- // parent type and/or argument names of the field.
- } catch (e) {
- errors.push(ERRORS.SOURCE_FIELD_HTTP_BODY_INVALID.err(
- `${sourceField} http.body not valid JSONSelection (error: ${e.message})`,
- { nodes: application.sourceAST },
- ));
- }
- }
- }
-
- if (selection) {
- try {
- parseJSONSelection(selection);
- // TODO Validate selection string matches the available fields of
- // the parent type and/or argument names of the field.
- } catch (e) {
- errors.push(ERRORS.SOURCE_FIELD_SELECTION_INVALID.err(
- `${sourceField} selection not valid JSONSelection (error: ${e.message})`,
- { nodes: application.sourceAST },
- ));
- }
- }
-
- // @sourceField is allowed only on root Query and Mutation fields or
- // fields of entity object types.
- const fieldParent = application.parent;
- if (fieldParent.sourceAST?.kind !== Kind.FIELD_DEFINITION) {
- errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err(
- `${sourceField} must be applied to field`,
- { nodes: application.sourceAST },
- ));
- } else {
- const typeGrandparent = fieldParent.parent as SchemaElement;
- if (typeGrandparent.sourceAST?.kind !== Kind.OBJECT_TYPE_DEFINITION) {
- errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err(
- `${sourceField} must be applied to field of object type`,
- { nodes: application.sourceAST },
- ));
- } else {
- const typeGrandparentName = typeGrandparent.sourceAST?.name.value;
- if (
- typeGrandparentName !== "Query" &&
- typeGrandparentName !== "Mutation" &&
- typeGrandparent.appliedDirectivesOf("key").length === 0
- ) {
- errors.push(ERRORS.SOURCE_FIELD_NOT_ON_ROOT_OR_ENTITY_FIELD.err(
- `${sourceField} must be applied to root Query or Mutation field or field of entity type`,
- { nodes: application.sourceAST },
- ));
- }
- }
- }
- });
- }
-}
-
-function isValidSourceAPIName(name: string): boolean {
- return /^[a-z-_][a-z0-9-_]*$/i.test(name);
-}
-
-function isValidHTTPHeaderName(name: string): boolean {
- // https://developers.cloudflare.com/rules/transform/request-header-modification/reference/header-format/
- return /^[a-zA-Z0-9-_]+$/.test(name);
-}
-
-function validateHTTPHeaders(
- headers: HTTPHeaderMapping[] | undefined,
- errors: GraphQLError[],
- directiveName: string,
-) {
- if (!directiveName.startsWith('@')) {
- directiveName = '@' + directiveName;
- }
- if (headers) {
- headers.forEach(({ name, as, value }, i) => {
- // Ensure name is a valid HTTP header name.
- if (!isValidHTTPHeaderName(name)) {
- errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err(
- `${directiveName} header ${JSON.stringify(headers[i])} specifies invalid name`,
- ));
- }
-
- if (as && !isValidHTTPHeaderName(as)) {
- errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err(
- `${directiveName} header ${JSON.stringify(headers[i])} specifies invalid 'as' name`,
- ));
- }
-
- if (as && value) {
- errors.push(ERRORS.SOURCE_HTTP_HEADERS_INVALID.err(
- `${directiveName} header ${JSON.stringify(headers[i])} should specify at most one of 'as' or 'value'`,
- ));
- }
-
- // TODO Validate value is valid HTTP header value?
- });
- }
-}
-
-function parseJSONSelection(_selection: string): any {
- // TODO
-}
-
-function parseURLPathTemplate(_template: string): any {
- // TODO
-}
-
-const HTTP_PROTOCOL = "http";
-const KNOWN_SOURCE_PROTOCOLS = [
- HTTP_PROTOCOL,
-] as const;
-type ProtocolName = (typeof KNOWN_SOURCE_PROTOCOLS)[number];
-
-export type SourceAPIDirectiveArgs = {
- name: string;
- http?: HTTPSourceAPI;
-};
-
-export type HTTPSourceAPI = {
- baseURL: string;
- headers?: HTTPHeaderMapping[];
-};
-
-export type HTTPHeaderMapping = {
- name: string;
- as?: string;
- value?: string;
-};
-
-export type SourceTypeDirectiveArgs = {
- api: string;
- http?: HTTPSourceType;
- selection: JSONSelection;
- keyTypeMap?: KeyTypeMap;
-};
-
-export type HTTPSourceType = {
- GET?: URLPathTemplate;
- POST?: URLPathTemplate;
- headers?: HTTPHeaderMapping[];
- body?: JSONSelection;
-};
-
-type URLPathTemplate = string;
-type JSONSelection = string;
-
-type KeyTypeMap = {
- key: string;
- typeMap: {
- [__typename: string]: string;
- };
-};
-
-export type SourceFieldDirectiveArgs = {
- api: string;
- http?: HTTPSourceField;
- selection?: JSONSelection;
- keyTypeMap?: KeyTypeMap;
-};
-
-export type HTTPSourceField = {
- GET?: URLPathTemplate;
- POST?: URLPathTemplate;
- PUT?: URLPathTemplate;
- PATCH?: URLPathTemplate;
- DELETE?: URLPathTemplate;
- body?: JSONSelection;
- headers?: HTTPHeaderMapping[];
-};
-
-export const SOURCE_VERSIONS = new FeatureDefinitions(sourceIdentity)
- .add(new SourceSpecDefinition(new FeatureVersion(0, 1), new FeatureVersion(2, 7)));
-
-registerKnownFeature(SOURCE_VERSIONS);
diff --git a/internals-js/src/supergraphs.ts b/internals-js/src/supergraphs.ts
index da4d52751..a4d637329 100644
--- a/internals-js/src/supergraphs.ts
+++ b/internals-js/src/supergraphs.ts
@@ -43,6 +43,7 @@ export const ROUTER_SUPPORTED_SUPERGRAPH_FEATURES = new Set([
'https://specs.apollo.dev/source/v0.1',
'https://specs.apollo.dev/context/v0.1',
'https://specs.apollo.dev/cost/v0.1',
+ 'https://specs.apollo.dev/connect/v0.1',
]);
const coreVersionZeroDotOneUrl = FeatureUrl.parse('https://specs.apollo.dev/core/v0.1');
@@ -156,7 +157,7 @@ export class Supergraph {
this.containedSubgraphs = extractSubgraphsNamesAndUrlsFromSupergraph(schema);
}
- static build(supergraphSdl: string | DocumentNode, options?: { supportedFeatures?: Set, validateSupergraph?: boolean }) {
+ static build(supergraphSdl: string | DocumentNode, options?: { supportedFeatures?: Set | null, validateSupergraph?: boolean }) {
// We delay validation because `checkFeatureSupport` in the constructor gives slightly more useful errors if, say, 'for' is used with core v0.1.
const schema = typeof supergraphSdl === 'string'
? buildSchema(supergraphSdl, { validate: false })
diff --git a/netlify.toml b/netlify.toml
deleted file mode 100644
index 93c1071b7..000000000
--- a/netlify.toml
+++ /dev/null
@@ -1,19 +0,0 @@
-[build]
- ignore = "exit 0"
-
-[build.environment]
- NODE_VERSION = "16"
-
-[context.deploy-preview.build]
- base = "docs"
- ignore = "git diff --quiet HEAD^ HEAD ."
- command = """\
- cd ../
- rm -rf monodocs
- git clone https://github.com/apollographql/docs --branch main --single-branch monodocs
- cd monodocs
- npm i
- cp -r ../docs local
- DOCS_LOCAL=true npm run build \
- """
- publish = "../monodocs/public"
diff --git a/package-lock.json b/package-lock.json
index 24d28b920..cfcbedb18 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -70,11 +70,11 @@
},
"composition-js": {
"name": "@apollo/composition",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"license": "Elastic-2.0",
"dependencies": {
- "@apollo/federation-internals": "2.9.3",
- "@apollo/query-graphs": "2.9.3"
+ "@apollo/federation-internals": "2.10.0-alpha.4",
+ "@apollo/query-graphs": "2.10.0-alpha.4"
},
"engines": {
"node": ">=14.15.0"
@@ -85,7 +85,7 @@
},
"federation-integration-testsuite-js": {
"name": "apollo-federation-integration-testsuite",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"license": "Elastic-2.0",
"dependencies": {
"graphql-tag": "^2.12.6",
@@ -94,12 +94,12 @@
},
"gateway-js": {
"name": "@apollo/gateway",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"license": "Elastic-2.0",
"dependencies": {
- "@apollo/composition": "2.9.3",
- "@apollo/federation-internals": "2.9.3",
- "@apollo/query-planner": "2.9.3",
+ "@apollo/composition": "2.10.0-alpha.4",
+ "@apollo/federation-internals": "2.10.0-alpha.4",
+ "@apollo/query-planner": "2.10.0-alpha.4",
"@apollo/server-gateway-interface": "^1.1.0",
"@apollo/usage-reporting-protobuf": "^4.1.0",
"@apollo/utils.createhash": "^2.0.0",
@@ -125,7 +125,7 @@
},
"internals-js": {
"name": "@apollo/federation-internals",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"license": "Elastic-2.0",
"dependencies": {
"@types/uuid": "^9.0.0",
@@ -9133,22 +9133,10 @@
"loose-envify": "^1.0.0"
}
},
- "node_modules/ip-address": {
- "version": "9.0.5",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
- "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
- "dependencies": {
- "jsbn": "1.1.0",
- "sprintf-js": "^1.1.3"
- },
- "engines": {
- "node": ">= 12"
- }
- },
- "node_modules/ip-address/node_modules/sprintf-js": {
- "version": "1.1.3",
- "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
- "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="
+ "node_modules/ip": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz",
+ "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
@@ -12604,11 +12592,6 @@
"js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/jsbn": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
- "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
- },
"node_modules/jsdom": {
"version": "16.7.0",
"dev": true,
@@ -15995,15 +15978,14 @@
}
},
"node_modules/socks": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz",
- "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==",
+ "version": "2.7.1",
+ "license": "MIT",
"dependencies": {
- "ip-address": "^9.0.5",
+ "ip": "^2.0.0",
"smart-buffer": "^4.2.0"
},
"engines": {
- "node": ">= 10.0.0",
+ "node": ">= 10.13.0",
"npm": ">= 3.0.0"
}
},
@@ -17859,10 +17841,10 @@
},
"query-graphs-js": {
"name": "@apollo/query-graphs",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"license": "Elastic-2.0",
"dependencies": {
- "@apollo/federation-internals": "2.9.3",
+ "@apollo/federation-internals": "2.10.0-alpha.4",
"deep-equal": "^2.0.5",
"ts-graphviz": "^1.5.4",
"uuid": "^9.0.0"
@@ -17876,11 +17858,11 @@
},
"query-planner-js": {
"name": "@apollo/query-planner",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"license": "Elastic-2.0",
"dependencies": {
- "@apollo/federation-internals": "2.9.3",
- "@apollo/query-graphs": "2.9.3",
+ "@apollo/federation-internals": "2.10.0-alpha.4",
+ "@apollo/query-graphs": "2.10.0-alpha.4",
"@apollo/utils.keyvaluecache": "^2.1.0",
"chalk": "^4.1.0",
"deep-equal": "^2.0.5",
@@ -17909,11 +17891,11 @@
},
"subgraph-js": {
"name": "@apollo/subgraph",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"license": "MIT",
"dependencies": {
"@apollo/cache-control-types": "^1.0.2",
- "@apollo/federation-internals": "2.9.3"
+ "@apollo/federation-internals": "2.10.0-alpha.4"
},
"engines": {
"node": ">=14.15.0"
diff --git a/query-graphs-js/package.json b/query-graphs-js/package.json
index 4f92cceb7..8ce2cc082 100644
--- a/query-graphs-js/package.json
+++ b/query-graphs-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@apollo/query-graphs",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"description": "Apollo Federation library to work with 'query graphs'",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -23,7 +23,7 @@
"node": ">=14.15.0"
},
"dependencies": {
- "@apollo/federation-internals": "2.9.3",
+ "@apollo/federation-internals": "2.10.0-alpha.4",
"deep-equal": "^2.0.5",
"ts-graphviz": "^1.5.4",
"uuid": "^9.0.0"
diff --git a/query-planner-js/package.json b/query-planner-js/package.json
index 22ca42be2..b1b474ff5 100644
--- a/query-planner-js/package.json
+++ b/query-planner-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@apollo/query-planner",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"description": "Apollo Query Planner",
"author": "Apollo ",
"main": "dist/index.js",
@@ -25,8 +25,8 @@
"access": "public"
},
"dependencies": {
- "@apollo/federation-internals": "2.9.3",
- "@apollo/query-graphs": "2.9.3",
+ "@apollo/federation-internals": "2.10.0-alpha.4",
+ "@apollo/query-graphs": "2.10.0-alpha.4",
"@apollo/utils.keyvaluecache": "^2.1.0",
"chalk": "^4.1.0",
"deep-equal": "^2.0.5",
diff --git a/subgraph-js/package.json b/subgraph-js/package.json
index 16aa69954..a91831c64 100644
--- a/subgraph-js/package.json
+++ b/subgraph-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@apollo/subgraph",
- "version": "2.9.3",
+ "version": "2.10.0-alpha.4",
"description": "Apollo Subgraph Utilities",
"main": "dist/index.js",
"types": "dist/index.d.ts",
@@ -25,7 +25,7 @@
},
"dependencies": {
"@apollo/cache-control-types": "^1.0.2",
- "@apollo/federation-internals": "2.9.3"
+ "@apollo/federation-internals": "2.10.0-alpha.4"
},
"peerDependencies": {
"graphql": "^16.5.0"
diff --git a/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts b/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts
index 223253f3b..58243240c 100644
--- a/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts
+++ b/subgraph-js/src/__tests__/buildSubgraphSchema.test.ts
@@ -439,6 +439,112 @@ describe('buildSubgraphSchema', () => {
}
`);
});
+
+ it('skips `resolveType` if `resolveReference` returns null', async () => {
+ const query = `#graphql
+ query Product {
+ _entities(representations: [{ __typename: "Product", key: "unknown" }]) {
+ __typename
+ ... on Product {
+ type
+ key
+ }
+ }
+ }
+ `;
+ const schema = buildSubgraphSchema([
+ {
+ typeDefs: gql`
+ extend schema
+ @link(
+ url: "https://specs.apollo.dev/federation/v2.8"
+ import: ["@key"]
+ )
+
+ enum ProductType {
+ A
+ B
+ }
+
+ interface Product @key(fields: "key") {
+ type: ProductType!
+ key: String!
+ }
+
+ type ProductA implements Product @key(fields: "key") {
+ type: ProductType!
+ key: String!
+ }
+
+ type ProductB implements Product @key(fields: "key") {
+ type: ProductType!
+ key: String!
+ }
+ `,
+ resolvers: {
+ Product: {
+ __resolveReference: async ({ key }: { key: string }) => {
+ if (key === 'a') {
+ return {
+ type: 'A',
+ key,
+ };
+ } else if (key === 'b') {
+ return {
+ type: 'B',
+ key,
+ };
+ } else {
+ return null;
+ }
+ },
+ __resolveType: async (entity: { type: 'A' | 'B' }) => {
+ switch (entity.type) {
+ case 'A':
+ return 'ProductA';
+ case 'B':
+ return 'ProductB';
+ default:
+ throw new Error('Unknown type');
+ }
+ },
+ },
+ ProductA: {
+ __resolveReference: async ({ key }: { key: string }) => {
+ return {
+ type: 'ProductA',
+ key,
+ };
+ },
+ },
+ ProductB: {
+ __resolveReference: async ({ key }: { key: string }) => {
+ return {
+ type: 'ProductB',
+ key,
+ };
+ },
+ },
+ },
+ },
+ ]);
+
+ const { data, errors } = await graphql({
+ schema,
+ source: query,
+ rootValue: null,
+ contextValue: null,
+ variableValues: null,
+ });
+ expect(errors).toBeUndefined();
+ expect(data).toMatchInlineSnapshot(`
+ Object {
+ "_entities": Array [
+ null,
+ ],
+ }
+ `);
+ });
});
describe('_service root field', () => {
diff --git a/subgraph-js/src/types.ts b/subgraph-js/src/types.ts
index 13333e234..ecd1faaa2 100644
--- a/subgraph-js/src/types.ts
+++ b/subgraph-js/src/types.ts
@@ -162,15 +162,20 @@ async function withResolvedType({
info: GraphQLResolveInfo,
callback: (runtimeType: GraphQLObjectType) => PromiseOrValue,
}): Promise {
+ const resolvedValue = await value;
+ if (resolvedValue === null) {
+ return resolvedValue;
+ }
+
const resolveTypeFn = type.resolveType ?? defaultTypeResolver;
- const runtimeType = resolveTypeFn(await value, context, info, type);
+ const runtimeType = resolveTypeFn(resolvedValue, context, info, type);
if (isPromise(runtimeType)) {
return runtimeType.then((name) => (
- callback(ensureValidRuntimeType(name, info.schema, type, value))
+ callback(ensureValidRuntimeType(name, info.schema, type, resolvedValue))
));
}
- return callback(ensureValidRuntimeType(runtimeType, info.schema, type, value));
+ return callback(ensureValidRuntimeType(runtimeType, info.schema, type, resolvedValue));
}
function definedResolveReference(type: GraphQLObjectType | GraphQLInterfaceType): GraphQLReferenceResolver | undefined {
|