Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@pothos/plugin-federation

hayes56.7kISC4.4.2TypeScript support: included

A Pothos plugin for implementing apollo federation subGraphs

pothos, giraphql, graphql, schema, typescript, apollo, federation, subgraph

readme

Federation Plugin

A plugin for building subGraphs that are compatible with Apollo Federation 2.

Usage

This page will describe the basics of the Pothos API for federation, but will not cover detailed information on how federation works, or what all the terms on this page mean. For more general information on federation, see the official docs

Install

You will need to install the plugin, as well as the directives plugin (@pothos/plugin-directives) and @apollo/subgraph

yarn add @pothos/plugin-federation @pothos/plugin-directives @apollo/subgraph

You will likely want to install @apollo/server as well, but it is not required if you want to use a different server

yarn add @apollo/server

Setup

import DirectivePlugin from '@pothos/plugin-directives';
import FederationPlugin from '@pothos/plugin-federation';
const builder = new SchemaBuilder({
  // If you are using other plugins, the federation plugin should be listed after plugins like auth that wrap resolvers
  plugins: [DirectivePlugin, FederationPlugin],
});

Defining entities

Defining entities for you schema is a 2 step process. First you will need to define an object type as you would normally, then you can convert that object type to an entity by providing a key (or keys), and a method to load that entity.

const UserType = builder.objectRef<User>('User').implement({
  fields: (t) => ({
    id: t.exposeID('id'),
    name: t.exposeString('name'),
    username: t.exposeString('username'),
  }),
});

builder.asEntity(UserType, {
  key: builder.selection<{ id: string }>('id'),
  resolveReference: (user, users) => users.find(({ id }) => user.id === id),
});

keys are defined using builder.selection. This method MUST be called with a generic argument that defines the types for any fields that are part of the key. key may also be an array. resolveReference will be called with the type used by the key selection.

Entities are Object types that may be extended with or returned by fields in other services. builder.asEntity describes how the Entity will be loaded when used by another services. The key select (or selection) should use the types of scalars your server will produce for inputs. For example, Apollo server will convert all ID fields to strings, even if resolvers in other services returns IDs as numbers.

Extending external entities

External entities can be extended by calling builder.externalRef, and then calling implement on the returned ref.

builder.externalRef takes the name of the entity, a selection (using builder.selection, just like a key on an entity object), and a resolve method that loads an object given a key. The return type of the resolver is used as the backing type for the ref, and will be the type of the parent arg when defining fields for this type. The key also describes what fields will be selected from another service to use as the parent object in resolvers for fields added when implementing the externalRef.

const ProductRef = builder.externalRef(
  'Product',
  builder.selection<{ upc: string }>('upc'),
  (entity) => {
    const product = inventory.find(({ upc }) => upc === entity.upc);

    // extends the entity ({upc: string}) with other product details available in this service
    return product && { ...entity, ...product };
  },
);

ProductRef.implement({
  // Additional external fields can be defined here which can be used by `requires` or `provides` directives
  externalFields: (t) => ({
    price: t.float(),
    weight: t.float(),
  }),
  fields: (t) => ({
    // exposes properties added during loading of the entity above
    upc: t.exposeString('upc'),
    inStock: t.exposeBoolean('inStock'),
    shippingEstimate: t.float({
      // fields can add a `requires` directive for any of the externalFields defined above
      // which will be made available as part of the first arg in the resolver.
      requires: builder.selection<{ weight?: number; price?: number }>('price weight'),
      resolve: (data) => {
        // free for expensive items
        if ((data.price ?? 0) > 1000) {
          return 0;
        }
        // estimate is based on weight
        return (data.weight ?? 0) * 0.5;
      },
    }),
  }),
});

To set the resolvable property of an external field to false, can use builder.keyDirective:

const ProductRef = builder.externalRef(
  'Product',
  builder.keyDirective(builder.selection<{ upc: string }>('upc'), false),
);

Adding a provides directive

To add a @provides directive, you will need to implement the Parent type of the field being provided as an external ref, and then use the .provides method of the returned ref when defining the field that will have the @provides directive. The provided field must be listed as an externalField in the external type.

const UserType = builder.externalRef('User', builder.selection<{ id: string }>('id')).implement({
  externalFields: (t) => ({
    // The field that will be provided
    username: t.string(),
  }),
  fields: (t) => ({
    id: t.exposeID('id'),
  }),
});

