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

Fix merging of Input objects and enum types #1672

Merged
merged 2 commits into from
Apr 5, 2022

Conversation

pcmanus
Copy link
Contributor

@pcmanus pcmanus commented Apr 4, 2022

At a high level, the merging guiding principle is meant to be:

  • output types are merged by "union": meaning an element of an output
    types can (in general) be in the supergraph as soon as it is in any
    subgraph. The idea being that it's ok (and desirable) that different
    subgraph can return different "sub-parts" of an output type and the
    query planner is here direct queries appropriately. Of course, in
    practice, the "composition validation" algorithm imposes some limits
    to this based on the reachability of the type.
  • input types are merged by "intersection": meaning that an element
    an input type is only in the supergraph if it is in all the subgraph
    defining the type in question. The idea being that the gateway don't
    control the inputs it gets, and so in general, all subgraphs needs
    be able to handle all the inputs exposed by the supergraph API.

This guiding principle was not implemented properly for both Input
Object types and Enum types and this is fixed by this commit.

Note that:

  • for input object types, this has the consequence of aligning the
    merging of input fields with that of (output) field arguments. Indeed,
    passing multiple arguments is extremely similar semantically to
    passing a single input object having the same fields, so it does not
    make sense for the behiavour of those to differ, and it doesn't
    anymore after this patch.
  • enum types are a special case because they can be use as both output
    types and input types (this is true for scalar too, but scalar merging
    is trivial). The patch makes the behaviour depend on how the enum
    type is used:
    • if it is only ever used as an input type, then we merge it by
      "intersection".
    • if it is only ever used as an output type, then we merge it by
      "union".
    • otherwise, if it used as both, then merging is essentially
      "equality", meaning that we have a composition error unless all
      the subgraphs defined the same values.

@netlify
Copy link

netlify bot commented Apr 4, 2022

👷 Deploy request for apollo-federation-docs pending review.

Visit the deploys page to approve it

Name Link
🔨 Latest commit 54591ad

@codesandbox-ci
Copy link

codesandbox-ci bot commented Apr 4, 2022

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

