Skip to content

Commit

Permalink
Remove 'modules', '__resolveObject', and apollo-tooling dep
Browse files Browse the repository at this point in the history
This removes the dependency on the deprecated
`@apollographql/apollo-tooling` package by removing support for two
undocumented features that it provides.

The `modules` constructor option was the beginning of an attempt to
provide guidance about how to structure one's schema into "modules".
While there were bigger goals for it, in practice all it allowed you to
do is specify your schema as an array of typeDefs/resolvers objects. It
used a different schema-making function that is similar to but
technically not the same as the `makeExecutableSchema` from
graphql-tools. `makeExecutableSchema` lets you specify both `typeDefs`
and `resolvers` these days, so you should be able to replace any use of
`modules` with

    new ApolloServer({
      typeDefs: modules.map({ typeDefs } => typeDefs,
      resolvers: modules.map({ resolvers } => resolvers,
    })

Or of course, you can use the `@apollographql/apollo-tooling`
`buildServiceDefinition` yourself and pass it to the `schema`
constructor option. The basic idea here is that `schema` is the main
option for specifying a (non-gateway) schema and that we provide
`typeDefs`/`resolvers` as syntactic sugar for the common case, but we
don't need to provide two different kinds of syntactic sugar for
specifying typeDefs and resolvers in slightly different data structures
which use surprisingly different code to implement it.

The other undocumented feature implemented by
`@apollographql/apollo-tooling` was the ability to put a
`__resolveObject` pseudo-resolver on a type. This was a predecessor to
the subgraph `__resolveReference` method which offered a superset of its
functionality. Whereas `__resolveReference` is specific to the
`Query._entities` field, `__resolveObject` would run everywhere: it
would run on that field's return values but also on any other field's
return value. Interestingly it was implemented inside the "schema
instrumentation" code which also implements the willResolveField plugin
hook. In v3.6.0 we stopped running schema instrumentation if you aren't
using a willResolveField hook (though admittedly the on-by-default cache
control plugin uses that hook) which would break this feature!

We don't know of any usage of this feature other than federation tests
(which we're updating in
apollographql/federation#1658) so for now we
will delete it. This would better be done in graphql-js anyway, or
perhaps as part of makeExecutableSchema.
  • Loading branch information
glasser committed Mar 29, 2022
1 parent 7e27f25 commit 1081080
Show file tree
Hide file tree
Showing 7 changed files with 9 additions and 97 deletions.
14 changes: 0 additions & 14 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"npm": "^8.5.0"
},
"devDependencies": {
"@apollographql/apollo-tools": "0.5.2",
"@apollo/client": "3.5.10",
"@graphql-codegen/cli": "2.6.2",
"@graphql-codegen/typescript-operations": "2.3.5",
Expand Down
1 change: 0 additions & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
"dependencies": {
"@apollo/server-types": "^3.5.1",
"@apollo/usage-reporting-protobuf": "^3.3.0",
"@apollographql/apollo-tools": "^0.5.1",
"@apollographql/graphql-playground-html": "1.6.29",
"@graphql-tools/mock": "^8.1.2",
"@graphql-tools/schema": "^8.0.0",
Expand Down
9 changes: 0 additions & 9 deletions packages/server/src/ApolloServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ import {
APQ_CACHE_PREFIX,
} from './requestPipeline';

import { buildServiceDefinition } from '@apollographql/apollo-tools';
import isNodeLike from './utils/isNodeLike';
import { determineApolloConfig } from './determineApolloConfig';
import {
Expand Down Expand Up @@ -609,14 +608,6 @@ export class ApolloServerBase<TContext extends BaseContext> {
return config.schema;
}

if (config.modules) {
const { schema, errors } = buildServiceDefinition(config.modules);
if (errors && errors.length > 0) {
throw new Error(errors.map((error) => error.message).join('\n\n'));
}
return schema!;
}

const { typeDefs, resolvers, parseOptions } = config;
const augmentedTypeDefs = Array.isArray(typeDefs) ? typeDefs : [typeDefs];

Expand Down
17 changes: 7 additions & 10 deletions packages/server/src/__tests__/ApolloServerBase.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import { ApolloServerBase } from '../ApolloServer';
import {
buildServiceDefinition,
GraphQLSchemaModule,
} from '@apollographql/apollo-tools';
import { ApolloServerOptions, GatewayInterface, gql } from '../';
import type { GraphQLSchema } from 'graphql';
import type {
Expand All @@ -14,6 +10,7 @@ import {
ApolloServerPluginCacheControlDisabled,
ApolloServerPluginUsageReporting,
} from '../plugin';
import { makeExecutableSchema } from '@graphql-tools/schema';

const typeDefs = gql`
type Query {
Expand Down Expand Up @@ -46,7 +43,7 @@ describe('ApolloServerBase construction', () => {
expect(
() =>
new ApolloServerBase({
schema: buildServiceDefinition([{ typeDefs, resolvers }]).schema!,
schema: makeExecutableSchema({ typeDefs, resolvers }),
}),
).not.toThrow();
});
Expand Down Expand Up @@ -95,33 +92,33 @@ describe('ApolloServerBase construction', () => {
});

it('TypeScript enforces schema-related option combinations', async () => {
const schema = buildServiceDefinition([{ typeDefs, resolvers }]).schema!;
const schema = makeExecutableSchema({ typeDefs, resolvers });
const gateway: GatewayInterface = {
async load() {
return { schema, executor: null };
},
async stop() {},
};
const modules: GraphQLSchemaModule[] = [];

function takesConfig(_c: ApolloServerOptions<BaseContext>) {}

takesConfig({ gateway });
takesConfig({ schema });
takesConfig({ modules });
takesConfig({ typeDefs });
takesConfig({ typeDefs, resolvers });

// @ts-expect-error
takesConfig({ gateway, schema });
// @ts-expect-error
takesConfig({ gateway, modules });
// @ts-expect-error
takesConfig({ gateway, typeDefs });
// @ts-expect-error
takesConfig({ schema, resolvers });
// @ts-expect-error
takesConfig({ schema, typeDefs });

// This used to exist in AS3.
// @ts-expect-error
takesConfig({ modules: [] });
});
});

Expand Down
19 changes: 1 addition & 18 deletions packages/server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@ import type {
Logger,
} from '@apollo/server-types';

import type { GraphQLSchemaModule } from '@apollographql/apollo-tools';

export type { GraphQLSchemaModule };

import type { KeyValueCache } from 'apollo-server-caching';
export type { KeyValueCache };

Expand Down Expand Up @@ -119,7 +115,6 @@ export interface ApolloServerOptionsWithGateway<TContext extends BaseContext>
extends ApolloServerOptionsBase<TContext> {
gateway: GatewayInterface;
schema?: undefined;
modules?: undefined;
typeDefs?: undefined;
resolvers?: undefined;
}
Expand All @@ -128,36 +123,24 @@ export interface ApolloServerOptionsWithSchema<TContext extends BaseContext>
extends ApolloServerOptionsBase<TContext> {
schema: GraphQLSchema;
gateway?: undefined;
modules?: undefined;
typeDefs?: undefined;
resolvers?: undefined;
}

export interface ApolloServerOptionsWithModules<TContext extends BaseContext>
extends ApolloServerOptionsBase<TContext> {
modules: GraphQLSchemaModule[];
gateway?: undefined;
schema?: undefined;
typeDefs?: undefined;
resolvers?: undefined;
}

export interface ApolloServerOptionsWithTypeDefs<TContext extends BaseContext>
extends ApolloServerOptionsBase<TContext> {
// These three options are always only passed directly through to
// These two options are always only passed directly through to
// makeExecutableSchema. (If you don't want to use makeExecutableSchema, pass
// `schema` instead.)
typeDefs: IExecutableSchemaDefinition<TContext>['typeDefs'];
resolvers?: IExecutableSchemaDefinition<TContext>['resolvers'];
gateway?: undefined;
schema?: undefined;
modules?: undefined;
}

export type ApolloServerOptionsWithStaticSchema<TContext extends BaseContext> =
(
| ApolloServerOptionsWithSchema<TContext>
| ApolloServerOptionsWithModules<TContext>
| ApolloServerOptionsWithTypeDefs<TContext>
) & {
mocks?: boolean | IMocks;
Expand Down
45 changes: 1 addition & 44 deletions packages/server/src/utils/schemaInstrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import {
GraphQLSchema,
GraphQLField,
ResponsePath,
getNamedType,
GraphQLObjectType,
GraphQLFieldResolver,
} from 'graphql/type';
import { defaultFieldResolver } from 'graphql/execution';
import type { FieldNode } from 'graphql/language';
import type {
BaseContext,
GraphQLRequestExecutionListener,
} from '@apollo/server-types';
import type { GraphQLObjectResolver } from '@apollographql/apollo-tools';

export const symbolExecutionDispatcherWillResolveField = Symbol(
'apolloServerExecutionDispatcherWillResolveField',
Expand Down Expand Up @@ -58,15 +55,6 @@ function wrapField<TContext extends BaseContext>(
const originalFieldResolve = field.resolve;

field.resolve = (source, args, context, info) => {
// This is a bit of a hack, but since `ResponsePath` is a linked list,
// a new object gets created every time a path segment is added.
// So we can use that to share our `whenObjectResolved` promise across
// all field resolvers for the same object.
const parentPath = info.path.prev as ResponsePath & {
__fields?: Record<string, ReadonlyArray<FieldNode>>;
__whenObjectResolved?: Promise<any>;
};

const willResolveField = context?.[
symbolExecutionDispatcherWillResolveField
] as
Expand All @@ -87,42 +75,11 @@ function wrapField<TContext extends BaseContext>(
typeof willResolveField === 'function' &&
willResolveField({ source, args, context, info });

const resolveObject: GraphQLObjectResolver<any, any> | undefined = (
info.parentType as any
).resolveObject;

let whenObjectResolved: Promise<any> | undefined;

if (parentPath && resolveObject) {
if (!parentPath.__fields) {
parentPath.__fields = {};
}

parentPath.__fields[info.fieldName] = info.fieldNodes;

whenObjectResolved = parentPath.__whenObjectResolved;
if (!whenObjectResolved) {
// Use `Promise.resolve().then()` to delay executing
// `resolveObject()` so we can collect all the fields first.
whenObjectResolved = Promise.resolve().then(() => {
return resolveObject(source, parentPath.__fields!, context, info);
});
parentPath.__whenObjectResolved = whenObjectResolved;
}
}

const fieldResolver =
originalFieldResolve || userFieldResolver || defaultFieldResolver;

try {
let result: any;
if (whenObjectResolved) {
result = whenObjectResolved.then((resolvedObject: any) => {
return fieldResolver(resolvedObject, args, context, info);
});
} else {
result = fieldResolver(source, args, context, info);
}
let result = fieldResolver(source, args, context, info);

// Call the stack's handlers either immediately (if result is not a
// Promise) or once the Promise is done. Then return that same
Expand Down

0 comments on commit 1081080

Please sign in to comment.