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

Limit ParentType to only contain fields from the @key and @requires directives #4232

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 66 additions & 9 deletions dev-test/test-schema/resolvers-federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,23 @@ export type Query = {

export type User = {
__typename?: 'User';
id: Scalars['ID'];
name?: Maybe<Scalars['String']>;
username?: Maybe<Scalars['String']>;
id: Scalars['Int'];
name: Scalars['String'];
email: Scalars['String'];
address?: Maybe<Address>;
};

export type Address = {
__typename?: 'Address';
lines: Lines;
city?: Maybe<Scalars['String']>;
state?: Maybe<Scalars['String']>;
};

export type Lines = {
__typename?: 'Lines';
line1: Scalars['String'];
line2?: Maybe<Scalars['String']>;
};

export type Book = {
Expand All @@ -36,6 +50,11 @@ export type ReferenceResolver<TResult, TReference, TContext> = (
info: GraphQLResolveInfo
) => Promise<TResult> | TResult;

type ScalarCheck<T, S> = S extends true ? T : NullableCheck<T, S>;
type NullableCheck<T, S> = Maybe<T> extends T ? Maybe<ListCheck<NonNullable<T>, S>> : ListCheck<T, S>;
type ListCheck<T, S> = T extends (infer U)[] ? NullableCheck<U, S>[] : GraphQLRecursivePick<T, S>;
export type GraphQLRecursivePick<T, S> = { [K in keyof T & keyof S]: ScalarCheck<T[K], S[K]> };

export type LegacyStitchingResolver<TResult, TParent, TContext, TArgs> = {
fragment: string;
resolve: ResolverFn<TResult, TParent, TContext, TArgs>;
Expand Down Expand Up @@ -113,19 +132,25 @@ export type DirectiveResolverFn<TResult = {}, TParent = {}, TContext = {}, TArgs
export type ResolversTypes = {
Query: ResolverTypeWrapper<{}>;
User: ResolverTypeWrapper<User>;
ID: ResolverTypeWrapper<Scalars['ID']>;
Int: ResolverTypeWrapper<Scalars['Int']>;
String: ResolverTypeWrapper<Scalars['String']>;
Address: ResolverTypeWrapper<Address>;
Lines: ResolverTypeWrapper<Lines>;
Book: ResolverTypeWrapper<Book>;
ID: ResolverTypeWrapper<Scalars['ID']>;
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
};

/** Mapping between all available schema types and the resolvers parents */
export type ResolversParentTypes = {
Query: {};
User: User;
ID: Scalars['ID'];
Int: Scalars['Int'];
String: Scalars['String'];
Address: Address;
Lines: Lines;
Book: Book;
ID: Scalars['ID'];
Boolean: Scalars['Boolean'];
};

Expand All @@ -142,12 +167,42 @@ export type UserResolvers<
> = {
__resolveReference?: ReferenceResolver<
Maybe<ResolversTypes['User']>,
{ __typename: 'User' } & Pick<ParentType, 'id'>,
{ __typename: 'User' } & (
| GraphQLRecursivePick<ParentType, { id: true }>
| GraphQLRecursivePick<ParentType, { name: true }>
),
ContextType
>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;

email?: Resolver<
ResolversTypes['String'],
{ __typename: 'User' } & (
| GraphQLRecursivePick<ParentType, { id: true }>
| GraphQLRecursivePick<ParentType, { name: true }>
) &
GraphQLRecursivePick<ParentType, { address: { city: true; lines: { line2: true } } }>,
ContextType
>;

__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};

export type AddressResolvers<
ContextType = any,
ParentType extends ResolversParentTypes['Address'] = ResolversParentTypes['Address']
> = {
lines?: Resolver<ResolversTypes['Lines'], ParentType, ContextType>;
city?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
state?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};

export type LinesResolvers<
ContextType = any,
ParentType extends ResolversParentTypes['Lines'] = ResolversParentTypes['Lines']
> = {
line1?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
line2?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};

Expand All @@ -162,6 +217,8 @@ export type BookResolvers<
export type Resolvers<ContextType = any> = {
Query?: QueryResolvers<ContextType>;
User?: UserResolvers<ContextType>;
Address?: AddressResolvers<ContextType>;
Lines?: LinesResolvers<ContextType>;
Book?: BookResolvers<ContextType>;
};

Expand Down
20 changes: 16 additions & 4 deletions dev-test/test-schema/schema-federation.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,22 @@ extend type Query {
users: [User]
}

type User @key(fields: "id") {
id: ID!
name: String
username: String
extend type User @key(fields: "id") @key(fields: "name") {
id: Int! @external
name: String! @external
email: String! @requires(fields: """address(works: "with params!") { city lines { line2 } }""")
address: Address @external
}

extend type Address {
lines: Lines!
city: String
state: String
}

extend type Lines {
line1: String!
line2: String
}

type Book {
Expand Down
2 changes: 2 additions & 0 deletions packages/plugins/flow/resolvers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export const plugin: PluginFunction<RawFlowResolversConfig, Types.ComplexPluginO
info: GraphQLResolveInfo
) => Promise<TResult> | TResult;
`);

defsToInclude.push(`export type RecursivePick<T, U> = T`);
}

const header = `export type Resolver<Result, Parent = {}, Context = {}, Args = {}> = (
Expand Down
7 changes: 7 additions & 0 deletions packages/plugins/typescript/resolvers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ export type NewStitchingResolver<TResult, TParent, TContext, TArgs> = {
context: TContext,
info: GraphQLResolveInfo
) => Promise<TResult> | TResult;`);

defsToInclude.push(`
type ScalarCheck<T, S> = S extends true ? T : NullableCheck<T, S>;
type NullableCheck<T, S> = Maybe<T> extends T ? Maybe<ListCheck<NonNullable<T>, S>> : ListCheck<T, S>;
type ListCheck<T, S> = T extends (infer U)[] ? NullableCheck<U, S>[] : GraphQLRecursivePick<T, S>;
export type GraphQLRecursivePick<T, S> = { [K in keyof T & keyof S]: ScalarCheck<T[K], S[K]> };
`);
}

if (noSchemaStitching) {
Expand Down
150 changes: 134 additions & 16 deletions packages/plugins/typescript/resolvers/tests/federation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {

// User should have it
expect(content).toBeSimilarStringTo(`
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & Pick<ParentType, 'id'>, ContextType>;
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
`);
// Foo shouldn't because it doesn't have @key
expect(content).not.toBeSimilarStringTo(`
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Book']>, ParentType, ContextType>;
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Book']>, { __typename: 'Book' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
`);
});

Expand Down Expand Up @@ -84,11 +84,11 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {

// User should have it
expect(content).toBeSimilarStringTo(`
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & Pick<ParentType, 'id'>, ContextType>;
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
`);
// Foo shouldn't because it doesn't have @key
expect(content).not.toBeSimilarStringTo(`
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Book']>, ParentType, ContextType>;
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['Book']>, { __typename: 'Book' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
`);
});

Expand Down Expand Up @@ -116,14 +116,128 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
// User should have it
expect(content).toBeSimilarStringTo(`
export type UserResolvers<ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & Pick<ParentType, 'id'>, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
id?: Resolver<ResolversTypes['ID'], { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}> & GraphQLRecursivePick<ParentType, {"name":true,"age":true}>, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};
`);
});

it('should handle nested fields from @requires directive', async () => {
const federatedSchema = /* GraphQL */ `
type Query {
users: [User]
}

extend type User @key(fields: "id") {
id: ID! @external
name: String @external
age: Int! @external
address: Address! @external
username: String @requires(fields: "name age address { street }")
}

extend type Address {
street: String!
zip: Int!
}
`;

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

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>;
username?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}> & GraphQLRecursivePick<ParentType, {"name":true,"age":true,"address":{"street":true}}>, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};
`);
});

it('should handle nested fields from @key directive', async () => {
const federatedSchema = /* GraphQL */ `
type Query {
users: [User]
}

type User @key(fields: "name { first last }") {
name: Name!
username: String
}

type Name {
first: String!
last: String!
}
`;

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

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

it.skip('should handle interface types', async () => {
const federatedSchema = /* GraphQL */ `
type Query {
people: [Person]
}

extend interface Person @key(fields: "name { first last }") {
name: Name! @external
age: Int @requires(fields: "name")
}

extend type User implements Person @key(fields: "name { first last }") {
name: Name! @external
age: Int @requires(fields: "name { first last }")
username: String
}

type Admin implements Person @key(fields: "name { first last }") {
name: Name! @external
age: Int @requires(fields: "name { first last }")
permissions: [String!]!
}

extend type Name {
first: String! @external
last: String! @external
}
`;

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

expect(content).toBeSimilarStringTo(`
export type PersonResolvers<ContextType = any, ParentType extends ResolversParentTypes['Person'] = ResolversParentTypes['Person']> = {
__resolveType: TypeResolveFn<'User' | 'Admin', ParentType, ContextType>;
age?: Resolver<Maybe<ResolversTypes['Int']>, { __typename: 'User' | 'Admin' } & GraphQLRecursivePick<ParentType, {"name":{"first":true,"last":true}}>, ContextType>;
};
`);
});

it('should skip to generate resolvers of fields with @external directive', async () => {
const federatedSchema = /* GraphQL */ `
type Query {
Expand Down Expand Up @@ -151,9 +265,9 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
// UserResolver should not have a resolver function of name field
expect(content).toBeSimilarStringTo(`
export type UserResolvers<ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & Pick<ParentType, 'id'>, ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
id?: Resolver<ResolversTypes['ID'], { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};
`);
Expand Down Expand Up @@ -278,10 +392,10 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
// User should have it
expect(content).toBeSimilarStringTo(`
export type UserResolvers<ContextType = any, ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User']> = {
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & (Pick<ParentType, 'id'> | Pick<ParentType, 'name'>), ContextType>;
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & (GraphQLRecursivePick<ParentType, {"id":true}> | GraphQLRecursivePick<ParentType, {"name":true}>), ContextType>;
id?: Resolver<ResolversTypes['ID'], { __typename: 'User' } & (GraphQLRecursivePick<ParentType, {"id":true}> | GraphQLRecursivePick<ParentType, {"name":true}>), ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & (GraphQLRecursivePick<ParentType, {"id":true}> | GraphQLRecursivePick<ParentType, {"name":true}>), ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & (GraphQLRecursivePick<ParentType, {"id":true}> | GraphQLRecursivePick<ParentType, {"name":true}>), ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};
`);
Expand Down Expand Up @@ -381,9 +495,13 @@ describe('TypeScript Resolvers Plugin + Apollo Federation', () => {
});

// __resolveReference should be unwrapped
expect(content).toBeSimilarStringTo(`{ __typename: 'User' } & Pick<UnwrappedObject<ParentType>, 'id'>`);
expect(content).toBeSimilarStringTo(`
__resolveReference?: ReferenceResolver<Maybe<ResolversTypes['User']>, { __typename: 'User' } & GraphQLRecursivePick<UnwrappedObject<ParentType>, {"id":true}>, ContextType>;
`);
// but ID should not
expect(content).toBeSimilarStringTo(`id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>`);
expect(content).toBeSimilarStringTo(
`id?: Resolver<ResolversTypes['ID'], { __typename: 'User' } & GraphQLRecursivePick<ParentType, {"id":true}>, ContextType>`
);
});
});
});
1 change: 1 addition & 0 deletions packages/utils/plugins-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"common-tags": "1.8.0",
"constant-case": "3.0.3",
"import-from": "3.0.0",
"lodash": "~4.17.15",
"lower-case": "2.0.1",
"param-case": "3.0.3",
"pascal-case": "3.1.1",
Expand Down