Skip to content

Commit

Permalink
Limit ParentType to only contain fields from the @key and @requires d…
Browse files Browse the repository at this point in the history
…irectives (#4232)

* transformParentType now handles nested selection sets.

* Added RecursivePick type def to typescript resolvers plugin.

* Updated recursivePick to not include Pick type.

* Updated RecursivePick type to use Maybe type.

* Added Pick type back in to RecursivePick.

* Refactored a few federation tests.

* Refactored unwrappedObject test.

* Added unit tests for nested key and requires fields.

* Cleaned up federation logic.

* Updated federation schema in dev-test.

* Added RecursivePick type def to flow resolvers plugin.

* Added array handling to RecursivePick type.

* Added types to make GraphqlRecursivePick more readable.

* Updated dev-test files.

* Updated federation test.

* Updated dev-test files.

* Removed unnecessary Pick type from GraphQLRecursivePick.

* Updated types to pascal case.

* Added interface test. Skip for now since it fails.

Co-authored-by: Blake Jackson <blakejackson@Blakes-MBP.lan>
Co-authored-by: j125398 <jackson.blake@heb.com>
  • Loading branch information
3 people committed Jul 23, 2020
1 parent 4e37c3d commit 48e0932
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 57 deletions.
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 @@ -73,6 +73,13 @@ export type NewStitchingResolver<TResult, TParent, TContext, TArgs> = {
context: TContext,
info${optionalSignForInfoArg}: 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

0 comments on commit 48e0932

Please sign in to comment.