-
Notifications
You must be signed in to change notification settings - Fork 257
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
When we see that @provides
specifies an overridden field, remove it from the field selection.
#3191
base: main
Are you sure you want to change the base?
Conversation
… from the field selection.
✅ Docs Preview ReadyNo new or changed pages found. |
🦋 Changeset detectedLatest commit: 12158af The changes in this PR will be included in the next version bump. This PR includes changesets to release 7 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
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. |
@@ -5290,3 +5290,37 @@ describe('@source* directives', () => { | |||
assertCompositionSuccess(result); | |||
}); | |||
}); | |||
|
|||
it('errors on unused @external', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is essentially moved from subgraphValidation.test.ts
@@ -2058,6 +2072,69 @@ class Merger { | |||
}); | |||
} | |||
} | |||
|
|||
private removeOverriddenFieldFromFieldSet <T extends FieldDefinition<ObjectType | InterfaceType> | InputFieldDefinition>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I think the naming is somewhat confusing as this is specifically for @provides
but function name doesnt mention it at all
// now we should have a SelectionSet with an array of _selections the same length as `validSelectionsIndex` | ||
// We should be able to filter out the ones that are invalid and we'll be left with the valid selection string | ||
const selections = selectionSet.selections(); | ||
assert(selections.length === validSelectionsIndex.length, 'selection parsing failed'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be possible to have some overriddenCoordinates
that are applicable to different @provides
? (so there would be no field set filtering required for this @provides
)
if (!metadata.isFieldExternal(field) || metadata.isFieldUsed(field)) { | ||
continue; | ||
} | ||
this.mismatchReporter.pushError(ERRORS.EXTERNAL_UNUSED.err( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This kind of error is not a mismatch, so instead of using mismatch reporter, you should use this.errors.push()
.
this.mismatchReporter.pushError(ERRORS.EXTERNAL_UNUSED.err( | ||
`[${subgraph.name}] Field "${field.coordinate}" is marked @external but is not used in any federation directive (@key, @provides, @requires) or to satisfy an interface;` | ||
+ ' the field declaration has no use and should be removed (or the field should not be @external).', | ||
{ nodes: field.sourceAST }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this was a subgraph validation, the addSubgraphToError()
function would prefix the message with the subgraph name (which you've done above), but it would do other things as well (e.g. add the subgraph name as a key in the AST nodes). It would be easier to call that function here instead of manually copying its logic.
@@ -3561,4 +3657,25 @@ class Merger { | |||
} | |||
return fields; | |||
} | |||
|
|||
private validateAllExternalFieldsUsed(): void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit confused why this particular validation was moved from subgraph validation to (the constructor of) merge validation. My understanding of @external
is that the meaning and validity of it is subgraph-local. Is there a specific problem we're trying to solve here?
// cannot be considered valid in the supgraph | ||
let validSelectionsIndex: boolean[] = []; | ||
|
||
const selectionSet = parseSelectionSet({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two things here:
- It looks like you're only looking for top-level fields in the
@provides
, when we really want to check all the fields. @override
can only be on (non-interface-object) object fields, and the fields being overridden, if they exist, must also be object fields (if the field doesn't exist, it can't be "effectively" implemented by an interface object either).- The effect of this is that when looking at fields
f
in@provides
, you want to find the possible runtime types of that field's parent typeP
, and then for each such possible runtime typeT
, you want to check ifT.f
is inoverriddenCoordinates
. It's not sufficient to just checkP.f
.
- The effect of this is that when looking at fields
In terms of how to do this, I would suggest a similar approach to removeInactiveApplications()
, which modifies the given directive to exclude non-external leaf fields (potentially removing the directive entirely). The gist is:
- Call
this.getFieldSet(source, sourceMeta.providesDirective())
, asaddJoinField()
does, to get the (string) field set given to@provides
. - Call
parseSelectionSet()
to parse that field set into aSelectionSet
(you can usevalidate: false
, since it should already be done by this point). - You'll want to create an analog to
selectsNonExternalLeafField()
, e.g.selectsOverriddenField()
, to check whether the@provides
selection set uses an overridden field at all.- If it doesn't, then you should just return the original
@provides
string. This is in part to avoid unnecessary selection set construction, and in part to avoid unnecessary diffs in the supergraph schema.
- If it doesn't, then you should just return the original
- When it does contain an overridden field, you'll need to call an analog to
withoutNonExternalLeafFields()
that removes overridden fields.- It'll help here to create an analog to
isExternalOrHasExternalImplementations()
that checks whether the field (or one of its implementers) is inoverriddenCoordinates
. You can call that in the analog forselectsNonExternalLeafField()
as well. - Note that the behavior when an empty selection set occurs is to remove the parent selection, which should be fine. When the top-level selection set is empty, this results in the
@provides
being effectively dropped entirely.
- It'll help here to create an analog to
@@ -2045,10 +2055,14 @@ class Merger { | |||
const external = this.isExternal(idx, source); | |||
const sourceMeta = this.subgraphs.values()[idx].metadata(); | |||
const name = this.joinSpecName(idx); | |||
|
|||
// fields in this subgraph that are no longer viable because they've been overridden | |||
const overriddenFields = this.completelyOverriddenFieldMap.get(idx); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that:
- Since the result of overridden field removal may be to remove the
@provides
entirely, this could result in the@join__field
becoming unnecessary. This means the computation of the "updated"@provides
needs to be done before that, so thatneedsJoinField()
can be aware of it. (Might want to put it inFieldMergeContextProperties
.) - In
@join__field
, theusedOverridden
argument is set based on whether the overridden field is "used" (i.e. is in@key
/@requires
/@provides
/@fromContext
or is an object field that implements some interface field in the subgraph). If it's used, then the field still needs to be in the overridden subgraph, but otherwise (for non-progressive overrides) it is effectively dropped from supergraph schema metadata.- This definition of "used" should account for the
@provides
effectively omitting (non-progressive) overridden fields. My suggestion here is that thecollectUsedFields()
function should collect two separate sets ofFieldDefinition
s: one for@provides
, and one for the rest. Both of these should be stored inFederationMetadata
. - The
FederationMetadata.isFieldUsed()
method should check both sets. You should add a new method toFederationMetadata
called e.g.isFieldUsedIgnoringProvides()
that only checks the non-@provides
set. - In
validateOverride()
, when computingoverriddenFieldIsReferenced
, useisFieldUsedIgnoringProvides()
instead ofisFieldUsed()
ifoverrideLabel
is unset.
- This definition of "used" should account for the
I moved the logic that checked to ensure all
@external
directives were used from the subgraph validations into the merge validations. Other than that, I'm part of me is unsure about the assert inside ofremoveOverriddenFieldFromFieldSet
and wants to just return undefined, but I think the test harness should catch if I've created any problems there