Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: Federation/stitching/composition/… #61

Open
mxstbr opened this issue Dec 6, 2023 · 4 comments
Open

RFC: Federation/stitching/composition/… #61

mxstbr opened this issue Dec 6, 2023 · 4 comments
Labels
RFC A discussion about a future feature

Comments

@mxstbr
Copy link
Member

mxstbr commented Dec 6, 2023

Summary

Many companies that use GraphQL at scale today use GraphQL Federation, which essentially composes many microservices that expose a part of the GraphQL schema.

When there is a backend team that's already using GraphQL and liking it, being able to stitch their schema into the data layer might be appealing.

Another use case is third-party APIs that are already GraphQL.

Another use case is companies that are already using GraphQL and want to stitch their existing API into Fuse—but I'm unsure why they would use Fuse.js on top of that 🤔

Proposed Solution

Fuse.js could either:

  1. Support composing/connecting subgraphs (which would require us to reimplement Federation)
  2. Support stitching in other GraphQL APIs, including existing supergraphs
@mxstbr mxstbr added the RFC A discussion about a future feature label Dec 6, 2023
@mxstbr mxstbr changed the title RFC: Incremental adoption for federated APIs RFC: Federation/stitching/composition/… Dec 14, 2023
@mxstbr
Copy link
Member Author

mxstbr commented Dec 14, 2023

Related to #89; potentially data sources could also be the solution for stitching at least? 🤔 (probably not true federated subgraph support though) cc @JoviDeCroock

@mxstbr
Copy link
Member Author

mxstbr commented Dec 14, 2023

With existing GraphQL APIs as a migration path to Fuse.js.

CleanShot 2023-12-14 at 16 39 46@2x

https://x.com/jamannnnnn/status/1735319901089267799?s=20

@JoviDeCroock
Copy link
Member

JoviDeCroock commented Dec 16, 2023

There are a few scenario's of where people could have an existing GraphQL API, listing them up here

  • They have an existing GraphQL micro-service
  • They have an existing GraphQL service
  • They have an existing Federation setup

I think the first two could quite easily be solved by leveraging schema-stitching where the existing (micro-)service would be a stitched endpoint with its own executor.

For Federation this becomes a bit more complex as either we become the router, which has a lot of complexities involved as is or we stitch in the router. With the former approach I feel like there isn't much prior art in terms of doing schema-extensions from the router, which in this case we would most likely need to provide value over just using federation... The latter approach I haven't looked too deep into.

By going with the stitching approach we would need to solve the directional issue of connecting both graphs, where in the stitching world this is done by means of programatically specifying the touching points i.e.

stitchSchemas({
  subschemas: [
    {
      schema: schema1,
      merge: {
        Entity: {
          fieldName: 'entityFromSchema1ById',
          selectionSet: '{ id }',
          args: obj => ({ id: obj.id })
        }
      }
    },
    {
      schema: schema2,
      merge: {
        Entity: {
          fieldName: 'entityFromSchema2ById',
          selectionSet: '{ id }',
          args: obj => ({ id: obj.id })
        }
      }
    }
  ],
})

this basically needs us to specify how to merge the same entity of schema1 with the one from schema2 and how each of these schemas queries them. From our point of view we could do this both with node as well as the automatically generated entry-point so in that regards we should have a pretty easy job of creating heuristics for these merges, however it might not be as easy for the unknown schema's coming in and would require a lot of checks in dev mode and a pre-compile mode in build.

From a runtime perspective all of this seems very solve-able, this however brings me to the editor experience, early on in the fuse design process we opted to not expose the SchemaBuilder to the user and instead provide our own set of plugins and expose our own set of functions that we think you need to build a good API.

In theory we can add all these types in the SchemaBuilder generic which we don't have access to or isn't really dynamically insert-able. A solution to that could be that we override the Pothos.defaultTypes in a global type, which would be similar to how we extend the FieldBuilder for the list() as seen here.

Not sure yet about the typing path but this could be a way forward.

Pothos does offer the Add plugin for embedding types, however reasoning more about connecting graphs in stitching it might not be needed as types can be merged, which means that when we create a node or an object that in theory it can just be merged, the only question-mark arising here is how the global-ids will interact...

Sorry for the braindump 😅

@mxstbr
Copy link
Member Author

mxstbr commented Dec 16, 2023

I wonder if we could connect this with #89; conceptually, you could see another GraphQL API (whether a full graph or a subgraph) as a data source for your data layer.

Another thought is that potentially stitching a whole schema into the data layer isn't the right way to think about it. Maybe a way to think about it that'd feel more native would be to say a node can be sourced from a type from another GraphQL API.

That could look something like:

export const GitHubRepositoryNode = node({
  name: 'GitHubRepository',
  datasource: new GraphQLDatasource({
    url: gitHubAPIUrl,
    schema: , // Required if `url` isn't introspectable or something?
    type: 'Repository' // Source for this node is the Repository type of the underlying schema
  }),
  fields: (t) => ({
    // Has access to all scalar fields
    name: t.exposeString('name')
    owner: t.exposeString('owner')
    issue: t.expose('issue', {
      // Tradeoff: Connections to other types requires implementing a node for them, too
      type: GitHubIssue,
    })
  })
})

export const GitHubIssue = node({
  name: 'GitHubIssue',
  datasource: new GraphQLDatasource({
    url: gitHubAPIUrl,
    schema: ,
    type: 'Issue'
  }),
  fields: (t) => ({
    number: t.exposeInt('number')
  })
})

The main tradeoff (as noted in the code comment) is that connections to other types require implementing a node for them, too which is obviously more work than "just" stitching a whole API into another API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
RFC A discussion about a future feature
Projects
None yet
Development

No branches or pull requests

2 participants