const ReviewType = builder.objectRef<Review>('Review');
ReviewType.implement({
  fields: (t) => ({
    id: t.exposeID('id'),
    body: t.exposeString('body'),
    author: t.field({
      // using UserType.provides<...>(...) instead of just UserType adds the provide annotations
      // and ensures the resolved value includes data for the provided field
      // The generic in Type.provides works the same as the `builder.selection` method.
      type: UserType.provides<{ username: string }>('username'),
      resolve: (review) => ({
        id: review.authorID,
        username: usernames.find((username) => username.id === review.authorID)!.username,
      }),
    }),
    product: t.field({
      type: Product,
      resolve: (review) => ({ upc: review.product.upc }),
    }),
  }),
});

Building your schema and starting a server

// Use new `toSubGraphSchema` method to add subGraph specific types and queries to the schema
const schema = builder.toSubGraphSchema({
  // defaults to v2.6
  linkUrl: 'https://specs.apollo.dev/federation/v2.3',
  // defaults to the list of directives used in your schema
  federationDirectives: ['@key', '@external', '@requires', '@provides'],
});

const server = new ApolloServer({
  schema,
});

startStandaloneServer(server, { listen: { port: 4000 } })
  .then(({ url }) => {
    console.log(`🚀 Server ready at ${url}`);
  })
  .catch((error) => {
    throw error;
  });

For a functional example that combines multiple graphs built with Pothos into a single schema see https://github.com/hayes/pothos/tree/main/packages/plugin-federation/tests/example

Printing the schema

If you are printing the schema as a string for any reason, and then using the printed schema for Apollo Federation(submitting if using Managed Federation, or composing manually with rover), you must use printSubgraphSchema(from @apollo/subgraph) or another compatible way of printing the schema(that includes directives) in order for it to work.

Field directives directives

Several federation directives can be configured directly when defining a field includes @shareable, @tag, @inaccessible, and @override.

t.field({
  type: 'String',
  shareable: true,
  tag: ['someTag'],
  inaccessible: true,
  override: { from: 'users' },
});

For more details on these directives, see the official Federation documentation.

interface entities and @interfaceObject

Federation 2.3 introduces new features for federating interface definitions.

You can now pass interfaces to asEntity to defined keys for an interface:

const Media = builder.interfaceRef<{ id: string }>('Media').implement({
  fields: (t) => ({
    id: t.exposeID('id'),
    ...
  }),
});

builder.asEntity(Media, {
  key: builder.selection<{ id: string }>('id'),
  resolveReference: ({ id }) => loadMediaById(id),
});

You can also extend interfaces from another subGraph by creating an interfaceObject:

const Media = builder.objectRef<{ id: string }>('Media').implement({
  fields: (t) => ({
    id: t.exposeID('id'),
    // add new MediaFields here that are available on all implementors of the `Media` type
  }),
});

builder.asEntity(Media, {
  interfaceObject: true,
  key: builder.selection<{ id: string }>('id'),
  resolveReference: (ref) => ref,
});

See federation documentation for more details on interfaceObjects

composeDirective

You can apply the composeDirective directive when building the subgraph schema:

export const schema = builder.toSubGraphSchema({
  // This adds the @composeDirective directive
  composeDirectives: ['@custom'],
  // composeDirective requires an @link directive on the schema pointing the the url for your directive
  schemaDirectives: {
    link: { url: 'https://myspecs.dev/myCustomDirective/v1.0', import: ['@custom'] },
  },
  // You currently also need to provide an actual implementation for your Directive
  directives: [
    new GraphQLDirective({
      locations: [DirectiveLocation.OBJECT, DirectiveLocation.INTERFACE],
      name: 'custom',
    }),
  ],
});

changelog

@pothos/plugin-federation

4.4.2

Patch Changes

  • 20bf95b: Add @pothos/core to peerDependencies

4.4.1

Patch Changes

  • 1622740: update dependencies

4.4.0

Minor Changes

  • 86bd0b1: Add support for federation directives - cost,listsize

4.3.3

Patch Changes

  • cd7f309: Update dependencies

4.3.2

Patch Changes

  • d874bce: Improve inference of multiple interfaces

4.3.1

Patch Changes

  • cf83a15: Support Query type as entity

4.3.0

Minor Changes

  • 456da16: Add support for custom names on Root types

4.2.1

Patch Changes

  • da8d7c4: Fix handling of list items in builder.selection

4.2.0

Minor Changes

  • de28b57: expose hasResolvableKey helper

4.1.0

Minor Changes

  • 27af377: replace eslint and prettier with biome

4.0.1

Patch Changes

  • 9bd203e: Fix graphql peer dependency version to match documented minumum version

