Skip to content

Commit

Permalink
Add support for willResolveField and didResolveField.
Browse files Browse the repository at this point in the history
This adds two of the life-cycle hooks which were available in the
`graphql-extensions` API but missing from the new request pipeline plugin API.

These omissions have stood in the way of our own ability to migrate our
Apollo-related extensions (e.g. `apollo-cache-control`,
`apollo-engine-reporting` in federated and non-federated forms,
`apollo-tracing`) to the new plugin API and our intention to deprecate that
API which was never intended to be public (and was certainly never
documented!).

That's not to say that any of the effort to do those migrations is easy (it
will absolutely not be), however, this unblocks those efforts.
  • Loading branch information
abernix committed Apr 15, 2020
1 parent c3af619 commit ab35c45
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 1 deletion.
12 changes: 11 additions & 1 deletion packages/apollo-server-core/src/requestPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ import {
enableGraphQLExtensions,
} from 'graphql-extensions';
import { DataSource } from 'apollo-datasource';
import { PersistedQueryOptions } from '.';
import {
PersistedQueryOptions,
symbolRequestListenerDispatcher,
enablePluginsForSchemaResolvers,
} from '.';
import {
CacheControlExtension,
CacheControlExtensionOptions,
Expand Down Expand Up @@ -127,6 +131,9 @@ export async function processGraphQLRequest<TContext>(
(requestContext.context as any)._extensionStack = extensionStack;

const dispatcher = initializeRequestListenerDispatcher();
Object.defineProperty(requestContext.context, symbolRequestListenerDispatcher, {
value: dispatcher,
});

await initializeDataSources();

Expand Down Expand Up @@ -571,6 +578,8 @@ export async function processGraphQLRequest<TContext>(
function initializeRequestListenerDispatcher(): Dispatcher<
GraphQLRequestListener
> {
enablePluginsForSchemaResolvers(config.schema);

const requestListeners: GraphQLRequestListener<TContext>[] = [];
if (config.plugins) {
for (const plugin of config.plugins) {
Expand Down Expand Up @@ -633,3 +642,4 @@ export async function processGraphQLRequest<TContext>(
}
}
}

155 changes: 155 additions & 0 deletions packages/apollo-server-core/src/requestPipelineAPI.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
import {
GraphQLField,
getNamedType,
GraphQLObjectType,
GraphQLSchema,
ResponsePath,
} from 'graphql/type';
import { defaultFieldResolver } from "graphql/execution";
import { FieldNode } from "graphql/language";
import { Dispatcher } from "./utils/dispatcher";
import { GraphQLRequestListener } from "apollo-server-plugin-base";
import { GraphQLObjectResolver } from "@apollographql/apollo-tools";

export {
GraphQLServiceContext,
GraphQLRequest,
Expand All @@ -10,3 +23,145 @@ export {
GraphQLExecutor,
GraphQLExecutionResult,
} from 'apollo-server-types';

export const symbolRequestListenerDispatcher =
Symbol("apolloServerRequestListenerDispatcher");
export const symbolPluginsEnabled = Symbol("apolloServerPluginsEnabled");

export function enablePluginsForSchemaResolvers(
schema: GraphQLSchema & { [symbolPluginsEnabled]?: boolean },
) {
if (schema[symbolPluginsEnabled]) {
return schema;
}
Object.defineProperty(schema, symbolPluginsEnabled, {
value: true,
});

forEachField(schema, wrapField);

return schema;
}

function wrapField(field: GraphQLField<any, any>): void {
const fieldResolver = field.resolve || defaultFieldResolver;

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>;
};

// The technique for implementing a "did resolve field" is accomplished by
// returning a function from the `willResolveField` handler. The
// dispatcher will return a callback which will invoke all of those handlers
// and we'll save that to call when the object resolution is complete.
const endHandler = context && context[symbolRequestListenerDispatcher] &&
(context[symbolRequestListenerDispatcher] as Dispatcher<GraphQLRequestListener>)
.invokeDidStartHook('willResolveField', source, args, context, info) ||
((_err: Error | null, _result?: any) => { /* do nothing */ });

const resolveObject: GraphQLObjectResolver<
any,
any
> = (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;
}
}

try {
let result: any;
if (whenObjectResolved) {
result = whenObjectResolved.then((resolvedObject: any) => {
return fieldResolver(resolvedObject, args, context, info);
});
} else {
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
// maybe-Promise value.
whenResultIsFinished(result, endHandler);
return result;
} catch (error) {
// Normally it's a bad sign to see an error both handled and
// re-thrown. But it is useful to allow extensions to track errors while
// still handling them in the normal GraphQL way.
endHandler(error);
throw error;
}
};;
}

function isPromise(x: any): boolean {
return x && typeof x.then === 'function';
}

// Given result (which may be a Promise or an array some of whose elements are
// promises) Promises, set up 'callback' to be invoked when result is fully
// resolved.
export function whenResultIsFinished(
result: any,
callback: (err: Error | null, result?: any) => void,
) {
if (isPromise(result)) {
result.then((r: any) => callback(null, r), (err: Error) => callback(err));
} else if (Array.isArray(result)) {
if (result.some(isPromise)) {
Promise.all(result).then(
(r: any) => callback(null, r),
(err: Error) => callback(err),
);
} else {
callback(null, result);
}
} else {
callback(null, result);
}
}

function forEachField(schema: GraphQLSchema, fn: FieldIteratorFn): void {
const typeMap = schema.getTypeMap();
Object.keys(typeMap).forEach(typeName => {
const type = typeMap[typeName];

if (
!getNamedType(type).name.startsWith('__') &&
type instanceof GraphQLObjectType
) {
const fields = type.getFields();
Object.keys(fields).forEach(fieldName => {
const field = fields[fieldName];
fn(field, typeName, fieldName);
});
}
});
}

type FieldIteratorFn = (
fieldDef: GraphQLField<any, any>,
typeName: string,
fieldName: string,
) => void;
6 changes: 6 additions & 0 deletions packages/apollo-server-plugin-base/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
GraphQLRequestContextExecutionDidStart,
GraphQLRequestContextWillSendResponse,
} from 'apollo-server-types';
import { GraphQLFieldResolver } from "graphql";

// We re-export all of these so plugin authors only need to depend on a single
// package. The overall concept of `apollo-server-types` and this package
Expand Down Expand Up @@ -51,6 +52,8 @@ export type GraphQLRequestListenerValidationDidEnd =
((err?: ReadonlyArray<Error>) => void) | void;
export type GraphQLRequestListenerExecutionDidEnd =
((err?: Error) => void) | void;
export type GraphQLRequestListenerDidResolveField =
((error: Error | null, result?: any) => void) | void

export interface GraphQLRequestListener<TContext = Record<string, any>> {
parsingDidStart?(
Expand All @@ -76,6 +79,9 @@ export interface GraphQLRequestListener<TContext = Record<string, any>> {
executionDidStart?(
requestContext: GraphQLRequestContextExecutionDidStart<TContext>,
): GraphQLRequestListenerExecutionDidEnd;
willResolveField?(
...fieldResolverArgs: Parameters<GraphQLFieldResolver<any, TContext>>
): GraphQLRequestListenerDidResolveField;
willSendResponse?(
requestContext: GraphQLRequestContextWillSendResponse<TContext>,
): ValueOrPromise<void>;
Expand Down

0 comments on commit ab35c45

Please sign in to comment.