Giter Club home page Giter Club logo

schema-stitching-handbook's Introduction

Schema Stitching Handbook

Guided examples of GraphQL Stitching doing useful things. While this book focuses on GraphQL Tools (JavaScript) and its specific capabilities, many of these general schema stitching concepts apply to implementations across languages, see similar projects:

Table of Contents

Installation

From the root directory, run:

yarn install

Foundation

  • Combining local and remote schemas

    • Adding a locally-executable schema.
    • Adding a remote schema, fetched via introspection.
    • Adding a remote schema, fetched from a custom SDL service.
    • Avoiding schema conflicts using transforms.
    • Authorization headers.
    • Basic error handling.
  • Mutations & subscriptions

    • Adding a remote mutation service.
    • Adding a remote subscription service.
    • Adding a subscriber proxy.
  • Single-record type merging

    • Type merging using single-record queries.
    • Query/execution batching.
  • Array-batched type merging

    • Type merging using array queries.
    • Handling array errors.
    • Nullability & error remapping.
  • Merged types with multiple keys

    • Configuring multiple key entry points for a merged type.
  • Nullable merges

    • Selecting nullability for merged fields.
    • Returning nullable and not-nullable results.
  • Custom merge resolvers

    • Using valuesFromResults to normalize resulting query data.
    • Adapting type merging to query through namespaced scopes.
    • Adapting type merging to query through non-root fields.
    • Using batchDelegateToSchema and delegateToSchema.
  • Cross-service interfaces

    • Distributing a GraphQL interface across services.
  • Computed fields

    • Configuring computed fields.
    • Sending complex inputs to subservices.
    • Normalizing subservice deprecations in the gateway.
  • Stitching directives SDL

    • @key directive for type-level selection sets.
    • @merge directive for type merging services.
    • @computed directive for computed fields.
    • @canonical directive for preferred element definitions.

Architecture

  • Hot schema reloading

    • Hot reload of the combined gateway schema (no server restart).
    • Polling for remote subschema changes.
    • Mutations for adding/removing remote subservices.
    • Handling subservice request timeouts.
  • Versioning schema releases

    • Using GitHub API to manage a simple schema registry.
    • Hot reloading from a remote Git registry.
    • Running development and production environments.
  • Continuous Integration (CI) testing

    • Adding test coverage to a stitched schema.
    • Mocking subservices as local test fixtures.
  • Public and private APIs

    • Filtering unwanted fields from the final stitched schema.
    • Serving public (filtered) and private (unfiltered) API versions.

Other Integrations

  • Federation services

    • Integrating Apollo Federation services into a stitched schema.
    • Fetching and parsing Federation SDLs.
  • Subservice languages

    • JavaScript schemas created with:

      • graphql-js
      • nexus
      • type-graphql
    • Ruby schemas created with:

      • Class-based definitions
      • Parsed definitions string
  • GraphQL Upload

    • Adding GraphQL Upload to the gateway server

Appendices

schema-stitching-handbook's People

Contributors

a-ogilvie avatar alesso-x avatar ardatan avatar gmac avatar yaacovcr avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

schema-stitching-handbook's Issues

Proxied services occasionally don't respond

Running the federation-services examples, occasionally the gateway doesn't respond. After some debugging, it seems that the underlying services don't respond to the fetch operation. This is not deterministic. it can work perfectly for 3 consecutive requests and then timeout. It is also not related to a specific service.

Reproduction is very easy:

  1. Clone this repo
  2. Run federation-services (npm run start)
  3. open the Graphiql interface at http://localhost:4000/graphql
  4. run the following query:
query {
  product(upc: "2") {
    price
  }
  user(id: "1") {
    username
  }
}
  1. re-run the query consecutively a few times

NestJS support

Me and @dimatill working on NestJS integration right now. Is it something that you are interested to add here as an example?

Btw, we are trying to get custom directives into the sdl, having some troubles. trying this approach with no luck: subservice-languages/javascript/services/products/schema.ts

Pulling my hairs

Hi,
trying to get some help, slowly getting there. ;-)

I took your basic example, combining local and remote.
It works really neat -but then I started to change the implementation

