From c4115e96ac75e04cffe1c3353fc03ea65dcab909 Mon Sep 17 00:00:00 2001 From: David Glasser Date: Thu, 4 Aug 2022 14:02:53 -0700 Subject: [PATCH] Move cache declarations to @apollo/cache-control-types (#6764) (This package temporarily resides in the apollo-utils repo so that we can publish it as non-alpha during the AS4 alpha process, eg for use in `@apollo/subgraph`.) Notably, this removes the `declare module` declaration. This does mean that TypeScript users will have to work a tad harder (as shown in the migration guide) to access dynamic cacheControl, but it also means that we no longer have to worry about multiple copies of `declare module` in a TypeScript build conflicting with each other, which has been a serious source of pain in this past. Co-authored-by: Rose M Koron <32436232+rkoron007@users.noreply.github.com> --- .changeset/pink-eggs-draw.md | 7 ++ docs/source/migration.mdx | 36 +++++++++- package-lock.json | 19 ++++++ package.json | 1 + packages/integration-testsuite/package.json | 1 + .../src/httpServerTests.ts | 3 +- .../src/ApolloServerPluginResponseCache.ts | 2 +- packages/server/package.json | 1 + .../server/src/__tests__/cachePolicy.test.ts | 2 +- .../cacheControl/cacheControlPlugin.test.ts | 3 +- .../cacheControl/collectCacheControlHints.ts | 3 +- .../cacheControl/dynamicCacheControl.test.ts | 17 ++--- packages/server/src/cachePolicy.ts | 2 +- .../server/src/externalTypes/cacheControl.ts | 66 ------------------- packages/server/src/externalTypes/graphql.ts | 2 +- packages/server/src/externalTypes/index.ts | 6 -- .../server/src/plugin/cacheControl/index.ts | 33 +++++++--- 17 files changed, 107 insertions(+), 97 deletions(-) create mode 100644 .changeset/pink-eggs-draw.md delete mode 100644 packages/server/src/externalTypes/cacheControl.ts diff --git a/.changeset/pink-eggs-draw.md b/.changeset/pink-eggs-draw.md new file mode 100644 index 00000000000..706e0dd125f --- /dev/null +++ b/.changeset/pink-eggs-draw.md @@ -0,0 +1,7 @@ +--- +"@apollo/server-integration-testsuite": patch +"@apollo/server-plugin-response-cache": patch +"@apollo/server": patch +--- + +Get cache-control types from @apollo/cache-control-types; no more `declare module` for info.cacheControl diff --git a/docs/source/migration.mdx b/docs/source/migration.mdx index 8c387a43a22..d81b2c01309 100644 --- a/docs/source/migration.mdx +++ b/docs/source/migration.mdx @@ -1090,6 +1090,7 @@ In Apollo Server 4, if your server _hasn't_ set up draining and it receives an o If you are using the `startStandaloneServer` function, your server drains automatically. If you are using `expressMiddleware` or another `http.Server`-based web server, you can add draining using the [`ApolloServerPluginDrainHttpServer` plugin](/apollo-server/api/plugin/drain-http-server/#using-the-plugin). ### `CacheScope` type + In Apollo Server 4, `CacheScope` is now a union of strings (`PUBLIC` or `PRIVATE`) rather than an enum: ```ts @@ -1098,7 +1099,7 @@ export type CacheScope = 'PUBLIC' | 'PRIVATE'; You can no longer type `CacheScope.Public` or `CacheScope.Private`. Instead, just use the string `'PUBLIC'` or `'PRIVATE'`. Values defined as `CacheScope` will only accept those two values, so any typos are still caught at compile time. - +You can now import `CacheScope` from the new `@apollo/cache-control-types` package (instead of importing it from an Apollo Server package). This enables libraries that work with multiple GraphQL servers (such as `@apollo/subgraph`) to refer to `CacheScope` without depending on `@apollo/server`. ## Plugin API changes @@ -1322,6 +1323,39 @@ In Apollo Server 3, the `apollo-server-env` package primarily provides TypeScrip Apollo Server 4 introduces `@apollo/utils.fetcher`, which defines a minimal fetch API (`Fetcher`) that provides Fetch API TypeScript typings. It is similar to `apollo-server-env` but has a clearer name and only supports argument structures that are likely to be compatible across many implementations of the Fetch API. (Specifically, it does not allow you to pass `Request` or `Headers` objects to `fetch`, because libraries often only know how to recognize their own implementations of these interfaces.) +### `@apollo/cache-control-types` + +In Apollo Server 3, you could import the `CacheScope`, `CacheHint`, `CacheAnnotation`, `CachePolicy`, and `ResolveInfoCacheControl` types from your chosen Apollo Server package. + +In Apollo Server 4, the new `@apollo/cache-control-types` package exports the [`CacheScope`](#cachescope-type), `CacheHint`, `CacheAnnotation`, `CachePolicy`, and `ResolveInfoCacheControl` types. This enables libraries that work with multiple GraphQL servers (such as `@apollo/subgraph`) to refer to these types without depending on `@apollo/server`. + +Apollo Server 4 no longer uses the `declare module` TypeScript feature to declare that all `GraphQLResolveInfo` objects (i.e., the `info` argument to resolvers) have a `cacheControl` field. Instead, `@apollo/cache-control-types` provides a `GraphQLResolveInfoWithCacheControl` interface that you can cast `info` to (if you don't want run-time validation), or if you do want runtime validation, you can use the `maybeCacheControlFromInfo` and `cacheControlFromInfo` functions. + +For example, if you had this resolver in Apollo Server 3: + +```ts + someField(parent, args, context, { cacheControl }) { + cacheControl.setCacheHint({ maxAge: 100 }); + } +``` + +you can write this in Apollo Server 4: + +```ts +import { cacheControlFromInfo } from '@apollo/cache-control-types'; + +// ... + someField(parent, args, context, info) { + cacheControlFromInfo(info).setCacheHint({ maxAge: 100 }); + } +``` + +Alternatively, you can declare `info` to be of type `GraphQLResolveInfoWithCacheControl`. For example, if using `graphql-code-generator` with `typescript-resolvers`, you can use the `customResolveInfo` option. + +Note: this is a TypeScript-specific change. The runtime representation hasn't changed, and JavaScript code can continue to access `info.cacheControl` directly. + +The `CacheAnnotation` type is no longer exported from any package. + ### Renamed types This section lists the TypeScript-only types (i.e., interfaces, not classes) whose names changed in Apollo Server 4 (not including those mentioned elsewhere in this guide). diff --git a/package-lock.json b/package-lock.json index 5eb8c62f5f3..2afad75aaf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "packages/*" ], "devDependencies": { + "@apollo/cache-control-types": "1.0.2", "@apollo/client": "3.6.9", "@apollo/utils.createhash": "1.1.0", "@changesets/changelog-github": "0.4.6", @@ -92,6 +93,14 @@ "node": ">=6.0.0" } }, + "node_modules/@apollo/cache-control-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.2.tgz", + "integrity": "sha512-Por80co1eUm4ATsvjCOoS/tIR8PHxqVjsA6z76I6Vw0rFn4cgyVElQcmQDIZiYsy41k8e5xkrMRECkM2WR8pNw==", + "peerDependencies": { + "graphql": "14.x || 15.x || 16.x" + } + }, "node_modules/@apollo/client": { "version": "3.6.9", "license": "MIT", @@ -12768,6 +12777,7 @@ "version": "4.0.0-alpha.2", "license": "MIT", "dependencies": { + "@apollo/cache-control-types": "^1.0.2", "@apollo/client": "^3.6.9", "@apollo/usage-reporting-protobuf": "^4.0.0-alpha.1", "@apollo/utils.createhash": "^1.1.0", @@ -12811,6 +12821,7 @@ "version": "4.0.0-alpha.2", "license": "MIT", "dependencies": { + "@apollo/cache-control-types": "^1.0.2", "@apollo/usage-reporting-protobuf": "^4.0.0-alpha.1", "@apollo/utils.createhash": "^1.1.0", "@apollo/utils.fetcher": "^1.0.0", @@ -12894,6 +12905,12 @@ "@jridgewell/trace-mapping": "^0.3.0" } }, + "@apollo/cache-control-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@apollo/cache-control-types/-/cache-control-types-1.0.2.tgz", + "integrity": "sha512-Por80co1eUm4ATsvjCOoS/tIR8PHxqVjsA6z76I6Vw0rFn4cgyVElQcmQDIZiYsy41k8e5xkrMRECkM2WR8pNw==", + "requires": {} + }, "@apollo/client": { "version": "3.6.9", "requires": { @@ -12937,6 +12954,7 @@ "@apollo/server": { "version": "file:packages/server", "requires": { + "@apollo/cache-control-types": "^1.0.2", "@apollo/usage-reporting-protobuf": "^4.0.0-alpha.1", "@apollo/utils.createhash": "^1.1.0", "@apollo/utils.fetcher": "^1.0.0", @@ -12971,6 +12989,7 @@ "@apollo/server-integration-testsuite": { "version": "file:packages/integration-testsuite", "requires": { + "@apollo/cache-control-types": "^1.0.2", "@apollo/client": "^3.6.9", "@apollo/usage-reporting-protobuf": "^4.0.0-alpha.1", "@apollo/utils.createhash": "^1.1.0", diff --git a/package.json b/package.json index 6f916293e22..e625c66e67c 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "npm": "^8.5.0" }, "devDependencies": { + "@apollo/cache-control-types": "1.0.2", "@apollo/client": "3.6.9", "@apollo/utils.createhash": "1.1.0", "@changesets/changelog-github": "0.4.6", diff --git a/packages/integration-testsuite/package.json b/packages/integration-testsuite/package.json index 51514e6c36c..74ac32b9c67 100644 --- a/packages/integration-testsuite/package.json +++ b/packages/integration-testsuite/package.json @@ -27,6 +27,7 @@ "node": ">=14.0" }, "dependencies": { + "@apollo/cache-control-types": "^1.0.2", "@apollo/client": "^3.6.9", "@apollo/utils.keyvaluecache": "^1.0.1", "@apollo/utils.createhash": "^1.1.0", diff --git a/packages/integration-testsuite/src/httpServerTests.ts b/packages/integration-testsuite/src/httpServerTests.ts index 62864e0cc31..bafdd1c4ba7 100644 --- a/packages/integration-testsuite/src/httpServerTests.ts +++ b/packages/integration-testsuite/src/httpServerTests.ts @@ -46,6 +46,7 @@ import { afterEach, } from '@jest/globals'; import type { Mock, SpyInstance } from 'jest-mock'; +import { cacheControlFromInfo } from '@apollo/cache-control-types'; const QueryRootType = new GraphQLObjectType({ name: 'QueryRoot', @@ -132,7 +133,7 @@ const queryType = new GraphQLObjectType({ testPersonWithCacheControl: { type: personType, resolve(_source, _args, _context, info) { - info.cacheControl.setCacheHint({ maxAge: 11 }); + cacheControlFromInfo(info).setCacheHint({ maxAge: 11 }); return { firstName: 'Jane', lastName: 'Doe' }; }, }, diff --git a/packages/plugin-response-cache/src/ApolloServerPluginResponseCache.ts b/packages/plugin-response-cache/src/ApolloServerPluginResponseCache.ts index ed94b04f2a8..7db31fa26c5 100644 --- a/packages/plugin-response-cache/src/ApolloServerPluginResponseCache.ts +++ b/packages/plugin-response-cache/src/ApolloServerPluginResponseCache.ts @@ -1,7 +1,7 @@ +import type { CacheHint } from '@apollo/cache-control-types'; import type { ApolloServerPlugin, BaseContext, - CacheHint, GraphQLRequestContext, GraphQLRequestListener, GraphQLResponse, diff --git a/packages/server/package.json b/packages/server/package.json index c1618fea4b0..0aacffd64aa 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -89,6 +89,7 @@ "node": ">=12.0" }, "dependencies": { + "@apollo/cache-control-types": "^1.0.2", "@apollo/usage-reporting-protobuf": "^4.0.0-alpha.1", "@apollo/utils.createhash": "^1.1.0", "@apollo/utils.fetcher": "^1.0.0", diff --git a/packages/server/src/__tests__/cachePolicy.test.ts b/packages/server/src/__tests__/cachePolicy.test.ts index e0d05f7f38b..7bb0ace21ce 100644 --- a/packages/server/src/__tests__/cachePolicy.test.ts +++ b/packages/server/src/__tests__/cachePolicy.test.ts @@ -1,4 +1,4 @@ -import type { CachePolicy } from '..'; +import type { CachePolicy } from '@apollo/cache-control-types'; import { newCachePolicy } from '../cachePolicy.js'; describe('newCachePolicy', () => { diff --git a/packages/server/src/__tests__/plugin/cacheControl/cacheControlPlugin.test.ts b/packages/server/src/__tests__/plugin/cacheControl/cacheControlPlugin.test.ts index 3bca05f3a44..6313d28e45e 100644 --- a/packages/server/src/__tests__/plugin/cacheControl/cacheControlPlugin.test.ts +++ b/packages/server/src/__tests__/plugin/cacheControl/cacheControlPlugin.test.ts @@ -3,7 +3,8 @@ import { ApolloServerPluginCacheControl, ApolloServerPluginCacheControlOptions, } from '../../../plugin/cacheControl'; -import { ApolloServer, CacheHint, HTTPGraphQLResponse } from '../../..'; +import { ApolloServer, HTTPGraphQLResponse } from '../../..'; +import type { CacheHint } from '@apollo/cache-control-types'; describe('plugin', () => { describe('willSendResponse', () => { diff --git a/packages/server/src/__tests__/plugin/cacheControl/collectCacheControlHints.ts b/packages/server/src/__tests__/plugin/cacheControl/collectCacheControlHints.ts index 0bcdc1eb049..898c5b2a7a6 100644 --- a/packages/server/src/__tests__/plugin/cacheControl/collectCacheControlHints.ts +++ b/packages/server/src/__tests__/plugin/cacheControl/collectCacheControlHints.ts @@ -1,5 +1,6 @@ +import type { CacheHint } from '@apollo/cache-control-types'; import type { GraphQLSchema } from 'graphql'; -import { ApolloServer, CacheHint } from '../../..'; +import { ApolloServer } from '../../..'; import { ApolloServerPluginCacheControl, ApolloServerPluginCacheControlOptions, diff --git a/packages/server/src/__tests__/plugin/cacheControl/dynamicCacheControl.test.ts b/packages/server/src/__tests__/plugin/cacheControl/dynamicCacheControl.test.ts index c118af31a48..4f653bad064 100644 --- a/packages/server/src/__tests__/plugin/cacheControl/dynamicCacheControl.test.ts +++ b/packages/server/src/__tests__/plugin/cacheControl/dynamicCacheControl.test.ts @@ -1,3 +1,4 @@ +import { cacheControlFromInfo } from '@apollo/cache-control-types'; import type { GraphQLScalarType, GraphQLFieldResolver, @@ -38,8 +39,8 @@ describe('dynamic cache control', () => { const resolvers: GraphQLResolvers = { Query: { - droid: (_source, _args, _context, { cacheControl }) => { - cacheControl.setCacheHint({ maxAge: 60 }); + droid: (_source, _args, _context, info) => { + cacheControlFromInfo(info).setCacheHint({ maxAge: 60 }); return { id: 2001, name: 'R2-D2', @@ -82,8 +83,8 @@ describe('dynamic cache control', () => { const resolvers: GraphQLResolvers = { Query: { - droid: (_source, _args, _context, { cacheControl }) => { - cacheControl.cacheHint.restrict({ maxAge: 60 }); + droid: (_source, _args, _context, info) => { + cacheControlFromInfo(info).cacheHint.restrict({ maxAge: 60 }); return { id: 2001, name: 'R2-D2', @@ -126,8 +127,8 @@ describe('dynamic cache control', () => { const resolvers: GraphQLResolvers = { Query: { - droid: (_source, _args, _context, { cacheControl }) => { - cacheControl.setCacheHint({ scope: 'PRIVATE' }); + droid: (_source, _args, _context, info) => { + cacheControlFromInfo(info).setCacheHint({ scope: 'PRIVATE' }); return { id: 2001, name: 'R2-D2', @@ -172,8 +173,8 @@ describe('dynamic cache control', () => { const resolvers: GraphQLResolvers = { Query: { - droid: (_source, _args, _context, { cacheControl }) => { - cacheControl.setCacheHint({ maxAge: 120 }); + droid: (_source, _args, _context, info) => { + cacheControlFromInfo(info).setCacheHint({ maxAge: 120 }); return { id: 2001, name: 'R2-D2', diff --git a/packages/server/src/cachePolicy.ts b/packages/server/src/cachePolicy.ts index f0c088efe82..ac889951f76 100644 --- a/packages/server/src/cachePolicy.ts +++ b/packages/server/src/cachePolicy.ts @@ -1,4 +1,4 @@ -import type { CacheHint, CachePolicy } from './externalTypes'; +import type { CacheHint, CachePolicy } from '@apollo/cache-control-types'; export function newCachePolicy(): CachePolicy { return { diff --git a/packages/server/src/externalTypes/cacheControl.ts b/packages/server/src/externalTypes/cacheControl.ts deleted file mode 100644 index a84a790d8da..00000000000 --- a/packages/server/src/externalTypes/cacheControl.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { GraphQLCompositeType } from 'graphql'; - -/** - * CacheHint represents a contribution to an overall cache policy. It can - * specify a maxAge and/or a scope. - */ -export interface CacheHint { - maxAge?: number; - scope?: CacheScope; -} - -/** - * CacheAnnotation represents the contents of a `@cacheControl` directive. - * (`inheritMaxAge` is part of this interface and not CacheHint, because - * `inheritMaxAge` isn't a contributing piece of a cache policy: it just means - * to not apply default values in some contexts.) - */ -export interface CacheAnnotation extends CacheHint { - inheritMaxAge?: true; -} - -export type CacheScope = 'PUBLIC' | 'PRIVATE'; - -/** - * CachePolicy is a mutable CacheHint with helpful methods for updating its - * fields. - */ -export interface CachePolicy extends CacheHint { - /** - * Mutate this CachePolicy by replacing each field defined in `hint`. This can - * make the policy more restrictive or less restrictive. - */ - replace(hint: CacheHint): void; - - /** - * Mutate this CachePolicy by restricting each field defined in `hint`. This - * can only make the policy more restrictive: a previously defined `maxAge` - * can only be reduced, and a previously Private scope cannot be made Public. - */ - restrict(hint: CacheHint): void; - - /** - * If this policy has a positive `maxAge`, then return a copy of itself as a - * `CacheHint` with both fields defined. Otherwise return null. - */ - policyIfCacheable(): Required | null; -} - -/** - * When using Apollo Server with the cache control plugin (on by default), an - * object of this kind is available to resolvers on `info.cacheControl`. - */ -export interface ResolveInfoCacheControl { - cacheHint: CachePolicy; - // Shorthand for `cacheHint.replace(hint)`; also for compatibility with - // the Apollo Server 2.x API. - setCacheHint(hint: CacheHint): void; - - cacheHintFromType(t: GraphQLCompositeType): CacheHint | undefined; -} - -declare module 'graphql/type/definition' { - interface GraphQLResolveInfo { - cacheControl: ResolveInfoCacheControl; - } -} diff --git a/packages/server/src/externalTypes/graphql.ts b/packages/server/src/externalTypes/graphql.ts index 6cbbfe845fd..abdcd10ebb5 100644 --- a/packages/server/src/externalTypes/graphql.ts +++ b/packages/server/src/externalTypes/graphql.ts @@ -6,7 +6,7 @@ import type { GraphQLSchema, OperationDefinitionNode, } from 'graphql'; -import type { CachePolicy } from './cacheControl'; +import type { CachePolicy } from '@apollo/cache-control-types'; import type { BaseContext } from './context'; import type { HTTPGraphQLHead, HTTPGraphQLRequest } from './http'; import type { ApolloServer } from '../ApolloServer'; diff --git a/packages/server/src/externalTypes/index.ts b/packages/server/src/externalTypes/index.ts index 9ac59f755bc..be42a6c0fd4 100644 --- a/packages/server/src/externalTypes/index.ts +++ b/packages/server/src/externalTypes/index.ts @@ -4,12 +4,6 @@ * is re-exported by the root (via * export), so add exports to this file with * intention (it's public API). */ -export type { - CacheAnnotation, - CacheHint, - CachePolicy, - CacheScope, -} from './cacheControl.js'; export type { BaseContext, ContextFunction, ContextThunk } from './context.js'; export type { GraphQLRequest, diff --git a/packages/server/src/plugin/cacheControl/index.ts b/packages/server/src/plugin/cacheControl/index.ts index dc6107dadbb..bb94c65af22 100644 --- a/packages/server/src/plugin/cacheControl/index.ts +++ b/packages/server/src/plugin/cacheControl/index.ts @@ -1,10 +1,4 @@ -import type { - ApolloServerPlugin, - BaseContext, - CacheAnnotation, - CacheHint, - CacheScope, -} from '../../externalTypes'; +import type { ApolloServerPlugin, BaseContext } from '../../externalTypes'; import { DirectiveNode, getNamedType, @@ -18,6 +12,21 @@ import { import { newCachePolicy } from '../../cachePolicy.js'; import { internalPlugin } from '../../internalPlugin.js'; import LRUCache from 'lru-cache'; +import type { + CacheHint, + CacheScope, + GraphQLResolveInfoWithCacheControl, +} from '@apollo/cache-control-types'; + +/** + * CacheAnnotation represents the contents of a `@cacheControl` directive. + * (`inheritMaxAge` is part of this interface and not CacheHint, because + * `inheritMaxAge` isn't a contributing piece of a cache policy: it just means + * to not apply default values in some contexts.) + */ +interface CacheAnnotation extends CacheHint { + inheritMaxAge?: true; +} export interface ApolloServerPluginCacheControlOptions { /** @@ -131,7 +140,10 @@ export function ApolloServerPluginCacheControl( const fakeFieldPolicy = newCachePolicy(); return { willResolveField({ info }) { - info.cacheControl = { + // This `as` is "safe" in the sense that this is the statement + // that makes a GraphQLResolveInfo into a + // GraphQLResolveInfoWithCacheControl. + (info as GraphQLResolveInfoWithCacheControl).cacheControl = { setCacheHint: (dynamicHint: CacheHint) => { fakeFieldPolicy.replace(dynamicHint); }, @@ -184,7 +196,10 @@ export function ApolloServerPluginCacheControl( fieldPolicy.replace(fieldAnnotation); } - info.cacheControl = { + // This `as` is "safe" in the sense that this is the statement + // that makes a GraphQLResolveInfo into a + // GraphQLResolveInfoWithCacheControl. + (info as GraphQLResolveInfoWithCacheControl).cacheControl = { setCacheHint: (dynamicHint: CacheHint) => { fieldPolicy.replace(dynamicHint); },