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

feat(federation): Implement @core and @join directives to replace CSDL #588

Closed
wants to merge 8 commits into from

Conversation

trevor-scheer
Copy link
Member

WIP

Review by commit for simplicity - Initial baseline commit should make reviewing the changes from CSDL -> Core simpler to think about.

@trevor-scheer trevor-scheer changed the base branch from release-gateway-0.25.0 to main March 19, 2021 17:12
@martijnwalraven
Copy link
Contributor

martijnwalraven commented Mar 21, 2021

I'd suggest just @join__graph(endpoint: "https://foo")? That would allow us to add more arguments later if we need more bits of per-subgraph metadata.

I went with @join__endpoint(serviceName: "foo" url: "https://foo") for now in a09e433. I like endpoint as the directive name because that seems to be in line with what this is specifying. It also keeps open the possibility of including multiple @join__endpoint directives on a graph, if we ever wanted to perform load balancing or failover ourselves (probably something you'd want to leave to a service mesh or other infrastructure though).

We need the serviceName because the join__Graph enum value is uppercased, and that means we can't reverse that to get to the service name if that uses anything else than all lowercase (like productService). (If we decide to go with multiple endpoints, you may also want to use the service name to differentiate between different services for the same graph, like products-A and products-B.)

@@ -661,6 +662,7 @@ export function composeServices(services: ServiceDefinition[]): CompositionResul
return {
schema,
composedSdl: printComposedSdl(schema, services),
coreSchema: printCoreSchema(schema),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to continue to support both the old CSDL and core schema? If so, should we avoid printing both and expose those as separate print functions that act on the schema?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider composedSdl to be something that we ultimately remove, but I think you make a fair point that it's excessive to do both prints. Seems like we have a few reasonable options:

  1. leave as is and just remove it soon (and mark composedSdl as @deprecated?)
  2. remove composedSdl in this PR as a breaking change
  3. composeAndValidate is just responsible for returning the schema, most consumers will then pass that to the printXyz fn of their choice - also breaking. Also, undeprecate schema. Not the most ergonomic end result, but maybe the most future proof?
  4. ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is anyone using composedSdl, or was that added just to support CSDL? What would break if we removed that today?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just returning a schema would only work if we were sure a GraphQLSchema could always fully represent a core schema. I'd love for that to be true, but currently there is no established mechanism for core schema metadata, and we just rely on our custom extensions for composition results. (I can see a world where we define a CoreSchema subclass that has further guarantees about how core schema metadata is represented under extensions, but that's not something we can do in the short term.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, should coreSchema be coreSchemaSDL or coreSDL maybe?

federation-js/src/joinSpec.ts Outdated Show resolved Hide resolved
federation-js/src/service/printCoreSchema.ts Outdated Show resolved Hide resolved
@@ -211,15 +228,15 @@ function printFederationTypeDirectives(type: GraphQLObjectType): string {
const restEntries = Object.entries(restKeys);

return (
`\n @owner(graph: "${ownerService}")` +
`\n @join__owner(graph: ${ownerService.toUpperCase()})` +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may want a separate Graph type (in TypeScript) that would encapsulate metadata about a graph, including the graph name and endpoints. That means we could store the graph name uppercased in there and don't have to remember to do that when printing.

federation-js/src/service/printCoreSchema.ts Outdated Show resolved Hide resolved
Comment on lines 367 to 369
if (serviceName) {
printed += serviceName.toUpperCase();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way this currently works we end up generating invalid SDL when the serviceName is undefined or null (which seems to happen for value types). I tried to fix this in a09e433, but handling value types correctly may require more thought (see 081c59e).

federation-js/src/service/printCoreSchema.ts Outdated Show resolved Hide resolved
The generated core schema was invalid because the `@join__type` directive definition should be repeatable and have `key` as its argument (instead of `requires` and `provides`). We would also generate a `@join__field` directive without a graph name in case `serviceName` was undefined, which would lead to a syntax error when parsing.

This commit also renames `@http` to `@join__endpoint` and includes a directive definition for it (which was missing before).
Copy link
Member Author

@trevor-scheer trevor-scheer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glasser I went ahead and cherry-picked @martijnwalraven's changes (using @join__endpoint). Let me know if you have any concerns with that, but it seemed reasonable to me.

@glasser
Copy link
Member

glasser commented Mar 22, 2021

We need the serviceName because the join__Graph enum value is uppercased

@martijnwalraven Does it have to be uppercased? Does that mean that we are getting the subgraph ID from somewhere (the subgraph name in managed federation, the service list, etc) and then transforming it to uppercase just for the sake of the enum? Could we just... not do that?

Or I guess do we have to do some transform anyway because the service name might not be a valid GraphQL identifier? Are we doing some sort of cleanup somewhere?

Also can we not call it "serviceName"? I thought we are trying to put that terminology aside in favor of "subgraph" as much as possible.

Also, I am not going to super bikeshed this particular point too much, but it doesn't actually feel like the "subgraph name/service name" is a property of the "endpoint" (the server you're talking to) as much as it's a property of the subgraph itself, so it seems like it would go better on @join__graph or @join__subgraph than a @join__endpoint.

name: 'join__Graph',
values: Object.fromEntries(
serviceList.map((service) => [
service.name.toUpperCase(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So specifically we might have to do something more special here (or at an earlier place in the process?) to ensure that these values are (a) valid GraphQL names and (b) are unique after upper-casing.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(no insight onto whether the proper fix if these fail should be reporting an error vs sanitizing/uniquifying. we should make sure though that if we uniquify that we do so in a deterministic fashion... though I guess these enum values technically don't need to be stable across composition runs, it would be nice if they were.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Funny, we talked about the uppercasing issue today. Though it didn't cross our mind that the serviceName must now be a valid graphql name which is a great consideration. If we're going to go to the length of sanitizing/uniquifying, should we just generate the enum value itself with the knowledge that anything important is captured in the directive on it?

Suppose my services:
1products, 2products, etc.

We have to modify them, is there still a point in preserving some semblance of the original or...? I'm not sure, to be honest!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think having the enum values be something a human can look at in the rest of the schema and have associations with is valuable.

Frankly if we don't want to do sanitizing/uniquifying I'd rather we just go back to providing them as strings to the other directives rather than enums... But I think a light dusting of sanitization and uniquification would be reasonable. Something like:

1: Replace all non-alphanumerics with _
2: If it starts with a digit, prefix one _
3: (MAYBE, not sure this actually improves things) Upper-case all letters
4: If any string ends with an underscore followed by a string of digits (_[0-9]+$), add another underscore at the end.
5: In case of conflicts, postfix all strings with another _ and a number. Choose the numbers based on the original ordering of the strings before all translations, 1-indexed.

What's the point of step 4? It's so that if you start with A a A_1 you end up with A_1 A_2 A_1_ rather than the third one colliding with the first one.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is definitely more complicated than I thought it would be. But if we want to use graph names as enum values, we're going to need at least some variant of this algorithm. Uppercasing isn't strictly needed, but using uppercase for enum values is a strong convention in GraphQL, so it does make the SDL more humanly readable.

@martijnwalraven
Copy link
Contributor

Also can we not call it "serviceName"? I thought we are trying to put that terminology aside in favor of "subgraph" as much as possible.

That's a good point. We still use serviceName in our current composition and query planning code, but we should probably avoid that for new surface areas.

Also, I am not going to super bikeshed this particular point too much, but it doesn't actually feel like the "subgraph name/service name" is a property of the "endpoint" (the server you're talking to) as much as it's a property of the subgraph itself, so it seems like it would go better on @join__graph or @join__subgraph than a @join__endpoint.

My concern with @join__graph was that we already have a @join__Graph enum, so that seemed confusing. But thinking about it now, using a lowercased version of the enum name as a directive on values could also be a convention we want to adopt.

That would give us something like this:

enum join__Graph {
    ACCOUNTS @join__graph(name: "accounts" url: "https://foo")
    BOOKS @join__graph(name: "books" url: "https://bar")
}

@trevor-scheer
Copy link
Member Author

trevor-scheer commented Mar 24, 2021

@martijnwalraven and I have agreed to close this PR in favor of the https://github.com/apollographql/federation/compare/typescript-redux branch which has these changes merged and then some. We'll be collaborating on that branch.

Leaving this open because of comments which might still need to be resolved.

trevor-scheer added a commit that referenced this pull request Mar 31, 2021
This commit re-introduces the TypeScript query planner, and adds support for core schema as input. This builds on the work @trevor-scheer has done in #588 to have composition output core schema instead of the previous incarnation of CSDL.

We've verified the output of the query planner through a large number of tests and feel confident these changes shouldn't affect the generated query plans.

@apollo/query-planner currently exposes the same API as before, but we'll change this in a follow-up PR to avoid repeated printing/parsing of schemas and queries.

Any references to CSDL both internally and outward facing have been updated to their new counterpart:
`supergraphSdl`. Supergraph SDL implements both the core and join specs, necessary for powering a router.

Co-authored-by: Trevor Scheer <trevor.scheer@gmail.com>
Co-authored-by: Jesse Rosenberger <git@jro.cc>
@abernix
Copy link
Member

abernix commented Apr 1, 2021

Superseded by (included in) #622

@abernix abernix closed this Apr 1, 2021
trevor-scheer added a commit that referenced this pull request Apr 1, 2021
This commit re-introduces the TypeScript query planner, and adds support for core schema as input. This builds on the work @trevor-scheer has done in #588 to have composition output core schema instead of the previous incarnation of CSDL.

We've verified the output of the query planner through a large number of tests and feel confident these changes shouldn't affect the generated query plans.

@apollo/query-planner currently exposes the same API as before, but we'll change this in a follow-up PR to avoid repeated printing/parsing of schemas and queries.

Any references to CSDL both internally and outward facing have been updated to their new counterpart:
`supergraphSdl`. Supergraph SDL implements both the core and join specs, necessary for powering a router.

Co-authored-by: Trevor Scheer <trevor.scheer@gmail.com>
Co-authored-by: Jesse Rosenberger <git@jro.cc>
@trevor-scheer trevor-scheer deleted the trevor/print-core-schema branch April 1, 2021 17:42
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

Successfully merging this pull request may close these issues.

None yet

4 participants