@subsquid/apollo-server-core
Version:
Core engine for Apollo GraphQL server
178 lines (155 loc) • 5.81 kB
text/typescript
import {
GraphQLSchema,
GraphQLField,
ResponsePath,
getNamedType,
GraphQLObjectType,
GraphQLFieldResolver,
} from 'graphql/type';
import { defaultFieldResolver } from 'graphql/execution';
import type { FieldNode } from 'graphql/language';
import type { GraphQLRequestExecutionListener } from 'apollo-server-plugin-base';
import type { GraphQLObjectResolver } from '@apollographql/apollo-tools';
export const symbolExecutionDispatcherWillResolveField = Symbol(
'apolloServerExecutionDispatcherWillResolveField',
);
export const symbolUserFieldResolver = Symbol('apolloServerUserFieldResolver');
const symbolPluginsEnabled = Symbol('apolloServerPluginsEnabled');
export function enablePluginsForSchemaResolvers(
schema: GraphQLSchema & { [symbolPluginsEnabled]?: boolean },
) {
if (pluginsEnabledForSchemaResolvers(schema)) {
return schema;
}
Object.defineProperty(schema, symbolPluginsEnabled, {
value: true,
});
forEachField(schema, wrapField);
return schema;
}
export function pluginsEnabledForSchemaResolvers(
schema: GraphQLSchema & { [symbolPluginsEnabled]?: boolean },
): boolean {
return !!schema[symbolPluginsEnabled];
}
function wrapField(field: GraphQLField<any, any>): void {
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 GraphQLRequestExecutionListener['willResolveField'] | undefined;
const userFieldResolver = context?.[symbolUserFieldResolver] as
| GraphQLFieldResolver<any, any>
| undefined;
// The technique for implementing a "did resolve field" is accomplished by
// returning a function from the `willResolveField` handler. While there
// may be several callbacks, depending on the number of plugins which have
// implemented a `willResolveField` hook, this hook will call them all
// as dictated by the dispatcher. We will call this when object
// resolution is complete.
const didResolveField =
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);
}
// 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.
if (typeof didResolveField === 'function') {
whenResultIsFinished(result, didResolveField);
}
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.
if (typeof didResolveField === 'function') {
didResolveField(error as 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;