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

Cache control support with federation #870

Merged
merged 5 commits into from
Aug 3, 2021
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
15 changes: 13 additions & 2 deletions federation-integration-testsuite-js/src/fixtures/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,25 @@ export const typeDefs = gql`
directive @transform(from: String!) on FIELD
directive @tag(name: String!) repeatable on FIELD_DEFINITION

enum CacheControlScope {
PUBLIC
PRIVATE
}

directive @cacheControl(
maxAge: Int
scope: CacheControlScope
inheritMaxAge: Boolean
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION

schema {
query: RootQuery
mutation: Mutation
}

extend type RootQuery {
user(id: ID!): User
me: User
me: User @cacheControl(maxAge: 1000, scope: PRIVATE)
}

type PasswordAccount @key(fields: "email") {
Expand All @@ -36,7 +47,7 @@ export const typeDefs = gql`

type User @key(fields: "id") @key(fields: "username name { first last }") {
id: ID! @tag(name: "accounts")
name: Name
name: Name @cacheControl(inheritMaxAge: true)
username: String
birthDate(locale: String): String @tag(name: "admin") @tag(name: "dev")
account: AccountType
Expand Down
14 changes: 13 additions & 1 deletion federation-integration-testsuite-js/src/fixtures/books.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ export const typeDefs = gql`
directive @stream on FIELD
directive @transform(from: String!) on FIELD

enum CacheControlScope {
PUBLIC
PRIVATE
}

directive @cacheControl(
maxAge: Int
scope: CacheControlScope
inheritMaxAge: Boolean
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION


extend type Query {
book(isbn: String!): Book
books: [Book]
Expand All @@ -26,7 +38,7 @@ export const typeDefs = gql`

# extend union AccountType = LibraryAccount

type Book @key(fields: "isbn") {
type Book @key(fields: "isbn") @cacheControl(maxAge: 700) {
isbn: String!
title: String
year: Int
Expand Down
20 changes: 17 additions & 3 deletions federation-integration-testsuite-js/src/fixtures/product.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,21 @@ export const typeDefs = gql`
directive @stream on FIELD
directive @transform(from: String!) on FIELD

enum CacheControlScope {
PUBLIC
PRIVATE
}

directive @cacheControl(
maxAge: Int
scope: CacheControlScope
inheritMaxAge: Boolean
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION

extend type Query {
product(upc: String!): Product
vehicle(id: String!): Vehicle
topProducts(first: Int = 5): [Product]
topProducts(first: Int = 5): [Product] @cacheControl(maxAge: 40)
topCars(first: Int = 5): [Car]
}

Expand Down Expand Up @@ -49,7 +60,7 @@ export const typeDefs = gql`
type Furniture implements Product @key(fields: "upc") @key(fields: "sku") {
upc: String!
sku: String!
name: String
name: String @cacheControl(maxAge: 30)
price: String
brand: Brand
metadata: [MetadataOrError]
Expand Down Expand Up @@ -184,7 +195,10 @@ export const resolvers: GraphQLResolverMap<any> = {
},
},
Book: {
__resolveReference(object) {
__resolveReference(object, _context, info) {
// For testing dynamic cache control; use `?.` because we don't always run
// this fixture in a real ApolloServer.
info.cacheControl?.cacheHint?.restrict({ maxAge: 30 });
if (object.isbn) {
const fetchedObject = products.find(
product => product.isbn === object.isbn,
Expand Down
2 changes: 2 additions & 0 deletions federation-js/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section.

- When resolving the `Query._entities` field, honor `@cacheControl` directives on the object types that are members of the `_Entity` union. This feature is only enabled when your subgraph is running Apollo Server 3.0.2 or later. [PR #870](https://github.com/apollographql/apollo-server/pull/870) [Related docs PR](https://github.com/apollographql/apollo-server/pull/5536)

## v0.27.1

- Narrow `graphql` peer dependency to a more fitting range `^15.4.0` based on our current usage of the package. This requirement was introduced by, but not captured in, changes within the recently released `@apollo/federation@0.27.0`. As such, this change will be released as a `patch` since the breaking change already accidentally happened and this is a correction to that oversight. [PR #913](https://github.com/apollographql/federation/pull/913)
Expand Down
1 change: 1 addition & 0 deletions federation-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"dependencies": {
"apollo-graphql": "^0.9.3",
"apollo-server-types": "^3.0.2",
"lodash.xorby": "^4.7.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ describe('printFederatedSchema', () => {

union Brand = Amazon | Ikea

enum CacheControlScope {
PRIVATE
PUBLIC
}

type Car implements Vehicle @key(fields: \\"id\\") {
description: String
id: String!
Expand Down
10 changes: 10 additions & 0 deletions federation-js/src/service/__tests__/printSupergraphSdl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ describe('printSupergraphSdl', () => {

union Brand = Amazon | Ikea

enum CacheControlScope {
PRIVATE
PUBLIC
}

type Car implements Vehicle
@join__owner(graph: PRODUCT)
@join__type(graph: PRODUCT, key: \\"id\\")
Expand Down Expand Up @@ -373,6 +378,11 @@ describe('printSupergraphSdl', () => {

union Brand = Amazon | Ikea

enum CacheControlScope {
PRIVATE
PUBLIC
}

type Car implements Vehicle
@join__owner(graph: PRODUCT)
@join__type(graph: PRODUCT, key: \\"id\\")
Expand Down
14 changes: 14 additions & 0 deletions federation-js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
isObjectType,
} from 'graphql';
import { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue';
import { CacheHint } from 'apollo-server-types';

export const EntityType = new GraphQLUnionType({
name: '_Entity',
Expand Down Expand Up @@ -87,6 +88,19 @@ export const entitiesField: GraphQLFieldConfig<any, any> = {
);
}

// Note that while our TypeScript types (as of apollo-server-types@3.0.2)
// tell us that cacheControl and restrict are always defined, we want to
// avoid throwing when used with Apollo Server 2 which doesn't have
// `restrict`, or if the cache control plugin has been disabled.
if (info.cacheControl?.cacheHint?.restrict) {
const cacheHint: CacheHint | undefined =
info.cacheControl.cacheHintFromType(type);

if (cacheHint) {
info.cacheControl.cacheHint.restrict(cacheHint);
}
}

const resolveReference = type.resolveReference
? type.resolveReference
: function defaultResolveReference() {
Expand Down
3 changes: 3 additions & 0 deletions gateway-js/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

> The changes noted within this `vNEXT` section have not been released yet. New PRs and commits which introduce changes should include an entry in this `vNEXT` section as part of their development. When a release is being prepared, a new header will be (manually) created below and the appropriate changes within that release will be moved into the new section.

- In `RemoteGraphQLDataSource`, if the subgraph response has a `cache-control` header, use it to affect the current request's overall cache policy. You can disable this by passing `honorSubgraphCacheControlHeader: false` to the `RemoteGraphQLDataSource constructor`. This feature is only enabled when your subgraph is running Apollo Server 3.0.2 or later. [PR #870](https://github.com/apollographql/apollo-server/pull/870) [Related docs PR](https://github.com/apollographql/apollo-server/pull/5536)
- Provide the full incoming `GraphQLRequestContext` to `GraphQLDataSource.process`, as well as a `kind` allowing your implementation to differentiate between requests that come from incoming GraphQL operations, health checks, and schema fetches. [PR #870](https://github.com/apollographql/apollo-server/pull/870) [Issue #419](https://github.com/apollographql/apollo-server/issues/419) [Issue #835](https://github.com/apollographql/apollo-server/issues/835)

## v0.35.1

- Narrow `graphql` peer dependency to a more fitting range `^15.4.0` based on our current usage of the package. This requirement was introduced by, but not captured in, changes within the recently released `@apollo/gateway@0.35.0`. As such, this change will be released as a `patch` since the breaking change already accidentally happened and this is a correction to that oversight. [PR #913](https://github.com/apollographql/federation/pull/913)
Expand Down
10 changes: 8 additions & 2 deletions gateway-js/src/__tests__/gateway/buildService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ApolloServerBase as ApolloServer } from 'apollo-server-core';
import { RemoteGraphQLDataSource } from '../../datasources/RemoteGraphQLDataSource';
import { ApolloGateway, SERVICE_DEFINITION_QUERY } from '../../';
import { fixtures } from 'apollo-federation-integration-testsuite';
import { GraphQLDataSourceRequestKind } from '../../datasources/types';

beforeEach(() => {
fetch.mockReset();
Expand Down Expand Up @@ -36,8 +37,13 @@ it('correctly passes the context from ApolloServer to datasources', async () =>
buildService: _service => {
return new RemoteGraphQLDataSource({
url: 'https://api.example.com/foo',
willSendRequest: ({ request, context }) => {
request.http?.headers.set('x-user-id', context.userId);
willSendRequest: (options) => {
if (options.kind === GraphQLDataSourceRequestKind.INCOMING_OPERATION) {
options.request.http?.headers.set(
'x-user-id',
options.context.userId,
);
}
},
});
},
Expand Down
166 changes: 166 additions & 0 deletions gateway-js/src/__tests__/gateway/endToEnd.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { GraphQLSchemaModule } from 'apollo-graphql';
import { buildFederatedSchema } from '@apollo/federation';
import { ApolloServer } from 'apollo-server';
import fetch from 'node-fetch';
import { ApolloGateway } from '../..';
import { fixtures } from 'apollo-federation-integration-testsuite';
import { ApolloServerPluginInlineTrace } from 'apollo-server-core';

async function startFederatedServer(modules: GraphQLSchemaModule[]) {
const schema = buildFederatedSchema(modules);
const server = new ApolloServer({
schema,
// Manually installing the inline trace plugin means it doesn't log a message.
plugins: [ApolloServerPluginInlineTrace()],
});
const { url } = await server.listen({ port: 0 });
return { url, server };
}

describe('end-to-end', () => {
let backendServers: ApolloServer[];
let gatewayServer: ApolloServer;
let gatewayUrl: string;

beforeEach(async () => {
backendServers = [];
const serviceList = [];
for (const fixture of fixtures) {
const { server, url } = await startFederatedServer([fixture]);
backendServers.push(server);
serviceList.push({ name: fixture.name, url });
}

const gateway = new ApolloGateway({ serviceList });
gatewayServer = new ApolloServer({
gateway,
});
({ url: gatewayUrl } = await gatewayServer.listen({ port: 0 }));
});

afterEach(async () => {
for (const server of backendServers) {
await server.stop();
}
if (gatewayServer) {
await gatewayServer.stop();
}
});

it(`cache control`, async () => {
const query = `
query {
me {
name {
first
last
}
}
topProducts {
name
}
}
`;

const response = await fetch(gatewayUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query }),
});
const result = await response.json();
expect(result).toMatchInlineSnapshot(`
Object {
"data": Object {
"me": Object {
"name": Object {
"first": "Ada",
"last": "Lovelace",
},
},
"topProducts": Array [
Object {
"name": "Table",
},
Object {
"name": "Couch",
},
Object {
"name": "Chair",
},
Object {
"name": "Structure and Interpretation of Computer Programs (1996)",
},
Object {
"name": "Object Oriented Software Construction (1997)",
},
],
},
}
`);
expect(response.headers.get('cache-control')).toBe('max-age=30, private');
});

it(`cache control, uncacheable`, async () => {
const query = `
query {
me {
name {
first
last
}
}
topProducts {
name
... on Book {
details { # This field has no cache policy.
pages
}
}
}
}
`;

const response = await fetch(gatewayUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ query }),
});
const result = await response.json();
expect(result).toMatchInlineSnapshot(`
Object {
"data": Object {
"me": Object {
"name": Object {
"first": "Ada",
"last": "Lovelace",
},
},
"topProducts": Array [
Object {
"name": "Table",
},
Object {
"name": "Couch",
},
Object {
"name": "Chair",
},
Object {
"details": null,
"name": "Structure and Interpretation of Computer Programs (1996)",
},
Object {
"details": null,
"name": "Object Oriented Software Construction (1997)",
},
],
},
}
`);
expect(response.headers.get('cache-control')).toBe(null);
});
});
Loading