I simplified to two remote schemas, and the implementation is done with prisma and nexus.

When I stick the two, I get in igraphql

{
  "errors": [
    {
      "message": "Type PartyAccount must define one or more fields."
    },
    {
      "message": "Type Product must define one or more fields."
    }
  ]
}

I checked my code 20 times, replaces introspection with an sdl query. still the same result.

I trapped the fetch call.. my two schemas are pulled properly as:

[server:start:gateway] >>>> SDL
[server:start:gateway] ### This file was generated by Nexus Schema
[server:start:gateway] ### Do not make changes to this file directly
[server:start:gateway] 
[server:start:gateway] scalar DateTime
[server:start:gateway] 
[server:start:gateway] type PartyAccount {
[server:start:gateway]   avatar: String
[server:start:gateway]   createdAt: DateTime!
[server:start:gateway]   id: String!
[server:start:gateway]   lastSeen: DateTime!
[server:start:gateway]   updatedAt: DateTime!
[server:start:gateway]   username: String!
[server:start:gateway] }
[server:start:gateway] 
[server:start:gateway] type Query {
[server:start:gateway]   me: PartyAccount
[server:start:gateway]   partyAccounts(
[server:start:gateway]     """
[server:start:gateway]     list of accounts
[server:start:gateway]     """
[server:start:gateway]     users: [String!]!
[server:start:gateway]   ): [PartyAccount]
[server:start:gateway]   sdl: String
[server:start:gateway] }
[server:start:gateway] 
[server:start:gateway] >>>> SDL
[server:start:gateway] fetching from http://localhost:4002
[server:start:gateway] >>>> SDL
[server:start:gateway] ### This file was generated by Nexus Schema
[server:start:gateway] ### Do not make changes to this file directly
[server:start:gateway] 
[server:start:gateway] scalar DateTime
[server:start:gateway] 
[server:start:gateway] type Product {
[server:start:gateway]   createdAt: DateTime!
[server:start:gateway]   name: String!
[server:start:gateway]   price: Float!
[server:start:gateway]   upc: String!
[server:start:gateway]   updatedAt: DateTime!
[server:start:gateway] }
[server:start:gateway] 
[server:start:gateway] type Query {
[server:start:gateway]   products(
[server:start:gateway]     """
[server:start:gateway]     list of accounts
[server:start:gateway]     """
[server:start:gateway]     upcs: [String!]!
[server:start:gateway]   ): [Product]
[server:start:gateway]   sdl: String
[server:start:gateway] }
[server:start:gateway] 
[server:start:gateway] >>>> SDL

Still the stitching doesn't work...

Then I came across your example with nexus, there is commented code which says:

        // Or:
        //
        // resolve(buildSchema(data._sdl, { assumeValidSDL: true }));
        //
        // `assumeValidSDL: true` is necessary if a code-first schema implements directive
        // usage, either directly or by extensions, but not addition of actual custom
        // directives. Alternatively, a new schema with the directives could be created
        // from the nexus schema using:
        //
        // const newSchema = new GraphQLSchema({
        //   ...originalSchema.toConfig(),
        //   directives: [...originalSchema.getDirectives(), ...allStitchingDirectives]
        // });
        //

Do you think this applies to the above?
I am thinking that it could be the DataTime scalar that breaks the schema parsing.

Just asking if you ever had similar issues,

Sincerely,

Complex authorization with schema stitching

Hi, big thanks first of all to all contributors to graphql-tools and this awesome handbook repo. I was hesitating going the apollo federation route for quite some time and for good reason it appears.

The only big question I have still left is how to handle complex authorization requirements with a schema gateway and the graphql services behind it. If we stick to the shopping scenario, how would one handle the following in a good way?

The desired gateway schema should look similar to this:

type User {
  id: ID!
  ownedStorefronts: [Storefront]!
}

type Storefront {
  id: ID!
  name: String!
  owners: [User]!
  products: [Product]!
}

type Product {
  upc: ID!
  storefront: Storefront!
  name: String!
  price: Float!
  purchasingPrice: Float!
}

type Mutation  {
  updateProductPrice(upc: ID!, price: Float!): Product
}

