Skip to content

feat: hook into published subscribe values #428

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

Merged
merged 9 commits into from
Jul 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fifty-kids-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@envelop/core': minor
'@envelop/types': minor
---

allow hooking into published subscribe values
5 changes: 5 additions & 0 deletions .changeset/six-crabs-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@envelop/execute-subscription-event': patch
---

initial release
14 changes: 7 additions & 7 deletions benchmark/k6.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const options = buildOptions({
'graphql-js': {
no_errors: ['rate=1.0'],
expected_result: ['rate=1.0'],
http_req_duration: ['p(95)<=28'],
http_req_duration: ['p(95)<=35'],
graphql_execute: ['p(95)<=2'],
graphql_context: ['p(95)<=1'],
graphql_validate: ['p(95)<=1'],
Expand All @@ -61,7 +61,7 @@ export const options = buildOptions({
'envelop-just-cache': {
no_errors: ['rate=1.0'],
expected_result: ['rate=1.0'],
http_req_duration: ['p(95)<=20'],
http_req_duration: ['p(95)<=23'],
graphql_execute: ['p(95)<=1'],
graphql_context: ['p(95)<=1'],
graphql_validate: ['p(95)<=1'],
Expand All @@ -72,23 +72,23 @@ export const options = buildOptions({
'prom-tracing': {
no_errors: ['rate=1.0'],
expected_result: ['rate=1.0'],
http_req_duration: ['p(95)<=45'],
graphql_execute: ['p(95)<=4'],
http_req_duration: ['p(95)<=52'],
graphql_execute: ['p(95)<=6'],
graphql_context: ['p(95)<=1'],
graphql_validate: ['p(95)<=1'],
graphql_parse: ['p(95)<=1'],
envelop_init: ['p(95)<=1'],
envelop_total: ['p(95)<=4'],
envelop_total: ['p(95)<=6'],
},
'envelop-cache-and-no-internal-tracing': {
no_errors: ['rate=1.0'],
expected_result: ['rate=1.0'],
http_req_duration: ['p(95)<=15'],
http_req_duration: ['p(95)<=18'],
},
'envelop-cache-jit': {
no_errors: ['rate=1.0'],
expected_result: ['rate=1.0'],
http_req_duration: ['p(95)<=14'],
http_req_duration: ['p(95)<=16'],
graphql_execute: ['p(95)<=1'],
graphql_context: ['p(95)<=1'],
graphql_validate: ['p(95)<=1'],
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/graphql-typings.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
declare module 'graphql/jsutils/isAsyncIterable' {
function isAsyncIterable(input: unknown): input is AsyncIterable<any>;
function isAsyncIterable(input: unknown): input is AsyncIterableIterator<any>;
export default isAsyncIterable;
}
132 changes: 72 additions & 60 deletions packages/core/src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,30 @@ import {
SubscribeResultHook,
TypedSubscriptionArgs,
TypedExecutionArgs,
SubscribeFunction,
OnSubscribeResultResultOnNextHook,
OnSubscribeResultResultOnEndHook,
OnExecuteDoneHookResultOnNextHook,
OnExecuteDoneHookResultOnEndHook,
ExecuteFunction,
AsyncIterableIteratorOrValue,
} from '@envelop/types';
import isAsyncIterable from 'graphql/jsutils/isAsyncIterable';
import {
DocumentNode,
execute,
ExecutionArgs,
ExecutionResult,
GraphQLError,
GraphQLFieldResolver,
GraphQLSchema,
GraphQLTypeResolver,
parse,
specifiedRules,
subscribe,
SubscriptionArgs,
validate,
ValidationRule,
} from 'graphql';
import { Maybe } from 'graphql/jsutils/Maybe';
import { prepareTracedSchema, resolversHooksSymbol } from './traced-schema';
import { finalAsyncIterator, makeExecute, makeSubscribe, mapAsyncIterator } from './utils';

export type EnvelopOrchestrator<
InitialContext extends ArbitraryObject = ArbitraryObject,
Expand Down Expand Up @@ -272,32 +277,9 @@ export function createEnvelopOrchestrator<PluginsContext = any>(plugins: Plugin[
}
: initialContext => orchestratorCtx => orchestratorCtx ? { ...initialContext, ...orchestratorCtx } : initialContext;

const customSubscribe = async (
argsOrSchema: SubscriptionArgs | GraphQLSchema,
document?: DocumentNode,
rootValue?: any,
contextValue?: any,
variableValues?: Maybe<{ [key: string]: any }>,
operationName?: Maybe<string>,
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
subscribeFieldResolver?: Maybe<GraphQLFieldResolver<any, any>>
) => {
const args: SubscriptionArgs =
argsOrSchema instanceof GraphQLSchema
? {
schema: argsOrSchema,
document: document!,
rootValue,
contextValue,
variableValues,
operationName,
fieldResolver,
subscribeFieldResolver,
}
: argsOrSchema;

const customSubscribe = makeSubscribe(async args => {
const onResolversHandlers: OnResolverCalledHook[] = [];
let subscribeFn: typeof subscribe = subscribe;
let subscribeFn = subscribe as SubscribeFunction;

const afterCalls: SubscribeResultHook[] = [];
let context = args.contextValue || {};
Expand Down Expand Up @@ -333,46 +315,49 @@ export function createEnvelopOrchestrator<PluginsContext = any>(plugins: Plugin[
contextValue: context,
});

const onNextHandler: OnSubscribeResultResultOnNextHook[] = [];
const onEndHandler: OnSubscribeResultResultOnEndHook[] = [];

for (const afterCb of afterCalls) {
afterCb({
const hookResult = afterCb({
result,
setResult: newResult => {
result = newResult;
},
});
if (hookResult) {
if (hookResult.onNext) {
onNextHandler.push(hookResult.onNext);
}
if (hookResult.onEnd) {
onEndHandler.push(hookResult.onEnd);
}
}
}

if (onNextHandler.length && isAsyncIterable(result)) {
result = mapAsyncIterator(result, async result => {
for (const onNext of onNextHandler) {
await onNext({ result, setResult: newResult => (result = newResult) });
}
return result;
});
}
if (onEndHandler.length && isAsyncIterable(result)) {
result = finalAsyncIterator(result, () => {
for (const onEnd of onEndHandler) {
onEnd();
}
});
}
return result;
};
});

const customExecute = beforeCallbacks.execute.length
? async (
argsOrSchema: ExecutionArgs | GraphQLSchema,
document?: DocumentNode,
rootValue?: any,
contextValue?: any,
variableValues?: Maybe<{ [key: string]: any }>,
operationName?: Maybe<string>,
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>,
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>
) => {
const args: ExecutionArgs =
argsOrSchema instanceof GraphQLSchema
? {
schema: argsOrSchema,
document: document!,
rootValue,
contextValue,
variableValues,
operationName,
fieldResolver,
typeResolver,
}
: argsOrSchema;

? makeExecute(async args => {
const onResolversHandlers: OnResolverCalledHook[] = [];
let executeFn: typeof execute = execute;
let result: ExecutionResult;
let executeFn = execute as ExecuteFunction;
let result: AsyncIterableIteratorOrValue<ExecutionResult>;

const afterCalls: OnExecuteDoneHook[] = [];
let context = args.contextValue || {};
Expand Down Expand Up @@ -429,17 +414,44 @@ export function createEnvelopOrchestrator<PluginsContext = any>(plugins: Plugin[
contextValue: context,
});

const onNextHandler: OnExecuteDoneHookResultOnNextHook[] = [];
const onEndHandler: OnExecuteDoneHookResultOnEndHook[] = [];

for (const afterCb of afterCalls) {
afterCb({
const hookResult = afterCb({
result,
setResult: newResult => {
result = newResult;
},
});
if (hookResult) {
if (hookResult.onNext) {
onNextHandler.push(hookResult.onNext);
}
if (hookResult.onEnd) {
onEndHandler.push(hookResult.onEnd);
}
}
}

if (onNextHandler.length && isAsyncIterable(result)) {
result = mapAsyncIterator(result, async result => {
for (const onNext of onNextHandler) {
await onNext({ result, setResult: newResult => (result = newResult) });
}
return result;
});
}
if (onEndHandler.length && isAsyncIterable(result)) {
result = finalAsyncIterator(result, () => {
for (const onEnd of onEndHandler) {
onEnd();
}
});
}

return result;
}
})
: execute;

initDone = true;
Expand All @@ -463,7 +475,7 @@ export function createEnvelopOrchestrator<PluginsContext = any>(plugins: Plugin[
init,
parse: customParse,
validate: customValidate,
execute: customExecute,
execute: customExecute as ExecuteFunction,
subscribe: customSubscribe,
contextFactory: customContextFactory,
};
Expand Down
28 changes: 23 additions & 5 deletions packages/core/src/plugins/use-error-handler.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
import { Plugin } from '@envelop/types';
import { GraphQLError } from 'graphql';
import { ExecutionResult, GraphQLError } from 'graphql';
import isAsyncIterable from 'graphql/jsutils/isAsyncIterable';

export const useErrorHandler = (errorHandler: (errors: readonly GraphQLError[]) => void): Plugin => ({
export type ErrorHandler = (errors: readonly GraphQLError[]) => void;

const makeHandleResult =
(errorHandler: ErrorHandler) =>
({ result }: { result: ExecutionResult }) => {
if (result.errors?.length) {
errorHandler(result.errors);
}
};

export const useErrorHandler = (errorHandler: ErrorHandler): Plugin => ({
onExecute() {
const handleResult = makeHandleResult(errorHandler);
return {
onExecuteDone: ({ result }) => {
if (result.errors?.length) {
errorHandler(result.errors);
onExecuteDone({ result }) {
if (isAsyncIterable(result)) {
return {
onNext({ result }) {
handleResult({ result });
},
};
}
handleResult({ result });
return undefined;
},
};
},
Expand Down
26 changes: 21 additions & 5 deletions packages/core/src/plugins/use-masked-errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Plugin } from '@envelop/types';
import { GraphQLError } from 'graphql';
import { ExecutionResult, GraphQLError } from 'graphql';
import isAsyncIterable from 'graphql/jsutils/isAsyncIterable';

export class EnvelopError extends GraphQLError {
constructor(message: string, extensions?: Record<string, any>) {
Expand All @@ -21,16 +22,31 @@ export type UseMaskedErrorsOpts = {
formatError?: FormatErrorHandler;
};

const makeHandleResult =
(format: FormatErrorHandler) =>
({ result, setResult }: { result: ExecutionResult; setResult: (result: ExecutionResult) => void }) => {
if (result.errors != null) {
setResult({ ...result, errors: result.errors.map(error => format(error)) });
}
};

export const useMaskedErrors = (opts?: UseMaskedErrorsOpts): Plugin => {
const format = opts?.formatError ?? formatError;
const handleResult = makeHandleResult(format);

return {
onExecute: () => {
onExecute() {
return {
onExecuteDone: ({ result, setResult }) => {
if (result.errors != null) {
setResult({ ...result, errors: result.errors.map(error => format(error)) });
onExecuteDone({ result, setResult }) {
if (isAsyncIterable(result)) {
return {
onNext: ({ result, setResult }) => {
handleResult({ result, setResult });
},
};
}
handleResult({ result, setResult });
return undefined;
},
};
},
Expand Down
30 changes: 24 additions & 6 deletions packages/core/src/plugins/use-payload-formatter.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
import { Plugin } from '@envelop/types';
import { ExecutionResult } from 'graphql';
import isAsyncIterable from 'graphql/jsutils/isAsyncIterable';

export const usePayloadFormatter = (formatter: (result: ExecutionResult<any, any>) => false | ExecutionResult<any, any>): Plugin => ({
export type FormatterFunction = (result: ExecutionResult<any, any>) => false | ExecutionResult<any, any>;

const makeHandleResult =
(formatter: FormatterFunction) =>
({ result, setResult }: { result: ExecutionResult; setResult: (result: ExecutionResult) => void }) => {
const modified = formatter(result);
if (modified !== false) {
setResult(modified);
}
};

export const usePayloadFormatter = (formatter: FormatterFunction): Plugin => ({
onExecute() {
const handleResult = makeHandleResult(formatter);
return {
onExecuteDone: ({ result, setResult }) => {
const modified = formatter(result);

if (modified !== false) {
setResult(modified);
onExecuteDone({ result, setResult }) {
if (isAsyncIterable(result)) {
return {
onNext({ result, setResult }) {
handleResult({ result, setResult });
},
};
}

handleResult({ result, setResult });
return undefined;
},
};
},
Expand Down
Loading