Enforcing entity ownership in Apollo Federation
Designating entity ownership in Apollo Federation 2
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.
type Product @key(fields: "id") {id: ID!name: String}
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 inconsistentdescriptions across subgraphs. The supergraph will use description(from subgraph "one"):"""The Product type lorem ipsum dolar sit amet."""In subgraph "two", the description is:"""This is my description of the Product type."""
ⓘ NOTE
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
functionality introduced in Federation 2.2.
extend schema@link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@composeDirective"])@link(url: "https://graphql.mycompany.dev/owner/v1.0")@composeDirective(name: "@owner")directive @owner(team: String!) on OBJECTtype 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.
Writing a lint rule using the @owner
directive
Here's an example of a @graphql-eslint
rule for subgraph schemas that uses the @owner
directive to determine if a description is required:
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.
Add a
pull_request
workflow:.github/workflows/add-reviewers.yamlname: Add required reviewers for owned GraphQL typeson: [pull_request]Determine the affected types in the schema change:
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"}]*/Obtain the supergraph schema.
You can use
rover supergraph fetch
or retrieve it using the Apollo Platform API.Extract the owners for the affected types:
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);}}Add the team as reviewers on the pull request:
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,});