-
Notifications
You must be signed in to change notification settings - Fork 1.4k
[typescript-resolvers][federation] Fix federation @requires type #10366
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
base: federation-fixes
Are you sure you want to change the base?
Conversation
🦋 Changeset detectedLatest commit: 39faa7e The changes in this PR will be included in the next version bump. This PR includes changesets to release 10 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 |
🚀 Snapshot Release (
|
Package | Version | Info |
---|---|---|
@graphql-codegen/cli |
5.0.7-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/core |
4.0.3-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/add |
5.0.4-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/fragment-matcher |
5.1.1-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/introspection |
4.0.4-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/schema-ast |
4.1.1-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/time |
5.0.2-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/visitor-plugin-common |
6.0.0-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript-document-nodes |
4.0.17-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/gql-tag-operations |
4.0.18-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript-operations |
4.6.2-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript-resolvers |
5.0.0-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typed-document-node |
5.1.2-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/typescript |
4.1.7-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/client-preset |
4.8.2-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/graphql-modules-preset |
4.0.18-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/testing |
3.0.5-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
@graphql-codegen/plugin-helpers |
6.0.0-alpha-20250612134622-3e749e4a24e17afbaed625e3dbc38b1f1f974ab4 |
npm ↗︎ unpkg ↗︎ |
016484a
to
7295def
Compare
public buildFederationReferenceTypes(): string { | ||
const federationMeta = this._federation.getMeta(); | ||
|
||
if (Object.keys(federationMeta).length === 0) { | ||
return ''; | ||
} | ||
|
||
const declarationKind = 'type'; | ||
return new DeclarationBlock(this._declarationBlockConfig) | ||
.export() | ||
.asKind(declarationKind) | ||
.withName(this.convertName('FederationReferenceTypes')) | ||
.withComment('Mapping of federation reference types') | ||
.withBlock( | ||
Object.entries(federationMeta) | ||
.map(([typeName, { referenceSelectionSetsString }]) => { | ||
if (!referenceSelectionSetsString) { | ||
return undefined; | ||
} | ||
|
||
return indent(`${typeName}: ${referenceSelectionSetsString}${this.getPunctuation(declarationKind)}`); | ||
}) | ||
.filter(v => v) | ||
.join('\n') | ||
).string; | ||
} |
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.
Having buildFederationReferenceTypes
to build standalone FederationReferenceTypes
allows us to easily refer to the complex reference
param types in different scenarios:
- When using mappers:
import type { FederationReferenceTypes } from "./types.generated";
export type UserMapper = DatabaseUser | FederationReferenceTypes["User"]
This allows us to return reference as-is without type errors:
export const User: UserResolvers = {
__resolveReference: (ref) => {
return ref;
},
}
- When not using mappers:
export type ResolversParentTypes = {
User: User | FederationReferenceTypes["User"];
};
Similar to the mapper case, the parent of each normal resolver can receive a normal type or reference type
- And for object resolvers:
export type UserResolvers<
ContextType = ServerContext,
ParentType extends
ResolversParentTypes["User"] = ResolversParentTypes["User"],
FederationReferenceType extends
FederationReferenceTypes["User"] = FederationReferenceTypes["User"],
> = {
__resolveReference?: ReferenceResolver<
Maybe<ResolversTypes["User"]> | FederationReferenceType,
FederationReferenceType,
ContextType
>;
};
Again, FederationReferenceTypes["User"]
is used:
- As
reference
type (2nd generic param) - and
__resolveReference
must return: (1)User
value normally, or (2)FederationReferenceType
const parentTypeSignature = this._federation.transformFieldParentType({ | ||
fieldNode: original, | ||
parentType, | ||
parentTypeSignature: this.getParentTypeForSignature(node), | ||
federationTypeSignature: 'FederationType', | ||
}); |
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.
Since federation reference type is extracted to FederationReferenceTypes, we don't need complex, inline reference types.
So, transformFieldParentType
can be completely removed 🎉
printReferenceSelectionSet({ | ||
typeName, | ||
referenceSelectionSet, | ||
}: { | ||
typeName: string; | ||
referenceSelectionSet: ReferenceSelectionSet; | ||
}): string { | ||
return `GraphQLRecursivePick<${typeName}, ${JSON.stringify(referenceSelectionSet)}>`; | ||
} | ||
|
||
printReferenceSelectionSets({ | ||
typeName, | ||
baseFederationType, | ||
}: { | ||
typeName: string; | ||
baseFederationType: string; | ||
}): string | false { | ||
const federationMeta = this.getMeta()[typeName]; | ||
|
||
if (!federationMeta?.hasResolveReference) { | ||
return false; | ||
} | ||
|
||
return `\n ( { __typename: '${typeName}' }\n & ${federationMeta.referenceSelectionSets | ||
.map(referenceSelectionSetArray => { | ||
const result = referenceSelectionSetArray.map(referenceSelectionSet => { | ||
return this.printReferenceSelectionSet({ | ||
referenceSelectionSet, | ||
typeName: baseFederationType, | ||
}); | ||
}); | ||
|
||
return result.length > 1 ? `( ${result.join(' | ')} )` : result.join(' | '); | ||
}) | ||
.join('\n & ')} )`; | ||
} |
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 is now moved to addFederationReferencesToSchema
because it's only used there.
I'm thinking to create a addFederationReferencesToSchema.ts
file to shorten this file.
addFederationReferencesToSchema
started as the function to mutate the schema (to re-use a lot of core plugin's functionalities).
However, since it is run once at the start on each interface and object, it is convenient to gather metadata (e.g. reference type) and do stuff with said metadata (e.g. turn reference metadata to TypeScript type)
interface DirectiveSelectionSet { | ||
name: string; | ||
selection: boolean | ReferenceSelectionSet[]; | ||
selection: boolean | DirectiveSelectionSet[]; | ||
} | ||
|
||
type ReferenceSelectionSet = Record<string, boolean>; // TODO: handle nested |
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 now understand extractReferenceSelectionSet
better. It is a series of functions that turn GraphQL selection set -> DirectiveSelectionSet
-> ReferenceSelectionSet
The previously named ReferenceSelectionSet
is now correctly named DirectiveSelectionSet
(It's only the immediary step)
7295def
to
44802c0
Compare
/** | ||
* Function to find all combinations of selection sets and push them into the `result` | ||
* This is used for `@requires` directive because depending on the operation selection set, different | ||
* combination of fields are sent from the router. | ||
* | ||
* @example | ||
* Input: [ | ||
* { a: true }, | ||
* { b: true }, | ||
* { c: true }, | ||
* { d: true}, | ||
* ] | ||
* Output: [ | ||
* { a: true }, | ||
* { a: true, b: true }, | ||
* { a: true, c: true }, | ||
* { a: true, d: true }, | ||
* { a: true, b: true, c: true }, | ||
* { a: true, b: true, d: true }, | ||
* { a: true, c: true, d: true }, | ||
* { a: true, b: true, c: true, d: true } | ||
* | ||
* { b: true }, | ||
* { b: true, c: true }, | ||
* { b: true, d: true }, | ||
* { b: true, c: true, d: true } | ||
* | ||
* { c: true }, | ||
* { c: true, d: true }, | ||
* | ||
* { d: true }, | ||
* ] | ||
* ``` | ||
*/ | ||
function findAllSelectionSetCombinations( | ||
selectionSets: ReferenceSelectionSet[], | ||
result: ReferenceSelectionSet[] | ||
): void { | ||
if (selectionSets.length === 0) { | ||
return; | ||
} | ||
|
||
for (let baseIndex = 0; baseIndex < selectionSets.length; baseIndex++) { | ||
const base = selectionSets.slice(0, baseIndex + 1); | ||
const rest = selectionSets.slice(baseIndex + 1, selectionSets.length); | ||
|
||
const currentSelectionSet = base.reduce((acc, selectionSet) => { | ||
acc = { ...acc, ...selectionSet }; | ||
return acc; | ||
}, {}); | ||
|
||
if (baseIndex === 0) { | ||
result.push(currentSelectionSet); | ||
} | ||
|
||
for (const selectionSet of rest) { | ||
result.push({ ...currentSelectionSet, ...selectionSet }); | ||
} | ||
} | ||
|
||
const next = selectionSets.slice(1, selectionSets.length); | ||
|
||
if (next.length > 0) { | ||
findAllSelectionSetCombinations(next, result); | ||
} | ||
} |
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.
findAllSelectionSetCombinations
is core to @requires
(based on my understanding of this directive, feel free to correct if I misunderstood it):
If a field is marked with @requires
:
- and a client requests for said field, then the reference object contains fields in the selection set declared by
@requires
- otherwise, if the client doesn't request for said field, the declared selection set won't be in the reference object
This means when there are multiple fields with @requires
, the selection set fields can appear in any combination.
For example:
type Foo @key(fields: "id") {
id: ID!
a: String @external
aRequires: String @requires(fields: "a")
b: String @external
bRequires: String @requires(fields: "b")
c: String @external
cRequires: String @requires(fields: "c")
}
- If a client doesn't request for
aRequires
,bRequires
, orcRequires
, then the reference shape is{ __typename: 'Foo', id: 'something' }
- If a client requests for
aRequires
, then the reference shape is{ __typename: 'Foo', id: 'something', a: 'a-value' }
- If a client requests for
aRequires
andbRequires
, then the reference shape is{ __typename: 'Foo', id: 'something', a: 'a-value', b: 'b-value' }
- If a client requests for
aRequires
andcRequires
, then the reference shape is{ __typename: 'Foo', id: 'something', a: 'a-value', c: 'c-value' }
- etc.
findAllSelectionSetCombinations
will find all the combination of selection sets declared for aRequires
, bRequires
and cRequires
Description
FederationReferenceTypes
to declare the type of references of each object. This is used by all generated types, and can be used in mappers. More details in comment@requires
in different fields and put them inFederationReferenceTypes
. More details in commentRelated #10206
Type of change
How Has This Been Tested?