Skip to content

Commit

Permalink
Use _federation to keep track of its meta
Browse files Browse the repository at this point in the history
  • Loading branch information
eddeee888 committed Aug 25, 2024
1 parent 89be355 commit 18a679b
Show file tree
Hide file tree
Showing 5 changed files with 214 additions and 48 deletions.
6 changes: 5 additions & 1 deletion .changeset/fifty-dodos-marry.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@
'@graphql-codegen/plugin-helpers': minor
---

Avoid generating reference resolvers if federation object is not resolvable
Add `generateInternalResolversIfNeeded` option

This option can be used to generate more correct types for internal resolvers. For example, only generate `__resolveReference` if the federation object has a resolvable `@key`.

In the future, this option can be extended to support other internal resolvers e.g. `__isTypeOf` is only generated for implementing types and union members.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
ConvertOptions,
DeclarationKind,
EnumValuesMap,
type NormalizedGenerateInternalResolversIfNeededConfig,
type GenerateInternalResolversIfNeededConfig,
NormalizedAvoidOptionalsConfig,
NormalizedScalarsMap,
ParsedEnumValuesMap,
Expand Down Expand Up @@ -75,6 +77,7 @@ export interface ParsedResolversConfig extends ParsedConfig {
resolverTypeSuffix: string;
allResolversTypeName: string;
internalResolversPrefix: string;
generateInternalResolversIfNeeded: NormalizedGenerateInternalResolversIfNeededConfig;
onlyResolveTypeForInterfaces: boolean;
directiveResolverMappings: Record<string, string>;
resolversNonOptionalTypename: ResolversNonOptionalTypenameConfig;
Expand Down Expand Up @@ -577,6 +580,16 @@ export interface RawResolversConfig extends RawConfig {
* If you are using `mercurius-js`, please set this field to empty string for better compatibility.
*/
internalResolversPrefix?: string;
/**
* @type object
* @default { __resolveReference: false }
* @description If relevant internal resolvers are set to `true`, the resolver type will only be generated if the right conditions are met.
* Enabling this allows a more correct type generation for the resolvers.
* For example:
* - `__isTypeOf` is generated for implementing types and union members
* - `__resolveReference` is generated for federation types that have at least one resolvable `@key` directive
*/
generateInternalResolversIfNeeded?: GenerateInternalResolversIfNeededConfig;
/**
* @type boolean
* @default false
Expand Down Expand Up @@ -652,7 +665,6 @@ export class BaseResolversVisitor<
[key: string]: {
typename: string;
baseGeneratedTypename?: string;
federation?: { hasResolveReference: boolean };
};
} = {};
protected _collectedDirectiveResolvers: { [key: string]: string } = {};
Expand All @@ -669,7 +681,6 @@ export class BaseResolversVisitor<
protected _globalDeclarations = new Set<string>();
protected _federation: ApolloFederation;
protected _hasScalars = false;
protected _hasFederation = false;
protected _fieldContextTypeMap: FieldContextTypeMap;
protected _directiveContextTypesMap: FieldContextTypeMap;
protected _checkedTypesWithNestedAbstractTypes: Record<string, { checkStatus: 'yes' | 'no' | 'checking' }> = {};
Expand Down Expand Up @@ -709,6 +720,9 @@ export class BaseResolversVisitor<
mappers: transformMappers(rawConfig.mappers || {}, rawConfig.mapperTypeSuffix),
scalars: buildScalarsFromConfig(_schema, rawConfig, defaultScalars),
internalResolversPrefix: getConfigValue(rawConfig.internalResolversPrefix, '__'),
generateInternalResolversIfNeeded: {
__resolveReference: rawConfig.generateInternalResolversIfNeeded?.__resolveReference ?? false,
},
resolversNonOptionalTypename: normalizeResolversNonOptionalTypename(
getConfigValue(rawConfig.resolversNonOptionalTypename, false)
),
Expand Down Expand Up @@ -1282,7 +1296,7 @@ export class BaseResolversVisitor<
}

public hasFederation(): boolean {
return this._hasFederation;
return Object.keys(this._federation.getMeta()).length > 0;
}

public getRootResolver(): RootResolver {
Expand All @@ -1305,8 +1319,10 @@ export class BaseResolversVisitor<
userDefinedTypes[schemaTypeName] = {
name: resolverType.baseGeneratedTypename,
};
if (resolverType.federation) {
userDefinedTypes[schemaTypeName].federation = resolverType.federation;

const federationMeta = this._federation.getMeta()[schemaTypeName];
if (federationMeta) {
userDefinedTypes[schemaTypeName].federation = federationMeta;
}
}

Expand Down Expand Up @@ -1492,9 +1508,20 @@ export class BaseResolversVisitor<
};

if (this._federation.isResolveReferenceField(node)) {
this._hasFederation = true;
signature.type = 'ReferenceResolver';
if (this.config.generateInternalResolversIfNeeded.__resolveReference) {
const federationDetails = checkObjectTypeFederationDetails(
parentType.astNode as ObjectTypeDefinitionNode,
this._schema
);

if (!federationDetails || federationDetails.resolvableKeyDirectives.length === 0) {
return '';
}
signature.modifier = ''; // if a federation type has resolvable @key, then it should be required
}

this._federation.setMeta(parentType.name, { hasResolveReference: true });
signature.type = 'ReferenceResolver';
if (signature.genericTypes.length >= 3) {
signature.genericTypes = signature.genericTypes.slice(0, 3);
}
Expand Down Expand Up @@ -1537,7 +1564,7 @@ export class BaseResolversVisitor<
return `Partial<${argsType}>`;
}

ObjectTypeDefinition(node: ObjectTypeDefinitionNode, key: number, parent: any): string {
ObjectTypeDefinition(node: ObjectTypeDefinitionNode): string {
const declarationKind = 'type';
const name = this.convertName(node, {
suffix: this.config.resolverTypeSuffix,
Expand Down Expand Up @@ -1584,19 +1611,6 @@ export class BaseResolversVisitor<
.withName(name, `<ContextType = ${this.config.contextType.type}, ${this.transformParentGenericType(parentType)}>`)
.withBlock(fieldsContent.join('\n'));

this._collectedResolvers[node.name as any] = {
typename: name + '<ContextType>',
baseGeneratedTypename: name,
};

if (this.config.federation) {
const originalNode = parent[key] as ObjectTypeDefinitionNode;
const federationDetails = checkObjectTypeFederationDetails(originalNode, this._schema);
this._collectedResolvers[node.name as any].federation = {
hasResolveReference: federationDetails ? federationDetails.resolvableKeyDirectives.length > 0 : false,
};
}

return block.string;
}

Expand Down
5 changes: 5 additions & 0 deletions packages/plugins/other/visitor-plugin-common/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,8 @@ export interface ResolversNonOptionalTypenameConfig {
interfaceImplementingType?: boolean;
excludeTypes?: string[];
}

export interface GenerateInternalResolversIfNeededConfig {
__resolveReference?: boolean;
}
export type NormalizedGenerateInternalResolversIfNeededConfig = Required<GenerateInternalResolversIfNeededConfig>;
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function generate({ schema, config }: { schema: string; config: TypeScriptResolv
}

describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
it('should add __resolveReference to objects that have @key and is resolvable', async () => {
it('should add optional __resolveReference to objects that have @key', async () => {
const federatedSchema = /* GraphQL */ `
type Query {
allUsers: [User]
Expand Down Expand Up @@ -83,12 +83,16 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
},
});

// User should have it
expect(content).toBeSimilarStringTo(`
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
export type UserResolvers<ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

// SingleResolvable should have __resolveReference because it has resolvable: true
expect(content).toBeSimilarStringTo(`
export type SingleResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['SingleResolvable'] = ResolversParentTypes['SingleResolvable']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['SingleResolvable']>, { __typename: 'SingleResolvable' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
Expand All @@ -97,15 +101,14 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
};
`);

// SingleNonResolvable shouldn't have __resolveReference because it has resolvable: false
expect(content).toBeSimilarStringTo(`
export type SingleNonResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['SingleNonResolvable'] = ResolversParentTypes['SingleNonResolvable']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['SingleNonResolvable']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

// AtLeastOneResolvable should have __resolveReference because it at least one resolvable
expect(content).toBeSimilarStringTo(`
export type AtLeastOneResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['AtLeastOneResolvable'] = ResolversParentTypes['AtLeastOneResolvable']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['AtLeastOneResolvable']>, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick<ParentType, {"id2":true}>, ContextType>;
Expand All @@ -116,7 +119,6 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
};
`);

// MixedResolvable should have __resolveReference and references for resolvable keys
expect(content).toBeSimilarStringTo(`
export type MixedResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['MixedResolvable'] = ResolversParentTypes['MixedResolvable']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['MixedResolvable']>, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick<ParentType, {"id":true}> | GraphQLRecursivePick<ParentType, {"id2":true}>), ContextType>;
Expand All @@ -127,19 +129,151 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
};
`);

// MultipleNonResolvableResolvers does not have __resolveReference because all keys are non-resolvable
expect(content).toBeSimilarStringTo(`
export type MultipleNonResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['MultipleNonResolvable'] = ResolversParentTypes['MultipleNonResolvable']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['MultipleNonResolvable']>, ParentType, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
id2?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
id3?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

