Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for willResolveField and corresponding end handler. #3988

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fb99d93
Use named types for the "DidEnd" hooks.
abernix Mar 26, 2020
c3af619
Merge branch 'abernix/named-DidEnd-hooks' into abernix/graphql-extens…
abernix Apr 15, 2020
ab35c45
Add support for `willResolveField` and `didResolveField`.
abernix Mar 26, 2020
835a68d
chore: Convert `schemaHash` from `string` to a faux-paque type.
abernix Apr 15, 2020
ab3855d
Introduce a plugin test harness to facilitate testing of plugins.
abernix Apr 14, 2020
dae59a5
Allow an optional `logger` to be passed into the test harness.
abernix Apr 16, 2020
771683f
chore: Convert `schemaHash` from `string` to a faux-paque type. (#3989)
abernix Apr 16, 2020
eec87a6
Introduce an internal plugin test harness to facilitate plugin… (#3990)
abernix Apr 16, 2020
0d31d1b
Apply suggestions from code review
abernix Apr 16, 2020
4b59b02
Rejigger `ensurePluginInstantiation` to accommodate upcoming changes.
abernix Apr 16, 2020
532e80f
Merge remote-tracking branch 'origin/abernix/graphql-extensions-depre…
abernix Apr 16, 2020
6217f16
Merge branch 'abernix/fauxpaque-SchemaHash' into abernix/add-wrf
abernix Apr 16, 2020
7ca6840
Correct typo of "precidence".
abernix Apr 27, 2020
cd754b0
noop: Remove trailing line.
abernix Apr 27, 2020
11e885c
Relocate schema instrumentation to `utils/schemaInstrumentation.ts`.
abernix Apr 27, 2020
231817e
Import `PersistedQueryOptions` from private, rather than public, API.
abernix Apr 27, 2020
a03587c
Merge remote-tracking branch 'origin/master' into abernix/add-willRes…
abernix May 6, 2020
ad9fefa
noop: Fix unrelated typing error in `runQuery.test.ts`.
abernix May 6, 2020
6575625
Reintroduce genericism to `Dispatcher`.
abernix May 4, 2020
d5408ac
Correct recently added plugin types to exclude `void`.
abernix May 6, 2020
ab78e48
Introduce `BaseContext` and `DefaultContext`.
abernix May 6, 2020
9b5be32
tests: Add additional plugin API hook tests.
abernix May 6, 2020
4fa6ddd
Introduce a `callTargets` method on the `Dispatcher`.
abernix May 6, 2020
f2a7490
Switch `willResolveField` to be nested within `executionDidStart`.
abernix May 6, 2020
60cd32d
tests: Introduce a test which demonstrates `fieldResolver` behavior.
abernix May 7, 2020
129c255
tests: Add further life-cycle ordering tests for parsing and validation.
abernix May 7, 2020
66d5869
refactor(tests): Better helpers for APQ tests in intgr. testsuite.
abernix May 7, 2020
570bc4e
feat(plugins): Intro. `didResolveSource` to indicate availability of …
abernix May 7, 2020
fe971b3
docs: Add `didResolveSource` to plugin Mermaid workflow.
abernix May 8, 2020
464e4f2
noop: Add comment indicating where to find `didResolveSource` APQ tests.
abernix May 8, 2020
1356f00
Merge pull request #4076 from apollographql/abernix/add-didResolveSource
abernix May 8, 2020
1121785
chore(types): Remove `DefaultContext` and just leverage `BaseContext`.
abernix May 8, 2020
a926b7e
Use an object, rather than positional params for `willResolveField`.
abernix May 8, 2020
b7ea447
Remove unreachable code after `callTargets` decomposition.
abernix May 8, 2020
41b103e
tests: Expand on tests for `willResolveField` parameters.
abernix May 11, 2020
6564081
Attach user-defined `fieldResolver` to context.
abernix May 11, 2020
f905eb1
Condense guards in `schemaInstrumentation`.
abernix May 12, 2020
db0e378
types: Improve `Dispatcher`'s `callTargets` and `invokeHookAsync` types.
abernix May 12, 2020
52418a4
comment: Add note about typing question and reference to issue.
abernix May 12, 2020
18d95fd
comment: Leave traces/suggestions about future work.
abernix May 12, 2020
99d4fa7
Merge remote-tracking branch 'origin/master' into abernix/add-willRes…
abernix May 12, 2020
1b98f87
Merge branch 'release-2.14.0' into abernix/add-willResolveField-and-d…
abernix May 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 '.';
abernix marked this conversation as resolved.
Show resolved Hide resolved
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>(
}
}
}

abernix marked this conversation as resolved.
Show resolved Hide resolved
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 {
abernix marked this conversation as resolved.
Show resolved Hide resolved
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;
abernix marked this conversation as resolved.
Show resolved Hide resolved

field.resolve = (source, args, context, info) => {
abernix marked this conversation as resolved.
Show resolved Hide resolved
// 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
abernix marked this conversation as resolved.
Show resolved Hide resolved
// 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);
});
abernix marked this conversation as resolved.
Show resolved Hide resolved
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];
abernix marked this conversation as resolved.
Show resolved Hide resolved

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

type FieldIteratorFn = (
fieldDef: GraphQLField<any, any>,
typeName: string,
fieldName: string,
) => void;
19 changes: 16 additions & 3 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 @@ -45,13 +46,22 @@ export interface ApolloServerPlugin<TContext extends Record<string, any> = Recor
): GraphQLRequestListener<TContext> | void;
}

export type GraphQLRequestListenerParsingDidEnd =
((err?: Error) => void) | void;
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?(
requestContext: GraphQLRequestContextParsingDidStart<TContext>,
): ((err?: Error) => void) | void;
): GraphQLRequestListenerParsingDidEnd;
validationDidStart?(
requestContext: GraphQLRequestContextValidationDidStart<TContext>,
): ((err?: ReadonlyArray<Error>) => void) | void;
): GraphQLRequestListenerValidationDidEnd;
didResolveOperation?(
requestContext: GraphQLRequestContextDidResolveOperation<TContext>,
): ValueOrPromise<void>;
Expand All @@ -68,7 +78,10 @@ export interface GraphQLRequestListener<TContext = Record<string, any>> {
): ValueOrPromise<GraphQLResponse | null>;
executionDidStart?(
requestContext: GraphQLRequestContextExecutionDidStart<TContext>,
): ((err?: Error) => void) | void;
): GraphQLRequestListenerExecutionDidEnd;
willResolveField?(
...fieldResolverArgs: Parameters<GraphQLFieldResolver<any, TContext>>
): GraphQLRequestListenerDidResolveField;
willSendResponse?(
requestContext: GraphQLRequestContextWillSendResponse<TContext>,
): ValueOrPromise<void>;
Expand Down