-
Notifications
You must be signed in to change notification settings - Fork 2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Relocate schema instrumentation to
utils/schemaInstrumentation.ts
.
The `requestPipelineAPI.ts`'s purpose was originally to keep typings by themselves. It was compiled using a separate TypeScript compilation stage to avoid some circular dependencies within the repository itself. However, it still proved to be problematic since it required external packages which depended on the entire `apollo-server-core` just to utilize those types (e.g. plugins!) The work in #2990 offloaded the types to their own package that could be depended on but the assertion in [[1]] correctly notes that introducing new functionality, which is largely incompatible with the original intent of the `requestPipelineAPI` file (even though it is now deprecated) is largely a step backward. Therefore, this moves the functionality to a new file called `schemaInstrumentation`, as suggested in the following comment. [1]: https://github.com/apollographql/apollo-server/pull/3988/files#r414666538
- Loading branch information
Showing
4 changed files
with
149 additions
and
156 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 146 additions & 0 deletions
146
packages/apollo-server-core/src/utils/schemaInstrumentation.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
import { GraphQLSchema, GraphQLField, ResponsePath, getNamedType, GraphQLObjectType } from "graphql/type"; | ||
import { defaultFieldResolver } from "graphql/execution"; | ||
import { FieldNode } from "graphql/language"; | ||
import { Dispatcher } from "./dispatcher"; | ||
import { GraphQLRequestListener } from "apollo-server-plugin-base"; | ||
import { GraphQLObjectResolver } from "@apollographql/apollo-tools"; | ||
|
||
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.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; |