In this case only users who own the product via the intermediate storefront relation should be able to update its price or see its purchasingPrice. But since the product service has only the storefront foreign ID to work with it knows nothing about users and therefore cannot check if the current user is actually allowed to update the product.

Since the number of products a user can own could become quite large it's not feasable to store this information in the user token I fear.

To handle the logic for the authorization in the gateway service could be possible I guess, but feels wrong too. That's kinda what we want to avoid with schema stitching in the first place, right? ;)

All I can think of right now is to send a request to the other service(s) from inside the product service resolvers. Would that be the best way to handle it or am I missing something obvious?

I hope this is the right place to ask these kind of questions and thanks in advance for any ideas or feedback.

A complex example using type-graphql

Thanks for nice examples.

I am a bit struggling on my services that are built code-first approach with type-graphql to implement all type-merging mechanisms.

Would be nice to convert one of the more complex example e.g. stitch-directives-sdl example to type-graphql only. Its really hard to reverse engineer the code-first approach with plain gql.

Passing headers from Gateway to subservices

Hi @gmac - I was wondering what would be the right way to pass the headers from Gateway to subservices when doing typemerging. I am able to get the headers in the gateway but was wondering the right way to pass it on.

Since I am not using Apollo Link like here: apollographql/apollo-server#737 I was wondering how to do the same with a normal fetch executor also since the executor is getting built when the gateway starts up.

Looked up the examples in this repo but that is not discussed so far. Any tips? Thanks.

Issue with Types when stitching schema

Hi. I tried doing fetchRemoteSDL and retrieving the SDL from the remote service instead of doing introspection as mentioned here: https://github.com/gmac/schema-stitching-demos/blob/master/01-combining-local-and-remote-schemas/index.js#L30 and it worked well.

But there was a typings issue as below:

1

where the object is nothing but same as in the example:

return {
    schema: buildSchema(remoteSchema),
    executor: httpExecutor
  };

Currently, working around this by adding a ts-ignore like this:

subschemas: [
      // @ts-ignore
      {
        schema: authService.schema,
        executor: authService.executor
      }
]

Thanks.

CC: @gmac

Fetch merged type in parallel

Hey gmac, thanks for the quality content!

I'm working on type merging and was wondering why the subschemas cannot be executed in parallel if the ID of the type is known upfront (ie included in the initial user query). I'm running through the basic example on this page https://the-guild.dev/graphql/tools/docs/schema-stitching/stitch-type-merging

What I've noticed is that the gateway schema waits for the response from users service before calling out to the posts service. Given that the user ID is already known, why does the post service have to wait until the user service returns the same ID that was originally included? thanks!

Example of a remote schema with a local delegation to a mutation?

I've been trying to update an older version of graphql-tools to the newest version and running into an issue which I can't find in any documentation, so hoping you might be able to add a simple worked example. The use case is a remote schema which I want to override a mutation for and then delegate to the remote schema's mutation. So the flow would be when the mutation is called, it flows into the local code which then uses delegateToSchema against the remote schema. When I try to do this I end up with the error Error: "Mutation" defined in resolvers, but not in schema so I must be doing something wrong, but the official docs don't really have a worked example and while you have a custom-merge-resolvers that's not quite it either, as that's working only with local schema's and not a remote schema with an executor.

Type Transformation in Stitched Schema Example

Hello,
The combining-local-and-remote-schemas example is not complete. Here looks like a typo, the port is already used for productExecutor on a previous line. Here the transformation on the Rainforest type is incomplete since there is no Rainforest type in this example. Notice here that none of the local services contain a Rainforest type.

I am hoping we can build out an example for using the RenameTypes and RenameRootFields as shown here and here.

Thank you

Possible bug in subscription?

Hi @gmac Thanks for creating this repo, it's so helpful!

I was wondering if the way you are creating the remote subscriber here is the right way. Wouldn't you want to create a separate instance of the client for each subscription request? Currently, the same client is shared for every request here. In this case how can we use different connection params for different users?

connectionParams: {
  userId: context?.userId,
},

Types merge (array of string to another type)

Hi folks,
Thank you for your handbook. It really saves a lot of time.

