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

Allow list-type entity @key's #2841

Merged
merged 2 commits into from
Jun 14, 2019
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/federation/advanced-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ type Organization {
}
```

> Note that although the fields argument is parsed as a selection set, some restrictions apply to make the result suitable as a key. For example, fields shouldn't return lists.
> Note that although the fields argument is parsed as a selection set, some restrictions apply to make the result suitable as a key. For example, fields shouldn't return unions or interfaces.

## Computed fields

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,53 +42,6 @@ describe('keyFieldsSelectInvalidType', () => {
expect(warnings).toHaveLength(0);
});

it('warns if @key references fields of a list type', () => {
const serviceA = {
typeDefs: gql`
type Product @key(fields: "myList myOptionalList") {
sku: String!
myList: [String]!
myOptionalList: [String]
upc: String!
color: Color!
}

type Color {
id: ID!
value: String!
}
`,
name: 'serviceA',
};

const serviceB = {
typeDefs: gql`
extend type Product {
sku: String! @external
price: Int! @requires(fields: "sku")
}
`,
name: 'serviceB',
};

const { schema, errors } = composeServices([serviceA, serviceB]);
expect(errors).toHaveLength(0);

const warnings = validateKeyFieldsSelectInvalidType(schema);
expect(warnings).toMatchInlineSnapshot(`
Array [
Object {
"code": "KEY_FIELDS_SELECT_INVALID_TYPE",
"message": "[serviceA] Product -> A @key selects Product.myList, which is a list type. Keys cannot select lists.",
},
Object {
"code": "KEY_FIELDS_SELECT_INVALID_TYPE",
"message": "[serviceA] Product -> A @key selects Product.myOptionalList, which is a list type. Keys cannot select lists.",
},
]
`);
});

it('warns if @key references fields of an interface type', () => {
const serviceA = {
typeDefs: gql`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {
GraphQLSchema,
isObjectType,
FieldNode,
isListType,
isInterfaceType,
isNonNullType,
getNullableType,
Expand Down Expand Up @@ -46,19 +45,6 @@ export const keyFieldsSelectInvalidType = (schema: GraphQLSchema) => {
}

if (matchingField) {
if (
isListType(matchingField.type) ||
(isNonNullType(matchingField.type) &&
isListType(getNullableType(matchingField.type)))
) {
errors.push(
errorWithCode(
'KEY_FIELDS_SELECT_INVALID_TYPE',
logServiceAndType(serviceName, typeName) +
`A @key selects ${typeName}.${name}, which is a list type. Keys cannot select lists.`,
),
);
}
if (
isInterfaceType(matchingField.type) ||
(isNonNullType(matchingField.type) &&
Expand Down
125 changes: 125 additions & 0 deletions packages/apollo-gateway/src/__tests__/integration/list-key.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import gql from 'graphql-tag';
import { execute, ServiceDefinitionModule } from '../execution-utils';
import { astSerializer, queryPlanSerializer } from '../../snapshotSerializers';

expect.addSnapshotSerializer(astSerializer);
expect.addSnapshotSerializer(queryPlanSerializer);

const users = [
{ id: ['1', '1'], name: 'Trevor Scheer', __typename: 'User' },
{ id: ['2', '2'], name: 'James Baxley', __typename: 'User' },
];

const reviews = [
{ id: '1', authorId: ['1', '1'], body: 'Good', __typename: 'Review' },
{ id: '2', authorId: ['2', '2'], body: 'Bad', __typename: 'Review' },
];

const reviewService: ServiceDefinitionModule = {
name: 'review',
typeDefs: gql`
type Query {
reviews: [Review!]!
}

type Review {
id: ID!
author: User!
body: String!
}

extend type User @key(fields: "id") {
id: [ID!]! @external
}
`,
resolvers: {
Query: {
reviews() {
return reviews;
},
},
Review: {
author(review) {
return {
id: review.authorId,
};
},
},
},
};

const listsAreEqual = <T>(as: T[], bs: T[]) =>
as.length === bs.length && as.every((a, i) => bs[i] === as[i]);

const userService: ServiceDefinitionModule = {
name: 'user',
typeDefs: gql`
type User @key(fields: "id") {
id: [ID!]!
name: String!
}
`,
resolvers: {
User: {
__resolveReference(reference) {
return users.find(user => listsAreEqual(user.id, reference.id));
},
},
},
};

it('fetches data correctly list type @key fields', async () => {
const query = gql`
query Reviews {
reviews {
body
author {
name
}
}
}
`;

const { data, queryPlan } = await execute([userService, reviewService], {
query,
});

expect(data).toEqual({
reviews: [
{ body: 'Good', author: { name: 'Trevor Scheer' } },
{ body: 'Bad', author: { name: 'James Baxley' } },
],
});
expect(queryPlan).toMatchInlineSnapshot(`
QueryPlan {
Sequence {
Fetch(service: "review") {
{
reviews {
body
author {
__typename
id
}
}
}
},
Flatten(path: "reviews.@.author") {
Fetch(service: "user") {
{
... on User {
__typename
id
}
} =>
{
... on User {
name
}
}
},
},
},
}
`);
});