diff --git a/packages/server-types/src/plugins.ts b/packages/server-types/src/plugins.ts index a33cfe6fdbf..8fc6f3ba0d0 100644 --- a/packages/server-types/src/plugins.ts +++ b/packages/server-types/src/plugins.ts @@ -24,6 +24,14 @@ export interface ApolloServerPlugin { requestDidStart?( requestContext: GraphQLRequestContext, ): Promise | void>; + + // See the similarly named field on ApolloServer for details. Note that it + // appears that this only works if it is a *field*, not a *method*, which is + // why `requestDidStart` (which takes a TContext wrapped in something) is not + // sufficient. + // + // TODO(AS4): Upgrade to TS 4.7 and use `in` instead. + __forceTContextToBeContravariant?: (contextValue: TContext) => void; } export interface GraphQLServerListener { diff --git a/packages/server-types/src/types.ts b/packages/server-types/src/types.ts index 9a6c03ade5f..73875cba28b 100644 --- a/packages/server-types/src/types.ts +++ b/packages/server-types/src/types.ts @@ -64,7 +64,7 @@ export type HTTPGraphQLResponse = { } ); -export type BaseContext = Record; +export type BaseContext = {}; export type WithRequired = T & Required>; diff --git a/packages/server/src/ApolloServer.ts b/packages/server/src/ApolloServer.ts index 3cb07b8824e..deefa87dba7 100644 --- a/packages/server/src/ApolloServer.ts +++ b/packages/server/src/ApolloServer.ts @@ -144,7 +144,7 @@ class UnreachableCaseError extends Error { // TODO(AS4): Move this to its own file or something. Also organize the fields. -export interface ApolloServerInternals { +export interface ApolloServerInternals { formatError?: (error: GraphQLError) => GraphQLFormattedError; // TODO(AS4): Is there a way (with generics/codegen?) to make // this "any" more specific? In AS3 there was technically a @@ -206,6 +206,8 @@ export class ApolloServer { // once `out TContext` is available. Note that when we replace this with `out // TContext`, we may make it so that users of older TypeScript versions no // longer have this protection. + // + // TODO(AS4): upgrade to TS 4.7 when it is released and use that instead. protected __forceTContextToBeContravariant?: (contextValue: TContext) => void; constructor(config: ApolloServerOptions) { @@ -636,7 +638,7 @@ export class ApolloServer { ); } - private static constructSchema( + private static constructSchema( config: ApolloServerOptionsWithStaticSchema, ): GraphQLSchema { if (config.schema) { @@ -659,7 +661,7 @@ export class ApolloServer { }); } - private static maybeAddMocksToConstructedSchema( + private static maybeAddMocksToConstructedSchema( schema: GraphQLSchema, config: ApolloServerOptionsWithStaticSchema, ): GraphQLSchema { @@ -807,7 +809,7 @@ export class ApolloServer { // This is called in the constructor before this.internals has been // initialized, so we make it static to make it clear it can't assume that // `this` has been fully initialized. - private static ensurePluginInstantiation( + private static ensurePluginInstantiation( userPlugins: PluginDefinition[] = [], isDev: boolean, apolloConfig: ApolloConfig, @@ -1146,7 +1148,7 @@ export type ImplicitlyInstallablePlugin = __internal_installed_implicitly__: boolean; }; -export function isImplicitlyInstallablePlugin( +export function isImplicitlyInstallablePlugin( p: ApolloServerPlugin, ): p is ImplicitlyInstallablePlugin { return '__internal_installed_implicitly__' in p; diff --git a/packages/server/src/__tests__/ApolloServer.test.ts b/packages/server/src/__tests__/ApolloServer.test.ts index e1c10463527..de2ae5f0550 100644 --- a/packages/server/src/__tests__/ApolloServer.test.ts +++ b/packages/server/src/__tests__/ApolloServer.test.ts @@ -305,91 +305,127 @@ describe('ApolloServer executeOperation', () => { expect(result.data?.contextFoo).toBe('bla'); }); - it('typing for context objects works', async () => { - const server = new ApolloServer<{ foo: number }>({ - typeDefs: 'type Query { n: Int!, n2: String! }', - resolvers: { - Query: { - n(_parent: any, _args: any, context): number { - return context.foo; - }, - n2(_parent: any, _args: any, context): string { - // It knows that context.foo is a number so it doesn't work as a string. - // @ts-expect-error - return context.foo; + describe('context generic typing', () => { + it('typing for context objects works', async () => { + const server = new ApolloServer<{ foo: number }>({ + typeDefs: 'type Query { n: Int!, n2: String! }', + resolvers: { + Query: { + n(_parent: any, _args: any, context): number { + return context.foo; + }, + n2(_parent: any, _args: any, context): string { + // It knows that context.foo is a number so it doesn't work as a string. + // @ts-expect-error + return context.foo; + }, }, }, - }, - plugins: [ - { - // Works with plugins too! - async requestDidStart({ contextValue }) { - let n: number = contextValue.foo; - // @ts-expect-error - let s: string = contextValue.foo; - // Make sure both variables are used (so the only expected error - // is the type error). - JSON.stringify({ n, s }); + plugins: [ + { + // Works with plugins too! + async requestDidStart({ contextValue }) { + let n: number = contextValue.foo; + // @ts-expect-error + let s: string = contextValue.foo; + // Make sure both variables are used (so the only expected error + // is the type error). + JSON.stringify({ n, s }); + }, }, - }, - // Plugins declared to be still work. - ApolloServerPluginCacheControlDisabled(), - ], + // Plugins declared to be still work. + ApolloServerPluginCacheControlDisabled(), + ], + }); + await server.start(); + const result = await server.executeOperation( + { query: '{ n }' }, + { foo: 123 }, + ); + expect(result.errors).toBeUndefined(); + expect(result.data?.n).toBe(123); + + const result2 = await server.executeOperation( + { query: '{ n }' }, + // It knows that context.foo is a number so it doesn't work as a string. + // @ts-expect-error + { foo: 'asdf' }, + ); + // GraphQL will be sad that a string was returned from an Int! field. + expect(result2.errors).toBeDefined(); }); - await server.start(); - const result = await server.executeOperation( - { query: '{ n }' }, - { foo: 123 }, - ); - expect(result.errors).toBeUndefined(); - expect(result.data?.n).toBe(123); - const result2 = await server.executeOperation( - { query: '{ n }' }, - // It knows that context.foo is a number so it doesn't work as a string. + // This works due to the __forceTContextToBeContravariant hack. + it('context is contravariant', () => { // @ts-expect-error - { foo: 'asdf' }, - ); - // GraphQL will be sad that a string was returned from an Int! field. - expect(result2.errors).toBeDefined(); - }); + const server1: ApolloServer<{}> = new ApolloServer<{ + foo: number; + }>({ typeDefs: 'type Query{id: ID}' }); + // avoid the expected error just being an unused variable + expect(server1).toBeDefined(); + + // The opposite is OK: we can pass a more specific context object to + // something expecting less. + const server2: ApolloServer<{ + foo: number; + }> = new ApolloServer<{}>({ typeDefs: 'type Query{id: ID}' }); + expect(server2).toBeDefined(); + }); - // This works due to the __forceTContextToBeContravariant hack. - it('context is contravariant', () => { - // @ts-expect-error - const server1: ApolloServer<{}> = new ApolloServer<{ - foo: number; - }>({ typeDefs: 'type Query{id: ID}' }); - // avoid the expected error just being an unused variable - expect(server1).toBeDefined(); - - // The opposite is OK: we can pass a more specific context object to - // something expecting less. - const server2: ApolloServer<{ - foo: number; - }> = new ApolloServer<{}>({ typeDefs: 'type Query{id: ID}' }); - expect(server2).toBeDefined(); - }); + it('typing for context objects works with argument to usage reporting', () => { + new ApolloServer<{ foo: number }>({ + typeDefs: 'type Query { n: Int! }', + plugins: [ + ApolloServerPluginUsageReporting({ + generateClientInfo({ contextValue }) { + let n: number = contextValue.foo; + // @ts-expect-error + let s: string = contextValue.foo; + // Make sure both variables are used (so the only expected error + // is the type error). + return { + clientName: `client ${n} ${s}`, + }; + }, + }), + ], + }); - it('typing for context objects works with argument to usage reporting', async () => { - new ApolloServer<{ foo: number }>({ - typeDefs: 'type Query { n: Int! }', - plugins: [ - ApolloServerPluginUsageReporting({ - generateClientInfo({ contextValue }) { - let n: number = contextValue.foo; - // @ts-expect-error - let s: string = contextValue.foo; - // Make sure both variables are used (so the only expected error - // is the type error). - return { - clientName: `client ${n} ${s}`, - }; - }, - }), - ], + // Don't start the server because we don't actually want any usage reporting. }); - // Don't start the server because we don't actually want any usage reporting. + it('typing for plugins works appropriately', () => { + type SpecificContext = { someField: boolean }; + + function takesPlugin( + _p: ApolloServerPlugin, + ) {} + + const specificPlugin: ApolloServerPlugin = { + async requestDidStart({ contextValue }) { + console.log(contextValue.someField); // this doesn't actually run + }, + }; + + const basePlugin: ApolloServerPlugin = { + async requestDidStart({ contextValue }) { + console.log(contextValue); // this doesn't actually run + }, + }; + + // @ts-expect-error + takesPlugin(specificPlugin); + takesPlugin(basePlugin); + + new ApolloServer({ + typeDefs: 'type Query { x: ID }', + // @ts-expect-error + plugins: [specificPlugin], + }); + new ApolloServer({ + typeDefs: 'type Query { x: ID }', + plugins: [basePlugin], + }); + }); }); }); diff --git a/packages/server/src/httpBatching.ts b/packages/server/src/httpBatching.ts index 51617c5ca9f..1b458c08ed0 100644 --- a/packages/server/src/httpBatching.ts +++ b/packages/server/src/httpBatching.ts @@ -1,11 +1,12 @@ import type { + BaseContext, HTTPGraphQLRequest, HTTPGraphQLResponse, } from '@apollo/server-types'; import type { ApolloServerInternals, SchemaDerivedData } from './ApolloServer'; import { HeaderMap, HttpQueryError, runHttpQuery } from './runHttpQuery'; -export async function runBatchHttpQuery( +export async function runBatchHttpQuery( batchRequest: Omit & { body: any[] }, contextValue: TContext, schemaDerivedData: SchemaDerivedData, @@ -53,7 +54,9 @@ export async function runBatchHttpQuery( return combinedResponse; } -export async function runPotentiallyBatchedHttpQuery( +export async function runPotentiallyBatchedHttpQuery< + TContext extends BaseContext, +>( httpGraphQLRequest: HTTPGraphQLRequest, contextValue: TContext, schemaDerivedData: SchemaDerivedData, diff --git a/packages/server/src/plugin/cacheControl/index.ts b/packages/server/src/plugin/cacheControl/index.ts index 864f319652f..fc4845bb011 100644 --- a/packages/server/src/plugin/cacheControl/index.ts +++ b/packages/server/src/plugin/cacheControl/index.ts @@ -344,13 +344,3 @@ function cacheAnnotationFromField( function isRestricted(hint: CacheHint) { return hint.maxAge !== undefined || hint.scope !== undefined; } - -// This plugin does nothing, but it ensures that ApolloServer won't try -// to add a default ApolloServerPluginCacheControl. -export function ApolloServerPluginCacheControlDisabled(): InternalApolloServerPlugin { - return { - __internal_plugin_id__() { - return 'CacheControl'; - }, - }; -} diff --git a/packages/server/src/plugin/index.ts b/packages/server/src/plugin/index.ts index 0593d27f7e1..de4b86fadad 100644 --- a/packages/server/src/plugin/index.ts +++ b/packages/server/src/plugin/index.ts @@ -16,6 +16,19 @@ // The goal is that the generated `dist/plugin/index.js` file has no top-level // require calls. import type { ApolloServerPlugin, BaseContext } from '@apollo/server-types'; +import type { + InternalApolloServerPlugin, + InternalPluginId, +} from '../internalPlugin'; + +function disabledPlugin(id: InternalPluginId): ApolloServerPlugin { + const plugin: InternalApolloServerPlugin = { + __internal_plugin_id__() { + return id; + }, + }; + return plugin; +} //#region Usage reporting import type { ApolloServerPluginUsageReportingOptions } from './usageReporting'; @@ -35,7 +48,7 @@ export function ApolloServerPluginUsageReporting( return require('./usageReporting').ApolloServerPluginUsageReporting(options); } export function ApolloServerPluginUsageReportingDisabled(): ApolloServerPlugin { - return require('./usageReporting').ApolloServerPluginUsageReportingDisabled(); + return disabledPlugin('UsageReporting'); } //#endregion @@ -62,7 +75,7 @@ export function ApolloServerPluginInlineTrace( return require('./inlineTrace').ApolloServerPluginInlineTrace(options); } export function ApolloServerPluginInlineTraceDisabled(): ApolloServerPlugin { - return require('./inlineTrace').ApolloServerPluginInlineTraceDisabled(); + return disabledPlugin('InlineTrace'); } //#endregion @@ -76,7 +89,7 @@ export function ApolloServerPluginCacheControl( return require('./cacheControl').ApolloServerPluginCacheControl(options); } export function ApolloServerPluginCacheControlDisabled(): ApolloServerPlugin { - return require('./cacheControl').ApolloServerPluginCacheControlDisabled(); + return disabledPlugin('CacheControl'); } //#endregion @@ -93,14 +106,8 @@ export function ApolloServerPluginDrainHttpServer( //#endregion //#region LandingPage -import type { InternalApolloServerPlugin } from '../internalPlugin'; export function ApolloServerPluginLandingPageDisabled(): ApolloServerPlugin { - const plugin: InternalApolloServerPlugin = { - __internal_plugin_id__() { - return 'LandingPageDisabled'; - }, - }; - return plugin; + return disabledPlugin('LandingPageDisabled'); } import type { diff --git a/packages/server/src/plugin/inlineTrace/index.ts b/packages/server/src/plugin/inlineTrace/index.ts index f42892661de..46ab718e227 100644 --- a/packages/server/src/plugin/inlineTrace/index.ts +++ b/packages/server/src/plugin/inlineTrace/index.ts @@ -129,13 +129,3 @@ export function ApolloServerPluginInlineTrace( }, }; } - -// This plugin does nothing, but it ensures that ApolloServer won't try -// to add a default ApolloServerPluginInlineTrace. -export function ApolloServerPluginInlineTraceDisabled(): InternalApolloServerPlugin { - return { - __internal_plugin_id__() { - return 'InlineTrace'; - }, - }; -} diff --git a/packages/server/src/plugin/usageReporting/index.ts b/packages/server/src/plugin/usageReporting/index.ts index 572ea1b58dc..24f96e5c1b9 100644 --- a/packages/server/src/plugin/usageReporting/index.ts +++ b/packages/server/src/plugin/usageReporting/index.ts @@ -1,7 +1,4 @@ -export { - ApolloServerPluginUsageReporting, - ApolloServerPluginUsageReportingDisabled, -} from './plugin'; +export { ApolloServerPluginUsageReporting } from './plugin'; export { ApolloServerPluginUsageReportingOptions, SendValuesBaseOptions, diff --git a/packages/server/src/plugin/usageReporting/plugin.ts b/packages/server/src/plugin/usageReporting/plugin.ts index 98f439d423f..683abe08d99 100644 --- a/packages/server/src/plugin/usageReporting/plugin.ts +++ b/packages/server/src/plugin/usageReporting/plugin.ts @@ -858,15 +858,3 @@ function defaultGenerateClientInfo({ request }: GraphQLRequestContext) { return {}; } } - -// This plugin does nothing, but it ensures that ApolloServer won't try -// to add a default ApolloServerPluginUsageReporting. -export function ApolloServerPluginUsageReportingDisabled< - TContext extends BaseContext, ->(): InternalApolloServerPlugin { - return { - __internal_plugin_id__() { - return 'UsageReporting'; - }, - }; -} diff --git a/packages/server/src/requestPipeline.ts b/packages/server/src/requestPipeline.ts index fc241e340d8..c0d4211c84b 100644 --- a/packages/server/src/requestPipeline.ts +++ b/packages/server/src/requestPipeline.ts @@ -42,6 +42,7 @@ import type { GraphQLRequestContextWillSendResponse, GraphQLRequestContextDidEncounterErrors, GraphQLRequestExecutionListener, + BaseContext, } from '@apollo/server-types'; import { Dispatcher } from './utils/dispatcher'; @@ -76,7 +77,7 @@ function isBadUserInputGraphQLError(error: GraphQLError): Boolean { ); } -export async function processGraphQLRequest( +export async function processGraphQLRequest( schemaDerivedData: SchemaDerivedData, internals: ApolloServerInternals, requestContext: Mutable>,