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

Schema Stitching Part 1 #341

Closed
michaelstaib opened this issue Nov 13, 2018 · 11 comments
Closed

Schema Stitching Part 1 #341

michaelstaib opened this issue Nov 13, 2018 · 11 comments
Assignees
Milestone

Comments

@michaelstaib
Copy link
Member

michaelstaib commented Nov 13, 2018

Schema Stitching with Hot Chocolate

Hot Chocolate allows schema stitching through directives. There are basically two directives needed in the first proto-type.

  • @schema
    The schema directive tells the stitching API to which schema/executer a field belongs.

  • @delegate
    The delegate directive is a executable directive and basically describes how to fetch data from the other schema.

We call the schema that the Hot Chocolate is providing to the outside world local schema and the schemas to which Hot Chocolate is delegating a remote schema.

A remote schema is represented to the stitching API as IQueryExecuter and can basically be another local schema, a schema hosted by another GraphQL server over HTTP, a rest API, a database or even a file on your hard drive.

The stitching API can request a specific IQueryExecuter by name through the IStitchingContext.

So, lets look at a simple example:

We have two schemas that we want to stitch together.

Remote Schema A

type Query { 
  foo: Foo 
}

type Foo { 
  name: String 
}

Remote Schema B

type Query {
  bar: Bar 
}

type Bar { 
  name: String 
}

We now want our local schema to look like the following:

type Query {
  foo: Foo
}

type Foo { 
  name: String 
  bar: Bar
}

type Bar { 
  name: String 
}

So, this schema is very easy to stitch. We just have to add a view directives to that and the query engine will do the rest.

type Query {
  foo: Foo
    @schema(name: "a")
    @delegate
}

type Foo @schema(name: "a") { 
  name: String 
  bar: Bar 
    @schema(name: "b")
    @delegate
}

type Bar @schema(name: "b") { 
  name: String 
}

@delegate basically delegates the query to the annotated schema. By default delegate will delegate the request to that field to a query with the name of the field. In our query the local schema woul create the following query to fetch the field:

{
    bar {
        name
    }
}

But what when we have to fetch bar with the name of the Foo type from a deeper structure. In such a case we can use the delegate path attribute:

@delegate(path: "foo(name: {parent.name}).bar(skip:1 take:2)")

if there had been a delegate like thsat than the query to the remote schema would have looked like the following:

{
    foo(name: "foo") {
        bar(skip:1 take:2) {
            name
        }
    }
}

We than take the data that is in bar and provide that to the query engine for integration.
In further iterations we will add more directives that are able to transform the resulting data.

So, how would we set this schema up? First, lets setup our stitching context:

string schema_a = @"
    type Query { foo: Foo }
    type Foo { name: String }";

string schema_b = @"
    type Query { bar: Bar }
    type Bar { name: String }";

var schemas = new Dictionary<string, IQueryExecuter>();

schemas["a"] = new QueryExecuter(
    Schema.Create(schema_a, c => c.Use(next => context =>
    {
        context.Result = "foo";
        return Task.CompletedTask;
    })));

schemas["b"] = new QueryExecuter(
    Schema.Create(schema_b, c => c.Use(next => context =>
    {
        context.Result = "bar";
        return Task.CompletedTask;
    })));

var stitchingContext = new StitchingContext(schemas);

So, in our example we specified two schemas with the graphql SDL. These schemas have a middleware that just returns foo or bar for every field. These schemas will act as our remote schemas. Each schema is referenced in a dictionary under a name that we will use in our local schema to refer to these remote schemas. The dictionary is than passed into out schema context.

So next we will setup our local schema that will stitch those two schemas together:

string schema_stitched = @"
    type Query {
        foo: Foo
            @schema(name: ""a"")
            @delegate
    }

    type Foo @schema(name: ""a"") { 
        name: String 
        bar: Bar 
            @schema(name: ""b"")
            @delegate
    }

    type Bar @schema(name: ""b"") { 
        name: String 
    }";

ISchema schema = ISchema schema = Schema.Create(schema_stitched, c =>
{
    c.AddStitching(stitchingContext);
});

That is basically it. But, there is more ... you can like with every local schema override parts of your fields with resolvers etc. So, if you would like to add local calculated fields you can do so.

string schema_stitched = @"
    type Query {
        foo: Foo
            @schema(name: ""a"")
            @delegate
    }

    type Foo @schema(name: ""a"") { 
        name: String 
        bar: Bar 
            @schema(name: ""b"")
            @delegate
    }

    type Bar @schema(name: ""b"") { 
        name: String 
        local: String
    }";

ISchema schema = ISchema schema = Schema.Create(schema_stitched, c =>
{
    c.AddStitching(stitchingContext);
    c.BindResolver(() => "baz").To("Bar", "local");
});

Or you could use directives to transform your results even further. Since our directives are working like a pipeline add your transforming directives to the and... Lets say we have a directive that makes every thing upper string.

string schema_stitched = @"
    type Query {
        foo: Foo
            @schema(name: ""a"")
            @delegate
    }

    type Foo @schema(name: ""a"") { 
        name: String 
        bar: Bar 
            @schema(name: ""b"")
            @delegate
            @upper
    }

    type Bar @schema(name: ""b"") { 
        name: String 
        local: String
    }";

ISchema schema = ISchema schema = Schema.Create(schema_stitched, c =>
{
    c.AddStitching(stitchingContext);
    c.BindResolver(() => "baz").To("Bar", "local");
});

