Skip to content

Commit

Permalink
feat: pass abortSignal to resolvers (#4261)
Browse files Browse the repository at this point in the history
this allows e.g. passing the signal to fetch

Note: the `abortSignal` is now the fifth argument to a GraphQLFieldResolverFn. If no resolver if provided, and the parent is an object with a key for the field name with a value that is a function, the `abortSignal` will be the fourth argument, as in the included test, with the `parent` accessible via the `this` keyword.
  • Loading branch information
yaacovCR authored Oct 28, 2024
1 parent fbb191a commit 12a5ec9
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 5 deletions.
47 changes: 47 additions & 0 deletions src/execution/__tests__/abort-signal-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,53 @@ describe('Execute: Cancellation', () => {
});
});

it('should provide access to the abort signal within resolvers', async () => {
const abortController = new AbortController();
const document = parse(`
query {
todo {
id
}
}
`);

const cancellableAsyncFn = async (abortSignal: AbortSignal) => {
await resolveOnNextTick();
abortSignal.throwIfAborted();
};

const resultPromise = execute({
document,
schema,
abortSignal: abortController.signal,
rootValue: {
todo: {
id: (_args: any, _context: any, _info: any, signal: AbortSignal) =>
cancellableAsyncFn(signal),
},
},
});

abortController.abort();

const result = await resultPromise;

expectJSON(result).toDeepEqual({
data: {
todo: {
id: null,
},
},
errors: [
{
message: 'This operation was aborted',
path: ['todo', 'id'],
locations: [{ line: 4, column: 11 }],
},
],
});
});

it('should stop the execution when aborted during object field completion with a custom error', async () => {
const abortController = new AbortController();
const document = parse(`
Expand Down
11 changes: 6 additions & 5 deletions src/execution/execute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ function executeField(
deferMap: ReadonlyMap<DeferUsage, DeferredFragmentRecord> | undefined,
): PromiseOrValue<GraphQLWrappedResult<unknown>> | undefined {
const validatedExecutionArgs = exeContext.validatedExecutionArgs;
const { schema, contextValue, variableValues, hideSuggestions } =
const { schema, contextValue, variableValues, hideSuggestions, abortSignal } =
validatedExecutionArgs;
const fieldName = fieldDetailsList[0].node.name.value;
const fieldDef = schema.getField(parentType, fieldName);
Expand Down Expand Up @@ -833,7 +833,7 @@ function executeField(
// The resolve function's optional third argument is a context value that
// is provided to every resolve function within an execution. It is commonly
// used to represent an authenticated user, or request-specific caches.
const result = resolveFn(source, args, contextValue, info);
const result = resolveFn(source, args, contextValue, info, abortSignal);

if (isPromise(result)) {
return completePromisedValue(
Expand Down Expand Up @@ -1955,12 +1955,12 @@ export const defaultTypeResolver: GraphQLTypeResolver<unknown, unknown> =
* of calling that function while passing along args and context value.
*/
export const defaultFieldResolver: GraphQLFieldResolver<unknown, unknown> =
function (source: any, args, contextValue, info) {
function (source: any, args, contextValue, info, abortSignal) {
// ensure source is a value for which property access is acceptable.
if (isObjectLike(source) || typeof source === 'function') {
const property = source[info.fieldName];
if (typeof property === 'function') {
return source[info.fieldName](args, contextValue, info);
return source[info.fieldName](args, contextValue, info, abortSignal);
}
return property;
}
Expand Down Expand Up @@ -2115,6 +2115,7 @@ function executeSubscription(
operation,
variableValues,
hideSuggestions,
abortSignal,
} = validatedExecutionArgs;

const rootType = schema.getSubscriptionType();
Expand Down Expand Up @@ -2180,7 +2181,7 @@ function executeSubscription(
// The resolve function's optional third argument is a context value that
// is provided to every resolve function within an execution. It is commonly
// used to represent an authenticated user, or request-specific caches.
const result = resolveFn(rootValue, args, contextValue, info);
const result = resolveFn(rootValue, args, contextValue, info, abortSignal);

if (isPromise(result)) {
return result
Expand Down
1 change: 1 addition & 0 deletions src/type/definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,6 +990,7 @@ export type GraphQLFieldResolver<
args: TArgs,
context: TContext,
info: GraphQLResolveInfo,
abortSignal: AbortSignal | undefined,
) => TResult;

export interface GraphQLResolveInfo {
Expand Down

0 comments on commit 12a5ec9

Please # to comment.