4.0.0

Major Changes

Patch Changes

  • c1e6dcb: update readmes

4.0.0-next.1

Patch Changes

  • update readmes

4.0.0-next.0

Major Changes

3.16.1

Patch Changes

  • 1ecea46: revert accidental pinning of graphql peer dependency

3.16.0

Minor Changes

  • 8164cba: Add label option to the @override directive

3.15.0

Minor Changes

  • b62b7ca: revert on asEntity method, and add a new method that accepts a selection and a resolvable boolean

3.14.1

Patch Changes

  • 0debcce: Exclude non resolveAble entities from Entity union

3.14.0

Minor Changes

  • a534c4a: Add a new resolvable option to asEntity to allow setting resolvable: false on an entities key directive

3.13.1

Patch Changes

  • e1b2a69: Fix incorrectly configured exports in package.json

3.13.0

Minor Changes

  • 126cd76: Allow imported federation directives to be manually configured, and only import used directives by default

3.12.0

Minor Changes

  • 0979efd: Update referenced version of Apollo Federation specs to v2.6

3.11.0

Minor Changes

  • 27b0638d: Update plugin imports so that no named imports are imported from files with side-effects

3.10.1

Patch Changes

  • 4c6bc638: Add provinance to npm releases

3.10.0

Minor Changes

  • 487b810a: Add support for @interfaceObject and @composeDirective

3.9.1

Patch Changes

  • dfe34dae: correctly set linkUrl

3.9.0

Minor Changes

  • bce97784: Add linkUrl option to buildSubGraphSchema

3.8.0

Minor Changes

3.7.0

Minor Changes

  • cd1c0502: Add support for nested lists

3.6.4

Patch Changes

  • d4d41796: Update dev dependencies

3.6.3

Patch Changes

  • 6f00194c: Fix an issue with esm import transform

3.6.2

Patch Changes

  • b12f9122: Fix issue with esm build script

3.6.1

Patch Changes

  • d350f842: update dev deps

3.6.0

Minor Changes

  • 4c086637: print deprecated directives in schema sdl field

3.5.4

Patch Changes

  • 9fa27cf7: Transform dynamic type imports in d.ts files

3.5.3

Patch Changes

  • 3a82d645: Apply esm transform to esm d.ts definitions

3.5.2

Patch Changes

  • 218fc68b: Fix script for copying ems d.ts definitions

3.5.1

Patch Changes

  • 67531f1e: Create separate typescript definitions for esm files

3.5.0

Minor Changes

  • 11929311: Update type definitions to work with module: "nodeNext"

3.4.3

Patch Changes

  • aa18acb7: update dev dependencies

3.4.2

Patch Changes

3.4.1

Patch Changes

  • 3ead60ae: update dev deps

3.4.0

Minor Changes

  • 3a7ff291: Refactor internal imports to remove import cycles

Patch Changes

  • 3a7ff291: Update dev dependencies

3.3.0

Minor Changes

  • c8ed6b03: Add @shareable @tag @inaccessible and @override directives

3.2.1

Patch Changes

  • 7311904e: Update dev deps

3.2.0

Minor Changes

  • c8f75aa1: Fix compatibility with latest @apollo/subgraph

Patch Changes

  • c8f75aa1: Update dev dependencies

3.1.1

Patch Changes

  • 4e5756ca: Update dev dependencies

3.1.0

Minor Changes

  • ecb2714c: Add types entry to export map in package.json and update dev dependencies

    This should fix compatibility with typescripts new "moduleResolution": "node12"

3.0.0

Major Changes

  • 971f1aad: Initial stable release

Patch Changes

  • 971f1aad: Update dev dependencies

0.4.0

Minor Changes

  • 21a2454e: Update to apollo dependencies to preview.9 which includes some important fixes for prisma and introspection
  • 6279235f: Update build process to use swc and move type definitions to dts directory

Patch Changes

  • 21a2454e: update dev dependencies

0.3.0

Minor Changes

  • 9b6353d4: Add support for context and info in resolveReference

0.2.5

Patch Changes

  • ad8d119b: update dev dependencies

0.2.4

Patch Changes

  • 03aecf76: update .npmignore

0.2.3

Patch Changes

  • 43ca3031: Update dev dependencies

0.2.2

Patch Changes

  • 2d9b21cd: Use workspace:* for dev dependencies on pothos packages

0.2.1

Patch Changes

  • 84d339e0: Update readme

0.2.0

Minor Changes

  • 4094e70a: Add initial support for new federation plugin