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 12 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/apollo-engine-reporting/src/agent.ts
Expand Up @@ -12,7 +12,7 @@ import { fetch, RequestAgent, Response } from 'apollo-server-env';
import retry from 'async-retry';

import { EngineReportingExtension } from './extension';
import { GraphQLRequestContext, Logger } from 'apollo-server-types';
import { GraphQLRequestContext, Logger, SchemaHash } from 'apollo-server-types';
import { InMemoryLRUCache } from 'apollo-server-caching';
import { defaultEngineReportingSignature } from 'apollo-graphql';

Expand Down Expand Up @@ -202,7 +202,7 @@ export interface AddTraceArgs {
trace: Trace;
operationName: string;
queryHash: string;
schemaHash: string;
schemaHash: SchemaHash;
queryString?: string;
documentAST?: DocumentNode;
}
Expand Down Expand Up @@ -278,7 +278,7 @@ export class EngineReportingAgent<TContext = any> {
handleLegacyOptions(this.options);
}

public newExtension(schemaHash: string): EngineReportingExtension<TContext> {
public newExtension(schemaHash: SchemaHash): EngineReportingExtension<TContext> {
return new EngineReportingExtension<TContext>(
this.options,
this.addTrace.bind(this),
Expand Down
9 changes: 7 additions & 2 deletions packages/apollo-engine-reporting/src/extension.ts
@@ -1,4 +1,9 @@
import { GraphQLRequestContext, WithRequired, Logger } from 'apollo-server-types';
import {
GraphQLRequestContext,
WithRequired,
Logger,
SchemaHash,
} from 'apollo-server-types';
import { Request, Headers } from 'apollo-server-env';
import {
GraphQLResolveInfo,
Expand Down Expand Up @@ -42,7 +47,7 @@ export class EngineReportingExtension<TContext = any>
public constructor(
options: EngineReportingOptions<TContext>,
addTrace: (args: AddTraceArgs) => Promise<void>,
private schemaHash: string,
private schemaHash: SchemaHash,
) {
this.options = {
...options,
Expand Down
18 changes: 11 additions & 7 deletions packages/apollo-server-core/src/ApolloServer.ts
Expand Up @@ -69,7 +69,7 @@ import {

import { Headers } from 'apollo-server-env';
import { buildServiceDefinition } from '@apollographql/apollo-tools';
import { Logger } from "apollo-server-types";
import { Logger, SchemaHash } from "apollo-server-types";

const NoIntrospection = (context: ValidationContext) => ({
Field(node: FieldDefinitionNode) {
Expand Down Expand Up @@ -134,7 +134,7 @@ type SchemaDerivedData = {
// on the same operation to be executed immediately.
documentStore?: InMemoryLRUCache<DocumentNode>;
schema: GraphQLSchema;
schemaHash: string;
schemaHash: SchemaHash;
extensions: Array<() => GraphQLExtension>;
};

Expand Down Expand Up @@ -782,12 +782,16 @@ export class ApolloServerBase {
return sdlFieldType.name == 'String';
}

private ensurePluginInstantiation(plugins?: PluginDefinition[]): void {
if (!plugins || !plugins.length) {
return;
}
private ensurePluginInstantiation(plugins: PluginDefinition[] = []): void {
const pluginsToInit: PluginDefinition[] = [];

// Internal plugins should be added to `pluginsToInit` here.
// User's plugins, provided as an argument to this method, will be added
// at the end of that list so they take precidence.
abernix marked this conversation as resolved.
Show resolved Hide resolved
// A follow-up commit will actually introduce this.

this.plugins = plugins.map(plugin => {
pluginsToInit.push(...plugins);
this.plugins = pluginsToInit.map(plugin => {
if (typeof plugin === 'function') {
return plugin();
}
Expand Down
12 changes: 11 additions & 1 deletion packages/apollo-server-core/src/requestPipeline.ts
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
153 changes: 153 additions & 0 deletions packages/apollo-server-core/src/requestPipelineAPI.ts
@@ -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,143 @@ 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.entries(typeMap).forEach(([typeName, type]) => {

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

type FieldIteratorFn = (
fieldDef: GraphQLField<any, any>,
typeName: string,
fieldName: string,
) => void;