One more question about type merging. Could you please share your experience one more time?
I'm trying to extend the remote contentful schema. In this schema I have Blog.

type Blog {
  users: [String!]
}

And I have another rest API where I can request users by id. On my server I request schema from contentful and would like to respond:

type User {
  id: String
  name: String
}
type Blog {
  users: [User]
}

I tried to use mergeSchemas.

const contentfulSchema = async () =>
  wrapSchema({
    schema: await introspectSchema(executor),
    executor,
  });

const commonSchema = makeExecutableSchema({
  typeDefs: [blog],
  resolvers,
});
const schema = async () => {
  return mergeSchemas({
    schemas: [await contentfulSchema(), commonSchema],
  });
};

It returns an error: Error: Unable to merge GraphQL type "Blog": Field "users" already defined with a different type. Declared as "String", but you tried to override with "User"
At this moment I stopped with mergeSchemas and move to stitchSchemas.

const schema = async () => {
  const remoteSchema = await introspectSchema(executor);

  return stitchSchemas({
    subschemas: [
      {
        schema: remoteSchema,
        executor,
      },
      {
        schema: commonSchema,
      },
    ],
  });
};

Now I got an error in Appolo:
No type was found for field node { kind: \"Field\", alias: undefined, name: { kind: \"Name\", value: \"id\", loc: [Object] }, arguments: [], directives: [], selectionSet: undefined, loc: [Object] }.
But the type of users was changed to User...

Does it possible to change the type of user from [String!] to [User]?
And what approach do I need to choose? Do I need stitchSchemas? Do I need delegateToSchema and merge type? Maybe do you have any examples? Or an example that is similar to my task?

Not able to create batchDelegateToSchema with stitching directive SDL

I was following your schema-stitching-handbook and it's a great resource. I was trying to migrate from gateway level stitching to stitching directive SDL but unable to convert batchDelegateToSchema to stitching directive. And after debugging I can see dataloader is throwing error because of the key & value length mismatch. Please check the code below.

// Division schema
const typeDefs = `${stitchingDirectivesTypeDefs}

type Division {
  id: ID
  name: String
  districts: [District]
}

type District {
  id: ID
}

type Query {
  divisions(ids: [ID]): [Division] @merge(
    keyField: "id"
    keyArg: "ids"
  ) 
  _sdl: String!
}`

// division resolvers

const resolvers = {
  Division: {
    districts: (division) => ([{id: division.id}])
  },
  Query: {
    divisions: async (_, { ids }) => {
      if (!ids) {
        return divisions;
      }
      return divisions.filter(division => ids.includes(division.id))
    },
    _sdl: () => typeDefs
  }
}

// ------------------------------------------------------------

// District schema
const typeDefs = `
${stitchingDirectivesTypeDefs}

type District {
  id: ID
  name: String
  divisionId: ID
  division: Division
}

type Division {
  id: ID
}

type Query {
  districts(ids: [ID]): [District] @merge(
    keyField: "id"
    keyArg: "ids"
  )
  _sdl: String!
}`

const resolvers = {
    District: {
      division: (district) => ({id: district.divisionId})
    },
    Query: {
      districts: async (_, { ids }) => {
        if (!ids) {
          return districts;
        }
        return districts.filter(district => ids.includes(district.divisionId))
      },
      _sdl: () => typeDefs
    }
  }

While districts works as expected divisions query throws error in dataloader - > https://github.com/graphql/dataloader/blob/3e62fbe7d42b7ab1ec54818a1491cb0107dd828a/src/index.js#L330

# throws error
query divisions{
  divisions {
    id
    name
    districts {
      id
      name
      divisionId
    }
  }
}

# works fine
query districts{
  districts {
    id
    name
    division {
      id
      name
    }
  }
}

Resolver working as expected and returning batch response but dataloader unable to map key value together due to mismatch length.

Am I doing it wrong? I would love to hear your suggestion about this. Thanks in advance.

I have a repo where you can reproduce the issue -> https://github.com/fahadbillah/stitching-directive

Thank you

Greg & Yaacov,

Not an issue, just a big thank you for putting together and maintaining all these examples.
It helps a lot to understand the capabilities, tech and differences with federation.

Sincerely

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.