diff --git a/CHANGELOG.md b/CHANGELOG.md index e1dd0811d9a..d5d8aa2233f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ The version headers in this history reflect the versions of Apollo Server itself - Remove internal dependency on `apollo-server-caching`, switch over to `@apollo/utils.keyvaluecache`. This PR specifically also introduces Keyv as an unbounded cache solution, but will replace with our own simple implementation in a follow-up PR targeting this minor version release. [PR #6522](https://github.com/apollographql/apollo-server/pull/6522) - Remove dependency on `keyv`/`@apollo/utils.keyvadapter` in favor of a simple `Map`-backed cache which implements TTL [PR #6535](https://github.com/apollographql/apollo-server/pull/6535) +- Add `cache: "bounded"` configuration option, allowing users to opt into bounded request cache (recommended) [PR #6536](https://github.com/apollographql/apollo-server/pull/6536) ## v3.8.2 diff --git a/docs/source/api/apollo-server.mdx b/docs/source/api/apollo-server.mdx index 544f313176f..3cc6fd38110 100644 --- a/docs/source/api/apollo-server.mdx +++ b/docs/source/api/apollo-server.mdx @@ -20,6 +20,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, // highly recommended + cache: 'bounded', }); ``` @@ -228,6 +229,25 @@ Available in Apollo Server v3.4.0 and later. + + + +##### `cache` + +`KeyValueCache | "bounded"` + + + +A `KeyValueCache` which Apollo Server uses for a number of features like APQs and full response caching. This cache is also provided to DataSources and plugins. + +By default, the cache is unbounded. We don't recommend this, since a malicious client can run your server out of memory and cause it to crash by filling it with APQs. + +If you don't want to configure your own cache, you should set `cache: "bounded"`. The bounded cache is an [`InMemoryLRUCache`](https://www.npmjs.com/package/@apollo/utils.keyvaluecache) with a default size of roughly 30MiB. + +FIXME: link to new cache backend page + + + @@ -726,6 +746,7 @@ async function startApolloServer() { typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); diff --git a/docs/source/api/plugin/cache-control.md b/docs/source/api/plugin/cache-control.md index 31e85398e68..e21cbedef6c 100644 --- a/docs/source/api/plugin/cache-control.md +++ b/docs/source/api/plugin/cache-control.md @@ -21,6 +21,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginCacheControl({ // Cache everything for 1 second by default. @@ -42,6 +43,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ApolloServerPluginCacheControlDisabled()], }); ``` diff --git a/docs/source/api/plugin/drain-http-server.mdx b/docs/source/api/plugin/drain-http-server.mdx index d7c7f3d1e5b..261dc0ad2e8 100644 --- a/docs/source/api/plugin/drain-http-server.mdx +++ b/docs/source/api/plugin/drain-http-server.mdx @@ -39,6 +39,7 @@ async function startApolloServer() { typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); diff --git a/docs/source/api/plugin/inline-trace.md b/docs/source/api/plugin/inline-trace.md index 3ed2615b17d..43e8a12c083 100644 --- a/docs/source/api/plugin/inline-trace.md +++ b/docs/source/api/plugin/inline-trace.md @@ -21,6 +21,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginInlineTrace({ rewriteError: (err) => err.message.match(SENSITIVE_REGEX) ? null : err, @@ -39,6 +40,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ApolloServerPluginInlineTraceDisabled()], }); ``` diff --git a/docs/source/api/plugin/landing-pages.md b/docs/source/api/plugin/landing-pages.md index 25d1e23a839..c4595cb8674 100644 --- a/docs/source/api/plugin/landing-pages.md +++ b/docs/source/api/plugin/landing-pages.md @@ -29,17 +29,19 @@ To configure these default plugins while still using same `NODE_ENV`-based logic ```js import { ApolloServer } from "apollo-server"; -import { ApolloServerPluginLandingPageLocalDefault, - ApolloServerPluginLandingPageProductionDefault +import { + ApolloServerPluginLandingPageLocalDefault, + ApolloServerPluginLandingPageProductionDefault } from "apollo-server-core"; const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ // Install a landing page plugin based on NODE_ENV - process.env.NODE_ENV === 'production' + process.env.NODE_ENV === "production" ? ApolloServerPluginLandingPageProductionDefault({ graphRef: "my-graph-id@my-graph-variant", footer: false, @@ -428,6 +430,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginLandingPageGraphQLPlayground(), ], @@ -512,6 +515,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginLandingPageDisabled(), ], diff --git a/docs/source/api/plugin/schema-reporting.md b/docs/source/api/plugin/schema-reporting.md index 10d3a5d521f..80547bcc647 100644 --- a/docs/source/api/plugin/schema-reporting.md +++ b/docs/source/api/plugin/schema-reporting.md @@ -23,6 +23,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginSchemaReporting(), ], diff --git a/docs/source/api/plugin/usage-reporting.md b/docs/source/api/plugin/usage-reporting.md index 7ae01520b7b..edc82631a2d 100644 --- a/docs/source/api/plugin/usage-reporting.md +++ b/docs/source/api/plugin/usage-reporting.md @@ -25,6 +25,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginUsageReporting({ fieldLevelInstrumentation: 0.5, @@ -454,6 +455,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ApolloServerPluginUsageReportingDisabled()], }); ``` diff --git a/docs/source/builtin-plugins.md b/docs/source/builtin-plugins.md index 1cb2c7ef176..de66fa819bd 100644 --- a/docs/source/builtin-plugins.md +++ b/docs/source/builtin-plugins.md @@ -31,6 +31,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ // Sets a non-default option on the usage reporting plugin ApolloServerPluginUsageReporting({ diff --git a/docs/source/data/data-sources.mdx b/docs/source/data/data-sources.mdx index 672bcdec87a..10104a5857c 100644 --- a/docs/source/data/data-sources.mdx +++ b/docs/source/data/data-sources.mdx @@ -55,6 +55,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", dataSources: () => { return { moviesAPI: new MoviesAPI(), diff --git a/docs/source/data/errors.mdx b/docs/source/data/errors.mdx index 44ddbc28c3d..babf00169c1 100644 --- a/docs/source/data/errors.mdx +++ b/docs/source/data/errors.mdx @@ -343,6 +343,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", formatError: (err) => { // Don't give the specific errors to the client. if (err.message.startsWith('Database Error: ')) { @@ -398,6 +399,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginUsageReporting({ rewriteError(err) { @@ -429,6 +431,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginUsageReporting({ rewriteError(err) { @@ -467,6 +470,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginUsageReporting({ rewriteError(err) { @@ -512,6 +516,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', plugins: [setHttpPlugin], }); ``` diff --git a/docs/source/data/file-uploads.mdx b/docs/source/data/file-uploads.mdx index 79c3e60f5b3..4fdf60debe4 100644 --- a/docs/source/data/file-uploads.mdx +++ b/docs/source/data/file-uploads.mdx @@ -79,6 +79,7 @@ async function startServer() { resolvers, // Using graphql-upload without CSRF prevention is very insecure. csrfPrevention: true, + cache: 'bounded', }); await server.start(); @@ -181,6 +182,7 @@ const start = async () => { resolvers, // Using graphql-upload without CSRF prevention is very insecure. csrfPrevention: true, + cache: 'bounded', }); // Start Apollo Server diff --git a/docs/source/data/resolvers.mdx b/docs/source/data/resolvers.mdx index 3ae46b62ab1..3e5344db43b 100644 --- a/docs/source/data/resolvers.mdx +++ b/docs/source/data/resolvers.mdx @@ -145,7 +145,12 @@ const resolvers = { // Pass schema definition and resolvers to the // ApolloServer constructor -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); // Launch the server server.listen().then(({ url }) => { @@ -323,7 +328,12 @@ const resolvers = { // Pass schema definition and resolvers to the // ApolloServer constructor -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); // Launch the server server.listen().then(({ url }) => { @@ -388,6 +398,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', context: ({ req }) => ({ authScope: getScope(req.headers.authorization) }) diff --git a/docs/source/data/subscriptions.mdx b/docs/source/data/subscriptions.mdx index cf22ea95702..7628b542430 100644 --- a/docs/source/data/subscriptions.mdx +++ b/docs/source/data/subscriptions.mdx @@ -82,6 +82,7 @@ To run both an Express app _and_ a separate WebSocket server for subscriptions, const server = new ApolloServer({ schema, csrfPrevention: true, + cache: "bounded", }); ``` @@ -108,6 +109,7 @@ To run both an Express app _and_ a separate WebSocket server for subscriptions, const server = new ApolloServer({ schema, csrfPrevention: true, + cache: "bounded", plugins: [ // Proper shutdown for the HTTP server. ApolloServerPluginDrainHttpServer({ httpServer }), @@ -168,6 +170,7 @@ const serverCleanup = useServer({ schema }, wsServer); const server = new ApolloServer({ schema, csrfPrevention: true, + cache: "bounded", plugins: [ // Proper shutdown for the HTTP server. ApolloServerPluginDrainHttpServer({ httpServer }), diff --git a/docs/source/deployment/azure-functions.mdx b/docs/source/deployment/azure-functions.mdx index c54e4f47fc6..66aa5de6d6a 100644 --- a/docs/source/deployment/azure-functions.mdx +++ b/docs/source/deployment/azure-functions.mdx @@ -95,7 +95,12 @@ const resolvers = { }; // Create our server. -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); exports.graphqlHandler = server.createHandler(); ``` diff --git a/docs/source/deployment/gcp-functions.mdx b/docs/source/deployment/gcp-functions.mdx index 09693d8e25d..17e06ad027e 100644 --- a/docs/source/deployment/gcp-functions.mdx +++ b/docs/source/deployment/gcp-functions.mdx @@ -27,6 +27,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', }); exports.handler = server.createHandler(); @@ -164,6 +165,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', context: ({ req, res }) => ({ headers: req.headers, req, diff --git a/docs/source/deployment/lambda.md b/docs/source/deployment/lambda.md index 2e2c0011823..a6550ec0aed 100644 --- a/docs/source/deployment/lambda.md +++ b/docs/source/deployment/lambda.md @@ -50,7 +50,12 @@ const resolvers = { }, }; -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); exports.graphqlHandler = server.createHandler(); ``` @@ -211,6 +216,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', context: ({ event, context, express }) => ({ headers: event.headers, functionName: context.functionName, diff --git a/docs/source/getting-started.mdx b/docs/source/getting-started.mdx index 6f9a96ee406..1562ca8f4fe 100644 --- a/docs/source/getting-started.mdx +++ b/docs/source/getting-started.mdx @@ -152,6 +152,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', }); // The `listen` method launches a web server. diff --git a/docs/source/integrations/middleware.mdx b/docs/source/integrations/middleware.mdx index ef9003a9121..b9a4eab6a49 100644 --- a/docs/source/integrations/middleware.mdx +++ b/docs/source/integrations/middleware.mdx @@ -130,7 +130,12 @@ Let's say our `apollo-server` implementation uses the following code: import { ApolloServer } from "apollo-server"; async function startApolloServer(typeDefs, resolvers) { - const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); + const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: "bounded", + }); const { url } = await server.listen(); console.log(`🚀 Server ready at ${url}`); } @@ -166,6 +171,7 @@ async function startApolloServer(typeDefs, resolvers) { typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); @@ -228,7 +234,12 @@ npm install apollo-server graphql import { ApolloServer } from 'apollo-server'; async function startApolloServer(typeDefs, resolvers) { - const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); + const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', + }); const { url } = await server.listen(); console.log(`🚀 Server ready at ${url}`); } @@ -267,6 +278,7 @@ async function startApolloServer(typeDefs, resolvers) { typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); await server.start(); @@ -326,6 +338,7 @@ async function startApolloServer(typeDefs, resolvers) { typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', plugins: [ fastifyAppClosePlugin(app), ApolloServerPluginDrainHttpServer({ httpServer: app.server }), @@ -377,6 +390,7 @@ async function startApolloServer(typeDefs, resolvers) { typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', plugins: [ApolloServerPluginStopHapiServer({ hapiServer: app })], }); @@ -423,6 +437,7 @@ async function startApolloServer(typeDefs, resolvers) { typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); @@ -466,7 +481,12 @@ npm install apollo-server-micro micro graphql ```ts title="index.ts" import { ApolloServer } from 'apollo-server-micro'; -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); module.exports = server.start().then(() => server.createHandler()); ``` @@ -503,7 +523,12 @@ npm install apollo-server-lambda graphql ```ts title="index.ts" import { ApolloServer } from 'apollo-server-lambda'; -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); exports.handler = server.createHandler(); ``` @@ -529,7 +554,12 @@ npm install apollo-server-cloud-functions graphql ```ts title="index.ts" import { ApolloServer } from 'apollo-server-cloud-functions'; -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); exports.handler = server.createHandler(); ``` @@ -554,7 +584,12 @@ npm install apollo-server-azure-functions graphql ```ts title="index.ts" import { ApolloServer } from 'apollo-server-azure-functions'; -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); exports.handler = server.createHandler(); ``` diff --git a/docs/source/integrations/plugins.md b/docs/source/integrations/plugins.md index 547caafc255..02aed5bf63d 100644 --- a/docs/source/integrations/plugins.md +++ b/docs/source/integrations/plugins.md @@ -207,6 +207,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', // You can import plugins or define them in-line, as shown: plugins: [ diff --git a/docs/source/monitoring/health-checks.md b/docs/source/monitoring/health-checks.md index b4a3a9929b6..a9d46903e4a 100644 --- a/docs/source/monitoring/health-checks.md +++ b/docs/source/monitoring/health-checks.md @@ -29,7 +29,7 @@ You can pass a string `healthCheckPath` to the `ApolloServer` constructor to cha If you'd like the health check to do more than just "always return success", you can pass an async function `onHealthCheck` function to the `ApolloServer` constructor. If defined, this `onHealthCheck` async function should return if the server is deemed _ready_ or `throw` if there is an error. Returning (resolving the `Promise`) will result in an HTTP status code of 200, which is generally desired by most health-check tooling (e.g. Kubernetes, AWS, etc.), while `throw`ing (rejecting the `Promise`) will result in an HTTP status code of 503. -```js {10-17} +```js {10-18} import { ApolloServer, gql } from 'apollo-server'; // Undefined for brevity. @@ -40,6 +40,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', async onHealthCheck() { if (everythingLooksHealthy()) { return; diff --git a/docs/source/monitoring/metrics.md b/docs/source/monitoring/metrics.md index 9dda6ba0554..321c4878843 100644 --- a/docs/source/monitoring/metrics.md +++ b/docs/source/monitoring/metrics.md @@ -55,7 +55,7 @@ version in the [`ApolloClient` constructor](https://www.apollographql.com/docs/r For more advanced cases, or to use headers other than the default headers, pass a `generateClientInfo` function into the [usage reporting plugin](../api/plugin/usage-reporting/): -```js {9-24} +```js {10-25} const { ApolloServer } = require("apollo-server"); const { ApolloServerPluginUsageReporting } = require("apollo-server-core"); @@ -63,6 +63,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: "bounded", plugins: [ ApolloServerPluginUsageReporting({ generateClientInfo: ({ @@ -71,8 +72,8 @@ const server = new ApolloServer({ const headers = request.http && request.http.headers; if(headers) { return { - clientName: headers['apollographql-client-name'], - clientVersion: headers['apollographql-client-version'], + clientName: headers["apollographql-client-name"], + clientVersion: headers["apollographql-client-version"], }; } else { return { @@ -126,6 +127,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', plugins: [ myPlugin ] diff --git a/docs/source/performance/apq.md b/docs/source/performance/apq.md index 56248efbeec..dd528f8b2de 100644 --- a/docs/source/performance/apq.md +++ b/docs/source/performance/apq.md @@ -134,6 +134,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', // The max age is calculated in seconds plugins: [ApolloServerPluginCacheControl({ defaultMaxAge: 5 })], }); @@ -206,6 +207,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', // highlight-start persistedQueries: { cache: new MemcachedCache( @@ -231,6 +233,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', // highlight-start persistedQueries: { cache: new BaseRedisCache({ @@ -257,6 +260,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', // highlight-start persistedQueries: { cache: new BaseRedisCache({ @@ -288,6 +292,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', // highlight-start persistedQueries: { cache: new BaseRedisCache({ @@ -318,6 +323,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', persistedQueries: { // highlight-start ttl: 900, // 15 minutes @@ -333,6 +339,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', persistedQueries: { ttl: null, // highlight-line }, @@ -350,6 +357,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', persistedQueries: false, // highlight-line }); ``` diff --git a/docs/source/proxy-configuration.md b/docs/source/proxy-configuration.md index dccced5aad8..c5cee22349f 100644 --- a/docs/source/proxy-configuration.md +++ b/docs/source/proxy-configuration.md @@ -38,6 +38,7 @@ const server = new ApolloServer({ typesDefs, resolvers, csrfPrevention: true, + cache: 'bounded', }); ``` diff --git a/docs/source/schema/creating-directives.mdx b/docs/source/schema/creating-directives.mdx index 776c68b8764..e7ea3f452ba 100644 --- a/docs/source/schema/creating-directives.mdx +++ b/docs/source/schema/creating-directives.mdx @@ -338,7 +338,11 @@ let schema = makeExecutableSchema({ schema = upperDirectiveTransformer(schema, 'upper'); // Provide the schema to the ApolloServer constructor -const server = new ApolloServer({schema, csrfPrevention: true}); +const server = new ApolloServer({ + schema, + csrfPrevention: true, + cache: 'bounded', +}); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`); diff --git a/docs/source/schema/custom-scalars.md b/docs/source/schema/custom-scalars.md index b55b8b80028..ff24b59907f 100644 --- a/docs/source/schema/custom-scalars.md +++ b/docs/source/schema/custom-scalars.md @@ -116,6 +116,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', }); ``` @@ -139,10 +140,10 @@ const typeDefs = gql` // Validation function for checking "oddness" function oddValue(value) { - if (typeof value === "number" && Number.isInteger(value) && value % 2 !== 0) { + if (typeof value === 'number' && Number.isInteger(value) && value % 2 !== 0) { return value; } - throw new UserInputError("Provided value is not an odd integer"); + throw new UserInputError('Provided value is not an odd integer'); } const resolvers = { @@ -155,7 +156,7 @@ const resolvers = { if (ast.kind === Kind.INT) { return oddValue(parseInt(ast.value, 10)); } - throw new UserInputError("Provided value is not an odd integer"); + throw new UserInputError('Provided value is not an odd integer'); }, }), Query: { @@ -165,7 +166,12 @@ const resolvers = { } }; -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`) @@ -207,7 +213,12 @@ const resolvers = { // ...other resolvers... }; -const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); +const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', +}); server.listen().then(({ url }) => { console.log(`🚀 Server ready at ${url}`) diff --git a/docs/source/schema/unions-interfaces.md b/docs/source/schema/unions-interfaces.md index 762e4482a37..6685c4bb2dc 100644 --- a/docs/source/schema/unions-interfaces.md +++ b/docs/source/schema/unions-interfaces.md @@ -132,6 +132,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', }); server.listen().then(({ url }) => { diff --git a/docs/source/security/authentication.md b/docs/source/security/authentication.md index f3a07969ddd..0dcf124aa0a 100644 --- a/docs/source/security/authentication.md +++ b/docs/source/security/authentication.md @@ -21,6 +21,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, + cache: 'bounded', context: ({ req }) => { // Note: This example uses the `req` argument to access headers, // but the arguments received by `context` vary by integration. @@ -229,7 +230,8 @@ One way of implementing the `@auth` directive is by constructing your schema usi new ApolloServer({ schema: makeExecutableSchema({ typeDefs, resolvers, schemaTransforms }), csrfPrevention: true, -}) + cache: 'bounded', +}); ``` ### Outside of GraphQL diff --git a/docs/source/security/cors.mdx b/docs/source/security/cors.mdx index 3af40701e36..4cc05158053 100644 --- a/docs/source/security/cors.mdx +++ b/docs/source/security/cors.mdx @@ -99,6 +99,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, // see below for more about this + cache: "bounded", cors: { origin: ["https://www.your-app.example", "https://studio.apollographql.com"] }, @@ -145,6 +146,7 @@ const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, // see below for more about this + cache: "bounded", cors: { origin: yourOrigin, credentials: true diff --git a/docs/source/security/terminating-ssl.mdx b/docs/source/security/terminating-ssl.mdx index 1b4bc4130b2..98d706d240a 100644 --- a/docs/source/security/terminating-ssl.mdx +++ b/docs/source/security/terminating-ssl.mdx @@ -30,7 +30,12 @@ async function startApolloServer() { const environment = process.env.NODE_ENV || 'production'; const config = configurations[environment]; - const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true }); + const server = new ApolloServer({ + typeDefs, + resolvers, + csrfPrevention: true, + cache: 'bounded', + }); await server.start(); const app = express(); diff --git a/packages/apollo-server-core/src/ApolloServer.ts b/packages/apollo-server-core/src/ApolloServer.ts index 1736f53a828..154cdef924e 100644 --- a/packages/apollo-server-core/src/ApolloServer.ts +++ b/packages/apollo-server-core/src/ApolloServer.ts @@ -261,10 +261,14 @@ export class ApolloServerBase< : noIntro; } - if (!requestOptions.cache) { + if (requestOptions.cache === 'bounded') { requestOptions.cache = new InMemoryLRUCache(); } + if (!requestOptions.cache) { + requestOptions.cache = new UnboundedCache(); + } + if (requestOptions.persistedQueries !== false) { const { cache: apqCache = requestOptions.cache!, ...apqOtherOptions } = requestOptions.persistedQueries || Object.create(null); diff --git a/packages/apollo-server-core/src/types.ts b/packages/apollo-server-core/src/types.ts index db3fa2244b2..37a0825da41 100644 --- a/packages/apollo-server-core/src/types.ts +++ b/packages/apollo-server-core/src/types.ts @@ -40,7 +40,6 @@ type BaseConfig = Pick< | 'formatResponse' | 'fieldResolver' | 'dataSources' - | 'cache' | 'logger' | 'allowBatchedHttpRequests' >; @@ -110,6 +109,7 @@ export interface Config extends BaseConfig { nodeEnv?: string; documentStore?: DocumentStore | null; csrfPrevention?: CSRFPreventionOptions | boolean; + cache?: KeyValueCache | 'bounded'; } export interface CSRFPreventionOptions { diff --git a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts index 6ba585f4bb9..90aedbaef29 100644 --- a/packages/apollo-server-integration-testsuite/src/ApolloServer.ts +++ b/packages/apollo-server-integration-testsuite/src/ApolloServer.ts @@ -59,8 +59,8 @@ import FakeTimers from '@sinonjs/fake-timers'; import type { AddressInfo } from 'net'; import request, { Response } from 'supertest'; import { - type KeyValueCache, InMemoryLRUCache, + type KeyValueCache, } from '@apollo/utils.keyvaluecache'; const quietLogger = loglevel.getLogger('quiet'); @@ -2265,45 +2265,38 @@ export function testApolloServer( clock.uninstall(); }); - it('basic caching', async () => { - class TTLTestingCache implements KeyValueCache { - private fakeTime = 0; - constructor( - private cache: Map< - string, - { value: string; deadline: number | null } - > = new Map(), - ) {} - - async get(key: string) { - const entry = this.cache.get(key); - if (!entry) return undefined; - if (entry.deadline && entry.deadline <= this.fakeTime) { - await this.delete(key); - return undefined; - } - return entry.value; - } + it('uses an unbounded cache by default', async () => { + const server = new ApolloServerBase({ + typeDefs: `type Query { hello: String }`, + }); - async set( - key: string, - value: string, - { ttl }: { ttl: number | null } = { ttl: null }, - ) { - this.cache.set(key, { - value, - deadline: ttl ? this.fakeTime + ttl * 1000 : null, - }); - } + // This could be an instanceof check but we don't really want to export + // the `UnboundedCache` class from `apollo-server-core` + expect(server['requestOptions'].cache!.constructor.name).toBe( + 'UnboundedCache', + ); + }); - async delete(key: string) { - this.cache.delete(key); - } + it('uses a bounded cache', async () => { + const server = new ApolloServerBase({ + typeDefs: `type Query { hello: String }`, + cache: 'bounded', + }); - advanceTime(ms: number) { - this.fakeTime += ms; - } - } + expect(server['requestOptions'].cache).toBeInstanceOf(InMemoryLRUCache); + }); + + it('uses a custom cache', async () => { + const customCache = {} as KeyValueCache; + const server = new ApolloServerBase({ + typeDefs: `type Query { hello: String }`, + cache: customCache, + }); + + expect(server['requestOptions'].cache).toBe(customCache); + }); + + it('basic caching', async () => { const typeDefs = gql` type Query { cached: String @cacheControl(maxAge: 10) @@ -2359,12 +2352,9 @@ export function testApolloServer( }; }); - const fakeTTLCache = new TTLTestingCache(); - const { url: uri } = await createApolloServer({ typeDefs, resolvers, - cache: fakeTTLCache, plugins: [ ApolloServerPluginResponseCache({ sessionId: (requestContext: GraphQLRequestContext) => { @@ -2488,7 +2478,6 @@ export function testApolloServer( } // Cache hit partway to ttl. - fakeTTLCache.advanceTime(5 * 1000); clock.tick(5 * 1000); { const result = await fetch(); @@ -2500,7 +2489,6 @@ export function testApolloServer( } // Cache miss after ttl. - fakeTTLCache.advanceTime(6 * 1000); clock.tick(6 * 1000); { const result = await fetch(); @@ -2778,7 +2766,6 @@ export function testApolloServer( } // Let's expire the cache, and run again, not writing to the cache. - fakeTTLCache.advanceTime(15 * 1000); clock.tick(15 * 1000); { const result = await doFetch({