@@ -226,11 +226,11 @@ export function computeSubgraphPaths(supergraphPath: RootPath<Transition>, subgr
assert(!supergraphPath.hasAnyEdgeConditions(), () => `A supergraph path should not have edge condition paths (as supergraph edges should not have conditions): ${supergraphPath}`);
const supergraphSchema = firstOf(supergraphPath.graph.sources.values())!;
const conditionResolver = new ConditionValidationResolver(supergraphSchema, subgraphs);
const initialState = ValidationState.initial({supergraph: supergraphPath.graph, supergraphSchema, kind: supergraphPath.root.rootKind, subgraphs, conditionResolver});
const initialState = ValidationState.initial({supergraph: supergraphPath.graph, kind: supergraphPath.root.rootKind, subgraphs, conditionResolver});
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to reviewers: the change to this file, to graphPath.ts and the removal of structuralSubtyping.ts are all knock-on effects (cleanups) from the change to matchesSupergraphTransition in queryGraph.ts (see comment there).

const transition = this.transition;
switch (transition.kind) {
case 'FieldCollection':
Copy link
Contributor Author

Choose a reason for hiding this comment

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

What this method was previously trying to do is enforce the "intersection" semantic for input types, but to do that during composition validation. It is, in a way, the part that was broken and is fixed in this patch.

The idea of this code was to merge inputs with a union (so, say, keeping any input type field defined in any subgraph), but then when doing composition validation for a field, to ignore any definition of that field that don't accept the "full" input object as merged in the supergraph API. Which really ends up requiring (in 99.99% of the case) that all the inputs are the same in all subgraphs, but is, in hindsight, an overly complex and confusing way to do this. Which is why this patch removes this and instead deal with input types during merging, which is easier to do.

Now, you can ignore this code since it's removed, but if you're wondering "I can admit this wasn't idea, but why wasn't this working?" then the answer is that there is a typo in the isAccessible method of structuralSubtyping.ts (a missing negation), which essentially made all this code a no-op. What happens if we fix that typo? Well, I tried and run into some assertion errors and I'm sure that assertion could been fixed easily enough but I didn't looked more closely because even if this code ended up working as I intended it to 6+ months ago when I wrote it, it would end up in pretty confusing composition validation error messages and we would have to work hard to make those errors understandable. That's one of the reason why dealing with this during merging is likely way better for input: it's much much easier to provide good error messages.

@@ -129,13 +129,14 @@ class Validator {
return this.errors;
}

private validateHasType(elt: { type?: Type, coordinate: string, sourceAST?: ASTNode }) {
private validateHasType(elt: { type?: Type, coordinate: string, sourceAST?: ASTNode }): boolean {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note to reviewers: the changes to this file are completely unrelated to the rest of the patch. It's just that when I was working on this, I run into this and that was easy enough to fix, so I did.

Namely, this method validates that we don't have some types unset in the schema (which can't really happen when we parse a schema because it's invalid syntax, but if one build a schema programmatically, this could happen), but in a few cases, we were following it by other validations that assumed the type was set. So the change here simply have the method return whether the type is set or not and the other changes in this file simply skip the dependent validation when the type is indeed unset. Concretely, this ensures we get good errors instead of some random "undefined"-access one thrown.

@clenfest
Copy link
Contributor

clenfest commented Apr 5, 2022

Before I get started at a high level, I have no problem with the idea that output types are unions, that makes sense to me. But for input types being intersections, it seems a little weird to me that a subgraph author can define a value that they may expect and to have composition silently remove the value they specify in an enum simply because it's not available elsewhere. Correct me if I'm wrong, but that would mean that it's impossible to invoke a query with that value, yes? Wouldn't we want a hint at the very least?

Edit: I see that there is a hint. I think this is the best we can do.

Copy link
Contributor

@clenfest clenfest left a comment

Choose a reason for hiding this comment

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

Basically one pretty minor comment. If you don't want to fix it now, please add a TODO in the code (not a big enough deal for a ticket).

@@ -356,13 +374,13 @@ class Merger {
// Then, for object and interface types, we merge the 'implements' relationship, and we merge the unions.
// We do this first because being able to know if a type is a subtype of another one (which relies on those
// 2 things) is used when merging fields.
for (const objectType of this.merged.types<ObjectType>('ObjectType')) {
for (const objectType of this.typesToMerge<ObjectType>('ObjectType')) {
Copy link
Contributor

Choose a reason for hiding this comment

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

These should probably be separate functions. this.typesToMerge<ObjectType>('InterfaceType'); is valid code, but could cause typing errors.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh yeah, the fact that we're casting inside of Schema::types before returning is definitely a code smell.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this.typesToMerge<ObjectType>('InterfaceType'); is valid code

I'll note that this isn't really true, that code did not compile (the kind argument has to match the kind of the type parameter because it's type is T['kind']).

That said, that method (and schema::types) was indeed a bit error prone as this.typesToMerge<ObjectType>() was valid code, yet didn't return only the object types. I initially went with it as I figured it would obvious that simply passing a type argument couldn't influence runtime behaviour and it would obvious you should do this, but admittedly not my best moment in hindsight.

Anyway, I cleaned this up a bit to avoid that potential problem, but the code in merge.ts still uses a single filterTypesOfKind<ObjectType>(typesToMerge, 'ObjectType') method because it is type safe now (the kind argument is not optional anymore) and it didn't felt cleaner adding a bunch of single-usage methods.

composition-js/src/merging/merge.ts Show resolved Hide resolved
composition-js/src/merging/merge.ts Outdated Show resolved Hide resolved
composition-js/src/merging/merge.ts Show resolved Hide resolved
Sylvain Lebresne added 2 commits April 5, 2022 10:54
At a high level, the merging guiding principle is meant to be:
- output types are merged by "union": meaning an element of an output
  types can (in general) be in the supergraph as soon as it is in any
  subgraph. The idea being that it's ok (and desirable) that different
  subgraph can return different "sub-parts" of an output type and the
  query planner is here direct queries appropriately. Of course, in
  practice, the "composition validation" algorithm imposes some limits
  to this based on the reachability of the type.
- input types are merged by "intersection": meaning that an element
  an input type is only in the supergraph if it is in all the subgraph
  defining the type in question. The idea being that the gateway don't
  control the inputs it gets, and so in general, all subgraphs needs
  be able to handle all the inputs exposed by the supergraph API.

This guiding principle was not implemented properly for both Input
Object types and Enum types and this is fixed by this commit.

Note that:
- for input object types, this has the consequence of aligning the
  merging of input fields with that of (output) field arguments. Indeed,
  passing multiple arguments is extremely similar semantically to
  passing a single input object having the same fields, so it does not
  make sense for the behiavour of those to differ, and it doesn't
  anymore after this patch.
- enum types are a special case because they can be use as both output
  types and input types (this is true for scalar too, but scalar merging
  is trivial). The patch makes the behaviour depend on how the enum
  type is used:
  - if it is only ever used as an input type, then we merge it by
    "intersection".
  - if it is only ever used as an output type, then we merge it by
    "union".
  - otherwise, if it used as both, then merging is essentially
    "equality", meaning that we have a composition error unless all
    the subgraphs defined the same values.
@pcmanus pcmanus merged commit 4abe6cc into apollographql:main Apr 5, 2022
benweatherman added a commit that referenced this pull request Apr 5, 2022
I noticed a few spelling changes we could make from #1672
benweatherman added a commit that referenced this pull request Apr 5, 2022
I noticed a few spelling changes we could make from #1672
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.

2 participants