Skip to content

Commit 616caf2

Browse files
authored
Merge pull request #870 from mandiwise/main
Cache control support with federation
2 parents 53b456e + 467ad6b commit 616caf2

24 files changed

+532
-104
lines changed

federation-integration-testsuite-js/src/fixtures/accounts.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,25 @@ export const typeDefs = gql`
88
directive @transform(from: String!) on FIELD
99
directive @tag(name: String!) repeatable on FIELD_DEFINITION
1010
11+
enum CacheControlScope {
12+
PUBLIC
13+
PRIVATE
14+
}
15+
16+
directive @cacheControl(
17+
maxAge: Int
18+
scope: CacheControlScope
19+
inheritMaxAge: Boolean
20+
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
21+
1122
schema {
1223
query: RootQuery
1324
mutation: Mutation
1425
}
1526
1627
extend type RootQuery {
1728
user(id: ID!): User
18-
me: User
29+
me: User @cacheControl(maxAge: 1000, scope: PRIVATE)
1930
}
2031
2132
type PasswordAccount @key(fields: "email") {
@@ -36,7 +47,7 @@ export const typeDefs = gql`
3647
3748
type User @key(fields: "id") @key(fields: "username name { first last }") {
3849
id: ID! @tag(name: "accounts")
39-
name: Name
50+
name: Name @cacheControl(inheritMaxAge: true)
4051
username: String
4152
birthDate(locale: String): String @tag(name: "admin") @tag(name: "dev")
4253
account: AccountType

federation-integration-testsuite-js/src/fixtures/books.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ export const typeDefs = gql`
77
directive @stream on FIELD
88
directive @transform(from: String!) on FIELD
99
10+
enum CacheControlScope {
11+
PUBLIC
12+
PRIVATE
13+
}
14+
15+
directive @cacheControl(
16+
maxAge: Int
17+
scope: CacheControlScope
18+
inheritMaxAge: Boolean
19+
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
20+
21+
1022
extend type Query {
1123
book(isbn: String!): Book
1224
books: [Book]
@@ -26,7 +38,7 @@ export const typeDefs = gql`
2638
2739
# extend union AccountType = LibraryAccount
2840
29-
type Book @key(fields: "isbn") {
41+
type Book @key(fields: "isbn") @cacheControl(maxAge: 700) {
3042
isbn: String!
3143
title: String
3244
year: Int

federation-integration-testsuite-js/src/fixtures/product.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,21 @@ export const typeDefs = gql`
77
directive @stream on FIELD
88
directive @transform(from: String!) on FIELD
99
10+
enum CacheControlScope {
11+
PUBLIC
12+
PRIVATE
13+
}
14+
15+
directive @cacheControl(
16+
maxAge: Int
17+
scope: CacheControlScope
18+
inheritMaxAge: Boolean
19+
) on FIELD_DEFINITION | OBJECT | INTERFACE | UNION
20+
1021
extend type Query {
1122
product(upc: String!): Product
1223
vehicle(id: String!): Vehicle
13-
topProducts(first: Int = 5): [Product]
24+
topProducts(first: Int = 5): [Product] @cacheControl(maxAge: 40)
1425
topCars(first: Int = 5): [Car]
1526
}
1627
@@ -49,7 +60,7 @@ export const typeDefs = gql`
4960
type Furniture implements Product @key(fields: "upc") @key(fields: "sku") {
5061
upc: String!
5162
sku: String!
52-
name: String
63+
name: String @cacheControl(maxAge: 30)
5364
price: String
5465
brand: Brand
5566
metadata: [MetadataOrError]
@@ -184,7 +195,10 @@ export const resolvers: GraphQLResolverMap<any> = {
184195
},
185196
},
186197
Book: {
187-
__resolveReference(object) {
198+
__resolveReference(object, _context, info) {
199+
// For testing dynamic cache control; use `?.` because we don't always run
200+
// this fixture in a real ApolloServer.
201+
info.cacheControl?.cacheHint?.restrict({ maxAge: 30 });
188202
if (object.isbn) {
189203
const fetchedObject = products.find(
190204
product => product.isbn === object.isbn,

federation-js/CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
> 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.
66
7+
- 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)
8+
79
## v0.27.1
810

911
- 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/[email protected]`. 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)

federation-js/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
},
2525
"dependencies": {
2626
"apollo-graphql": "^0.9.3",
27+
"apollo-server-types": "^3.0.2",
2728
"lodash.xorby": "^4.7.0"
2829
},
2930
"peerDependencies": {

federation-js/src/service/__tests__/printFederatedSchema.test.ts

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,11 @@ describe('printFederatedSchema', () => {
4343
4444
union Brand = Amazon | Ikea
4545
46+
enum CacheControlScope {
47+
PRIVATE
48+
PUBLIC
49+
}
50+
4651
type Car implements Vehicle @key(fields: \\"id\\") {
4752
description: String
4853
id: String!

federation-js/src/service/__tests__/printSupergraphSdl.test.ts

+10
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ describe('printSupergraphSdl', () => {
8686
8787
union Brand = Amazon | Ikea
8888
89+
enum CacheControlScope {
90+
PRIVATE
91+
PUBLIC
92+
}
93+
8994
type Car implements Vehicle
9095
@join__owner(graph: PRODUCT)
9196
@join__type(graph: PRODUCT, key: \\"id\\")
@@ -373,6 +378,11 @@ describe('printSupergraphSdl', () => {
373378
374379
union Brand = Amazon | Ikea
375380
381+
enum CacheControlScope {
382+
PRIVATE
383+
PUBLIC
384+
}
385+
376386
type Car implements Vehicle
377387
@join__owner(graph: PRODUCT)
378388
@join__type(graph: PRODUCT, key: \\"id\\")

federation-js/src/types.ts

+14
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
isObjectType,
1414
} from 'graphql';
1515
import { PromiseOrValue } from 'graphql/jsutils/PromiseOrValue';
16+
import { CacheHint } from 'apollo-server-types';
1617

1718
export const EntityType = new GraphQLUnionType({
1819
name: '_Entity',
@@ -87,6 +88,19 @@ export const entitiesField: GraphQLFieldConfig<any, any> = {
8788
);
8889
}
8990

91+
// Note that while our TypeScript types (as of [email protected])
92+
// tell us that cacheControl and restrict are always defined, we want to
93+
// avoid throwing when used with Apollo Server 2 which doesn't have
94+
// `restrict`, or if the cache control plugin has been disabled.
95+
if (info.cacheControl?.cacheHint?.restrict) {
96+
const cacheHint: CacheHint | undefined =
97+
info.cacheControl.cacheHintFromType(type);
98+
99+
if (cacheHint) {
100+
info.cacheControl.cacheHint.restrict(cacheHint);
101+
}
102+
}
103+
90104
const resolveReference = type.resolveReference
91105
? type.resolveReference
92106
: function defaultResolveReference() {

gateway-js/CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
> 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.
66
7+
- 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)
8+
- 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)
9+
710
## v0.35.1
811

912
- 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/[email protected]`. 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)

gateway-js/src/__tests__/gateway/buildService.test.ts

+8-2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ApolloServerBase as ApolloServer } from 'apollo-server-core';
44
import { RemoteGraphQLDataSource } from '../../datasources/RemoteGraphQLDataSource';
55
import { ApolloGateway, SERVICE_DEFINITION_QUERY } from '../../';
66
import { fixtures } from 'apollo-federation-integration-testsuite';
7+
import { GraphQLDataSourceRequestKind } from '../../datasources/types';
78

89
beforeEach(() => {
910
fetch.mockReset();
@@ -36,8 +37,13 @@ it('correctly passes the context from ApolloServer to datasources', async () =>
3637
buildService: _service => {
3738
return new RemoteGraphQLDataSource({
3839
url: 'https://api.example.com/foo',
39-
willSendRequest: ({ request, context }) => {
40-
request.http?.headers.set('x-user-id', context.userId);
40+
willSendRequest: (options) => {
41+
if (options.kind === GraphQLDataSourceRequestKind.INCOMING_OPERATION) {
42+
options.request.http?.headers.set(
43+
'x-user-id',
44+
options.context.userId,
45+
);
46+
}
4147
},
4248
});
4349
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { GraphQLSchemaModule } from 'apollo-graphql';
2+
import { buildFederatedSchema } from '@apollo/federation';
3+
import { ApolloServer } from 'apollo-server';
4+
import fetch from 'node-fetch';
5+
import { ApolloGateway } from '../..';
6+
import { fixtures } from 'apollo-federation-integration-testsuite';
7+
import { ApolloServerPluginInlineTrace } from 'apollo-server-core';
8+
9+
async function startFederatedServer(modules: GraphQLSchemaModule[]) {
10+
const schema = buildFederatedSchema(modules);
11+
const server = new ApolloServer({
12+
schema,
13+
// Manually installing the inline trace plugin means it doesn't log a message.
14+
plugins: [ApolloServerPluginInlineTrace()],
15+
});
16+
const { url } = await server.listen({ port: 0 });
17+
return { url, server };
18+
}
19+
20+
describe('end-to-end', () => {
21+
let backendServers: ApolloServer[];
22+
let gatewayServer: ApolloServer;
23+
let gatewayUrl: string;
24+
25+
beforeEach(async () => {
26+
backendServers = [];
27+
const serviceList = [];
28+
for (const fixture of fixtures) {
29+
const { server, url } = await startFederatedServer([fixture]);
30+
backendServers.push(server);
31+
serviceList.push({ name: fixture.name, url });
32+
}
33+
34+
const gateway = new ApolloGateway({ serviceList });
35+
gatewayServer = new ApolloServer({
36+
gateway,
37+
});
38+
({ url: gatewayUrl } = await gatewayServer.listen({ port: 0 }));
39+
});
40+
41+
afterEach(async () => {
42+
for (const server of backendServers) {
43+
await server.stop();
44+
}
45+
if (gatewayServer) {
46+
await gatewayServer.stop();
47+
}
48+
});
49+
50+
it(`cache control`, async () => {
51+
const query = `
52+
query {
53+
me {
54+
name {
55+
first
56+
last
57+
}
58+
}
59+
topProducts {
60+
name
61+
}
62+
}
63+
`;
64+
65+
const response = await fetch(gatewayUrl, {
66+
method: 'POST',
67+
headers: {
68+
'Content-Type': 'application/json',
69+
},
70+
body: JSON.stringify({ query }),
71+
});
72+
const result = await response.json();
73+
expect(result).toMatchInlineSnapshot(`
74+
Object {
75+
"data": Object {
76+
"me": Object {
77+
"name": Object {
78+
"first": "Ada",
79+
"last": "Lovelace",
80+
},
81+
},
82+
"topProducts": Array [
83+
Object {
84+
"name": "Table",
85+
},
86+
Object {
87+
"name": "Couch",
88+
},
89+
Object {
90+
"name": "Chair",
91+
},
92+
Object {
93+
"name": "Structure and Interpretation of Computer Programs (1996)",
94+
},
95+
Object {
96+
"name": "Object Oriented Software Construction (1997)",
97+
},
98+
],
99+
},
100+
}
101+
`);
102+
expect(response.headers.get('cache-control')).toBe('max-age=30, private');
103+
});
104+
105+
it(`cache control, uncacheable`, async () => {
106+
const query = `
107+
query {
108+
me {
109+
name {
110+
first
111+
last
112+
}
113+
}
114+
topProducts {
115+
name
116+
... on Book {
117+
details { # This field has no cache policy.
118+
pages
119+
}
120+
}
121+
}
122+
}
123+
`;
124+
125+
const response = await fetch(gatewayUrl, {
126+
method: 'POST',
127+
headers: {
128+
'Content-Type': 'application/json',
129+
},
130+
body: JSON.stringify({ query }),
131+
});
132+
const result = await response.json();
133+
expect(result).toMatchInlineSnapshot(`
134+
Object {
135+
"data": Object {
136+
"me": Object {
137+
"name": Object {
138+
"first": "Ada",
139+
"last": "Lovelace",
140+
},
141+
},
142+
"topProducts": Array [
143+
Object {
144+
"name": "Table",
145+
},
146+
Object {
147+
"name": "Couch",
148+
},
149+
Object {
150+
"name": "Chair",
151+
},
152+
Object {
153+
"details": null,
154+
"name": "Structure and Interpretation of Computer Programs (1996)",
155+
},
156+
Object {
157+
"details": null,
158+
"name": "Object Oriented Software Construction (1997)",
159+
},
160+
],
161+
},
162+
}
163+
`);
164+
expect(response.headers.get('cache-control')).toBe(null);
165+
});
166+
});

0 commit comments

Comments
 (0)