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

Release 2.14.1 #4175

Merged
merged 6 commits into from May 28, 2020
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -11,6 +11,10 @@ The version headers in this history reflect the versions of Apollo Server itself

- _Nothing yet! Stay tuned!_

### v2.14.1

- `apollo-server-testing`: Ensure that user-provided context is cloned when using `createTestClient`, per the instructions in the [intergration testing]() section of the Apollo Server documentation. [Issue #4170](https://github.com/apollographql/apollo-server/issues/4170) [PR #TODO](https://github.com/apollographql/apollo-server/pull/TODO)

### v2.14.0

- `apollo-server-core` / `apollo-server-plugin-base`: Add support for `willResolveField` and corresponding end-handler within `executionDidStart`. This brings the remaining bit of functionality that was previously only available from `graphql-extensions` to the new plugin API. The `graphql-extensions` API (which was never documented) will be deprecated in Apollo Server 3.x. To see the documentation for the request pipeline API, see [its documentation](https://www.apollographql.com/docs/apollo-server/integrations/plugins/). For more details, see the attached PR. [PR #3988](https://github.com/apollographql/apollo-server/pull/3988)
Expand Down
4 changes: 4 additions & 0 deletions packages/apollo-federation/CHANGELOG.md
Expand Up @@ -6,6 +6,10 @@

- _Nothing yet! Stay tuned._

## 0.16.2

- Only changes in the similarly versioned `@apollo/gateway` package.

## 0.16.1

- Only changes in the similarly versioned `@apollo/gateway` package.
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-federation/package.json
@@ -1,6 +1,6 @@
{
"name": "@apollo/federation",
"version": "0.16.1",
"version": "0.16.2",
"description": "Apollo Federation Utilities",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
4 changes: 4 additions & 0 deletions packages/apollo-gateway/CHANGELOG.md
Expand Up @@ -6,6 +6,10 @@

- _Nothing yet! Stay tuned!_

## 0.16.2

- __FIX__: Collapse nested required fields into a single body in the query plan. Before, some nested fields' selection sets were getting split, causing some of their subfields to be dropped when executing the query. This fix collapses the split selection sets into one. [#4064](https://github.com/apollographql/apollo-server/pull/4064)

## 0.16.1

- __NEW__: Provide the ability to pass a custom `fetcher` during `RemoteGraphQLDataSource` construction to be used when executing operations against downstream services. Providing a custom `fetcher` may be necessary to accommodate more advanced needs, e.g., configuring custom TLS certificates for internal services. [PR #4149](https://github.com/apollographql/apollo-server/pull/4149)
Expand Down
2 changes: 1 addition & 1 deletion packages/apollo-gateway/package.json
@@ -1,6 +1,6 @@
{
"name": "@apollo/gateway",
"version": "0.16.1",
"version": "0.16.2",
"description": "Apollo Gateway",
"author": "opensource@apollographql.com",
"main": "dist/index.js",
Expand Down
54 changes: 34 additions & 20 deletions packages/apollo-gateway/src/FieldSet.ts
Expand Up @@ -10,6 +10,7 @@ import {
GraphQLObjectType,
} from 'graphql';
import { getResponseName } from './utilities/graphql';
import { partition, groupBy } from './utilities/array';

export interface Field<
TParent extends GraphQLCompositeType = GraphQLCompositeType
Expand Down Expand Up @@ -45,25 +46,6 @@ export function matchesField(field: Field) {
};
}

function groupBy<T, U>(keyFunction: (element: T) => U) {
return (iterable: Iterable<T>) => {
const result = new Map<U, T[]>();

for (const element of iterable) {
const key = keyFunction(element);
const group = result.get(key);

if (group) {
group.push(element);
} else {
result.set(key, [element]);
}
}

return result;
};
}

export const groupByResponseName = groupBy<Field, string>(field =>
getResponseName(field.fieldNode)
);
Expand Down Expand Up @@ -147,6 +129,38 @@ function mergeSelectionSets(fieldNodes: FieldNode[]): SelectionSetNode {

return {
kind: 'SelectionSet',
selections,
selections: mergeFieldNodeSelectionSets(selections),
};
}

function mergeFieldNodeSelectionSets(
selectionNodes: SelectionNode[],
): SelectionNode[] {
const [fieldNodes, fragmentNodes] = partition(
selectionNodes,
(node): node is FieldNode => node.kind === Kind.FIELD,
);

const [aliasedFieldNodes, nonAliasedFieldNodes] = partition(
fieldNodes,
node => !!node.alias,
);

const mergedFieldNodes = Array.from(
groupBy((node: FieldNode) => node.name.value)(
nonAliasedFieldNodes,
).values(),
).map((nodesWithSameName) => {
const node = nodesWithSameName[0];
if (node.selectionSet) {
node.selectionSet.selections = mergeFieldNodeSelectionSets(
nodesWithSameName.flatMap(
(node) => node.selectionSet?.selections || [],
),
);
}
return node;
});

return [...mergedFieldNodes, ...aliasedFieldNodes, ...fragmentNodes];
}
Expand Up @@ -171,7 +171,6 @@ it('works fetches data correctly with complex / nested @key fields', async () =>
organization {
id
__typename
id
}
}
}
Expand Down
227 changes: 227 additions & 0 deletions packages/apollo-gateway/src/__tests__/integration/requires.test.ts
@@ -1,4 +1,6 @@
import gql from 'graphql-tag';
import { execute } from '../execution-utils';
import { serializeQueryPlan } from '../..';

it('supports passing additional fields defined by a requires', async () => {
const query = `#graphql
Expand Down Expand Up @@ -38,3 +40,228 @@ it('supports passing additional fields defined by a requires', async () => {
expect(queryPlan).toCallService('product');
expect(queryPlan).toCallService('books');
});

const serviceA = {
name: 'a',
typeDefs: gql`
type Query {
user: User
}

type User @key(fields: "id") {
id: ID!
preferences: Preferences
}

type Preferences {
favorites: Things
}

type Things {
color: String
animal: String
}
`,
resolvers: {
Query: {
user() {
return {
id: '1',
preferences: {
favorites: { color: 'limegreen', animal: 'platypus' },
},
};
},
},
},
};

const serviceB = {
name: 'b',
typeDefs: gql`
extend type User @key(fields: "id") {
id: ID! @external
preferences: Preferences @external
favoriteColor: String
@requires(fields: "preferences { favorites { color } }")
favoriteAnimal: String
@requires(fields: "preferences { favorites { animal } }")
}

extend type Preferences {
favorites: Things @external
}

extend type Things {
color: String @external
animal: String @external
}
`,
resolvers: {
User: {
favoriteColor(user: any) {
return user.preferences.favorites.color;
},
favoriteAnimal(user: any) {
return user.preferences.favorites.animal;
},
},
},
};

it('collapses nested requires', async () => {
const query = `#graphql
query UserFavorites {
user {
favoriteColor
favoriteAnimal
}
}
`;

const { data, errors, queryPlan } = await execute(
{
query,
},
[serviceA, serviceB],
);

expect(errors).toEqual(undefined);

expect(serializeQueryPlan(queryPlan)).toMatchInlineSnapshot(`
"QueryPlan {
Sequence {
Fetch(service: \\"a\\") {
{
user {
__typename
id
preferences {
favorites {
color
animal
}
}
}
}
},
Flatten(path: \\"user\\") {
Fetch(service: \\"b\\") {
{
... on User {
__typename
id
preferences {
favorites {
color
animal
}
}
}
} =>
{
... on User {
favoriteColor
favoriteAnimal
}
}
},
},
},
}"
`);

expect(data).toEqual({
user: {
favoriteAnimal: 'platypus',
favoriteColor: 'limegreen',
},
});

expect(queryPlan).toCallService('a');
expect(queryPlan).toCallService('b');
});

it('collapses nested requires with user-defined fragments', async () => {
const query = `#graphql
query UserFavorites {
user {
favoriteAnimal
...favoriteColor
}
}

fragment favoriteColor on User {
preferences {
favorites {
color
}
}
}
`;

const { data, errors, queryPlan } = await execute(
{
query,
},
[serviceA, serviceB],
);

expect(errors).toEqual(undefined);

expect(serializeQueryPlan(queryPlan)).toMatchInlineSnapshot(`
"QueryPlan {
Sequence {
Fetch(service: \\"a\\") {
{
user {
__typename
id
preferences {
favorites {
animal
color
}
}
}
}
},
Flatten(path: \\"user\\") {
Fetch(service: \\"b\\") {
{
... on User {
__typename
id
preferences {
favorites {
animal
color
}
}
}
} =>
{
... on User {
favoriteAnimal
}
}
},
},
},
}"
`);

expect(data).toEqual({
user: {
favoriteAnimal: 'platypus',
preferences: {
favorites: {
color: 'limegreen',
},
},
},
});

expect(queryPlan).toCallService('a');
expect(queryPlan).toCallService('b');
});