// Book shouldn't because it doesn't have @key
expect(content).not.toBeSimilarStringTo(`
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Book']>, { __typename: 'Book' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
// Book does NOT have __resolveReference because it doesn't have @key
expect(content).toBeSimilarStringTo(`
export type BookResolvers<ContextType = any, ParentType extends ResolversParentTypes['Book'] = ResolversParentTypes['Book']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);
});

it('generateInternalResolversIfNeeded - should add non-optional __resolveReference to objects that have resolvable @key', async () => {
const federatedSchema = /* GraphQL */ `
type Query {
allUsers: [User]
}
type User @key(fields: "id") {
id: ID!
name: String
username: String
}
type Book {
id: ID!
}
type SingleResolvable @key(fields: "id", resolvable: true) {
id: ID!
}
type SingleNonResolvable @key(fields: "id", resolvable: false) {
id: ID!
}
type AtLeastOneResolvable
@key(fields: "id", resolvable: false)
@key(fields: "id2", resolvable: true)
@key(fields: "id3", resolvable: false) {
id: ID!
id2: ID!
id3: ID!
}
type MixedResolvable
@key(fields: "id")
@key(fields: "id2", resolvable: true)
@key(fields: "id3", resolvable: false) {
id: ID!
id2: ID!
id3: ID!
}
type MultipleNonResolvable
@key(fields: "id", resolvable: false)
@key(fields: "id2", resolvable: false)
@key(fields: "id3", resolvable: false) {
id: ID!
id2: ID!
id3: ID!
}
`;

const content = await generate({
schema: federatedSchema,
config: {
federation: true,
generateInternalResolversIfNeeded: { __resolveReference: true },
},
});

// User should have __resolveReference because it has resolvable @key (by default)
expect(content).toBeSimilarStringTo(`
export type UserResolvers<ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']> = {
__resolveReference: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

// SingleResolvable has __resolveReference because it has resolvable: true
expect(content).toBeSimilarStringTo(`
export type SingleResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['SingleResolvable'] = ResolversParentTypes['SingleResolvable']> = {
__resolveReference: ReferenceResolver<Maybe<ResolversTypes['SingleResolvable']>, { __typename: 'SingleResolvable' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

// SingleNonResolvable does NOT have __resolveReference because it has resolvable: false
expect(content).toBeSimilarStringTo(`
export type SingleNonResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['SingleNonResolvable'] = ResolversParentTypes['SingleNonResolvable']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

// AtLeastOneResolvable has __resolveReference because it at least one resolvable
expect(content).toBeSimilarStringTo(`
export type AtLeastOneResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['AtLeastOneResolvable'] = ResolversParentTypes['AtLeastOneResolvable']> = {
__resolveReference: ReferenceResolver<Maybe<ResolversTypes['AtLeastOneResolvable']>, { __typename: 'AtLeastOneResolvable' } & GraphQLRecursivePick<ParentType, {"id2":true}>, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
id2?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
id3?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

// MixedResolvable has __resolveReference and references for resolvable keys
expect(content).toBeSimilarStringTo(`
export type MixedResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['MixedResolvable'] = ResolversParentTypes['MixedResolvable']> = {
__resolveReference: ReferenceResolver<Maybe<ResolversTypes['MixedResolvable']>, { __typename: 'MixedResolvable' } & (GraphQLRecursivePick<ParentType, {"id":true}> | GraphQLRecursivePick<ParentType, {"id2":true}>), ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
id2?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
id3?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

// MultipleNonResolvableResolvers does NOT have __resolveReference because all keys are non-resolvable
expect(content).toBeSimilarStringTo(`
export type MultipleNonResolvableResolvers<ContextType = any, ParentType extends ResolversParentTypes['MultipleNonResolvable'] = ResolversParentTypes['MultipleNonResolvable']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
id2?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
id3?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);

// Book does NOT have __resolveReference because it doesn't have @key
expect(content).toBeSimilarStringTo(`
export type BookResolvers<ContextType = any, ParentType extends ResolversParentTypes['Book'] = ResolversParentTypes['Book']> = {
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
};
`);
});

Expand Down
Loading

0 comments on commit 18a679b

Please sign in to comment.