Skip to content

Commit 970f3c1

Browse files
committed
Move Disabled plugins up a level and change some context typing
Two things got a bit mixed together here. One is that we want the Disabled plugins to all be defined directly in plugin/index.ts, so that loading ApolloServerPluginInlineTraceDisabled does not spend time loading the protobuf library. The other thing is that we started thinking a bit more carefully about plugin context generics. We wrote some tests to make sure that you can use an `ApolloServerPlugin<BaseContext>` (ie, a plugin that doesn't need any particular fields on the context) with any `ApolloServer`, but not vice versa. As part of getting this to work, we added another `__forceTContextToBeContravariant`. We also noticed that it made more sense for BaseContext to be `{}` ("an object with no particular fields") rather than `Record<string, any>` ("an object where you can do anything with any of its fields"). We investigated whether the new contravariance annotation coming in the next two months in TS 4.7 (microsoft/TypeScript#48240) would allow us to get rid of the `__forceTContextToBeContravariant` hack and the answer is yes! However, trying `4.7.0-beta` failed for two other reasons. One is that microsoft/TypeScript#48366 required us to add some missing `extends` clauses, which we are doing now in this PR. The other is that `graphql-tools` needs some fixes to work with TS4.7 which we hope they can do soon (ardatan/graphql-tools#4381).
1 parent 1dec8cc commit 970f3c1

File tree

11 files changed

+153
-131
lines changed

11 files changed

+153
-131
lines changed

packages/server-types/src/plugins.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ export interface ApolloServerPlugin<TContext extends BaseContext> {
2424
requestDidStart?(
2525
requestContext: GraphQLRequestContext<TContext>,
2626
): Promise<GraphQLRequestListener<TContext> | void>;
27+
28+
// See the similarly named field on ApolloServer for details. Note that it
29+
// appears that this only works if it is a *field*, not a *method*, which is
30+
// why `requestDidStart` (which takes a TContext wrapped in something) is not
31+
// sufficient.
32+
//
33+
// TODO(AS4): Upgrade to TS 4.7 and use `in` instead.
34+
__forceTContextToBeContravariant?: (contextValue: TContext) => void;
2735
}
2836

2937
export interface GraphQLServerListener {

packages/server-types/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export type HTTPGraphQLResponse = {
6464
}
6565
);
6666

67-
export type BaseContext = Record<string, any>;
67+
export type BaseContext = {};
6868

6969
export type WithRequired<T, K extends keyof T> = T & Required<Pick<T, K>>;
7070

packages/server/src/ApolloServer.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ class UnreachableCaseError extends Error {
144144

145145
// TODO(AS4): Move this to its own file or something. Also organize the fields.
146146

147-
export interface ApolloServerInternals<TContext> {
147+
export interface ApolloServerInternals<TContext extends BaseContext> {
148148
formatError?: (error: GraphQLError) => GraphQLFormattedError;
149149
// TODO(AS4): Is there a way (with generics/codegen?) to make
150150
// this "any" more specific? In AS3 there was technically a
@@ -206,6 +206,8 @@ export class ApolloServer<TContext extends BaseContext = BaseContext> {
206206
// once `out TContext` is available. Note that when we replace this with `out
207207
// TContext`, we may make it so that users of older TypeScript versions no
208208
// longer have this protection.
209+
//
210+
// TODO(AS4): upgrade to TS 4.7 when it is released and use that instead.
209211
protected __forceTContextToBeContravariant?: (contextValue: TContext) => void;
210212

211213
constructor(config: ApolloServerOptions<TContext>) {
@@ -636,7 +638,7 @@ export class ApolloServer<TContext extends BaseContext = BaseContext> {
636638
);
637639
}
638640

639-
private static constructSchema<TContext>(
641+
private static constructSchema<TContext extends BaseContext>(
640642
config: ApolloServerOptionsWithStaticSchema<TContext>,
641643
): GraphQLSchema {
642644
if (config.schema) {
@@ -659,7 +661,7 @@ export class ApolloServer<TContext extends BaseContext = BaseContext> {
659661
});
660662
}
661663

662-
private static maybeAddMocksToConstructedSchema<TContext>(
664+
private static maybeAddMocksToConstructedSchema<TContext extends BaseContext>(
663665
schema: GraphQLSchema,
664666
config: ApolloServerOptionsWithStaticSchema<TContext>,
665667
): GraphQLSchema {
@@ -807,7 +809,7 @@ export class ApolloServer<TContext extends BaseContext = BaseContext> {
807809
// This is called in the constructor before this.internals has been
808810
// initialized, so we make it static to make it clear it can't assume that
809811
// `this` has been fully initialized.
810-
private static ensurePluginInstantiation<TContext>(
812+
private static ensurePluginInstantiation<TContext extends BaseContext>(
811813
userPlugins: PluginDefinition<TContext>[] = [],
812814
isDev: boolean,
813815
apolloConfig: ApolloConfig,
@@ -1146,7 +1148,7 @@ export type ImplicitlyInstallablePlugin<TContext extends BaseContext> =
11461148
__internal_installed_implicitly__: boolean;
11471149
};
11481150

1149-
export function isImplicitlyInstallablePlugin<TContext>(
1151+
export function isImplicitlyInstallablePlugin<TContext extends BaseContext>(
11501152
p: ApolloServerPlugin<TContext>,
11511153
): p is ImplicitlyInstallablePlugin<TContext> {
11521154
return '__internal_installed_implicitly__' in p;

packages/server/src/__tests__/ApolloServer.test.ts

Lines changed: 112 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -305,91 +305,127 @@ describe('ApolloServer executeOperation', () => {
305305
expect(result.data?.contextFoo).toBe('bla');
306306
});
307307

308-
it('typing for context objects works', async () => {
309-
const server = new ApolloServer<{ foo: number }>({
310-
typeDefs: 'type Query { n: Int!, n2: String! }',
311-
resolvers: {
312-
Query: {
313-
n(_parent: any, _args: any, context): number {
314-
return context.foo;
315-
},
316-
n2(_parent: any, _args: any, context): string {
317-
// It knows that context.foo is a number so it doesn't work as a string.
318-
// @ts-expect-error
319-
return context.foo;
308+
describe('context generic typing', () => {
309+
it('typing for context objects works', async () => {
310+
const server = new ApolloServer<{ foo: number }>({
311+
typeDefs: 'type Query { n: Int!, n2: String! }',
312+
resolvers: {
313+
Query: {
314+
n(_parent: any, _args: any, context): number {
315+
return context.foo;
316+
},
317+
n2(_parent: any, _args: any, context): string {
318+
// It knows that context.foo is a number so it doesn't work as a string.
319+
// @ts-expect-error
320+
return context.foo;
321+
},
320322
},
321323
},
322-
},
323-
plugins: [
324-
{
325-
// Works with plugins too!
326-
async requestDidStart({ contextValue }) {
327-
let n: number = contextValue.foo;
328-
// @ts-expect-error
329-
let s: string = contextValue.foo;
330-
// Make sure both variables are used (so the only expected error
331-
// is the type error).
332-
JSON.stringify({ n, s });
324+
plugins: [
325+
{
326+
// Works with plugins too!
327+
async requestDidStart({ contextValue }) {
328+
let n: number = contextValue.foo;
329+
// @ts-expect-error
330+
let s: string = contextValue.foo;
331+
// Make sure both variables are used (so the only expected error
332+
// is the type error).
333+
JSON.stringify({ n, s });
334+
},
333335
},
334-
},
335-
// Plugins declared to be <BaseContext> still work.
336-
ApolloServerPluginCacheControlDisabled(),
337-
],
336+
// Plugins declared to be <BaseContext> still work.
337+
ApolloServerPluginCacheControlDisabled(),
338+
],
339+
});
340+
await server.start();
341+
const result = await server.executeOperation(
342+
{ query: '{ n }' },
343+
{ foo: 123 },
344+
);
345+
expect(result.errors).toBeUndefined();
346+
expect(result.data?.n).toBe(123);
347+
348+
const result2 = await server.executeOperation(
349+
{ query: '{ n }' },
350+
// It knows that context.foo is a number so it doesn't work as a string.
351+
// @ts-expect-error
352+
{ foo: 'asdf' },
353+
);
354+
// GraphQL will be sad that a string was returned from an Int! field.
355+
expect(result2.errors).toBeDefined();
338356
});
339-
await server.start();
340-
const result = await server.executeOperation(
341-
{ query: '{ n }' },
342-
{ foo: 123 },
343-
);
344-
expect(result.errors).toBeUndefined();
345-
expect(result.data?.n).toBe(123);
346357

347-
const result2 = await server.executeOperation(
348-
{ query: '{ n }' },
349-
// It knows that context.foo is a number so it doesn't work as a string.
358+
// This works due to the __forceTContextToBeContravariant hack.
359+
it('context is contravariant', () => {
350360
// @ts-expect-error
351-
{ foo: 'asdf' },
352-
);
353-
// GraphQL will be sad that a string was returned from an Int! field.
354-
expect(result2.errors).toBeDefined();
355-
});
361+
const server1: ApolloServer<{}> = new ApolloServer<{
362+
foo: number;
363+
}>({ typeDefs: 'type Query{id: ID}' });
364+
// avoid the expected error just being an unused variable
365+
expect(server1).toBeDefined();
366+
367+
// The opposite is OK: we can pass a more specific context object to
368+
// something expecting less.
369+
const server2: ApolloServer<{
370+
foo: number;
371+
}> = new ApolloServer<{}>({ typeDefs: 'type Query{id: ID}' });
372+
expect(server2).toBeDefined();
373+
});
356374

357-
// This works due to the __forceTContextToBeContravariant hack.
358-
it('context is contravariant', () => {
359-
// @ts-expect-error
360-
const server1: ApolloServer<{}> = new ApolloServer<{
361-
foo: number;
362-
}>({ typeDefs: 'type Query{id: ID}' });
363-
// avoid the expected error just being an unused variable
364-
expect(server1).toBeDefined();
365-
366-
// The opposite is OK: we can pass a more specific context object to
367-
// something expecting less.
368-
const server2: ApolloServer<{
369-
foo: number;
370-
}> = new ApolloServer<{}>({ typeDefs: 'type Query{id: ID}' });
371-
expect(server2).toBeDefined();
372-
});
375+
it('typing for context objects works with argument to usage reporting', () => {
376+
new ApolloServer<{ foo: number }>({
377+
typeDefs: 'type Query { n: Int! }',
378+
plugins: [
379+
ApolloServerPluginUsageReporting({
380+
generateClientInfo({ contextValue }) {
381+
let n: number = contextValue.foo;
382+
// @ts-expect-error
383+
let s: string = contextValue.foo;
384+
// Make sure both variables are used (so the only expected error
385+
// is the type error).
386+
return {
387+
clientName: `client ${n} ${s}`,
388+
};
389+
},
390+
}),
391+
],
392+
});
373393

374-
it('typing for context objects works with argument to usage reporting', async () => {
375-
new ApolloServer<{ foo: number }>({
376-
typeDefs: 'type Query { n: Int! }',
377-
plugins: [
378-
ApolloServerPluginUsageReporting({
379-
generateClientInfo({ contextValue }) {
380-
let n: number = contextValue.foo;
381-
// @ts-expect-error
382-
let s: string = contextValue.foo;
383-
// Make sure both variables are used (so the only expected error
384-
// is the type error).
385-
return {
386-
clientName: `client ${n} ${s}`,
387-
};
388-
},
389-
}),
390-
],
394+
// Don't start the server because we don't actually want any usage reporting.
391395
});
392396

393-
// Don't start the server because we don't actually want any usage reporting.
397+
it('typing for plugins works appropriately', () => {
398+
type SpecificContext = { someField: boolean };
399+
400+
function takesPlugin<TContext extends BaseContext>(
401+
_p: ApolloServerPlugin<TContext>,
402+
) {}
403+
404+
const specificPlugin: ApolloServerPlugin<SpecificContext> = {
405+
async requestDidStart({ contextValue }) {
406+
console.log(contextValue.someField); // this doesn't actually run
407+
},
408+
};
409+
410+
const basePlugin: ApolloServerPlugin<BaseContext> = {
411+
async requestDidStart({ contextValue }) {
412+
console.log(contextValue); // this doesn't actually run
413+
},
414+
};
415+
416+
// @ts-expect-error
417+
takesPlugin<BaseContext>(specificPlugin);
418+
takesPlugin<SpecificContext>(basePlugin);
419+
420+
new ApolloServer<BaseContext>({
421+
typeDefs: 'type Query { x: ID }',
422+
// @ts-expect-error
423+
plugins: [specificPlugin],
424+
});
425+
new ApolloServer<SpecificContext>({
426+
typeDefs: 'type Query { x: ID }',
427+
plugins: [basePlugin],
428+
});
429+
});
394430
});
395431
});

packages/server/src/httpBatching.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type {
2+
BaseContext,
23
HTTPGraphQLRequest,
34
HTTPGraphQLResponse,
45
} from '@apollo/server-types';
56
import type { ApolloServerInternals, SchemaDerivedData } from './ApolloServer';
67
import { HeaderMap, HttpQueryError, runHttpQuery } from './runHttpQuery';
78

8-
export async function runBatchHttpQuery<TContext>(
9+
export async function runBatchHttpQuery<TContext extends BaseContext>(
910
batchRequest: Omit<HTTPGraphQLRequest, 'body'> & { body: any[] },
1011
contextValue: TContext,
1112
schemaDerivedData: SchemaDerivedData,
@@ -53,7 +54,9 @@ export async function runBatchHttpQuery<TContext>(
5354
return combinedResponse;
5455
}
5556

56-
export async function runPotentiallyBatchedHttpQuery<TContext>(
57+
export async function runPotentiallyBatchedHttpQuery<
58+
TContext extends BaseContext,
59+
>(
5760
httpGraphQLRequest: HTTPGraphQLRequest,
5861
contextValue: TContext,
5962
schemaDerivedData: SchemaDerivedData,

packages/server/src/plugin/cacheControl/index.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,3 @@ function cacheAnnotationFromField(
344344
function isRestricted(hint: CacheHint) {
345345
return hint.maxAge !== undefined || hint.scope !== undefined;
346346
}
347-
348-
// This plugin does nothing, but it ensures that ApolloServer won't try
349-
// to add a default ApolloServerPluginCacheControl.
350-
export function ApolloServerPluginCacheControlDisabled(): InternalApolloServerPlugin<BaseContext> {
351-
return {
352-
__internal_plugin_id__() {
353-
return 'CacheControl';
354-
},
355-
};
356-
}

packages/server/src/plugin/index.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@
1616
// The goal is that the generated `dist/plugin/index.js` file has no top-level
1717
// require calls.
1818
import type { ApolloServerPlugin, BaseContext } from '@apollo/server-types';
19+
import type {
20+
InternalApolloServerPlugin,
21+
InternalPluginId,
22+
} from '../internalPlugin';
23+
24+
function disabledPlugin(id: InternalPluginId): ApolloServerPlugin<BaseContext> {
25+
const plugin: InternalApolloServerPlugin<BaseContext> = {
26+
__internal_plugin_id__() {
27+
return id;
28+
},
29+
};
30+
return plugin;
31+
}
1932

2033
//#region Usage reporting
2134
import type { ApolloServerPluginUsageReportingOptions } from './usageReporting';
@@ -35,7 +48,7 @@ export function ApolloServerPluginUsageReporting<TContext extends BaseContext>(
3548
return require('./usageReporting').ApolloServerPluginUsageReporting(options);
3649
}
3750
export function ApolloServerPluginUsageReportingDisabled(): ApolloServerPlugin<BaseContext> {
38-
return require('./usageReporting').ApolloServerPluginUsageReportingDisabled();
51+
return disabledPlugin('UsageReporting');
3952
}
4053
//#endregion
4154

@@ -62,7 +75,7 @@ export function ApolloServerPluginInlineTrace(
6275
return require('./inlineTrace').ApolloServerPluginInlineTrace(options);
6376
}
6477
export function ApolloServerPluginInlineTraceDisabled(): ApolloServerPlugin<BaseContext> {
65-
return require('./inlineTrace').ApolloServerPluginInlineTraceDisabled();
78+
return disabledPlugin('InlineTrace');
6679
}
6780
//#endregion
6881

@@ -76,7 +89,7 @@ export function ApolloServerPluginCacheControl(
7689
return require('./cacheControl').ApolloServerPluginCacheControl(options);
7790
}
7891
export function ApolloServerPluginCacheControlDisabled(): ApolloServerPlugin<BaseContext> {
79-
return require('./cacheControl').ApolloServerPluginCacheControlDisabled();
92+
return disabledPlugin('CacheControl');
8093
}
8194
//#endregion
8295

@@ -93,14 +106,8 @@ export function ApolloServerPluginDrainHttpServer(
93106
//#endregion
94107

95108
//#region LandingPage
96-
import type { InternalApolloServerPlugin } from '../internalPlugin';
97109
export function ApolloServerPluginLandingPageDisabled(): ApolloServerPlugin<BaseContext> {
98-
const plugin: InternalApolloServerPlugin<BaseContext> = {
99-
__internal_plugin_id__() {
100-
return 'LandingPageDisabled';
101-
},
102-
};
103-
return plugin;
110+
return disabledPlugin('LandingPageDisabled');
104111
}
105112

106113
import type {

packages/server/src/plugin/inlineTrace/index.ts

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -129,13 +129,3 @@ export function ApolloServerPluginInlineTrace(
129129
},
130130
};
131131
}
132-
133-
// This plugin does nothing, but it ensures that ApolloServer won't try
134-
// to add a default ApolloServerPluginInlineTrace.
135-
export function ApolloServerPluginInlineTraceDisabled(): InternalApolloServerPlugin<BaseContext> {
136-
return {
137-
__internal_plugin_id__() {
138-
return 'InlineTrace';
139-
},
140-
};
141-
}

0 commit comments

Comments
 (0)