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 12 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
74 changes: 65 additions & 9 deletions dev-test/test-schema/resolvers-federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,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 @@ -34,6 +48,16 @@ export type ReferenceResolver<TResult, TReference, TContext> = (
context: TContext,
info: GraphQLResolveInfo
) => Promise<TResult> | TResult;
export type RecursivePick<T, S> = Pick<
{
[K in keyof T & keyof S]: S[K] extends true
? T[K]
: Maybe<T[K]> extends T[K]
? Maybe<RecursivePick<NonNullable<T[K]>, S[K]>>
: RecursivePick<T[K], S[K]>;
},
keyof T & keyof S
>;

export type LegacyStitchingResolver<TResult, TParent, TContext, TArgs> = {
fragment: string;
Expand Down Expand Up @@ -114,8 +138,11 @@ export type ResolversTypes = {
Boolean: ResolverTypeWrapper<Scalars['Boolean']>;
Query: ResolverTypeWrapper<{}>;
User: ResolverTypeWrapper<User>;
ID: ResolverTypeWrapper<Scalars['ID']>;
Int: ResolverTypeWrapper<Scalars['Int']>;
Address: ResolverTypeWrapper<Address>;
Lines: ResolverTypeWrapper<Lines>;
Book: ResolverTypeWrapper<Book>;
ID: ResolverTypeWrapper<Scalars['ID']>;
};

/** Mapping between all available schema types and the resolvers parents */
Expand All @@ -124,8 +151,11 @@ export type ResolversParentTypes = {
Boolean: Scalars['Boolean'];
Query: {};
User: User;
ID: Scalars['ID'];
Int: Scalars['Int'];
Address: Address;
Lines: Lines;
Book: Book;
ID: Scalars['ID'];
};

export type QueryResolvers<
Expand All @@ -141,12 +171,36 @@ export type UserResolvers<
> = {
__resolveReference?: ReferenceResolver<
Maybe<ResolversTypes['User']>,
{ __typename: 'User' } & Pick<ParentType, 'id'>,
{ __typename: 'User' } & (RecursivePick<ParentType, { id: true }> | RecursivePick<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' } & (RecursivePick<ParentType, { id: true }> | RecursivePick<ParentType, { name: true }>) &
RecursivePick<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 @@ -161,6 +215,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
16 changes: 16 additions & 0 deletions packages/plugins/typescript/resolvers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,22 @@ export type NewStitchingResolver<TResult, TParent, TContext, TArgs> = {
context: TContext,
info: GraphQLResolveInfo
) => Promise<TResult> | TResult;`);

defsToInclude.push(`export type RecursivePick<T, S> = Pick<
{
[K in keyof T & keyof S]:
S[K] extends true ?
T[K]
: Maybe<T[K]> extends T[K] ?
NonNullable<T[K]> extends (infer U)[] ?
Maybe<RecursivePick<NonNullable<U>, S[K]>>[]
: Maybe<RecursivePick<NonNullable<T[K]>, S[K]>>
: T[K] extends (infer U)[] ?
RecursivePick<U, S[K]>[]
: RecursivePick<T[K], S[K]>;
},
keyof T & keyof S
>;`);
dotansimha marked this conversation as resolved.
Show resolved Hide resolved
}

if (noSchemaStitching) {
Expand Down
104 changes: 88 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' } & RecursivePick<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' } & RecursivePick<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' } & RecursivePick<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' } & RecursivePick<ParentType, {"id":true}>, ContextType>;
`);
});

Expand Down Expand Up @@ -116,9 +116,79 @@ 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' } & RecursivePick<ParentType, {"id":true}>, ContextType>;
id?: Resolver<ResolversTypes['ID'], { __typename: 'User' } & RecursivePick<ParentType, {"id":true}>, ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & RecursivePick<ParentType, {"id":true}> & RecursivePick<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' } & RecursivePick<ParentType, {"id":true}>, ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & RecursivePick<ParentType, {"id":true}> & RecursivePick<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' } & RecursivePick<ParentType, {"name":{"first":true,"last":true}}>, ContextType>;
name?: Resolver<ResolversTypes['Name'], { __typename: 'User' } & RecursivePick<ParentType, {"name":{"first":true,"last":true}}>, ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & RecursivePick<ParentType, {"name":{"first":true,"last":true}}>, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};
`);
Expand Down Expand Up @@ -151,9 +221,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' } & RecursivePick<ParentType, {"id":true}>, ContextType>;
id?: Resolver<ResolversTypes['ID'], { __typename: 'User' } & RecursivePick<ParentType, {"id":true}>, ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & RecursivePick<ParentType, {"id":true}>, ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};
`);
Expand Down Expand Up @@ -278,10 +348,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' } & (RecursivePick<ParentType, {"id":true}> | RecursivePick<ParentType, {"name":true}>), ContextType>;
id?: Resolver<ResolversTypes['ID'], { __typename: 'User' } & (RecursivePick<ParentType, {"id":true}> | RecursivePick<ParentType, {"name":true}>), ContextType>;
name?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & (RecursivePick<ParentType, {"id":true}> | RecursivePick<ParentType, {"name":true}>), ContextType>;
username?: Resolver<Maybe<ResolversTypes['String']>, { __typename: 'User' } & (RecursivePick<ParentType, {"id":true}> | RecursivePick<ParentType, {"name":true}>), ContextType>;
__isTypeOf?: IsTypeOfResolverFn<ParentType>;
};
`);
Expand Down Expand Up @@ -381,9 +451,11 @@ 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' } & RecursivePick<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' } & RecursivePick<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