Skip to content

Commit

Permalink
feat: replace/wrap the logic params handled - ParamsHandler (#3736)
Browse files Browse the repository at this point in the history
* feat(onParams-handler)

* Move error handling inside

* Format

* Unit tests for params handler

* Make linter happy

* Update packages/graphql-yoga/src/server.ts
  • Loading branch information
ardatan authored Feb 6, 2025
1 parent ba5f1fb commit d13b8a4
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 66 deletions.
20 changes: 20 additions & 0 deletions .changeset/friendly-actors-brake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'graphql-yoga': minor
---

Now it is possible to replace or wrap the logic how `GraphQLParams` handled;

By default Yoga calls Envelop to handle the parameters, but now you can replace it with your own logic.

Example: Wrap the GraphQL handling pipeline in an `AsyncLocalStorage`

```ts
function myPlugin(): Plugin {
const context = new AsyncLocalStorage();
return {
onParams({ paramsHandler, setParamsHandler }) {
const store = { foo: 'bar' }
setParamsHandler(payload => context.run(store, paramsHandler, payload))
}
}
```
37 changes: 37 additions & 0 deletions packages/graphql-yoga/__tests__/plugin-hooks.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createSchema, createYoga, type Plugin } from '../src';
import type { ParamsHandlerPayload } from '../src/plugins/types';
import { eventStream } from './utilities';

test('onParams -> setResult to single execution result', async () => {
Expand Down Expand Up @@ -60,6 +61,42 @@ test('onParams -> setResult to event stream execution result', async () => {
expect(counter).toBe(2);
});

test('onParams -> replaces the params handler correctly', async () => {
const paramsHandler = jest.fn((_payload: ParamsHandlerPayload<{}>) => ({
data: { hello: 'world' },
}));
const plugin: Plugin = {
async onParams({ setParamsHandler }) {
setParamsHandler(paramsHandler);
},
};

const yoga = createYoga({ plugins: [plugin] });

const params = {
query: '{ hello }',
};
const request = new yoga.fetchAPI.Request('http://yoga/graphql', {
method: 'POST',
body: JSON.stringify(params),
headers: {
'Content-Type': 'application/json',
},
});

const serverContext = {};

const result = await yoga.fetch(request, serverContext);

expect(result.status).toBe(200);
const body = await result.json();
expect(body).toEqual({ data: { hello: 'world' } });
expect(paramsHandler).toHaveBeenCalledTimes(1);
expect(paramsHandler).toHaveBeenCalledWith(
expect.objectContaining({ params, request, context: expect.objectContaining(serverContext) }),
);
});

test('context value identity stays the same in all hooks', async () => {
const contextValues = [] as Array<unknown>;
const yoga = createYoga({
Expand Down
18 changes: 17 additions & 1 deletion packages/graphql-yoga/src/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,30 @@ export type OnParamsHook<TServerContext> = (
) => PromiseOrValue<void>;

export interface OnParamsEventPayload<TServerContext = Record<string, unknown>> {
params: GraphQLParams;
request: Request;

params: GraphQLParams;
setParams: (params: GraphQLParams) => void;
paramsHandler: ParamsHandler<TServerContext>;

setParamsHandler: (handler: ParamsHandler<TServerContext>) => void;

setResult: (result: ExecutionResult | AsyncIterable<ExecutionResult>) => void;

fetchAPI: FetchAPI;
context: TServerContext;
}

export interface ParamsHandlerPayload<TServerContext> {
request: Request;
params: GraphQLParams;
context: TServerContext & ServerAdapterInitialContext & YogaInitialContext;
}

export type ParamsHandler<TServerContext> = (
payload: ParamsHandlerPayload<TServerContext>,
) => PromiseOrValue<ExecutionResult | AsyncIterable<ExecutionResult>>;

export type OnResultProcess<TServerContext> = (
payload: OnResultProcessEventPayload<TServerContext>,
) => PromiseOrValue<void>;
Expand Down
141 changes: 76 additions & 65 deletions packages/graphql-yoga/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
OnRequestParseDoneHook,
OnRequestParseHook,
OnResultProcess,
ParamsHandler,
Plugin,
RequestParser,
ResultProcessorInput,
Expand Down Expand Up @@ -462,6 +463,56 @@ export class YogaServer<
}
}

handleParams: ParamsHandler<TServerContext> = async ({ request, context, params }) => {
let result: ExecutionResult | AsyncIterable<ExecutionResult>;
try {
const additionalContext =
context['request'] === request
? {
params,
}
: {
request,
params,
};

Object.assign(context, additionalContext);

const enveloped = this.getEnveloped(context);

this.logger.debug(`Processing GraphQL Parameters`);
result = await processGraphQLParams({
params,
enveloped,
});
this.logger.debug(`Processing GraphQL Parameters done.`);
} catch (error) {
const errors = handleError(error, this.maskedErrorsOpts, this.logger);

result = {
errors,
};
}
if (isAsyncIterable(result)) {
result = mapAsyncIterator(
result,
v => v,
(error: Error) => {
if (error.name === 'AbortError') {
this.logger.debug(`Request aborted`);
throw error;
}

const errors = handleError(error, this.maskedErrorsOpts, this.logger);
return {
errors,
};
},
);
}
return result;
};

async getResultForParams(
{
params,
Expand All @@ -473,74 +524,33 @@ export class YogaServer<
context: TServerContext,
) {
let result: ExecutionResult | AsyncIterable<ExecutionResult> | undefined;
let paramsHandler = this.handleParams;

try {
for (const onParamsHook of this.onParamsHooks) {
await onParamsHook({
params,
request,
setParams(newParams) {
params = newParams;
},
setResult(newResult) {
result = newResult;
},
fetchAPI: this.fetchAPI,
context,
});
}

if (result == null) {
const additionalContext =
context['request'] === request
? {
params,
}
: {
request,
params,
};

Object.assign(context, additionalContext);

const enveloped = this.getEnveloped(context);

this.logger.debug(`Processing GraphQL Parameters`);
result = await processGraphQLParams({
params,
enveloped,
});

this.logger.debug(`Processing GraphQL Parameters done.`);
}

/** Ensure that error thrown from subscribe is sent to client */
// TODO: this should probably be something people can customize via a hook?
if (isAsyncIterable(result)) {
result = mapAsyncIterator(
result,
v => v,
(err: Error) => {
if (err.name === 'AbortError') {
this.logger.debug(`Request aborted`);
throw err;
}

const errors = handleError(err, this.maskedErrorsOpts, this.logger);
return {
errors,
};
},
);
}
} catch (error) {
const errors = handleError(error, this.maskedErrorsOpts, this.logger);

result = {
errors,
};
for (const onParamsHook of this.onParamsHooks) {
await onParamsHook({
params,
request,
setParams(newParams) {
params = newParams;
},
paramsHandler,
setParamsHandler(newHandler) {
paramsHandler = newHandler;
},
setResult(newResult) {
result = newResult;
},
fetchAPI: this.fetchAPI,
context,
});
}

result ??= await paramsHandler({
request,
params,
context: context as TServerContext & YogaInitialContext,
});

for (const onExecutionResult of this.onExecutionResultHooks) {
await onExecutionResult({
result,
Expand All @@ -551,6 +561,7 @@ export class YogaServer<
context: context as TServerContext & YogaInitialContext,
});
}

return result;
}

Expand Down

0 comments on commit d13b8a4

Please sign in to comment.