So, this is basically version 1 of the Hot Chocolate schema stitching.

@michaelstaib
Copy link
Member Author

michaelstaib commented Nov 13, 2018

  • SyntaxRewriter SyntaxRewrite #342
  • QueryBroker - Simple Cases
  • QueryBroker - Path Support
  • Http Schema Query Executer
  • Path Parser/Lexer

@michaelstaib michaelstaib changed the title Schema Stitching: Merge remote schemas without conflicts Schema Stitching Part 1 Nov 29, 2018
@michaelstaib
Copy link
Member Author

Path Spec

The path components are separated by .s. So a.b.c would lead to the following query structure:

{
  a {
    b {
      c {
       .....
      }
    }
  }
}

Variables in the path are marked by curly braces {variable}.

The following variables exist:

  • variable.name -> {variable.foo} takes a variable from the original query and inserts it.
  • parent.field -> {parent.field} this access a field from the parent query. It is possible to access even fields from the parent type that are not selected by the original query. Moreover, then query engine will figure out the order in which to fetch what.
  • property.name -> {property.name} accesses the custom request properties.

For version one this is basically what can be accessed.

@michaelstaib
Copy link
Member Author

Originally we wanted only to support remote schemas through IQueryExecuter. Since, we have covered more ground with our own use case for it we are now expanding and letting a remote schema be represented through ISchema with the default IQueryExecuter.

This will give us the ability to write custom middleware for the remote schema that can convert types and/or other stuff.

@michaelstaib
Copy link
Member Author

The path variable are now specified like the following:

$fields:foo -> accesses the field of the type for which a field is resolved.
$arguments:bar -> accesses the argument on the field that is being resolved
$variables:baz-> accesses the query variables

@michaelstaib
Copy link
Member Author

michaelstaib commented Jan 21, 2019

Remote Schemas

A remote schema is basically an external GraphQL schema to which the stitching layer will delegate queries.

A remote schema always has a local schema representation. This local schema representation basically allows us to do all the validation etc. in memory without having to delegate to the server.

Moreover, we do have the type context available to the stitching layer and we can write field middleware that further enhance results if needed.

In order to do that we create a schema that only has a StitchingResolver field middleware. We also add all the scalar types that we have referenced in our schema sdl.
Our schema would not be able to execute with the default query pipeline. In order to now create remote executor that fetches data from a GraphQL endpoint we have to use the stitching pipeline and give our schema a name.

ISchema schema = Schema.Create(
    FileResource.Open("Contract.graphql"),
    c =>
    {
        c.RegisterType<DateTimeType>();
        c.UseStitchingResolver();
    });

IQueryExecutor executor = schema.MakeExecutable(b =>
    b.UseStitchingPipeline("foo"));

The schema name, in our case foo, is used to create a http client via the IHttpClientFactory. So, all the tokens and session details can be setup with the client factory and are transparent to the stitching pipeline. The stitching pipeline only swapped out the ExecuteOperationMiddleware for the RemoteQueryMiddleware. This, gives us a much faster validation of the query itself and variables and so on. Moreover, we also now can write field middleware components on our schema or executor.

@michaelstaib
Copy link
Member Author

Subscriptions

We will move stitching subscriptions to the next release. The idea here is that you can include subscriptions from the stitched schemas and then you are able to join in queries into the result.

We need a new issue for that describing this in detail.

@michaelstaib
Copy link
Member Author

michaelstaib commented Jan 22, 2019

Batching

Similar to a DataLoader the stitching layer batches requests to the remote schema in order to reduce requests.

Lets say we have two queries that the stitching layer wants to redirected to a remote schema:

Query A:

query foo($bar: String) {
  foo(bar: $bar)
}

Query B:

query baz($qux: String) {
  baz(qux: $qux)
}

Then the RemoteQueryExecuter will include both like the following:

query fetch_batch($__1_bar: String $__2_qux: String) {
  __1: foo(bar: $bar)
  __2: baz(qux: $qux)
}

Since everything works similar to the DataLoader the consumer will not have to deal with the specifics here and can just consume the result.

The batching will be included with 0.7.0-preview.35.

We will add settings for this one so that the max complexity of a batch can be controlled.

Caching

We are moving the caching mechanisms to release 0.8.0. The caching basically that the same data is not fetched twice from the remote schema. We still haven't worked out all the edges here so we are not including caching into the 0.7.0 release.

@michaelstaib
Copy link
Member Author

michaelstaib commented Jan 22, 2019

Additional Path Functionality

Currently the selection path has now way to specify a type. So, lets say I want to you the node function use by many relay compatible schemas than I would have to specify the type that I want to fetch.

Currently I only can say `node(id:$fields:id).FooBar' but in order to have access to FooBar I would have to specify the type of the node.

Moreover, currently we cannot handle arrays with the path. So, even if I fetch an array with just one element I do not have a way to tell the stitching engine that I want the first or so on.

For the latter we could use additional directives:

type Query {
  foo: String @delegate @skip(length: 1) @take(length: 1) 
}

@michaelstaib
Copy link
Member Author

michaelstaib commented Jan 23, 2019

Previews:
0.7.0-preview.34 -> Stitching without Batching
0.7.0-rc.1 -> Stitching with Batching

@michaelstaib
Copy link
Member Author

#524

@michaelstaib
Copy link
Member Author

#527

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants