From 726519dc7ad5b4bba5dabdcebf37f933ac182f5b Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Wed, 17 Aug 2022 14:15:56 -0400 Subject: [PATCH 01/61] feat: non-breaking GraphQL Engine --- packages/core/src/create.ts | 5 +++-- packages/core/src/orchestrator.ts | 23 ++++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/core/src/create.ts b/packages/core/src/create.ts index d47665016d..9c7e03f13b 100644 --- a/packages/core/src/create.ts +++ b/packages/core/src/create.ts @@ -1,14 +1,15 @@ import { GetEnvelopedFn, ComposeContext, Plugin, ArbitraryObject } from '@envelop/types'; import { isPluginEnabled, PluginOrDisabledPlugin } from './enable-if.js'; -import { createEnvelopOrchestrator, EnvelopOrchestrator } from './orchestrator.js'; +import { createEnvelopOrchestrator, EnvelopOrchestrator, GraphQLEngine } from './orchestrator.js'; import { traceOrchestrator } from './traced-orchestrator.js'; export function envelop[]>(options: { plugins: Array; enableInternalTracing?: boolean; + engine?: typeof GraphQLEngine; }): GetEnvelopedFn> { const plugins = options.plugins.filter(isPluginEnabled); - let orchestrator = createEnvelopOrchestrator>(plugins); + let orchestrator = createEnvelopOrchestrator>(plugins, options.engine); if (options.enableInternalTracing) { orchestrator = traceOrchestrator(orchestrator); diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 759e22d91e..4b120ad2d1 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -64,10 +64,31 @@ export type EnvelopOrchestrator< getCurrentSchema: () => Maybe; }; +export const GraphQLEngine = ({ + parseFn, + executeFn, + validateFn, + subscribeFn, +}: { + parseFn?: typeof parse; + executeFn?: typeof execute; + validateFn?: typeof validate; + subscribeFn?: typeof subscribe; +}) => { + return { + parse: parseFn ?? parse, + execute: executeFn ?? execute, + validate: validateFn ?? validate, + subscribe: subscribeFn ?? subscribe, + }; +}; + export function createEnvelopOrchestrator( - plugins: Plugin[] + plugins: Plugin[], + engine: typeof GraphQLEngine = GraphQLEngine ): EnvelopOrchestrator { let schema: GraphQLSchema | undefined | null = null; + const { parse, execute, validate, subscribe } = engine({}); let initDone = false; const onResolversHandlers: OnResolverCalledHook[] = []; for (const plugin of plugins) { From a84ea4659f9db666a87c46d09f50ab06820be079 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Mon, 22 Aug 2022 11:08:46 -0400 Subject: [PATCH 02/61] feat: make envelop take in engine functions --- packages/core/src/create.ts | 16 +++++++-- packages/core/src/orchestrator.ts | 35 +++++++------------ packages/core/test/extends.spec.ts | 9 +++++ .../test/apollo-server-errors.spec.ts | 9 ++++- .../test/extended-validation.spec.ts | 5 +++ .../test/use-fragment-arguments.spec.ts | 9 ++++- packages/testing/src/index.ts | 21 ++++++++++- 7 files changed, 76 insertions(+), 28 deletions(-) diff --git a/packages/core/src/create.ts b/packages/core/src/create.ts index 9c7e03f13b..02706b94e6 100644 --- a/packages/core/src/create.ts +++ b/packages/core/src/create.ts @@ -1,15 +1,25 @@ import { GetEnvelopedFn, ComposeContext, Plugin, ArbitraryObject } from '@envelop/types'; +import type { execute, parse, subscribe, validate } from 'graphql'; import { isPluginEnabled, PluginOrDisabledPlugin } from './enable-if.js'; -import { createEnvelopOrchestrator, EnvelopOrchestrator, GraphQLEngine } from './orchestrator.js'; +import { createEnvelopOrchestrator, EnvelopOrchestrator } from './orchestrator.js'; import { traceOrchestrator } from './traced-orchestrator.js'; export function envelop[]>(options: { plugins: Array; enableInternalTracing?: boolean; - engine?: typeof GraphQLEngine; + parse: typeof parse; + execute: typeof execute; + validate: typeof validate; + subscribe: typeof subscribe; }): GetEnvelopedFn> { const plugins = options.plugins.filter(isPluginEnabled); - let orchestrator = createEnvelopOrchestrator>(plugins, options.engine); + let orchestrator = createEnvelopOrchestrator>({ + plugins, + parse: options.parse, + execute: options.execute, + validate: options.validate, + subscribe: options.subscribe, + }); if (options.enableInternalTracing) { orchestrator = traceOrchestrator(orchestrator); diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 4b120ad2d1..b2cae3cf1f 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -64,31 +64,22 @@ export type EnvelopOrchestrator< getCurrentSchema: () => Maybe; }; -export const GraphQLEngine = ({ - parseFn, - executeFn, - validateFn, - subscribeFn, -}: { - parseFn?: typeof parse; - executeFn?: typeof execute; - validateFn?: typeof validate; - subscribeFn?: typeof subscribe; -}) => { - return { - parse: parseFn ?? parse, - execute: executeFn ?? execute, - validate: validateFn ?? validate, - subscribe: subscribeFn ?? subscribe, - }; +type EnvelopOrchestratorOptions = { + plugins: Plugin[]; + parse: typeof parse; + execute: typeof execute; + subscribe: typeof subscribe; + validate: typeof validate; }; -export function createEnvelopOrchestrator( - plugins: Plugin[], - engine: typeof GraphQLEngine = GraphQLEngine -): EnvelopOrchestrator { +export function createEnvelopOrchestrator({ + plugins, + parse, + execute, + subscribe, + validate, +}: EnvelopOrchestratorOptions): EnvelopOrchestrator { let schema: GraphQLSchema | undefined | null = null; - const { parse, execute, validate, subscribe } = engine({}); let initDone = false; const onResolversHandlers: OnResolverCalledHook[] = []; for (const plugin of plugins) { diff --git a/packages/core/test/extends.spec.ts b/packages/core/test/extends.spec.ts index eebb8099d6..d325e7f524 100644 --- a/packages/core/test/extends.spec.ts +++ b/packages/core/test/extends.spec.ts @@ -2,6 +2,7 @@ import { createSpiedPlugin, createTestkit } from '@envelop/testing'; import { envelop, useExtendContext, useLogger, useSchema } from '../src/index.js'; import { useEnvelop } from '../src/plugins/use-envelop.js'; import { schema, query } from './common.js'; +import { parse, execute, validate, subscribe } from 'graphql'; describe('extending envelops', () => { it('should allow to extend envelops', async () => { @@ -9,6 +10,10 @@ describe('extending envelops', () => { const baseEnvelop = envelop({ plugins: [useLogger(), spiedPlugin.plugin], + parse, + execute, + validate, + subscribe, }); const onExecuteChildSpy = jest.fn(); @@ -21,6 +26,10 @@ describe('extending envelops', () => { onExecute: onExecuteChildSpy, }, ], + parse, + execute, + validate, + subscribe, }); const teskit = createTestkit(instance); diff --git a/packages/plugins/apollo-server-errors/test/apollo-server-errors.spec.ts b/packages/plugins/apollo-server-errors/test/apollo-server-errors.spec.ts index 5e3c02b6e4..c7a2c36afc 100644 --- a/packages/plugins/apollo-server-errors/test/apollo-server-errors.spec.ts +++ b/packages/plugins/apollo-server-errors/test/apollo-server-errors.spec.ts @@ -4,6 +4,7 @@ import { GraphQLSchema } from 'graphql'; import { envelop, useSchema } from '@envelop/core'; import { useApolloServerErrors } from '../src/index.js'; import { assertSingleExecutionValue } from '@envelop/testing'; +import { parse, execute, validate, subscribe } from 'graphql'; // Fix compat by mocking broken function // we can remove this once apollo fixed legacy usages of execute(schema, ...args) @@ -15,7 +16,13 @@ jest.mock('../../../../node_modules/apollo-server-core/dist/utils/schemaHash', ( describe('useApolloServerErrors', () => { const executeBoth = async (schema: GraphQLSchema, query: string, debug: boolean) => { const apolloServer = new ApolloServerBase({ schema, debug }); - const envelopRuntime = envelop({ plugins: [useSchema(schema), useApolloServerErrors({ debug })] })({}); + const envelopRuntime = envelop({ + plugins: [useSchema(schema), useApolloServerErrors({ debug })], + parse, + execute, + validate, + subscribe, + })({}); return { apollo: await apolloServer.executeOperation({ query }), diff --git a/packages/plugins/extended-validation/test/extended-validation.spec.ts b/packages/plugins/extended-validation/test/extended-validation.spec.ts index e99f740395..7cf58c0410 100644 --- a/packages/plugins/extended-validation/test/extended-validation.spec.ts +++ b/packages/plugins/extended-validation/test/extended-validation.spec.ts @@ -3,6 +3,7 @@ import { assertSingleExecutionValue, createTestkit } from '@envelop/testing'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { buildSchema, GraphQLError, parse } from 'graphql'; import { useExtendedValidation } from '../src/index.js'; +import { parse as gqlParse, execute as gqlExecute, validate as gqlValidate, subscribe as gqlSubscribe } from 'graphql'; describe('useExtendedValidation', () => { it('supports usage of multiple useExtendedValidation in different plugins', async () => { @@ -114,6 +115,10 @@ describe('useExtendedValidation', () => { rules: [() => ({})], }), ], + parse: gqlParse, + execute: gqlExecute, + validate: gqlValidate, + subscribe: gqlSubscribe, })(); await expect( execute({ diff --git a/packages/plugins/fragment-arguments/test/use-fragment-arguments.spec.ts b/packages/plugins/fragment-arguments/test/use-fragment-arguments.spec.ts index ab76dece19..4149932d95 100644 --- a/packages/plugins/fragment-arguments/test/use-fragment-arguments.spec.ts +++ b/packages/plugins/fragment-arguments/test/use-fragment-arguments.spec.ts @@ -3,6 +3,7 @@ import { oneLine, stripIndent } from 'common-tags'; import { diff } from 'jest-diff'; import { envelop, useSchema } from '@envelop/core'; import { useFragmentArguments } from '../src/index.js'; +import { parse as gqlParse, execute as gqlExecute, validate as gqlValidate, subscribe as gqlSubscribe } from 'graphql'; function compareStrings(a: string, b: string): boolean { return a.includes(b); @@ -66,7 +67,13 @@ describe('useFragmentArguments', () => { } `); test('can inline fragment with argument', () => { - const { parse } = envelop({ plugins: [useFragmentArguments(), useSchema(schema)] })({}); + const { parse } = envelop({ + plugins: [useFragmentArguments(), useSchema(schema)], + parse: gqlParse, + execute: gqlExecute, + validate: gqlValidate, + subscribe: gqlSubscribe, + })({}); const result = parse(/* GraphQL */ ` fragment TestFragment($c: String) on Query { a(b: $c) diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index 1bfad5498b..4b21137908 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -1,4 +1,15 @@ -import { DocumentNode, ExecutionResult, getOperationAST, GraphQLError, GraphQLSchema, print } from 'graphql'; +import { + DocumentNode, + ExecutionResult, + getOperationAST, + GraphQLError, + GraphQLSchema, + print, + execute, + parse, + subscribe, + validate, +} from 'graphql'; import { useSchema, envelop, PluginOrDisabledPlugin, isAsyncIterable } from '@envelop/core'; import { GetEnvelopedFn, Plugin } from '@envelop/types'; import { mapSchema as cloneSchema, isDocumentNode } from '@graphql-tools/utils'; @@ -101,6 +112,10 @@ export function createTestkit( let getEnveloped = Array.isArray(pluginsOrEnvelop) ? envelop({ plugins: [...(schema ? [useSchema(cloneSchema(schema))] : []), ...pluginsOrEnvelop], + parse, + execute, + validate, + subscribe, }) : pluginsOrEnvelop; @@ -108,6 +123,10 @@ export function createTestkit( modifyPlugins(modifyPluginsFn: ModifyPluginsFn) { getEnveloped = envelop({ plugins: [...(schema ? [useSchema(cloneSchema(schema))] : []), ...modifyPluginsFn(getEnveloped._plugins)], + parse, + execute, + validate, + subscribe, }); }, mockPhase(phaseReplacement: PhaseReplacementParams) { From 5320ae579ba464925fba82811294b447a930e776 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Mon, 22 Aug 2022 15:33:39 -0400 Subject: [PATCH 03/61] structural typings --- packages/core/src/orchestrator.ts | 47 ++++++++-------- packages/core/src/utils.ts | 57 ++++++++----------- packages/plugins/sentry/src/index.ts | 2 +- packages/types/src/graphql.ts | 82 ++++++++++++++-------------- packages/types/src/hooks.ts | 6 +- packages/types/src/utils.ts | 17 ++++++ 6 files changed, 108 insertions(+), 103 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index b2cae3cf1f..714c67676c 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -28,19 +28,10 @@ import { SubscribeErrorHook, DefaultContext, Maybe, -} from '@envelop/types'; -import { - DocumentNode, - execute, + ParseFunction, + ValidateFunction, ExecutionResult, - GraphQLError, - GraphQLSchema, - parse, - specifiedRules, - subscribe, - validate, - ValidationRule, -} from 'graphql'; +} from '@envelop/types'; import { prepareTracedSchema, resolversHooksSymbol } from './traced-schema.js'; import { errorAsyncIterator, @@ -61,15 +52,15 @@ export type EnvelopOrchestrator< execute: ReturnType>['execute']; subscribe: ReturnType>['subscribe']; contextFactory: EnvelopContextFnWrapper>['contextFactory'], PluginsContext>; - getCurrentSchema: () => Maybe; + getCurrentSchema: () => Maybe; }; type EnvelopOrchestratorOptions = { plugins: Plugin[]; - parse: typeof parse; - execute: typeof execute; - subscribe: typeof subscribe; - validate: typeof validate; + parse: ParseFunction; + execute: ExecuteFunction; + subscribe: SubscribeFunction; + validate: ValidateFunction; }; export function createEnvelopOrchestrator({ @@ -79,7 +70,7 @@ export function createEnvelopOrchestrator subscribe, validate, }: EnvelopOrchestratorOptions): EnvelopOrchestrator { - let schema: GraphQLSchema | undefined | null = null; + let schema: any | undefined | null = null; let initDone = false; const onResolversHandlers: OnResolverCalledHook[] = []; for (const plugin of plugins) { @@ -91,7 +82,7 @@ export function createEnvelopOrchestrator // Define the initial method for replacing the GraphQL schema, this is needed in order // to allow setting the schema from the onPluginInit callback. We also need to make sure // here not to call the same plugin that initiated the schema switch. - const replaceSchema = (newSchema: GraphQLSchema, ignorePluginIndex = -1) => { + const replaceSchema = (newSchema: any, ignorePluginIndex = -1) => { if (onResolversHandlers.length) { prepareTracedSchema(newSchema); } @@ -164,7 +155,7 @@ export function createEnvelopOrchestrator const customParse: EnvelopContextFnWrapper = beforeCallbacks.parse.length ? initialContext => (source, parseOptions) => { - let result: DocumentNode | Error | null = null; + let result: any | Error | null = null; let parseFn: typeof parse = parse; const context = initialContext; const afterCalls: AfterParseHook[] = []; @@ -223,9 +214,9 @@ export function createEnvelopOrchestrator const customValidate: EnvelopContextFnWrapper = beforeCallbacks.validate.length ? initialContext => (schema, documentAST, rules, typeInfo, validationOptions) => { - let actualRules: undefined | ValidationRule[] = rules ? [...rules] : undefined; + let actualRules: undefined | any[] = rules ? [...rules] : undefined; let validateFn = validate; - let result: null | readonly GraphQLError[] = null; + let result: null | readonly any[] = null; const context = initialContext; const afterCalls: AfterValidateHook[] = []; @@ -246,7 +237,10 @@ export function createEnvelopOrchestrator validateFn, addValidationRule: rule => { if (!actualRules) { - actualRules = [...specifiedRules]; + // Ideally we should provide default validation rules here. + // eslint-disable-next-line no-console + console.warn('No default validation rules provided.'); + actualRules = []; } actualRules.push(rule); @@ -266,6 +260,10 @@ export function createEnvelopOrchestrator result = validateFn(schema, documentAST, actualRules, typeInfo, validationOptions); } + if (!result) { + return; + } + const valid = result.length === 0; for (const afterCb of afterCalls) { @@ -396,6 +394,9 @@ export function createEnvelopOrchestrator // Can be removed once we drop support for GraphQL.js 15 }); } + if (!result) { + return; + } const onNextHandler: OnSubscribeResultResultOnNextHook[] = []; const onEndHandler: OnSubscribeResultResultOnEndHook[] = []; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 99edb724c8..a12c23f904 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -1,15 +1,3 @@ -import { - ASTNode, - DocumentNode, - Kind, - OperationDefinitionNode, - visit, - BREAK, - Source, - ExecutionResult, - SubscriptionArgs, - ExecutionArgs, -} from 'graphql'; import { AsyncIterableIteratorOrValue, ExecuteFunction, @@ -21,44 +9,47 @@ import { OnExecuteDoneEventPayload, OnExecuteDoneHookResult, OnExecuteDoneHookResultOnNextHook, + ExecutionArgs, } from '@envelop/types'; export const envelopIsIntrospectionSymbol = Symbol('ENVELOP_IS_INTROSPECTION'); -export function isOperationDefinition(def: ASTNode): def is OperationDefinitionNode { - return def.kind === Kind.OPERATION_DEFINITION; +export function isOperationDefinition(def: any): boolean { + return def.kind === 'OperationDefinition'; } -export function isIntrospectionOperation(operation: OperationDefinitionNode): boolean { +export function isIntrospectionOperation(operation: any): boolean { if (operation.kind === 'OperationDefinition') { - let hasIntrospectionField = false; + if (operation.name?.value === '__schema') { + return true; + } - visit(operation, { - Field: node => { - if (node.name.value === '__schema') { - hasIntrospectionField = true; - return BREAK; - } - }, + const nodesWithSchema = operation.selectionSet.selections.filter((selection: any) => { + if (selection.kind === 'Field' && selection.name.value === '__schema') { + return true; + } + return false; }); - return hasIntrospectionField; + if (nodesWithSchema.length > 0) { + return true; + } } return false; } -export function isIntrospectionDocument(document: DocumentNode): boolean { +export function isIntrospectionDocument(document: any): boolean { const operations = document.definitions.filter(isOperationDefinition); - return operations.some(op => isIntrospectionOperation(op)); + return operations.some((op: any) => isIntrospectionOperation(op)); } -export function isIntrospectionOperationString(operation: string | Source): boolean { +export function isIntrospectionOperationString(operation: string | any): boolean { return (typeof operation === 'string' ? operation : operation.body).indexOf('__schema') !== -1; } -function getSubscribeArgs(args: PolymorphicSubscribeArguments): SubscriptionArgs { +function getSubscribeArgs(args: PolymorphicSubscribeArguments): ExecutionArgs { return args.length === 1 ? args[0] : { @@ -76,10 +67,8 @@ function getSubscribeArgs(args: PolymorphicSubscribeArguments): SubscriptionArgs /** * Utility function for making a subscribe function that handles polymorphic arguments. */ -export const makeSubscribe = ( - subscribeFn: (args: SubscriptionArgs) => PromiseOrValue> -): SubscribeFunction => - ((...polyArgs: PolymorphicSubscribeArguments): PromiseOrValue> => +export const makeSubscribe = (subscribeFn: (args: ExecutionArgs) => any): SubscribeFunction => + ((...polyArgs: PolymorphicSubscribeArguments): PromiseOrValue> => subscribeFn(getSubscribeArgs(polyArgs))) as SubscribeFunction; export function mapAsyncIterator( @@ -148,9 +137,9 @@ function getExecuteArgs(args: PolymorphicExecuteArguments): ExecutionArgs { * Utility function for making a execute function that handles polymorphic arguments. */ export const makeExecute = ( - executeFn: (args: ExecutionArgs) => PromiseOrValue> + executeFn: (args: ExecutionArgs) => PromiseOrValue> ): ExecuteFunction => - ((...polyArgs: PolymorphicExecuteArguments): PromiseOrValue> => + ((...polyArgs: PolymorphicExecuteArguments): PromiseOrValue> => executeFn(getExecuteArgs(polyArgs))) as unknown as ExecuteFunction; /** diff --git a/packages/plugins/sentry/src/index.ts b/packages/plugins/sentry/src/index.ts index 38640ce26b..3d3e9ca371 100644 --- a/packages/plugins/sentry/src/index.ts +++ b/packages/plugins/sentry/src/index.ts @@ -289,7 +289,7 @@ export const useSentry = (options: SentryPluginOptions = {}): Plugin => { // Map index values in list to $index for better grouping of events. const errorPathWithIndex = (err.path ?? []) - .map(v => (typeof v === 'number' ? '$index' : v)) + .map((v: any) => (typeof v === 'number' ? '$index' : v)) .join(' > '); const eventId = Sentry.captureException(err, { diff --git a/packages/types/src/graphql.ts b/packages/types/src/graphql.ts index 9a8ac5e6ee..61cc7f76f5 100644 --- a/packages/types/src/graphql.ts +++ b/packages/types/src/graphql.ts @@ -1,48 +1,23 @@ -import type { - DocumentNode, - GraphQLFieldResolver, - GraphQLSchema, - SubscriptionArgs, - ExecutionArgs, - GraphQLTypeResolver, - subscribe, - execute, - parse, - validate, - GraphQLResolveInfo, -} from 'graphql'; -import type { Maybe } from './utils.js'; - -/** @private */ -export type PolymorphicExecuteArguments = - | [ExecutionArgs] - | [ - GraphQLSchema, - DocumentNode, - any, - any, - Maybe<{ [key: string]: any }>, - Maybe, - Maybe>, - Maybe> - ]; +import type { GraphQLResolveInfo } from 'graphql'; +import { ObjMap } from './utils.js'; +export interface ExecutionArgs { + schema: any; + document: any; + rootValue?: any; + contextValue?: any; + variableValues?: any; + operationName?: any; + fieldResolver?: any; + typeResolver?: any; + subscribeFieldResolver?: any; +} +declare function parse(source: any, options?: any): any; +declare function execute(args: ExecutionArgs): any; +declare function subscribe(args: ExecutionArgs): any; +declare function validate(schema: any, documentAST: any, rules?: any, options?: any, typeInfo?: any): any; export type ExecuteFunction = typeof execute; -/** @private */ -export type PolymorphicSubscribeArguments = - | [SubscriptionArgs] - | [ - GraphQLSchema, - DocumentNode, - any?, - any?, - Maybe<{ [key: string]: any }>?, - Maybe?, - Maybe>?, - Maybe>? - ]; - export type SubscribeFunction = typeof subscribe; export type ParseFunction = typeof parse; @@ -70,4 +45,27 @@ export type ValidateFunctionParameter = { options?: Parameters[4]; }; +/** @private */ +export type PolymorphicExecuteArguments = + | [ExecutionArgs] + | [ + ExecutionArgs['schema'], + ExecutionArgs['document'], + ExecutionArgs['rootValue'], + ExecutionArgs['contextValue'], + ExecutionArgs['variableValues'], + ExecutionArgs['operationName'], + ExecutionArgs['fieldResolver'], + ExecutionArgs['typeResolver'] + ]; + +/** @private */ +export type PolymorphicSubscribeArguments = PolymorphicExecuteArguments; + export type Path = GraphQLResolveInfo['path']; + +export interface ExecutionResult, TExtensions = ObjMap> { + errors?: ReadonlyArray; + data?: TData | null; + extensions?: TExtensions; +} diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index 5b47c1a09e..eb6bd57315 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -1,13 +1,12 @@ import type { DocumentNode, - ExecutionArgs, - ExecutionResult, - GraphQLError, GraphQLResolveInfo, GraphQLSchema, ParseOptions, Source, SubscriptionArgs, + ExecutionArgs, + GraphQLError, ValidationRule, } from 'graphql'; import { Maybe, PromiseOrValue, AsyncIterableIteratorOrValue } from './utils.js'; @@ -18,6 +17,7 @@ import { ValidateFunction, ValidateFunctionParameter, SubscribeFunction, + ExecutionResult, } from './graphql.js'; import { Plugin } from './plugin.js'; diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts index 57920b749e..bd2af53904 100644 --- a/packages/types/src/utils.ts +++ b/packages/types/src/utils.ts @@ -29,3 +29,20 @@ export type ArbitraryObject = Record; export type PromiseOrValue = T | Promise; export type AsyncIterableIteratorOrValue = T | AsyncIterableIterator; export type Maybe = T | null | undefined; + +export interface ObjMap { + [key: string]: T; +} +export type ObjMapLike = + | ObjMap + | { + [key: string]: T; + }; +export interface ReadOnlyObjMap { + readonly [key: string]: T; +} +export type ReadOnlyObjMapLike = + | ReadOnlyObjMap + | { + readonly [key: string]: T; + }; From 871208e438aaf9e63ddfd96528319ca2337d9cd8 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Mon, 22 Aug 2022 16:04:36 -0400 Subject: [PATCH 04/61] remove grapqhl as peer dep from types pkg --- packages/core/test/validate.spec.ts | 2 +- .../plugins/graphql-middleware/src/index.ts | 1 - packages/plugins/newrelic/src/index.ts | 1 + packages/plugins/sentry/src/index.ts | 1 + .../plugins/validation-cache/src/index.ts | 1 + packages/types/package.json | 4 -- packages/types/src/get-enveloped.ts | 3 +- packages/types/src/graphql.ts | 7 +++- packages/types/src/hooks.ts | 38 +++++++------------ 9 files changed, 24 insertions(+), 34 deletions(-) diff --git a/packages/core/test/validate.spec.ts b/packages/core/test/validate.spec.ts index 6931df84d6..b5f0a99074 100644 --- a/packages/core/test/validate.spec.ts +++ b/packages/core/test/validate.spec.ts @@ -108,7 +108,7 @@ describe('validate', () => { [ { onValidate: ({ addValidationRule }) => { - addValidationRule(context => { + addValidationRule((context: any) => { context.reportError(new GraphQLError('Invalid!')); return {}; }); diff --git a/packages/plugins/graphql-middleware/src/index.ts b/packages/plugins/graphql-middleware/src/index.ts index ece38586bf..a2cc564a2f 100644 --- a/packages/plugins/graphql-middleware/src/index.ts +++ b/packages/plugins/graphql-middleware/src/index.ts @@ -8,7 +8,6 @@ export const useGraphQLMiddleware = ): Plugin => { return { onSchemaChange({ schema, replaceSchema }) { - // @ts-expect-error See https://github.com/graphql/graphql-js/pull/3511 - remove this comments once merged if (schema.extensions?.[graphqlMiddlewareAppliedTransformSymbol]) { return; } diff --git a/packages/plugins/newrelic/src/index.ts b/packages/plugins/newrelic/src/index.ts index e3ceeb4e19..bad64cda5c 100644 --- a/packages/plugins/newrelic/src/index.ts +++ b/packages/plugins/newrelic/src/index.ts @@ -87,6 +87,7 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => { const spanContext = instrumentationApi.agent.tracer.getSpanContext(); const delimiter = transactionNameState.delimiter; const rootOperation = args.document.definitions.find( + // @ts-expect-error TODO: not sure how we will make it dev friendly definitionNode => definitionNode.kind === Kind.OPERATION_DEFINITION ) as OperationDefinitionNode; const operationType = rootOperation.operation; diff --git a/packages/plugins/sentry/src/index.ts b/packages/plugins/sentry/src/index.ts index 3d3e9ca371..8c32ecc754 100644 --- a/packages/plugins/sentry/src/index.ts +++ b/packages/plugins/sentry/src/index.ts @@ -176,6 +176,7 @@ export const useSentry = (options: SentryPluginOptions = {}): Plugin => { } const rootOperation = args.document.definitions.find( + // @ts-expect-error TODO: not sure how we will make it dev friendly o => o.kind === Kind.OPERATION_DEFINITION ) as OperationDefinitionNode; const operationType = rootOperation.operation; diff --git a/packages/plugins/validation-cache/src/index.ts b/packages/plugins/validation-cache/src/index.ts index acd655b962..d0bec4d43a 100644 --- a/packages/plugins/validation-cache/src/index.ts +++ b/packages/plugins/validation-cache/src/index.ts @@ -59,6 +59,7 @@ export const useValidationCache = (pluginOptions: ValidationCacheOptions = {}): } return ({ result }) => { + // @ts-expect-error TODO: not sure how we will make it dev friendly resultCache.set(key, result); }; }, diff --git a/packages/types/package.json b/packages/types/package.json index 39978e0a48..d08fd50e3e 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -48,12 +48,8 @@ }, "dependencies": {}, "devDependencies": { - "graphql": "16.3.0", "typescript": "4.7.4" }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" - }, "buildOptions": { "input": "./src/index.ts" }, diff --git a/packages/types/src/get-enveloped.ts b/packages/types/src/get-enveloped.ts index 800f0a5cad..b1505b8551 100644 --- a/packages/types/src/get-enveloped.ts +++ b/packages/types/src/get-enveloped.ts @@ -1,5 +1,4 @@ import { Plugin } from './plugin.js'; -import { GraphQLSchema } from 'graphql'; import { ExecuteFunction, ParseFunction, SubscribeFunction, ValidateFunction } from './graphql.js'; import { ArbitraryObject, Spread, PromiseOrValue } from './utils.js'; export { ArbitraryObject } from './utils.js'; @@ -17,7 +16,7 @@ export type GetEnvelopedFn = { contextFactory: ( contextExtension?: ContextExtension ) => PromiseOrValue>; - schema: GraphQLSchema; + schema: any; }; _plugins: Plugin[]; }; diff --git a/packages/types/src/graphql.ts b/packages/types/src/graphql.ts index 61cc7f76f5..ef83bf5e46 100644 --- a/packages/types/src/graphql.ts +++ b/packages/types/src/graphql.ts @@ -1,4 +1,3 @@ -import type { GraphQLResolveInfo } from 'graphql'; import { ObjMap } from './utils.js'; export interface ExecutionArgs { schema: any; @@ -62,7 +61,11 @@ export type PolymorphicExecuteArguments = /** @private */ export type PolymorphicSubscribeArguments = PolymorphicExecuteArguments; -export type Path = GraphQLResolveInfo['path']; +export type Path = { + readonly prev: Path | undefined; + readonly key: string | number; + readonly typename: string | undefined; +}; export interface ExecutionResult, TExtensions = ObjMap> { errors?: ReadonlyArray; diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index eb6bd57315..a31e7f6d85 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -1,14 +1,3 @@ -import type { - DocumentNode, - GraphQLResolveInfo, - GraphQLSchema, - ParseOptions, - Source, - SubscriptionArgs, - ExecutionArgs, - GraphQLError, - ValidationRule, -} from 'graphql'; import { Maybe, PromiseOrValue, AsyncIterableIteratorOrValue } from './utils.js'; import { DefaultContext } from './context-types.js'; import { @@ -18,17 +7,18 @@ import { ValidateFunctionParameter, SubscribeFunction, ExecutionResult, + ExecutionArgs, } from './graphql.js'; import { Plugin } from './plugin.js'; export type DefaultArgs = Record; -export type SetSchemaFn = (newSchema: GraphQLSchema) => void; +export type SetSchemaFn = (newSchema: any) => void; /** * The payload forwarded to the onSchemaChange hook. */ -export type OnSchemaChangeEventPayload = { schema: GraphQLSchema; replaceSchema: SetSchemaFn }; +export type OnSchemaChangeEventPayload = { schema: any; replaceSchema: SetSchemaFn }; /** * Invoked each time the schema is changed via a setSchema call. @@ -107,7 +97,7 @@ export type OnParseEventPayload = { /** * The parameters that are passed to the parse call. */ - params: { source: string | Source; options?: ParseOptions }; + params: { source: string | any; options?: any }; /** * The current parse function */ @@ -120,7 +110,7 @@ export type OnParseEventPayload = { * Set/overwrite the parsed document. * If a parsed document is set the call to the parseFn will be skipped. */ - setParsedDocument: (doc: DocumentNode) => void; + setParsedDocument: (doc: any) => void; }; export type AfterParseEventPayload = { @@ -135,11 +125,11 @@ export type AfterParseEventPayload = { /** * The result of the parse phase. */ - result: DocumentNode | Error | null; + result: any | Error | null; /** * Replace the parse result with a new result. */ - replaceParseResult: (newResult: DocumentNode | Error) => void; + replaceParseResult: (newResult: any | Error) => void; }; /** @@ -171,7 +161,7 @@ export type OnValidateEventPayload = { /** * Register a validation rule that will be used for the validate invocation. */ - addValidationRule: (rule: ValidationRule) => void; + addValidationRule: (rule: any) => void; /** * The current validate function that will be invoked. */ @@ -183,7 +173,7 @@ export type OnValidateEventPayload = { /** * Set a validation error result and skip the validate invocation. */ - setResult: (errors: readonly GraphQLError[]) => void; + setResult: (errors: readonly any[]) => void; }; /** @@ -206,11 +196,11 @@ export type AfterValidateEventPayload = { * An array of errors that were raised during the validation phase. * The array is empty if no errors were raised. */ - result: readonly GraphQLError[]; + result: readonly Error[] | any[]; /** * Replace the current error result with a new one. */ - setResult: (errors: GraphQLError[]) => void; + setResult: (errors: Error[] | any[]) => void; }; /** @@ -276,7 +266,7 @@ export type ResolverFn PromiseOrValue; export type OnBeforeResolverCalledEventPayload< @@ -288,7 +278,7 @@ export type OnBeforeResolverCalledEventPayload< root: ParentType; args: ArgsType; context: ContextType; - info: GraphQLResolveInfo; + info: any; resolverFn: ResolverFn; replaceResolverFn: (newResolver: ResolverFn) => void; }; @@ -428,7 +418,7 @@ export type OnExecuteHook = ( /** * Subscription arguments with inferred context value type. */ -export type TypedSubscriptionArgs = Omit & { contextValue: ContextType }; +export type TypedSubscriptionArgs = Omit & { contextValue: ContextType }; /** * Payload with which the onSubscribe hook is invoked. From 732ec5ee80eef97425171e6446dc9b9c2781b68e Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Mon, 22 Aug 2022 16:20:06 -0400 Subject: [PATCH 05/61] make core more agnostic --- packages/core/src/create.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/core/src/create.ts b/packages/core/src/create.ts index 02706b94e6..8e31237e81 100644 --- a/packages/core/src/create.ts +++ b/packages/core/src/create.ts @@ -1,5 +1,13 @@ -import { GetEnvelopedFn, ComposeContext, Plugin, ArbitraryObject } from '@envelop/types'; -import type { execute, parse, subscribe, validate } from 'graphql'; +import { + GetEnvelopedFn, + ComposeContext, + Plugin, + ArbitraryObject, + ExecuteFunction, + SubscribeFunction, + ParseFunction, + ValidateFunction, +} from '@envelop/types'; import { isPluginEnabled, PluginOrDisabledPlugin } from './enable-if.js'; import { createEnvelopOrchestrator, EnvelopOrchestrator } from './orchestrator.js'; import { traceOrchestrator } from './traced-orchestrator.js'; @@ -7,10 +15,10 @@ import { traceOrchestrator } from './traced-orchestrator.js'; export function envelop[]>(options: { plugins: Array; enableInternalTracing?: boolean; - parse: typeof parse; - execute: typeof execute; - validate: typeof validate; - subscribe: typeof subscribe; + parse: ParseFunction; + execute: ExecuteFunction; + validate: ValidateFunction; + subscribe: SubscribeFunction; }): GetEnvelopedFn> { const plugins = options.plugins.filter(isPluginEnabled); let orchestrator = createEnvelopOrchestrator>({ From 4d09e1da85e5c96094fae205ffae506bc2e1550f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Aug 2022 20:23:18 +0000 Subject: [PATCH 06/61] chore(dependencies): updated changesets for modified dependencies --- .changeset/@envelop_types-1487-dependencies.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/@envelop_types-1487-dependencies.md diff --git a/.changeset/@envelop_types-1487-dependencies.md b/.changeset/@envelop_types-1487-dependencies.md new file mode 100644 index 0000000000..8832115daf --- /dev/null +++ b/.changeset/@envelop_types-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/types": patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) From 4f6fe63ce9ffda9c40dbdf2f16e1486d7474c058 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 11:26:23 -0400 Subject: [PATCH 07/61] remove traced schema --- packages/core/src/index.ts | 1 + packages/core/src/orchestrator.ts | 4 +- packages/core/test/execute.spec.ts | 102 ------------------ packages/plugins/apollo-tracing/package.json | 1 + packages/plugins/apollo-tracing/src/index.ts | 4 + .../apollo-tracing}/src/traced-schema.ts | 4 +- packages/plugins/opentelemetry/package.json | 1 + packages/plugins/opentelemetry/src/index.ts | 4 + .../opentelemetry/src/traced-schema.ts | 75 +++++++++++++ packages/plugins/prometheus/package.json | 1 + packages/plugins/prometheus/src/index.ts | 2 + .../plugins/prometheus/src/traced-schema.ts | 75 +++++++++++++ packages/plugins/rate-limiter/package.json | 1 + packages/plugins/rate-limiter/src/index.ts | 4 + .../plugins/rate-limiter/src/traced-schema.ts | 75 +++++++++++++ 15 files changed, 248 insertions(+), 106 deletions(-) rename packages/{core => plugins/apollo-tracing}/src/traced-schema.ts (94%) create mode 100644 packages/plugins/opentelemetry/src/traced-schema.ts create mode 100644 packages/plugins/prometheus/src/traced-schema.ts create mode 100644 packages/plugins/rate-limiter/src/traced-schema.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ffbb821aa6..05c59423cf 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -11,3 +11,4 @@ export * from './plugins/use-payload-formatter.js'; export * from './plugins/use-masked-errors.js'; export * from './plugins/use-immediate-introspection.js'; export * from './enable-if.js'; +export { resolversHooksSymbol } from './orchestrator.js'; diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 714c67676c..da4b2476d3 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -32,7 +32,6 @@ import { ValidateFunction, ExecutionResult, } from '@envelop/types'; -import { prepareTracedSchema, resolversHooksSymbol } from './traced-schema.js'; import { errorAsyncIterator, finalAsyncIterator, @@ -62,6 +61,7 @@ type EnvelopOrchestratorOptions = { subscribe: SubscribeFunction; validate: ValidateFunction; }; +export const resolversHooksSymbol = Symbol('RESOLVERS_HOOKS'); export function createEnvelopOrchestrator({ plugins, @@ -84,7 +84,7 @@ export function createEnvelopOrchestrator // here not to call the same plugin that initiated the schema switch. const replaceSchema = (newSchema: any, ignorePluginIndex = -1) => { if (onResolversHandlers.length) { - prepareTracedSchema(newSchema); + // prepareTracedSchema(newSchema); } schema = newSchema; diff --git a/packages/core/test/execute.spec.ts b/packages/core/test/execute.spec.ts index f8ac840845..7c5c0c7d31 100644 --- a/packages/core/test/execute.spec.ts +++ b/packages/core/test/execute.spec.ts @@ -46,47 +46,6 @@ function createDeferred(): Deferred { } describe('execute', () => { - it('Should wrap and trigger events correctly', async () => { - const spiedPlugin = createSpiedPlugin(); - const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query, {}, { test: 1 }); - expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledTimes(1); - expect(spiedPlugin.spies.beforeResolver).toHaveBeenCalledTimes(3); - expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledWith({ - executeFn: expect.any(Function), - setExecuteFn: expect.any(Function), - extendContext: expect.any(Function), - setResultAndStopExecution: expect.any(Function), - args: { - contextValue: expect.objectContaining({ test: 1 }), - rootValue: {}, - schema: expect.any(GraphQLSchema), - operationName: undefined, - fieldResolver: undefined, - typeResolver: undefined, - variableValues: {}, - document: expect.objectContaining({ - definitions: expect.any(Array), - }), - }, - }); - - expect(spiedPlugin.spies.afterResolver).toHaveBeenCalledTimes(3); - expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledTimes(1); - expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledWith({ - args: expect.any(Object), - setResult: expect.any(Function), - result: { - data: { - me: { - id: '1', - name: 'Dotan Simha', - }, - }, - }, - }); - }); - it('Should allow to override execute function', async () => { const altExecute = jest.fn(execute); const teskit = createTestkit( @@ -210,67 +169,6 @@ describe('execute', () => { }); }); - it('Should allow to register to before and after resolver calls', async () => { - const afterResolver = jest.fn(); - const onResolverCalled = jest.fn(() => afterResolver); - - const teskit = createTestkit( - [ - { - onResolverCalled, - }, - ], - schema - ); - - await teskit.execute(query); - expect(onResolverCalled).toHaveBeenCalledTimes(3); - expect(onResolverCalled).toHaveBeenCalledWith({ - root: {}, - args: {}, - context: expect.any(Object), - info: expect.objectContaining({ - fieldName: 'me', - }), - resolverFn: expect.any(Function), - replaceResolverFn: expect.any(Function), - }); - expect(onResolverCalled).toHaveBeenCalledWith({ - root: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, - args: {}, - context: expect.any(Object), - info: expect.objectContaining({ - fieldName: 'id', - }), - resolverFn: expect.any(Function), - replaceResolverFn: expect.any(Function), - }); - expect(onResolverCalled).toHaveBeenCalledWith({ - root: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, - args: {}, - context: expect.any(Object), - info: expect.objectContaining({ - fieldName: 'name', - }), - resolverFn: expect.any(Function), - replaceResolverFn: expect.any(Function), - }); - - expect(afterResolver).toHaveBeenCalledTimes(3); - expect(afterResolver).toHaveBeenCalledWith({ - result: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, - setResult: expect.any(Function), - }); - expect(afterResolver).toHaveBeenCalledWith({ - result: 1, - setResult: expect.any(Function), - }); - expect(afterResolver).toHaveBeenCalledWith({ - result: 'Dotan Simha', - setResult: expect.any(Function), - }); - }); - it('Should be able to manipulate streams', async () => { const streamExecuteFn = async function* () { for (const value of ['a', 'b', 'c', 'd']) { diff --git a/packages/plugins/apollo-tracing/package.json b/packages/plugins/apollo-tracing/package.json index 42b4284971..56cecb8e8f 100644 --- a/packages/plugins/apollo-tracing/package.json +++ b/packages/plugins/apollo-tracing/package.json @@ -55,6 +55,7 @@ "typescript": "4.7.4" }, "peerDependencies": { + "@envelop/types": "^2.3.1", "@envelop/core": "^2.5.0", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, diff --git a/packages/plugins/apollo-tracing/src/index.ts b/packages/plugins/apollo-tracing/src/index.ts index f6d7a3c0a9..215a0a21c2 100644 --- a/packages/plugins/apollo-tracing/src/index.ts +++ b/packages/plugins/apollo-tracing/src/index.ts @@ -1,6 +1,7 @@ import { Plugin, handleStreamOrSingleExecutionResult } from '@envelop/core'; import { TracingFormat } from 'apollo-tracing'; import { GraphQLType, ResponsePath, responsePathAsArray } from 'graphql'; +import { prepareTracedSchema } from './traced-schema.js'; const HR_TO_NS = 1e9; const NS_TO_MS = 1e6; @@ -41,6 +42,9 @@ type TracingContextObject = { export const useApolloTracing = (): Plugin => { return { + onSchemaChange: ({ schema }) => { + prepareTracedSchema(schema); + }, onResolverCalled: ({ info, context }) => { const ctx = context[apolloTracingSymbol] as TracingContextObject; // Taken from https://github.com/apollographql/apollo-server/blob/main/packages/apollo-tracing/src/index.ts diff --git a/packages/core/src/traced-schema.ts b/packages/plugins/apollo-tracing/src/traced-schema.ts similarity index 94% rename from packages/core/src/traced-schema.ts rename to packages/plugins/apollo-tracing/src/traced-schema.ts index 9de218dadd..3e33a07888 100644 --- a/packages/core/src/traced-schema.ts +++ b/packages/plugins/apollo-tracing/src/traced-schema.ts @@ -1,8 +1,8 @@ import { defaultFieldResolver, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; +import { resolversHooksSymbol } from '@envelop/core'; -export const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); -export const resolversHooksSymbol = Symbol('RESOLVERS_HOOKS'); +const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): void { if (!schema || schema[trackedSchemaSymbol]) { diff --git a/packages/plugins/opentelemetry/package.json b/packages/plugins/opentelemetry/package.json index 722fd3c449..ad41dca4b1 100644 --- a/packages/plugins/opentelemetry/package.json +++ b/packages/plugins/opentelemetry/package.json @@ -55,6 +55,7 @@ "typescript": "4.7.4" }, "peerDependencies": { + "@envelop/types": "^2.3.1", "@envelop/core": "^2.5.0", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, diff --git a/packages/plugins/opentelemetry/src/index.ts b/packages/plugins/opentelemetry/src/index.ts index 2e2a6b56fb..77ba73d8ca 100644 --- a/packages/plugins/opentelemetry/src/index.ts +++ b/packages/plugins/opentelemetry/src/index.ts @@ -3,6 +3,7 @@ import { SpanAttributes, SpanKind } from '@opentelemetry/api'; import * as opentelemetry from '@opentelemetry/api'; import { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; import { print } from 'graphql'; +import { prepareTracedSchema } from './traced-schema.js'; export enum AttributeName { EXECUTION_ERROR = 'graphql.execute.error', @@ -45,6 +46,9 @@ export const useOpenTelemetry = ( const tracer = tracingProvider.getTracer(serviceName); return { + onSchemaChange: async ({ schema }) => { + prepareTracedSchema(schema); + }, onResolverCalled: options.resolvers ? ({ info, context, args }) => { if (context && typeof context === 'object' && context[tracingSpanSymbol]) { diff --git a/packages/plugins/opentelemetry/src/traced-schema.ts b/packages/plugins/opentelemetry/src/traced-schema.ts new file mode 100644 index 0000000000..3e33a07888 --- /dev/null +++ b/packages/plugins/opentelemetry/src/traced-schema.ts @@ -0,0 +1,75 @@ +import { defaultFieldResolver, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; +import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; +import { resolversHooksSymbol } from '@envelop/core'; + +const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); + +export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): void { + if (!schema || schema[trackedSchemaSymbol]) { + return; + } + + schema[trackedSchemaSymbol] = true; + const entries = Object.values(schema.getTypeMap()); + + for (const type of entries) { + if (!isIntrospectionType(type) && isObjectType(type)) { + const fields = Object.values(type.getFields()); + + for (const field of fields) { + let resolverFn: ResolverFn = (field.resolve || defaultFieldResolver) as ResolverFn; + + field.resolve = async (root, args, context, info) => { + if (context && context[resolversHooksSymbol]) { + const hooks: OnResolverCalledHook[] = context[resolversHooksSymbol]; + const afterCalls: AfterResolverHook[] = []; + + for (const hook of hooks) { + const afterFn = await hook({ + root, + args, + context, + info, + resolverFn, + replaceResolverFn: newFn => { + resolverFn = newFn as ResolverFn; + }, + }); + afterFn && afterCalls.push(afterFn); + } + + try { + let result = await resolverFn(root, args, context, info); + + for (const afterFn of afterCalls) { + afterFn({ + result, + setResult: newResult => { + result = newResult; + }, + }); + } + + return result; + } catch (e) { + let resultErr = e; + + for (const afterFn of afterCalls) { + afterFn({ + result: resultErr, + setResult: newResult => { + resultErr = newResult; + }, + }); + } + + throw resultErr; + } + } else { + return resolverFn(root, args, context, info); + } + }; + } + } + } +} diff --git a/packages/plugins/prometheus/package.json b/packages/plugins/prometheus/package.json index 70929f5659..10a61674b9 100644 --- a/packages/plugins/prometheus/package.json +++ b/packages/plugins/prometheus/package.json @@ -54,6 +54,7 @@ }, "peerDependencies": { "@envelop/core": "^2.5.0", + "@envelop/types": "^2.3.1", "prom-client": "^13 || ^14.0.0", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, diff --git a/packages/plugins/prometheus/src/index.ts b/packages/plugins/prometheus/src/index.ts index 97c7faee0e..f0e82db7d4 100644 --- a/packages/plugins/prometheus/src/index.ts +++ b/packages/plugins/prometheus/src/index.ts @@ -22,6 +22,7 @@ import { createSummary, } from './utils.js'; import { PrometheusTracingPluginConfig } from './config.js'; +import { prepareTracedSchema } from './traced-schema.js'; export { PrometheusTracingPluginConfig, createCounter, createHistogram, createSummary, FillLabelsFnParams }; @@ -350,6 +351,7 @@ export const usePrometheus = (config: PrometheusTracingPluginConfig = {}): Plugi }); }, onSchemaChange({ schema }) { + prepareTracedSchema(schema); typeInfo = new TypeInfo(schema); }, onParse, diff --git a/packages/plugins/prometheus/src/traced-schema.ts b/packages/plugins/prometheus/src/traced-schema.ts new file mode 100644 index 0000000000..3e33a07888 --- /dev/null +++ b/packages/plugins/prometheus/src/traced-schema.ts @@ -0,0 +1,75 @@ +import { defaultFieldResolver, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; +import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; +import { resolversHooksSymbol } from '@envelop/core'; + +const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); + +export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): void { + if (!schema || schema[trackedSchemaSymbol]) { + return; + } + + schema[trackedSchemaSymbol] = true; + const entries = Object.values(schema.getTypeMap()); + + for (const type of entries) { + if (!isIntrospectionType(type) && isObjectType(type)) { + const fields = Object.values(type.getFields()); + + for (const field of fields) { + let resolverFn: ResolverFn = (field.resolve || defaultFieldResolver) as ResolverFn; + + field.resolve = async (root, args, context, info) => { + if (context && context[resolversHooksSymbol]) { + const hooks: OnResolverCalledHook[] = context[resolversHooksSymbol]; + const afterCalls: AfterResolverHook[] = []; + + for (const hook of hooks) { + const afterFn = await hook({ + root, + args, + context, + info, + resolverFn, + replaceResolverFn: newFn => { + resolverFn = newFn as ResolverFn; + }, + }); + afterFn && afterCalls.push(afterFn); + } + + try { + let result = await resolverFn(root, args, context, info); + + for (const afterFn of afterCalls) { + afterFn({ + result, + setResult: newResult => { + result = newResult; + }, + }); + } + + return result; + } catch (e) { + let resultErr = e; + + for (const afterFn of afterCalls) { + afterFn({ + result: resultErr, + setResult: newResult => { + resultErr = newResult; + }, + }); + } + + throw resultErr; + } + } else { + return resolverFn(root, args, context, info); + } + }; + } + } + } +} diff --git a/packages/plugins/rate-limiter/package.json b/packages/plugins/rate-limiter/package.json index fe04254fd5..2d96985f07 100644 --- a/packages/plugins/rate-limiter/package.json +++ b/packages/plugins/rate-limiter/package.json @@ -54,6 +54,7 @@ "typescript": "4.7.4" }, "peerDependencies": { + "@envelop/types": "^2.3.1", "@envelop/core": "^2.5.0", "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, diff --git a/packages/plugins/rate-limiter/src/index.ts b/packages/plugins/rate-limiter/src/index.ts index 25dac08c4c..b27e799f32 100644 --- a/packages/plugins/rate-limiter/src/index.ts +++ b/packages/plugins/rate-limiter/src/index.ts @@ -2,6 +2,7 @@ import { Plugin } from '@envelop/core'; import { IntValueNode, StringValueNode, GraphQLResolveInfo } from 'graphql'; import { getDirective } from './utils.js'; import { getGraphQLRateLimiter } from 'graphql-rate-limit'; +import { prepareTracedSchema } from './traced-schema.js'; export * from './utils.js'; export class UnauthenticatedError extends Error {} @@ -27,6 +28,9 @@ export const useRateLimiter = ( const rateLimiterFn = getGraphQLRateLimiter({ identifyContext: options.identifyFn }); return { + onSchemaChange: ({ schema }) => { + prepareTracedSchema(schema); + }, async onContextBuilding({ extendContext }) { extendContext({ rateLimiterFn, diff --git a/packages/plugins/rate-limiter/src/traced-schema.ts b/packages/plugins/rate-limiter/src/traced-schema.ts new file mode 100644 index 0000000000..3e33a07888 --- /dev/null +++ b/packages/plugins/rate-limiter/src/traced-schema.ts @@ -0,0 +1,75 @@ +import { defaultFieldResolver, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; +import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; +import { resolversHooksSymbol } from '@envelop/core'; + +const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); + +export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): void { + if (!schema || schema[trackedSchemaSymbol]) { + return; + } + + schema[trackedSchemaSymbol] = true; + const entries = Object.values(schema.getTypeMap()); + + for (const type of entries) { + if (!isIntrospectionType(type) && isObjectType(type)) { + const fields = Object.values(type.getFields()); + + for (const field of fields) { + let resolverFn: ResolverFn = (field.resolve || defaultFieldResolver) as ResolverFn; + + field.resolve = async (root, args, context, info) => { + if (context && context[resolversHooksSymbol]) { + const hooks: OnResolverCalledHook[] = context[resolversHooksSymbol]; + const afterCalls: AfterResolverHook[] = []; + + for (const hook of hooks) { + const afterFn = await hook({ + root, + args, + context, + info, + resolverFn, + replaceResolverFn: newFn => { + resolverFn = newFn as ResolverFn; + }, + }); + afterFn && afterCalls.push(afterFn); + } + + try { + let result = await resolverFn(root, args, context, info); + + for (const afterFn of afterCalls) { + afterFn({ + result, + setResult: newResult => { + result = newResult; + }, + }); + } + + return result; + } catch (e) { + let resultErr = e; + + for (const afterFn of afterCalls) { + afterFn({ + result: resultErr, + setResult: newResult => { + resultErr = newResult; + }, + }); + } + + throw resultErr; + } + } else { + return resolverFn(root, args, context, info); + } + }; + } + } + } +} From dd616a17123cf6d58465b58bc58d918fd8b03474 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 11:55:46 -0400 Subject: [PATCH 08/61] remove traced orchestrator --- packages/core/src/create.ts | 7 +- packages/core/src/traced-orchestrator.ts | 176 ----------------------- 2 files changed, 1 insertion(+), 182 deletions(-) delete mode 100644 packages/core/src/traced-orchestrator.ts diff --git a/packages/core/src/create.ts b/packages/core/src/create.ts index 8e31237e81..6740a5ceb1 100644 --- a/packages/core/src/create.ts +++ b/packages/core/src/create.ts @@ -10,7 +10,6 @@ import { } from '@envelop/types'; import { isPluginEnabled, PluginOrDisabledPlugin } from './enable-if.js'; import { createEnvelopOrchestrator, EnvelopOrchestrator } from './orchestrator.js'; -import { traceOrchestrator } from './traced-orchestrator.js'; export function envelop[]>(options: { plugins: Array; @@ -21,7 +20,7 @@ export function envelop[]>(options: { subscribe: SubscribeFunction; }): GetEnvelopedFn> { const plugins = options.plugins.filter(isPluginEnabled); - let orchestrator = createEnvelopOrchestrator>({ + const orchestrator = createEnvelopOrchestrator>({ plugins, parse: options.parse, execute: options.execute, @@ -29,10 +28,6 @@ export function envelop[]>(options: { subscribe: options.subscribe, }); - if (options.enableInternalTracing) { - orchestrator = traceOrchestrator(orchestrator); - } - const getEnveloped = ( initialContext: TInitialContext = {} as TInitialContext ) => { diff --git a/packages/core/src/traced-orchestrator.ts b/packages/core/src/traced-orchestrator.ts deleted file mode 100644 index a3ca1b056d..0000000000 --- a/packages/core/src/traced-orchestrator.ts +++ /dev/null @@ -1,176 +0,0 @@ -import { - DocumentNode, - ExecutionArgs, - GraphQLFieldResolver, - GraphQLSchema, - GraphQLTypeResolver, - SubscriptionArgs, -} from 'graphql'; -import { ArbitraryObject, Maybe } from '@envelop/types'; -import { EnvelopOrchestrator } from './orchestrator.js'; -import { isAsyncIterable } from './utils.js'; - -const getTimestamp = - typeof globalThis !== 'undefined' && globalThis?.performance?.now - ? () => globalThis.performance.now() - : () => Date.now(); - -const measure = () => { - const start = getTimestamp(); - return () => { - const end = getTimestamp(); - return end - start; - }; -}; - -const tracingSymbol = Symbol('envelopTracing'); - -export function traceOrchestrator( - orchestrator: EnvelopOrchestrator -): EnvelopOrchestrator { - const createTracer = (name: string, ctx: Record) => { - const end = measure(); - - return () => { - ctx[tracingSymbol][name] = end(); - }; - }; - - return { - ...orchestrator, - init: (ctx = {} as TInitialContext) => { - ctx![tracingSymbol] = ctx![tracingSymbol] || {}; - const done = createTracer('init', ctx || {}); - - try { - return orchestrator.init(ctx); - } finally { - done(); - } - }, - parse: (ctx = {} as TInitialContext) => { - ctx[tracingSymbol] = ctx[tracingSymbol] || {}; - const actualFn = orchestrator.parse(ctx); - - return (...args) => { - const done = createTracer('parse', ctx); - - try { - return actualFn(...args); - } finally { - done(); - } - }; - }, - validate: (ctx = {} as TInitialContext) => { - ctx[tracingSymbol] = ctx[tracingSymbol] || {}; - const actualFn = orchestrator.validate(ctx); - - return (...args) => { - const done = createTracer('validate', ctx); - - try { - return actualFn(...args); - } finally { - done(); - } - }; - }, - execute: async ( - argsOrSchema: ExecutionArgs | GraphQLSchema, - document?: DocumentNode, - rootValue?: any, - contextValue?: any, - variableValues?: Maybe<{ [key: string]: any }>, - operationName?: Maybe, - fieldResolver?: Maybe>, - typeResolver?: Maybe> - ) => { - const args: ExecutionArgs = - argsOrSchema instanceof GraphQLSchema - ? { - schema: argsOrSchema, - document: document!, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - typeResolver, - } - : argsOrSchema; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore GraphQL.js types contextValue as unknown - const done = createTracer('execute', args.contextValue || {}); - - try { - const result = await orchestrator.execute(args); - done(); - - if (!isAsyncIterable(result)) { - result.extensions = result.extensions || {}; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore GraphQL.js types contextValue as unknown - result.extensions.envelopTracing = args.contextValue[tracingSymbol]; - } else { - // eslint-disable-next-line no-console - console.warn( - `"traceOrchestrator" encountered a AsyncIterator which is not supported yet, so tracing data is not available for the operation.` - ); - } - - return result; - } catch (e) { - done(); - - throw e; - } - }, - subscribe: async ( - argsOrSchema: SubscriptionArgs | GraphQLSchema, - document?: DocumentNode, - rootValue?: any, - contextValue?: any, - variableValues?: Maybe<{ [key: string]: any }>, - operationName?: Maybe, - fieldResolver?: Maybe>, - subscribeFieldResolver?: Maybe> - ) => { - const args: SubscriptionArgs = - argsOrSchema instanceof GraphQLSchema - ? { - schema: argsOrSchema, - document: document!, - rootValue, - contextValue, - variableValues, - operationName, - fieldResolver, - subscribeFieldResolver, - } - : argsOrSchema; - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore GraphQL.js types contextValue as unknown - const done = createTracer('subscribe', args.contextValue || {}); - - try { - return await orchestrator.subscribe(args); - } finally { - done(); - } - }, - contextFactory: (ctx = {} as TInitialContext) => { - const actualFn = orchestrator.contextFactory(ctx); - - return async childCtx => { - const done = createTracer('contextFactory', ctx); - - try { - return await actualFn(childCtx); - } finally { - done(); - } - }; - }, - }; -} From 1244549705a89fd75f3953c3f0c0ddf0ee5fc56b Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 12:03:46 -0400 Subject: [PATCH 09/61] make plugin agnostic --- packages/core/src/plugins/use-error-handler.ts | 5 ++--- packages/core/src/plugins/use-schema.ts | 7 +++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/core/src/plugins/use-error-handler.ts b/packages/core/src/plugins/use-error-handler.ts index 2ddcfbf864..7c5e29d9a7 100644 --- a/packages/core/src/plugins/use-error-handler.ts +++ b/packages/core/src/plugins/use-error-handler.ts @@ -1,8 +1,7 @@ -import { Plugin, DefaultContext, TypedExecutionArgs } from '@envelop/types'; -import { ExecutionResult, GraphQLError } from 'graphql'; +import { Plugin, DefaultContext, TypedExecutionArgs, ExecutionResult } from '@envelop/types'; import { handleStreamOrSingleExecutionResult } from '../utils.js'; -export type ErrorHandler = (errors: readonly GraphQLError[], context: Readonly) => void; +export type ErrorHandler = (errors: readonly Error[] | any[], context: Readonly) => void; type ErrorHandlerCallback = { result: ExecutionResult; diff --git a/packages/core/src/plugins/use-schema.ts b/packages/core/src/plugins/use-schema.ts index 61b3259dec..aab02040ab 100644 --- a/packages/core/src/plugins/use-schema.ts +++ b/packages/core/src/plugins/use-schema.ts @@ -1,7 +1,6 @@ -import { GraphQLSchema } from 'graphql'; import { DefaultContext, Maybe, Plugin } from '@envelop/types'; -export const useSchema = (schema: GraphQLSchema): Plugin => { +export const useSchema = (schema: any): Plugin => { return { onPluginInit({ setSchema }) { setSchema(schema); @@ -9,7 +8,7 @@ export const useSchema = (schema: GraphQLSchema): Plugin => { }; }; -export const useLazyLoadedSchema = (schemaLoader: (context: Maybe) => GraphQLSchema): Plugin => { +export const useLazyLoadedSchema = (schemaLoader: (context: Maybe) => any): Plugin => { return { onEnveloped({ setSchema, context }) { setSchema(schemaLoader(context)); @@ -17,7 +16,7 @@ export const useLazyLoadedSchema = (schemaLoader: (context: Maybe): Plugin => { +export const useAsyncSchema = (schemaPromise: Promise): Plugin => { return { onPluginInit({ setSchema }) { schemaPromise.then(schemaObj => { From 28bfd9285889e77d3a090c78f3ef934e93cf6ee3 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 12:09:51 -0400 Subject: [PATCH 10/61] drop EnvelopError --- packages/core/src/index.ts | 1 - .../core/src/plugins/use-masked-errors.ts | 138 ----- packages/core/test/context.spec.ts | 6 +- .../test/plugins/use-masked-errors.spec.ts | 538 ------------------ packages/plugins/auth0/src/index.ts | 6 +- packages/plugins/newrelic/src/index.ts | 10 +- .../operation-field-permissions/src/index.ts | 6 +- .../test/use-operation-permissions.spec.ts | 21 - packages/plugins/sentry/src/index.ts | 5 +- 9 files changed, 14 insertions(+), 717 deletions(-) delete mode 100644 packages/core/src/plugins/use-masked-errors.ts delete mode 100644 packages/core/test/plugins/use-masked-errors.spec.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 05c59423cf..227891bcd3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,7 +8,6 @@ export * from './plugins/use-schema.js'; export * from './plugins/use-error-handler.js'; export * from './plugins/use-extend-context.js'; export * from './plugins/use-payload-formatter.js'; -export * from './plugins/use-masked-errors.js'; export * from './plugins/use-immediate-introspection.js'; export * from './enable-if.js'; export { resolversHooksSymbol } from './orchestrator.js'; diff --git a/packages/core/src/plugins/use-masked-errors.ts b/packages/core/src/plugins/use-masked-errors.ts deleted file mode 100644 index 2328ae1bc5..0000000000 --- a/packages/core/src/plugins/use-masked-errors.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { Plugin } from '@envelop/types'; -import { ExecutionResult, GraphQLError, GraphQLErrorExtensions } from 'graphql'; -import { handleStreamOrSingleExecutionResult } from '../utils.js'; - -export const DEFAULT_ERROR_MESSAGE = 'Unexpected error.'; - -export class EnvelopError extends GraphQLError { - constructor(message: string, extensions?: GraphQLErrorExtensions) { - super(message, undefined, undefined, undefined, undefined, undefined, extensions); - } -} - -export type FormatErrorHandler = (error: GraphQLError | unknown, message: string, isDev: boolean) => GraphQLError; - -export const formatError: FormatErrorHandler = (err, message, isDev) => { - if (err instanceof GraphQLError) { - if ( - /** execution error */ - (err.originalError && err.originalError instanceof EnvelopError === false) || - /** validate and parse errors */ - (err.originalError === undefined && err instanceof EnvelopError === false) - ) { - return new GraphQLError( - message, - err.nodes, - err.source, - err.positions, - err.path, - undefined, - isDev - ? { - originalError: { - message: err.originalError?.message ?? err.message, - stack: err.originalError?.stack ?? err.stack, - }, - } - : undefined - ); - } - return err; - } - return new GraphQLError(message); -}; - -export type UseMaskedErrorsOpts = { - /** The function used for format/identify errors. */ - formatError?: FormatErrorHandler; - /** The error message that shall be used for masked errors. */ - errorMessage?: string; - /** - * Additional information that is forwarded to the `formatError` function. - * The default value is `process.env['NODE_ENV'] === 'development'` - */ - isDev?: boolean; - /** - * Whether parse errors should be processed by this plugin. - * In general it is not recommend to set this flag to `true` - * as a `parse` error contains useful information for debugging a GraphQL operation. - * A `parse` error never contains any sensitive information. - * @default false - */ - handleParseErrors?: boolean; - /** - * Whether validation errors should processed by this plugin. - * In general we recommend against setting this flag to `true` - * as a `validate` error contains useful information for debugging a GraphQL operation. - * A `validate` error contains "did you mean x" suggestions that make it easier - * to reverse-introspect a GraphQL schema whose introspection capabilities got disabled. - * Instead of disabling introspection and masking validation errors, using persisted operations - * is a safer solution for avoiding the execution of unwanted/arbitrary operations. - * @default false - */ - handleValidationErrors?: boolean; -}; - -const makeHandleResult = - (format: FormatErrorHandler, message: string, isDev: boolean) => - ({ result, setResult }: { result: ExecutionResult; setResult: (result: ExecutionResult) => void }) => { - if (result.errors != null) { - setResult({ ...result, errors: result.errors.map(error => format(error, message, isDev)) }); - } - }; - -export const useMaskedErrors = (opts?: UseMaskedErrorsOpts): Plugin => { - const format = opts?.formatError ?? formatError; - const message = opts?.errorMessage || DEFAULT_ERROR_MESSAGE; - // eslint-disable-next-line dot-notation - const isDev = opts?.isDev ?? (typeof process !== 'undefined' ? process.env['NODE_ENV'] === 'development' : false); - const handleResult = makeHandleResult(format, message, isDev); - - return { - onParse: - opts?.handleParseErrors === true - ? function onParse() { - return function onParseEnd({ result, replaceParseResult }) { - if (result instanceof Error) { - replaceParseResult(format(result, message, isDev)); - } - }; - } - : undefined, - onValidate: - opts?.handleValidationErrors === true - ? function onValidate() { - return function onValidateEnd({ valid, result, setResult }) { - if (valid === false) { - setResult(result.map(error => format(error, message, isDev))); - } - }; - } - : undefined, - onPluginInit(context) { - context.registerContextErrorHandler(({ error, setError }) => { - if (error instanceof GraphQLError === false && error instanceof Error) { - error = new GraphQLError(error.message, undefined, undefined, undefined, undefined, error); - } - setError(format(error, message, isDev)); - }); - }, - onExecute() { - return { - onExecuteDone(payload) { - return handleStreamOrSingleExecutionResult(payload, handleResult); - }, - }; - }, - onSubscribe() { - return { - onSubscribeResult(payload) { - return handleStreamOrSingleExecutionResult(payload, handleResult); - }, - onSubscribeError({ error, setError }) { - setError(format(error, message, isDev)); - }, - }; - }, - }; -}; diff --git a/packages/core/test/context.spec.ts b/packages/core/test/context.spec.ts index 77fae79514..740f6f75c8 100644 --- a/packages/core/test/context.spec.ts +++ b/packages/core/test/context.spec.ts @@ -1,4 +1,4 @@ -import { ContextFactoryFn, EnvelopError, useExtendContext } from '@envelop/core'; +import { ContextFactoryFn, useExtendContext } from '@envelop/core'; import { createSpiedPlugin, createTestkit } from '@envelop/testing'; import { schema, query } from './common.js'; @@ -122,7 +122,7 @@ describe('contextFactory', () => { }; const throwingContextFactory: ContextFactoryFn = () => { - throw new EnvelopError('The server was about to step on a turtle'); + throw new Error('The server was about to step on a turtle'); }; const teskit = createTestkit( @@ -155,7 +155,7 @@ describe('contextFactory', () => { test: true, variables: expect.any(Object), }), - error: new EnvelopError('The server was about to step on a turtle'), + error: new Error('The server was about to step on a turtle'), setError: expect.any(Function), }) ); diff --git a/packages/core/test/plugins/use-masked-errors.spec.ts b/packages/core/test/plugins/use-masked-errors.spec.ts deleted file mode 100644 index dd2e9774d3..0000000000 --- a/packages/core/test/plugins/use-masked-errors.spec.ts +++ /dev/null @@ -1,538 +0,0 @@ -import { makeExecutableSchema } from '@graphql-tools/schema'; -import { - assertSingleExecutionValue, - assertStreamExecutionValue, - collectAsyncIteratorValues, - createTestkit, -} from '@envelop/testing'; -import { - EnvelopError, - useMaskedErrors, - DEFAULT_ERROR_MESSAGE, - formatError, - FormatErrorHandler, -} from '../../src/plugins/use-masked-errors.js'; -import { Plugin, useExtendContext } from '@envelop/core'; -import { useAuth0 } from '../../../plugins/auth0/src/index.js'; -import { GraphQLError } from 'graphql'; - -describe('useMaskedErrors', () => { - const schema = makeExecutableSchema({ - typeDefs: /* GraphQL */ ` - type Query { - secret: String! - secretEnvelop: String! - secretWithExtensions: String! - } - type Subscription { - instantError: String - streamError: String - streamResolveError: String - instantEnvelopError: String - streamEnvelopError: String - streamResolveEnvelopError: String - } - `, - resolvers: { - Query: { - secret: () => { - throw new Error('Secret sauce that should not leak.'); - }, - secretEnvelop: () => { - throw new EnvelopError('This message goes to all the clients out there!', { foo: 1 }); - }, - secretWithExtensions: () => { - throw new EnvelopError('This message goes to all the clients out there!', { - code: 'Foo', - message: 'Bar', - }); - }, - }, - Subscription: { - instantError: { - subscribe: async function () { - throw new Error('Noop'); - }, - resolve: _ => _, - }, - streamError: { - subscribe: async function* () { - throw new Error('Noop'); - }, - resolve: _ => _, - }, - streamResolveError: { - subscribe: async function* () { - yield '1'; - }, - resolve: _ => { - throw new Error('Noop'); - }, - }, - instantEnvelopError: { - subscribe: async function () { - throw new EnvelopError('Noop'); - }, - resolve: _ => _, - }, - streamEnvelopError: { - subscribe: async function* () { - throw new EnvelopError('Noop'); - }, - resolve: _ => _, - }, - streamResolveEnvelopError: { - subscribe: async function* () { - yield '1'; - }, - resolve: _ => { - throw new EnvelopError('Noop'); - }, - }, - }, - }, - }); - - it('Should mask non EnvelopErrors', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { secret }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); - }); - - it('Should not mask expected errors', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { secretEnvelop }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.message).toEqual('This message goes to all the clients out there!'); - expect(error.extensions).toEqual({ foo: 1 }); - }); - - it('Should include the original error within the error extensions when `isDev` is set to `true`', async () => { - const testInstance = createTestkit([useMaskedErrors({ isDev: true })], schema); - const result = await testInstance.execute(`query { secret }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.extensions).toEqual({ - originalError: { - message: 'Secret sauce that should not leak.', - stack: expect.stringContaining('Error: Secret sauce that should not leak.'), - }, - }); - }); - - it('Should not include the original error within the error extensions when `isDev` is set to `false`', async () => { - const testInstance = createTestkit([useMaskedErrors({ isDev: false })], schema); - const result = await testInstance.execute(`query { secret }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.extensions).toEqual({}); - }); - - it('Should not mask GraphQL operation syntax errors (of course it does not since we are only hooking in after execute, but just to be sure)', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { idonotexist }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.message).toEqual('Cannot query field "idonotexist" on type "Query".'); - }); - - it('Should forward extensions from EnvelopError to final GraphQLError in errors array', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { secretWithExtensions }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.extensions).toEqual({ - code: 'Foo', - message: 'Bar', - }); - }); - - it('Should properly mask context creation errors with a custom error message', async () => { - expect.assertions(1); - const testInstance = createTestkit( - [ - useExtendContext((): {} => { - throw new Error('No context for you!'); - }), - useMaskedErrors({ errorMessage: 'My Custom Error Message.' }), - ], - schema - ); - try { - await testInstance.execute(`query { secretWithExtensions }`); - } catch (err) { - expect(err).toMatchInlineSnapshot(`[GraphQLError: My Custom Error Message.]`); - } - }); - it('Should properly mask context creation errors', async () => { - expect.assertions(1); - const testInstance = createTestkit( - [ - useExtendContext((): {} => { - throw new Error('No context for you!'); - }), - useMaskedErrors(), - ], - schema - ); - try { - await testInstance.execute(`query { secretWithExtensions }`); - } catch (err: any) { - expect(err.message).toEqual(DEFAULT_ERROR_MESSAGE); - } - }); - - it('Should not mask expected context creation errors', async () => { - expect.assertions(2); - const testInstance = createTestkit( - [ - useExtendContext((): {} => { - throw new EnvelopError('No context for you!', { foo: 1 }); - }), - useMaskedErrors(), - ], - schema - ); - try { - await testInstance.execute(`query { secretWithExtensions }`); - } catch (err) { - if (err instanceof EnvelopError) { - expect(err.message).toEqual(`No context for you!`); - expect(err.extensions).toEqual({ foo: 1 }); - } else { - throw err; - } - } - }); - it('Should include the original context error in extensions in dev mode for error thrown during context creation.', async () => { - expect.assertions(3); - const testInstance = createTestkit( - [ - useExtendContext((): {} => { - throw new Error('No context for you!'); - }), - useMaskedErrors({ isDev: true }), - ], - schema - ); - try { - await testInstance.execute(`query { secretWithExtensions }`); - } catch (err: any) { - expect(err).toBeInstanceOf(GraphQLError); - expect(err.message).toEqual('Unexpected error.'); - expect(err.extensions).toEqual({ - originalError: { - message: 'No context for you!', - stack: expect.stringContaining('Error: No context for you!'), - }, - }); - } - }); - it('Should mask subscribe (sync/promise) subscription errors', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`subscription { instantError }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); - }); - - it('Should mask subscribe (sync/promise) subscription errors with a custom error message', async () => { - const testInstance = createTestkit( - [useMaskedErrors({ errorMessage: 'My Custom subscription error message.' })], - schema - ); - const result = await testInstance.execute(`subscription { instantError }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: My Custom subscription error message.], - ] - `); - }); - - it('Should not mask subscribe (sync/promise) subscription envelop errors', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`subscription { instantEnvelopError }`); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: Noop], - ] - `); - }); - - it('Should mask subscribe (AsyncIterable) subscription errors', async () => { - expect.assertions(1); - const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamError }`); - assertStreamExecutionValue(resultStream); - try { - await collectAsyncIteratorValues(resultStream); - } catch (err: any) { - expect(err.message).toEqual(DEFAULT_ERROR_MESSAGE); - } - }); - - it('Should mask subscribe (AsyncIterable) subscription errors with a custom error message', async () => { - expect.assertions(1); - const testInstance = createTestkit( - [useMaskedErrors({ errorMessage: 'My AsyncIterable Custom Error Message.' })], - schema - ); - const resultStream = await testInstance.execute(`subscription { streamError }`); - assertStreamExecutionValue(resultStream); - try { - await collectAsyncIteratorValues(resultStream); - } catch (err) { - expect(err).toMatchInlineSnapshot(`[GraphQLError: My AsyncIterable Custom Error Message.]`); - } - }); - - it('Should not mask subscribe (AsyncIterable) subscription envelop errors', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamEnvelopError }`); - assertStreamExecutionValue(resultStream); - try { - await collectAsyncIteratorValues(resultStream); - } catch (err) { - expect(err).toMatchInlineSnapshot(`[GraphQLError: Noop]`); - } - }); - - it('Should mask resolve subscription errors', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamResolveError }`); - assertStreamExecutionValue(resultStream); - const allResults = await collectAsyncIteratorValues(resultStream); - expect(allResults).toHaveLength(1); - const [result] = allResults; - expect(result.errors).toBeDefined(); - expect(result.errors).toHaveLength(1); - const [error] = result.errors!; - expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); - }); - it('Should mask resolve subscription errors with a custom error message', async () => { - const testInstance = createTestkit( - [useMaskedErrors({ errorMessage: 'Custom resolve subscription errors.' })], - schema - ); - const resultStream = await testInstance.execute(`subscription { streamResolveError }`); - assertStreamExecutionValue(resultStream); - const allResults = await collectAsyncIteratorValues(resultStream); - expect(allResults).toHaveLength(1); - const [result] = allResults; - expect(result.errors).toBeDefined(); - expect(result.errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: Custom resolve subscription errors.], - ] - `); - }); - - it('Should not mask resolve subscription envelop errors', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamResolveEnvelopError }`); - assertStreamExecutionValue(resultStream); - const allResults = await collectAsyncIteratorValues(resultStream); - expect(allResults).toHaveLength(1); - const [result] = allResults; - expect(result.errors).toBeDefined(); - expect(result.errors).toMatchInlineSnapshot(` - Array [ - [GraphQLError: Noop], - ] - `); - }); - - it('Should not mask auth0 header errors', async () => { - expect.assertions(2); - const auto0Options = { - domain: 'domain.com', - audience: 'audience', - headerName: 'authorization', - preventUnauthenticatedAccess: false, - extendContextField: 'auth0', - tokenType: 'Bearer', - }; - const testInstance = createTestkit([useMaskedErrors(), useAuth0(auto0Options)], schema); - try { - await testInstance.execute(`query { secret }`, {}, { request: { headers: { authorization: 'Something' } } }); - } catch (err) { - expect(err).toMatchInlineSnapshot(`[GraphQLError: Invalid value provided for header "authorization"!]`); - } - - try { - await testInstance.execute(`query { secret }`, {}, { request: { headers: { authorization: 'Something else' } } }); - } catch (err) { - expect(err).toMatchInlineSnapshot(`[GraphQLError: Unsupported token type provided: "Something"!]`); - } - }); - - it('should not mask parse errors', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { a `, {}); - assertSingleExecutionValue(result); - expect(result).toMatchInlineSnapshot(` - Object { - "errors": Array [ - [GraphQLError: Syntax Error: Expected Name, found .], - ], - } - `); - }); - it('should mask parse errors with handleParseErrors option', async () => { - const testInstance = createTestkit([useMaskedErrors({ handleParseErrors: true })], schema); - const result = await testInstance.execute(`query { a `, {}); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - const [error] = result.errors!; - expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); - }); - it('should not mask validation errors', async () => { - const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); - assertSingleExecutionValue(result); - expect(result).toMatchInlineSnapshot(` - Object { - "errors": Array [ - [GraphQLError: Cannot query field "iDoNotExistsMyGuy" on type "Query".], - ], - } - `); - }); - it('should mask validation errors with handleValidationErrors option', async () => { - const testInstance = createTestkit([useMaskedErrors({ handleValidationErrors: true })], schema); - const result = await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - const [error] = result.errors!; - expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); - }); - - it('should use custom error formatter for execution errors', async () => { - const customErrorFormatter: FormatErrorHandler = e => - new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { - custom: true, - }); - const testInstance = createTestkit([useMaskedErrors({ formatError: customErrorFormatter })], schema); - const result = await testInstance.execute(`query { secret }`); - assertSingleExecutionValue(result); - expect(result).toMatchInlineSnapshot(` - Object { - "data": null, - "errors": Array [ - [GraphQLError: Custom error message for Secret sauce that should not leak. - - GraphQL request:1:9 - 1 | query { secret } - | ^], - ], - } - `); - }); - - it('should use custom error formatter for subscribe (AsyncIterable) subscription errors', async () => { - const customErrorFormatter: FormatErrorHandler = e => - new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { - custom: true, - }); - expect.assertions(2); - const testInstance = createTestkit([useMaskedErrors({ formatError: customErrorFormatter })], schema); - const resultStream = await testInstance.execute(`subscription { streamError }`); - assertStreamExecutionValue(resultStream); - try { - await collectAsyncIteratorValues(resultStream); - } catch (err: any) { - expect(err.message).toEqual('Custom error message for Error: Noop'); - expect(err.extensions.custom).toBe(true); - } - }); - - it('should use custom error formatter for parsing errors with handleParseErrors options', async () => { - const customErrorFormatter: FormatErrorHandler = e => - new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { - custom: true, - }); - const useMyFailingParser: Plugin = { - onParse(payload) { - payload.setParseFn(() => { - throw new GraphQLError('My custom error'); - }); - }, - }; - const testInstance = createTestkit( - [useMaskedErrors({ formatError: customErrorFormatter, handleParseErrors: true }), useMyFailingParser], - schema - ); - const result = await testInstance.execute(`query { a `, {}); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - const [error] = result.errors!; - expect(error.message).toEqual('Custom error message for My custom error'); - expect(error.extensions).toEqual({ custom: true }); - }); - it('should use custom error formatter for validation errors with handleValidationErrors option', async () => { - const customErrorFormatter: FormatErrorHandler = e => - new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { - custom: true, - }); - const useMyFailingValidator: Plugin = { - onValidate(payload) { - payload.setValidationFn(() => { - return [new GraphQLError('My custom error')]; - }); - }, - }; - const testInstance = createTestkit( - [useMaskedErrors({ formatError: customErrorFormatter, handleValidationErrors: true }), useMyFailingValidator], - schema - ); - const result = await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - const [error] = result.errors!; - expect(error.message).toEqual('Custom error message for My custom error'); - expect(error.extensions).toEqual({ custom: true }); - }); - it('should use custom error formatter for errors while building the context', async () => { - const customErrorFormatter: FormatErrorHandler = e => - new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { - custom: true, - }); - const testInstance = createTestkit( - [ - useMaskedErrors({ formatError: customErrorFormatter }), - useExtendContext(() => { - throw new GraphQLError('Custom error'); - return {}; - }), - ], - schema - ); - try { - await testInstance.execute(`query { secret }`, {}, {}); - } catch (e) { - expect((e as GraphQLError).message).toEqual('Custom error message for Custom error'); - } - expect.assertions(1); - }); -}); diff --git a/packages/plugins/auth0/src/index.ts b/packages/plugins/auth0/src/index.ts index 7c22ec4487..e1f2b98cc1 100644 --- a/packages/plugins/auth0/src/index.ts +++ b/packages/plugins/auth0/src/index.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ /* eslint-disable dot-notation */ -import { EnvelopError, Plugin } from '@envelop/core'; +import { Plugin } from '@envelop/core'; import * as JwksRsa from 'jwks-rsa'; import jwtPkg, { VerifyOptions, DecodeOptions } from 'jsonwebtoken'; import { GraphQLError } from 'graphql'; @@ -71,12 +71,12 @@ export const useAuth0 = (options: TOptions) const split = authHeader.split(' '); if (split.length !== 2) { - throw new EnvelopError(`Invalid value provided for header "${headerName}"!`); + throw new Error(`Invalid value provided for header "${headerName}"!`); } else { const [type, value] = split; if (type !== tokenType) { - throw new EnvelopError(`Unsupported token type provided: "${type}"!`); + throw new Error(`Unsupported token type provided: "${type}"!`); } else { return value; } diff --git a/packages/plugins/newrelic/src/index.ts b/packages/plugins/newrelic/src/index.ts index bad64cda5c..8dddaa8a3c 100644 --- a/packages/plugins/newrelic/src/index.ts +++ b/packages/plugins/newrelic/src/index.ts @@ -1,4 +1,4 @@ -import { Plugin, OnResolverCalledHook, Path, isAsyncIterable, EnvelopError, DefaultContext } from '@envelop/core'; +import { Plugin, OnResolverCalledHook, Path, isAsyncIterable, DefaultContext } from '@envelop/core'; import { print, FieldNode, Kind, OperationDefinitionNode, ExecutionResult, GraphQLError } from 'graphql'; enum AttributeName { @@ -29,7 +29,7 @@ export type UseNewRelicOptions = { extractOperationName?: (context: DefaultContext) => string | undefined; /** * Indicates whether or not to skip reporting a given error to NewRelic. - * By default, this plugin skips all `EnvelopError` errors and does not report them to NewRelic. + * By default, this plugin skips all `Error` errors and does not report them to NewRelic. */ skipError?: (error: GraphQLError) => boolean; }; @@ -46,13 +46,9 @@ const DEFAULT_OPTIONS: UseNewRelicOptions = { trackResolvers: false, includeResolverArgs: false, rootFieldsNaming: false, - skipError: defaultSkipError, + skipError: () => false, }; -export function defaultSkipError(error: GraphQLError): boolean { - return error.originalError instanceof EnvelopError; -} - export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => { const options: InternalOptions = { ...DEFAULT_OPTIONS, diff --git a/packages/plugins/operation-field-permissions/src/index.ts b/packages/plugins/operation-field-permissions/src/index.ts index 3d6d4d3df7..ea93be2675 100644 --- a/packages/plugins/operation-field-permissions/src/index.ts +++ b/packages/plugins/operation-field-permissions/src/index.ts @@ -1,4 +1,4 @@ -import { EnvelopError, Plugin, useExtendContext } from '@envelop/core'; +import { Plugin, useExtendContext } from '@envelop/core'; import { ExtendedValidationRule, useExtendedValidation } from '@envelop/extended-validation'; import { isUnionType, @@ -8,6 +8,7 @@ import { isInterfaceType, isIntrospectionType, getNamedType, + GraphQLError, } from 'graphql'; type PromiseOrValue = T | Promise; @@ -63,10 +64,9 @@ const OperationScopeRule = !permissionContext.wildcardTypes.has(objectType.name) && !permissionContext.schemaCoordinates.has(schemaCoordinate) ) { - // TODO: EnvelopError was a bad idea ;) // We should use GraphQLError once the object constructor lands in stable GraphQL.js // and useMaskedErrors supports it. - const error = new EnvelopError(options.formatError(schemaCoordinate)); + const error = new GraphQLError(options.formatError(schemaCoordinate)); (error as any).nodes = [node]; context.reportError(error); } diff --git a/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts b/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts index 93655b8aea..4811e67682 100644 --- a/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts +++ b/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts @@ -2,7 +2,6 @@ import { useOperationFieldPermissions } from '../src/index.js'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { assertSingleExecutionValue, createTestkit } from '@envelop/testing'; import { getIntrospectionQuery } from 'graphql'; -import { useMaskedErrors } from '@envelop/core'; const schema = makeExecutableSchema({ typeDefs: [ @@ -225,24 +224,4 @@ describe('useOperationPermissions', () => { const [error] = result.errors!; expect(error.nodes).toBeDefined(); }); - - it('is not masked by the masked errors plugin', async () => { - const kit = createTestkit( - [ - useOperationFieldPermissions({ - getPermissions: () => new Set([]), - }), - useMaskedErrors(), - ], - schema - ); - const result = await kit.execute(/* GraphQL */ ` - query { - __typename - } - `); - assertSingleExecutionValue(result); - expect(result.errors).toBeDefined(); - expect(result.errors![0].message).toEqual("Insufficient permissions for selecting 'Query.__typename'."); - }); }); diff --git a/packages/plugins/sentry/src/index.ts b/packages/plugins/sentry/src/index.ts index 8c32ecc754..a7d236db89 100644 --- a/packages/plugins/sentry/src/index.ts +++ b/packages/plugins/sentry/src/index.ts @@ -1,7 +1,6 @@ import { Plugin, OnResolverCalledHook, - EnvelopError, handleStreamOrSingleExecutionResult, OnExecuteDoneHookResultOnNextHook, } from '@envelop/core'; @@ -80,13 +79,13 @@ export type SentryPluginOptions = { skip?: (args: ExecutionArgs) => boolean; /** * Indicates whether or not to skip Sentry exception reporting for a given error. - * By default, this plugin skips all `EnvelopError` errors and does not report it to Sentry. + * By default, this plugin skips all `Error` errors and does not report it to Sentry. */ skipError?: (args: Error) => boolean; }; export function defaultSkipError(error: Error): boolean { - return error instanceof EnvelopError; + return error instanceof Error; } const sentryTracingSymbol = Symbol('sentryTracing'); From 6ba3999053a00c0911d0093070b7e59e4380a992 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 12:12:06 -0400 Subject: [PATCH 11/61] remove more graphql import --- packages/core/src/plugins/use-payload-formatter.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/core/src/plugins/use-payload-formatter.ts b/packages/core/src/plugins/use-payload-formatter.ts index 55ebcb77ef..ea2f6b8d91 100644 --- a/packages/core/src/plugins/use-payload-formatter.ts +++ b/packages/core/src/plugins/use-payload-formatter.ts @@ -1,6 +1,5 @@ -import { Plugin, TypedExecutionArgs } from '@envelop/types'; +import { Plugin, TypedExecutionArgs, ExecutionResult } from '@envelop/types'; import { handleStreamOrSingleExecutionResult } from '../utils.js'; -import { ExecutionResult } from 'graphql'; export type FormatterFunction = ( result: ExecutionResult, From ae21a6ee32362d127a9f4868111b7818df62279e Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 12:12:17 -0400 Subject: [PATCH 12/61] Drop useTiming --- packages/core/src/plugins/use-timing.ts | 185 ------------------------ 1 file changed, 185 deletions(-) delete mode 100644 packages/core/src/plugins/use-timing.ts diff --git a/packages/core/src/plugins/use-timing.ts b/packages/core/src/plugins/use-timing.ts deleted file mode 100644 index 3e8886294e..0000000000 --- a/packages/core/src/plugins/use-timing.ts +++ /dev/null @@ -1,185 +0,0 @@ -/* eslint-disable no-console */ -import { Plugin } from '@envelop/types'; -import { DocumentNode, ExecutionArgs, getOperationAST, GraphQLResolveInfo, Source, SubscriptionArgs } from 'graphql'; -import { isIntrospectionOperationString, envelopIsIntrospectionSymbol } from '../utils.js'; - -const HR_TO_NS = 1e9; -const NS_TO_MS = 1e6; - -export type ResultTiming = { ms: number; ns: number }; - -export type TimingPluginOptions = { - skipIntrospection?: boolean; - onContextBuildingMeasurement?: (timing: ResultTiming) => void; - onExecutionMeasurement?: (args: ExecutionArgs, timing: ResultTiming) => void; - onSubscriptionMeasurement?: (args: SubscriptionArgs, timing: ResultTiming) => void; - onParsingMeasurement?: (source: Source | string, timing: ResultTiming) => void; - onValidationMeasurement?: (document: DocumentNode, timing: ResultTiming) => void; - onResolverMeasurement?: (info: GraphQLResolveInfo, timing: ResultTiming) => void; -}; - -const DEFAULT_OPTIONS: TimingPluginOptions = { - onExecutionMeasurement: (args, timing) => - console.log(`Operation execution "${args.operationName}" done in ${timing.ms}ms`), - onSubscriptionMeasurement: (args, timing) => - console.log(`Operation subscription "${args.operationName}" done in ${timing.ms}ms`), - onParsingMeasurement: (source: Source | string, timing: ResultTiming) => - console.log(`Parsing "${source}" done in ${timing.ms}ms`), - onValidationMeasurement: (document: DocumentNode, timing: ResultTiming) => - console.log(`Validation "${getOperationAST(document)?.name?.value || '-'}" done in ${timing.ms}ms`), - onResolverMeasurement: (info: GraphQLResolveInfo, timing: ResultTiming) => - console.log(`\tResolver of "${info.parentType.toString()}.${info.fieldName}" done in ${timing.ms}ms`), - onContextBuildingMeasurement: (timing: ResultTiming) => console.log(`Context building done in ${timing.ms}ms`), -}; - -const deltaFrom = (hrtime: [number, number]): { ms: number; ns: number } => { - const delta = process.hrtime(hrtime); - const ns = delta[0] * HR_TO_NS + delta[1]; - - return { - ns, - get ms() { - return ns / NS_TO_MS; - }, - }; -}; - -type InternalPluginContext = { - [envelopIsIntrospectionSymbol]?: true; -}; - -export const useTiming = (rawOptions?: TimingPluginOptions): Plugin => { - const options = { - ...DEFAULT_OPTIONS, - ...rawOptions, - }; - - const result: Plugin = {}; - - if (options.onContextBuildingMeasurement) { - result.onContextBuilding = ({ context }) => { - if (context[envelopIsIntrospectionSymbol]) { - return; - } - - const contextStartTime = process.hrtime(); - - return () => { - options.onContextBuildingMeasurement!(deltaFrom(contextStartTime)); - }; - }; - } - - if (options.onParsingMeasurement) { - result.onParse = ({ params, extendContext }) => { - if (options.skipIntrospection && isIntrospectionOperationString(params.source)) { - extendContext({ - [envelopIsIntrospectionSymbol]: true, - }); - - return; - } - const parseStartTime = process.hrtime(); - - return () => { - options.onParsingMeasurement!(params.source, deltaFrom(parseStartTime)); - }; - }; - } - - if (options.onValidationMeasurement) { - result.onValidate = ({ params, context }) => { - if (context[envelopIsIntrospectionSymbol]) { - return; - } - - const validateStartTime = process.hrtime(); - - return () => { - options.onValidationMeasurement!(params.documentAST, deltaFrom(validateStartTime)); - }; - }; - } - - if (options.onExecutionMeasurement) { - if (options.onResolverMeasurement) { - result.onExecute = ({ args }) => { - if (args.contextValue[envelopIsIntrospectionSymbol]) { - return; - } - - const executeStartTime = process.hrtime(); - - return { - onExecuteDone: () => { - options.onExecutionMeasurement!(args, deltaFrom(executeStartTime)); - }, - }; - }; - - result.onResolverCalled = ({ info }) => { - const resolverStartTime = process.hrtime(); - - return () => { - options.onResolverMeasurement!(info, deltaFrom(resolverStartTime)); - }; - }; - } else { - result.onExecute = ({ args }) => { - if (args.contextValue[envelopIsIntrospectionSymbol]) { - return; - } - - const executeStartTime = process.hrtime(); - - return { - onExecuteDone: () => { - options.onExecutionMeasurement!(args, deltaFrom(executeStartTime)); - }, - }; - }; - } - } - - if (options.onSubscriptionMeasurement) { - if (options.onResolverMeasurement) { - result.onSubscribe = ({ args }) => { - if (args.contextValue[envelopIsIntrospectionSymbol]) { - return; - } - - const subscribeStartTime = process.hrtime(); - - return { - onSubscribeResult: () => { - options.onSubscriptionMeasurement && options.onSubscriptionMeasurement(args, deltaFrom(subscribeStartTime)); - }, - }; - }; - - result.onResolverCalled = ({ info }) => { - const resolverStartTime = process.hrtime(); - - return () => { - options.onResolverMeasurement && options.onResolverMeasurement(info, deltaFrom(resolverStartTime)); - }; - }; - } else { - result.onSubscribe = ({ args }) => { - if (args.contextValue[envelopIsIntrospectionSymbol]) { - return; - } - - const subscribeStartTime = process.hrtime(); - - return { - onSubscribeResult: () => { - options.onSubscriptionMeasurement && options.onSubscriptionMeasurement(args, deltaFrom(subscribeStartTime)); - }, - }; - }; - } - } - - return result; -}; From da7c81c4cd404ccf89a4ef51a4e3dab9c3ef1579 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 12:40:17 -0400 Subject: [PATCH 13/61] make core completely free of graphql-js --- packages/core/package.json | 3 - packages/core/src/index.ts | 2 - .../immediate-introspection/.npmignore | 2 + .../plugins/immediate-introspection/README.md | 1 + .../immediate-introspection/package.json | 67 +++++++++++++++++++ .../immediate-introspection/src/index.ts} | 0 .../test}/use-immediate-introspection.spec.ts | 6 +- 7 files changed, 73 insertions(+), 8 deletions(-) create mode 100644 packages/plugins/immediate-introspection/.npmignore create mode 100644 packages/plugins/immediate-introspection/README.md create mode 100644 packages/plugins/immediate-introspection/package.json rename packages/{core/src/plugins/use-immediate-introspection.ts => plugins/immediate-introspection/src/index.ts} (100%) rename packages/{core/test/plugins => plugins/immediate-introspection/test}/use-immediate-introspection.spec.ts (94%) diff --git a/packages/core/package.json b/packages/core/package.json index d66ed1238c..20726badf2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -64,9 +64,6 @@ "graphql": "16.3.0", "typescript": "4.7.4" }, - "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" - }, "buildOptions": { "input": "./src/index.ts" }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 227891bcd3..4ee40cd61f 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,11 +3,9 @@ export * from './create.js'; export * from './utils.js'; export * from './plugins/use-envelop.js'; export * from './plugins/use-logger.js'; -export * from './plugins/use-timing.js'; export * from './plugins/use-schema.js'; export * from './plugins/use-error-handler.js'; export * from './plugins/use-extend-context.js'; export * from './plugins/use-payload-formatter.js'; -export * from './plugins/use-immediate-introspection.js'; export * from './enable-if.js'; export { resolversHooksSymbol } from './orchestrator.js'; diff --git a/packages/plugins/immediate-introspection/.npmignore b/packages/plugins/immediate-introspection/.npmignore new file mode 100644 index 0000000000..3684decc03 --- /dev/null +++ b/packages/plugins/immediate-introspection/.npmignore @@ -0,0 +1,2 @@ +test +*.png diff --git a/packages/plugins/immediate-introspection/README.md b/packages/plugins/immediate-introspection/README.md new file mode 100644 index 0000000000..2dca89d0e4 --- /dev/null +++ b/packages/plugins/immediate-introspection/README.md @@ -0,0 +1 @@ +## `@envelop/immediate-introspection` diff --git a/packages/plugins/immediate-introspection/package.json b/packages/plugins/immediate-introspection/package.json new file mode 100644 index 0000000000..5feedb43ce --- /dev/null +++ b/packages/plugins/immediate-introspection/package.json @@ -0,0 +1,67 @@ +{ + "name": "@envelop/immediate-introspection", + "version": "0.0.0", + "author": "Saihajpreet Singh ", + "license": "MIT", + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/n1ru4l/envelop.git", + "directory": "packages/plugins/immediate-introspection" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "require": { + "types": "./dist/typings/index.d.cts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./*": { + "require": { + "types": "./dist/typings/*.d.cts", + "default": "./dist/cjs/*.js" + }, + "import": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + }, + "default": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "typescript": { + "definition": "dist/typings/index.d.ts" + }, + "dependencies": {}, + "devDependencies": { + "graphql": "16.3.0", + "typescript": "4.7.4" + }, + "peerDependencies": { + "@envelop/core": "^2.5.0", + "@sentry/node": "^6 || ^7", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + }, + "type": "module" +} diff --git a/packages/core/src/plugins/use-immediate-introspection.ts b/packages/plugins/immediate-introspection/src/index.ts similarity index 100% rename from packages/core/src/plugins/use-immediate-introspection.ts rename to packages/plugins/immediate-introspection/src/index.ts diff --git a/packages/core/test/plugins/use-immediate-introspection.spec.ts b/packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts similarity index 94% rename from packages/core/test/plugins/use-immediate-introspection.spec.ts rename to packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts index 9c563768c4..99ede68cba 100644 --- a/packages/core/test/plugins/use-immediate-introspection.spec.ts +++ b/packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts @@ -1,7 +1,7 @@ import { createTestkit } from '@envelop/testing'; -import { useImmediateIntrospection } from '../../src/plugins/use-immediate-introspection.js'; -import { useExtendContext } from '../../src/plugins/use-extend-context.js'; -import { schema } from '../common.js'; +import { useImmediateIntrospection } from '../src/index.js'; +import { useExtendContext } from '@envelop/core'; +import { schema } from '../../../core/test/common.js'; import { getIntrospectionQuery } from 'graphql'; describe('useImmediateIntrospection', () => { From 5010b9655f863b08c40225456ee9f397342d91be Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 13:10:05 -0400 Subject: [PATCH 14/61] add eslint rule --- .eslintrc.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/.eslintrc.json b/.eslintrc.json index 4c797a04e6..d00adcb847 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -29,6 +29,7 @@ { "devDependencies": ["**/*.test.ts", "**/*.spec.ts", "**/test/**/*.ts"] } ] }, + "env": { "es6": true, "node": true @@ -43,6 +44,26 @@ "@typescript-eslint/no-unused-vars": "off", "import/no-extraneous-dependencies": "off" } + }, + // Disallow `graphql-js` specific things in `core` + { + "files": ["packages/core/**"], + "env": { + "jest": true + }, + "rules": { + "no-restricted-imports": [ + "error", + { + "paths": [ + { + "name": "graphql", + "message": "You chose violence. Try to make it work without using GraphQL.js" + } + ] + } + ] + } } ], "ignorePatterns": ["dist", "node_modules", "dev-test", "website"] From 2f8c24b86235a03954bc0cbcd6ffb867ac479005 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 13:20:09 -0400 Subject: [PATCH 15/61] eslint disallow in types too --- .eslintrc.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index d00adcb847..96f3b7561a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,9 +45,9 @@ "import/no-extraneous-dependencies": "off" } }, - // Disallow `graphql-js` specific things in `core` + // Disallow `graphql-js` specific things in `core` or `types` { - "files": ["packages/core/**"], + "files": ["packages/core/**", "packages/types/**"], "env": { "jest": true }, From d55b6265bee9eaddaf850288c0055c62d14fa020 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 23 Aug 2022 16:33:45 -0400 Subject: [PATCH 16/61] more agnostic packages --- .eslintrc.json | 16 ++++++++++++++-- packages/plugins/auth0/package.json | 4 +--- packages/plugins/auth0/src/index.ts | 3 +-- packages/plugins/dataloader/package.json | 3 +-- packages/plugins/preload-assets/package.json | 3 +-- packages/plugins/statsd/package.json | 1 - packages/plugins/statsd/src/index.ts | 5 ++--- 7 files changed, 20 insertions(+), 15 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 96f3b7561a..cdc921ea6a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -45,9 +45,17 @@ "import/no-extraneous-dependencies": "off" } }, - // Disallow `graphql-js` specific things in `core` or `types` + // Disallow `graphql-js` specific things to get re-introduced in agnostic packages. { - "files": ["packages/core/**", "packages/types/**"], + "files": [ + "packages/core/**", + "packages/types/**", + "packages/plugins/apollo-datasources/**", + "packages/plugins/auth0/**", + "packages/plugins/dataloader/**", + "packages/plugins/preload-assets/**", + "packages/plugins/statsd/**" + ], "env": { "jest": true }, @@ -59,6 +67,10 @@ { "name": "graphql", "message": "You chose violence. Try to make it work without using GraphQL.js" + }, + { + "name": "@graphql-tools/*", + "message": "You chose violence. Try to make it work without using `graphql-tools`" } ] } diff --git a/packages/plugins/auth0/package.json b/packages/plugins/auth0/package.json index 8633266d0a..b31f25a3ac 100644 --- a/packages/plugins/auth0/package.json +++ b/packages/plugins/auth0/package.json @@ -52,12 +52,10 @@ }, "devDependencies": { "@types/jsonwebtoken": "8.5.8", - "graphql": "16.3.0", "typescript": "4.7.4" }, "peerDependencies": { - "@envelop/core": "^2.5.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "@envelop/core": "^2.5.0" }, "buildOptions": { "input": "./src/index.ts" diff --git a/packages/plugins/auth0/src/index.ts b/packages/plugins/auth0/src/index.ts index e1f2b98cc1..d7a1a6e7fc 100644 --- a/packages/plugins/auth0/src/index.ts +++ b/packages/plugins/auth0/src/index.ts @@ -3,7 +3,6 @@ import { Plugin } from '@envelop/core'; import * as JwksRsa from 'jwks-rsa'; import jwtPkg, { VerifyOptions, DecodeOptions } from 'jsonwebtoken'; -import { GraphQLError } from 'graphql'; const { decode, verify } = jwtPkg; @@ -23,7 +22,7 @@ export type Auth0PluginOptions = { headerName?: string; }; -export class UnauthenticatedError extends GraphQLError {} +export class UnauthenticatedError extends Error {} export type UserPayload = { sub: string; diff --git a/packages/plugins/dataloader/package.json b/packages/plugins/dataloader/package.json index 0680b88931..e78983e8c2 100644 --- a/packages/plugins/dataloader/package.json +++ b/packages/plugins/dataloader/package.json @@ -55,8 +55,7 @@ }, "peerDependencies": { "@envelop/core": "^2.5.0", - "dataloader": "^2.0.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "dataloader": "^2.0.0" }, "buildOptions": { "input": "./src/index.ts" diff --git a/packages/plugins/preload-assets/package.json b/packages/plugins/preload-assets/package.json index e4d2bd4eca..96288208c2 100644 --- a/packages/plugins/preload-assets/package.json +++ b/packages/plugins/preload-assets/package.json @@ -52,8 +52,7 @@ "typescript": "4.7.4" }, "peerDependencies": { - "@envelop/core": "^2.5.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + "@envelop/core": "^2.5.0" }, "buildOptions": { "input": "./src/index.ts" diff --git a/packages/plugins/statsd/package.json b/packages/plugins/statsd/package.json index 8728fd312f..72df645683 100644 --- a/packages/plugins/statsd/package.json +++ b/packages/plugins/statsd/package.json @@ -61,7 +61,6 @@ }, "peerDependencies": { "@envelop/core": "^2.5.0", - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", "hot-shots": "^8.0.0 || ^9.0.0" }, "buildOptions": { diff --git a/packages/plugins/statsd/src/index.ts b/packages/plugins/statsd/src/index.ts index 8a6316800d..d9f3fe3d15 100644 --- a/packages/plugins/statsd/src/index.ts +++ b/packages/plugins/statsd/src/index.ts @@ -1,5 +1,4 @@ import { Plugin, AfterParseEventPayload, isIntrospectionOperationString, isAsyncIterable } from '@envelop/core'; -import { DocumentNode, Kind, OperationDefinitionNode } from 'graphql'; import type { StatsD } from 'hot-shots'; export interface StatsDPluginOptions { @@ -31,8 +30,8 @@ interface PluginInternalContext { [statsDPluginExecutionStartTimeSymbol]: number; } -function getOperation(document: DocumentNode) { - return document.definitions.find(def => def.kind === Kind.OPERATION_DEFINITION) as OperationDefinitionNode; +function getOperation(document: any) { + return document.definitions.find((def: any) => def.kind === 'OperationDefinition'); } function isParseFailure(parseResult: AfterParseEventPayload['result']): parseResult is Error | null { From 184db1699fad47ff24ce963d6bf42ad5c16db3b7 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Wed, 24 Aug 2022 11:17:29 -0400 Subject: [PATCH 17/61] remove introspection util --- packages/core/src/utils.ts | 31 ------------------------------- packages/core/test/utils.spec.ts | 28 ---------------------------- 2 files changed, 59 deletions(-) diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index a12c23f904..fe0ebd1bd6 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -14,37 +14,6 @@ import { export const envelopIsIntrospectionSymbol = Symbol('ENVELOP_IS_INTROSPECTION'); -export function isOperationDefinition(def: any): boolean { - return def.kind === 'OperationDefinition'; -} - -export function isIntrospectionOperation(operation: any): boolean { - if (operation.kind === 'OperationDefinition') { - if (operation.name?.value === '__schema') { - return true; - } - - const nodesWithSchema = operation.selectionSet.selections.filter((selection: any) => { - if (selection.kind === 'Field' && selection.name.value === '__schema') { - return true; - } - return false; - }); - - if (nodesWithSchema.length > 0) { - return true; - } - } - - return false; -} - -export function isIntrospectionDocument(document: any): boolean { - const operations = document.definitions.filter(isOperationDefinition); - - return operations.some((op: any) => isIntrospectionOperation(op)); -} - export function isIntrospectionOperationString(operation: string | any): boolean { return (typeof operation === 'string' ? operation : operation.body).indexOf('__schema') !== -1; } diff --git a/packages/core/test/utils.spec.ts b/packages/core/test/utils.spec.ts index 59cf508ba9..f6831d08bc 100644 --- a/packages/core/test/utils.spec.ts +++ b/packages/core/test/utils.spec.ts @@ -1,36 +1,8 @@ import { useLogger, enableIf } from '@envelop/core'; import { createTestkit, createSpiedPlugin } from '@envelop/testing'; -import { getIntrospectionQuery, parse } from 'graphql'; -import { isIntrospectionDocument } from '../src/utils.js'; import { query, schema } from './common.js'; describe('Utils', () => { - describe('isIntrospectionDocument', () => { - it('Should detect original introspection query', () => { - const doc = getIntrospectionQuery(); - - expect(isIntrospectionDocument(parse(doc))).toBeTruthy(); - }); - - it('Should return false on non-introspection', () => { - const doc = `query test { f }`; - - expect(isIntrospectionDocument(parse(doc))).toBeFalsy(); - }); - - it('Should detect minimal introspection', () => { - const doc = `query { __schema { test }}`; - - expect(isIntrospectionDocument(parse(doc))).toBeTruthy(); - }); - - it('Should detect alias tricks', () => { - const doc = `query { test: __schema { test }}`; - - expect(isIntrospectionDocument(parse(doc))).toBeTruthy(); - }); - }); - describe('enableIf', () => { it('Should return a plugin', () => { const plugin = enableIf(true, useLogger()); From 69647023d6c1a7cced5100db15e5b47bcc636141 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 24 Aug 2022 15:21:56 +0000 Subject: [PATCH 18/61] chore(dependencies): updated changesets for modified dependencies --- .changeset/@envelop_apollo-tracing-1487-dependencies.md | 7 +++++++ .changeset/@envelop_auth0-1487-dependencies.md | 7 +++++++ .changeset/@envelop_core-1487-dependencies.md | 7 +++++++ .changeset/@envelop_dataloader-1487-dependencies.md | 7 +++++++ .changeset/@envelop_opentelemetry-1487-dependencies.md | 7 +++++++ .changeset/@envelop_preload-assets-1487-dependencies.md | 7 +++++++ .changeset/@envelop_prometheus-1487-dependencies.md | 7 +++++++ .changeset/@envelop_rate-limiter-1487-dependencies.md | 7 +++++++ .changeset/@envelop_statsd-1487-dependencies.md | 7 +++++++ 9 files changed, 63 insertions(+) create mode 100644 .changeset/@envelop_apollo-tracing-1487-dependencies.md create mode 100644 .changeset/@envelop_auth0-1487-dependencies.md create mode 100644 .changeset/@envelop_core-1487-dependencies.md create mode 100644 .changeset/@envelop_dataloader-1487-dependencies.md create mode 100644 .changeset/@envelop_opentelemetry-1487-dependencies.md create mode 100644 .changeset/@envelop_preload-assets-1487-dependencies.md create mode 100644 .changeset/@envelop_prometheus-1487-dependencies.md create mode 100644 .changeset/@envelop_rate-limiter-1487-dependencies.md create mode 100644 .changeset/@envelop_statsd-1487-dependencies.md diff --git a/.changeset/@envelop_apollo-tracing-1487-dependencies.md b/.changeset/@envelop_apollo-tracing-1487-dependencies.md new file mode 100644 index 0000000000..d591e4b214 --- /dev/null +++ b/.changeset/@envelop_apollo-tracing-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/apollo-tracing": patch +--- + +dependencies updates: + +- Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_auth0-1487-dependencies.md b/.changeset/@envelop_auth0-1487-dependencies.md new file mode 100644 index 0000000000..8efabb497b --- /dev/null +++ b/.changeset/@envelop_auth0-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/auth0": patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_core-1487-dependencies.md b/.changeset/@envelop_core-1487-dependencies.md new file mode 100644 index 0000000000..021b21ba9f --- /dev/null +++ b/.changeset/@envelop_core-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/core": patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_dataloader-1487-dependencies.md b/.changeset/@envelop_dataloader-1487-dependencies.md new file mode 100644 index 0000000000..a8bfbd9012 --- /dev/null +++ b/.changeset/@envelop_dataloader-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/dataloader": patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_opentelemetry-1487-dependencies.md b/.changeset/@envelop_opentelemetry-1487-dependencies.md new file mode 100644 index 0000000000..374db3f4ea --- /dev/null +++ b/.changeset/@envelop_opentelemetry-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/opentelemetry": patch +--- + +dependencies updates: + +- Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_preload-assets-1487-dependencies.md b/.changeset/@envelop_preload-assets-1487-dependencies.md new file mode 100644 index 0000000000..39f668bfaf --- /dev/null +++ b/.changeset/@envelop_preload-assets-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/preload-assets": patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_prometheus-1487-dependencies.md b/.changeset/@envelop_prometheus-1487-dependencies.md new file mode 100644 index 0000000000..49791e30f1 --- /dev/null +++ b/.changeset/@envelop_prometheus-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/prometheus": patch +--- + +dependencies updates: + +- Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_rate-limiter-1487-dependencies.md b/.changeset/@envelop_rate-limiter-1487-dependencies.md new file mode 100644 index 0000000000..40e3418c0d --- /dev/null +++ b/.changeset/@envelop_rate-limiter-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/rate-limiter": patch +--- + +dependencies updates: + +- Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_statsd-1487-dependencies.md b/.changeset/@envelop_statsd-1487-dependencies.md new file mode 100644 index 0000000000..3da17f81fb --- /dev/null +++ b/.changeset/@envelop_statsd-1487-dependencies.md @@ -0,0 +1,7 @@ +--- +"@envelop/statsd": patch +--- + +dependencies updates: + +- Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) From 6ec57928a92a8ffc589972d09a5b9dcd6816bf53 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Wed, 24 Aug 2022 11:23:08 -0400 Subject: [PATCH 19/61] prettier --- .changeset/@envelop_apollo-tracing-1487-dependencies.md | 4 ++-- .changeset/@envelop_auth0-1487-dependencies.md | 4 ++-- .changeset/@envelop_core-1487-dependencies.md | 4 ++-- .changeset/@envelop_dataloader-1487-dependencies.md | 4 ++-- .changeset/@envelop_opentelemetry-1487-dependencies.md | 4 ++-- .changeset/@envelop_preload-assets-1487-dependencies.md | 4 ++-- .changeset/@envelop_prometheus-1487-dependencies.md | 4 ++-- .changeset/@envelop_rate-limiter-1487-dependencies.md | 4 ++-- .changeset/@envelop_statsd-1487-dependencies.md | 4 ++-- .changeset/@envelop_types-1487-dependencies.md | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/.changeset/@envelop_apollo-tracing-1487-dependencies.md b/.changeset/@envelop_apollo-tracing-1487-dependencies.md index d591e4b214..5521db1409 100644 --- a/.changeset/@envelop_apollo-tracing-1487-dependencies.md +++ b/.changeset/@envelop_apollo-tracing-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/apollo-tracing": patch +'@envelop/apollo-tracing': patch --- -dependencies updates: +dependencies updates: - Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_auth0-1487-dependencies.md b/.changeset/@envelop_auth0-1487-dependencies.md index 8efabb497b..7c81732649 100644 --- a/.changeset/@envelop_auth0-1487-dependencies.md +++ b/.changeset/@envelop_auth0-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/auth0": patch +'@envelop/auth0': patch --- -dependencies updates: +dependencies updates: - Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_core-1487-dependencies.md b/.changeset/@envelop_core-1487-dependencies.md index 021b21ba9f..38b760f7fc 100644 --- a/.changeset/@envelop_core-1487-dependencies.md +++ b/.changeset/@envelop_core-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/core": patch +'@envelop/core': patch --- -dependencies updates: +dependencies updates: - Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_dataloader-1487-dependencies.md b/.changeset/@envelop_dataloader-1487-dependencies.md index a8bfbd9012..98d6d52b4e 100644 --- a/.changeset/@envelop_dataloader-1487-dependencies.md +++ b/.changeset/@envelop_dataloader-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/dataloader": patch +'@envelop/dataloader': patch --- -dependencies updates: +dependencies updates: - Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_opentelemetry-1487-dependencies.md b/.changeset/@envelop_opentelemetry-1487-dependencies.md index 374db3f4ea..feba642c7a 100644 --- a/.changeset/@envelop_opentelemetry-1487-dependencies.md +++ b/.changeset/@envelop_opentelemetry-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/opentelemetry": patch +'@envelop/opentelemetry': patch --- -dependencies updates: +dependencies updates: - Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_preload-assets-1487-dependencies.md b/.changeset/@envelop_preload-assets-1487-dependencies.md index 39f668bfaf..c457eb43ec 100644 --- a/.changeset/@envelop_preload-assets-1487-dependencies.md +++ b/.changeset/@envelop_preload-assets-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/preload-assets": patch +'@envelop/preload-assets': patch --- -dependencies updates: +dependencies updates: - Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_prometheus-1487-dependencies.md b/.changeset/@envelop_prometheus-1487-dependencies.md index 49791e30f1..26f1d6717e 100644 --- a/.changeset/@envelop_prometheus-1487-dependencies.md +++ b/.changeset/@envelop_prometheus-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/prometheus": patch +'@envelop/prometheus': patch --- -dependencies updates: +dependencies updates: - Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_rate-limiter-1487-dependencies.md b/.changeset/@envelop_rate-limiter-1487-dependencies.md index 40e3418c0d..2e99c1285a 100644 --- a/.changeset/@envelop_rate-limiter-1487-dependencies.md +++ b/.changeset/@envelop_rate-limiter-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/rate-limiter": patch +'@envelop/rate-limiter': patch --- -dependencies updates: +dependencies updates: - Added dependency [`@envelop/types@^2.3.1` ↗︎](https://www.npmjs.com/package/@envelop/types/v/null) (to `peerDependencies`) diff --git a/.changeset/@envelop_statsd-1487-dependencies.md b/.changeset/@envelop_statsd-1487-dependencies.md index 3da17f81fb..15f3d6bc8c 100644 --- a/.changeset/@envelop_statsd-1487-dependencies.md +++ b/.changeset/@envelop_statsd-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/statsd": patch +'@envelop/statsd': patch --- -dependencies updates: +dependencies updates: - Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) diff --git a/.changeset/@envelop_types-1487-dependencies.md b/.changeset/@envelop_types-1487-dependencies.md index 8832115daf..9e8a541b0e 100644 --- a/.changeset/@envelop_types-1487-dependencies.md +++ b/.changeset/@envelop_types-1487-dependencies.md @@ -1,7 +1,7 @@ --- -"@envelop/types": patch +'@envelop/types': patch --- -dependencies updates: +dependencies updates: - Removed dependency [`graphql@^14.0.0 || ^15.0.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/null) (from `peerDependencies`) From 680dbda497a358c258e29f15797bb4f829a13896 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Wed, 24 Aug 2022 11:30:16 -0400 Subject: [PATCH 20/61] TEMP: make bot calm down --- .github/workflows/pr.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 86380a46f3..4cca1b1990 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -5,10 +5,10 @@ on: - main jobs: - dependencies: - uses: the-guild-org/shared-config/.github/workflows/changesets-dependencies.yaml@main - secrets: - githubToken: ${{ secrets.GITHUB_TOKEN }} + # dependencies: + # uses: the-guild-org/shared-config/.github/workflows/changesets-dependencies.yaml@main + # secrets: + # githubToken: ${{ secrets.GITHUB_TOKEN }} release: uses: the-guild-org/shared-config/.github/workflows/release-snapshot.yml@main From f0373e1279a77a13ee942861f6b06cb2b997bfcc Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Wed, 24 Aug 2022 12:57:00 -0400 Subject: [PATCH 21/61] update all docs --- packages/core/docs/use-async-schema.md | 6 +- packages/core/docs/use-error-handler.md | 6 +- packages/core/docs/use-extend-context.md | 6 +- .../core/docs/use-immediate-introspection.md | 36 ------ packages/core/docs/use-lazy-loaded-schema.md | 6 +- packages/core/docs/use-logger.md | 6 +- packages/core/docs/use-masked-errors.md | 62 ----------- packages/core/docs/use-payload-formatter.md | 6 +- packages/core/docs/use-schema.md | 6 +- packages/core/docs/use-timing.md | 24 ---- packages/plugins/apollo-datasources/README.md | 5 + .../plugins/apollo-server-errors/README.md | 5 + packages/plugins/apollo-tracing/README.md | 5 + packages/plugins/auth0/README.md | 5 + packages/plugins/dataloader/README.md | 5 + packages/plugins/depth-limit/README.md | 5 + .../plugins/disable-introspection/README.md | 5 + .../execute-subscription-event/README.md | 5 + .../plugins/extended-validation/README.md | 8 +- .../plugins/filter-operation-type/README.md | 6 + packages/plugins/fragment-arguments/README.md | 5 + packages/plugins/generic-auth/README.md | 5 + packages/plugins/graphql-jit/README.md | 5 + packages/plugins/graphql-middleware/README.md | 5 + packages/plugins/graphql-modules/README.md | 5 + .../plugins/immediate-introspection/README.md | 48 ++++++++ packages/plugins/live-query/README.md | 5 + packages/plugins/newrelic/README.md | 5 + packages/plugins/opentelemetry/README.md | 10 ++ .../operation-field-permissions/README.md | 5 + packages/plugins/parser-cache/README.md | 5 + .../plugins/persisted-operations/README.md | 18 +++ packages/plugins/preload-assets/README.md | 5 + packages/plugins/prometheus/README.md | 16 +++ packages/plugins/rate-limiter/README.md | 5 + .../plugins/resource-limitations/README.md | 5 + .../plugins/response-cache-redis/README.md | 10 ++ packages/plugins/response-cache/README.md | 87 +++++++++++++++ packages/plugins/sentry/README.md | 5 + packages/plugins/statsd/README.md | 5 + packages/plugins/validation-cache/README.md | 5 + website/docs/README.mdx | 1 - website/docs/core.mdx | 105 ++++++------------ website/docs/getting-started.mdx | 10 ++ .../adding-a-graphql-response-cache.mdx | 14 ++- .../adding-authentication-with-auth0.mdx | 5 + .../docs/guides/integrating-with-databases.md | 5 + .../docs/guides/monitoring-and-tracing.mdx | 15 +++ ...ubscription-data-loader-caching-issues.mdx | 5 + .../docs/guides/securing-your-graphql-api.mdx | 33 +++++- website/docs/plugins/testing.mdx | 2 +- 51 files changed, 471 insertions(+), 206 deletions(-) delete mode 100644 packages/core/docs/use-immediate-introspection.md delete mode 100644 packages/core/docs/use-masked-errors.md delete mode 100644 packages/core/docs/use-timing.md diff --git a/packages/core/docs/use-async-schema.md b/packages/core/docs/use-async-schema.md index e4754c65d3..e44b847de0 100644 --- a/packages/core/docs/use-async-schema.md +++ b/packages/core/docs/use-async-schema.md @@ -4,13 +4,17 @@ This plugin is the simplest plugin for specifying your GraphQL schema. You can s ```ts import { envelop, useAsyncSchema } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getSchema = async (): Promise => { // return schema when it's ready } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useAsyncSchema(getSchema()) // ... other plugins ... diff --git a/packages/core/docs/use-error-handler.md b/packages/core/docs/use-error-handler.md index a6b16d4b9a..739b59c6cb 100644 --- a/packages/core/docs/use-error-handler.md +++ b/packages/core/docs/use-error-handler.md @@ -4,9 +4,13 @@ This plugin triggers a custom function when execution encounters an error. ```ts import { envelop, useErrorHandler } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useErrorHandler((errors, args) => { // This callback is called once, containing all GraphQLError emitted during execution phase diff --git a/packages/core/docs/use-extend-context.md b/packages/core/docs/use-extend-context.md index 7b160f7ef9..5267cbddc7 100644 --- a/packages/core/docs/use-extend-context.md +++ b/packages/core/docs/use-extend-context.md @@ -4,9 +4,13 @@ Easily extends the context with custom fields. ```ts import { envelop, useExtendContext } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useExtendContext(async contextSoFar => { return { diff --git a/packages/core/docs/use-immediate-introspection.md b/packages/core/docs/use-immediate-introspection.md deleted file mode 100644 index e22252102f..0000000000 --- a/packages/core/docs/use-immediate-introspection.md +++ /dev/null @@ -1,36 +0,0 @@ -#### `useImmediateIntrospection` - -Context building can be costly and require calling remote services. -For simple GraphQL operations that only select introspection fields building a context is not necessary. - -The `useImmediateIntrospection` can be used to short circuit any further context building if a GraphQL operation selection set only includes introspection fields within the selection set. - -```ts -import { envelop, useImmediateIntrospection } from '@envelop/core' -import { schema } from './schema' - -const getEnveloped = envelop({ - plugins: [ - useSchema(schema), - useImmediateIntrospection() - // additional plugins - ] -}) -``` - -In case you want to authorize that an user is authenticated before allowing introspection the plugin must be placed in front of the `useImmediateIntrospection()` call. - -```ts -import { envelop, useImmediateIntrospection } from '@envelop/core' -import { schema } from './schema' -import { useAuthorization } from './useAuthorization' - -const getEnveloped = envelop({ - plugins: [ - useSchema(schema), - useAuthorization(), // place this before - useImmediateIntrospection() - // additional plugins - ] -}) -``` diff --git a/packages/core/docs/use-lazy-loaded-schema.md b/packages/core/docs/use-lazy-loaded-schema.md index 783116a677..e363fc8205 100644 --- a/packages/core/docs/use-lazy-loaded-schema.md +++ b/packages/core/docs/use-lazy-loaded-schema.md @@ -4,7 +4,7 @@ This plugin is the simplest plugin for specifying your GraphQL schema. You can s ```ts import { envelop, useLazyLoadedSchema } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' async function getSchema({ req }): GraphQLSchema { if (req.isAdmin) { @@ -15,6 +15,10 @@ async function getSchema({ req }): GraphQLSchema { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useLazyLoadedSchema(getSchema) // ... other plugins ... diff --git a/packages/core/docs/use-logger.md b/packages/core/docs/use-logger.md index d2893b25e8..39a44e15d4 100644 --- a/packages/core/docs/use-logger.md +++ b/packages/core/docs/use-logger.md @@ -4,9 +4,13 @@ Logs parameters and information about the execution phases. You can easily plug ```ts import { envelop, useLogger } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useLogger({ logFn: (eventName, args) => { diff --git a/packages/core/docs/use-masked-errors.md b/packages/core/docs/use-masked-errors.md deleted file mode 100644 index 628b6b16af..0000000000 --- a/packages/core/docs/use-masked-errors.md +++ /dev/null @@ -1,62 +0,0 @@ -#### `useMaskedErrors` - -Prevent unexpected error messages from leaking to the GraphQL clients. - -```ts -import { envelop, useSchema, useMaskedErrors, EnvelopError } from '@envelop/core' -import { makeExecutableSchema } from 'graphql' - -const schema = makeExecutableSchema({ - typeDefs: /* GraphQL */ ` - type Query { - something: String! - somethingElse: String! - somethingSpecial: String! - } - `, - resolvers: { - Query: { - something: () => { - throw new EnvelopError('Error that is propagated to the clients.') - }, - somethingElse: () => { - throw new Error("Unsafe error that will be masked as 'Unexpected Error.'.") - }, - somethingSpecial: () => { - throw new EnvelopError('The error will have an extensions field.', { - code: 'ERR_CODE', - randomNumber: 123 - }) - } - } - } -}) - -const getEnveloped = envelop({ - plugins: [useSchema(schema), useMaskedErrors()] -}) -``` - -You may customize the default error message `Unexpected error.` with your own `errorMessage`: - -```ts -const getEnveloped = envelop({ - plugins: [useSchema(schema), useMaskedErrors({ errorMessage: 'Something went wrong.' })] -}) -``` - -Or provide a custom formatter when masking the output: - -```ts -export const customFormatError: FormatErrorHandler = err => { - if (err.originalError && err.originalError instanceof EnvelopError === false) { - return new GraphQLError('Sorry, something went wrong.') - } - - return err -} - -const getEnveloped = envelop({ - plugins: [useSchema(schema), useMaskedErrors({ formatError: customFormatError })] -}) -``` diff --git a/packages/core/docs/use-payload-formatter.md b/packages/core/docs/use-payload-formatter.md index 46373890cd..b83748ec59 100644 --- a/packages/core/docs/use-payload-formatter.md +++ b/packages/core/docs/use-payload-formatter.md @@ -6,9 +6,13 @@ The second argument `executionArgs` provides additional information for your for ```ts import { envelop, usePayloadFormatter } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ usePayloadFormatter((result, executionArgs) => { // Return a modified result here, diff --git a/packages/core/docs/use-schema.md b/packages/core/docs/use-schema.md index 10d1b8719c..4671d37817 100644 --- a/packages/core/docs/use-schema.md +++ b/packages/core/docs/use-schema.md @@ -4,11 +4,15 @@ This plugin is the simplest plugin for specifying your GraphQL schema. You can s ```ts import { envelop, useSchema } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const mySchema = buildSchema(/* ... */) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(mySchema) // ... other plugins ... diff --git a/packages/core/docs/use-timing.md b/packages/core/docs/use-timing.md deleted file mode 100644 index 10443e899d..0000000000 --- a/packages/core/docs/use-timing.md +++ /dev/null @@ -1,24 +0,0 @@ -#### `useTiming` - -Simple time metric collection, for every phase in your execution. You can easily customize the behavior of each timing measurement. By default, the timing is printed to the log, using `console.log`. - -```ts -import { envelop, useTiming } from '@envelop/core' -import { buildSchema } from 'graphql' - -const getEnveloped = envelop({ - plugins: [ - useTiming({ - // All options are optional. By default it just print it to the log. - // ResultTiming is an object built with { ms, ns } (milliseconds and nanoseconds) - onContextBuildingMeasurement: (timing: ResultTiming) => {}, - onExecutionMeasurement: (args: ExecutionArgs, timing: ResultTiming) => {}, - onSubscriptionMeasurement: (args: SubscriptionArgs, timing: ResultTiming) => {}, - onParsingMeasurement: (source: Source | string, timing: ResultTiming) => {}, - onValidationMeasurement: (document: DocumentNode, timing: ResultTiming) => {}, - onResolverMeasurement: (info: GraphQLResolveInfo, timing: ResultTiming) => {} - }) - // ... other plugins ... - ] -}) -``` diff --git a/packages/plugins/apollo-datasources/README.md b/packages/plugins/apollo-datasources/README.md index 76ffc8135d..2debde0172 100644 --- a/packages/plugins/apollo-datasources/README.md +++ b/packages/plugins/apollo-datasources/README.md @@ -11,6 +11,7 @@ yarn add @envelop/apollo-datasources ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useApolloDataSources } from '@envelop/apollo-datasources' import { RESTDataSource } from 'apollo-datasource-rest' @@ -35,6 +36,10 @@ class MoviesAPI extends RESTDataSource { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloDataSources({ diff --git a/packages/plugins/apollo-server-errors/README.md b/packages/plugins/apollo-server-errors/README.md index 51625fd3e4..a598ac3c32 100644 --- a/packages/plugins/apollo-server-errors/README.md +++ b/packages/plugins/apollo-server-errors/README.md @@ -11,10 +11,15 @@ yarn add @envelop/apollo-server-errors ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'grapqhl' import { envelop } from '@envelop/core' import { useApolloServerErrors } from '@envelop/apollo-server-errors' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloServerErrors({ diff --git a/packages/plugins/apollo-tracing/README.md b/packages/plugins/apollo-tracing/README.md index eec2b253ef..969af079f0 100644 --- a/packages/plugins/apollo-tracing/README.md +++ b/packages/plugins/apollo-tracing/README.md @@ -17,10 +17,15 @@ yarn add @envelop/apollo-tracing ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useApolloTracing } from '@envelop/apollo-tracing' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloTracing() diff --git a/packages/plugins/auth0/README.md b/packages/plugins/auth0/README.md index de98028bb9..c87789787a 100644 --- a/packages/plugins/auth0/README.md +++ b/packages/plugins/auth0/README.md @@ -14,10 +14,15 @@ We recommend using the [Adding Authentication with Auth0 guide](https://www.enve 4. Setup Envelop with that plugin: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useAuth0 } from '@envelop/auth0' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useAuth0({ diff --git a/packages/plugins/dataloader/README.md b/packages/plugins/dataloader/README.md index 5658f9aed5..5c41022461 100644 --- a/packages/plugins/dataloader/README.md +++ b/packages/plugins/dataloader/README.md @@ -11,11 +11,16 @@ yarn add dataloader @envelop/dataloader ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import DataLoader from 'dataloader' import { useDataLoader } from '@envelop/dataloader' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useDataLoader('users', context => new DataLoader(keys => myBatchGetUsers(keys))) diff --git a/packages/plugins/depth-limit/README.md b/packages/plugins/depth-limit/README.md index 90efe640eb..589ac67e4b 100644 --- a/packages/plugins/depth-limit/README.md +++ b/packages/plugins/depth-limit/README.md @@ -11,10 +11,15 @@ yarn add @envelop/depth-limit ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useDepthLimit } from '@envelop/depth-limit' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useDepthLimit({ diff --git a/packages/plugins/disable-introspection/README.md b/packages/plugins/disable-introspection/README.md index 8c546322e3..c0a7578c80 100644 --- a/packages/plugins/disable-introspection/README.md +++ b/packages/plugins/disable-introspection/README.md @@ -11,10 +11,15 @@ yarn add @envelop/disable-introspection ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useDisableIntrospection } from '@envelop/disable-introspection' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [useDisableIntrospection()] }) ``` diff --git a/packages/plugins/execute-subscription-event/README.md b/packages/plugins/execute-subscription-event/README.md index b4849c3e41..715f06d42a 100644 --- a/packages/plugins/execute-subscription-event/README.md +++ b/packages/plugins/execute-subscription-event/README.md @@ -7,11 +7,16 @@ Utilities for hooking into the [ExecuteSubscriptionEvent]( createContext()), useContextValuePerExecuteSubscriptionEvent(() => ({ diff --git a/packages/plugins/extended-validation/README.md b/packages/plugins/extended-validation/README.md index d1b90ca308..fbaded9aba 100644 --- a/packages/plugins/extended-validation/README.md +++ b/packages/plugins/extended-validation/README.md @@ -17,9 +17,15 @@ yarn add @envelop/extended-validation Then, use the plugin with your validation rules: ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' import { useExtendedValidation } from '@envelop/extended-validation' -const getEnveloped = evelop({ +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useExtendedValidation({ rules: [ diff --git a/packages/plugins/filter-operation-type/README.md b/packages/plugins/filter-operation-type/README.md index 86b080ac0c..eb02bddb1f 100644 --- a/packages/plugins/filter-operation-type/README.md +++ b/packages/plugins/filter-operation-type/README.md @@ -11,9 +11,15 @@ yarn add @envelop/filter-operation-type ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useFilterAllowedOperations } from '@envelop/filter-operation-type' + const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, // only allow execution of subscription operations plugins: [useFilterAllowedOperations(['subscription'])] }) diff --git a/packages/plugins/fragment-arguments/README.md b/packages/plugins/fragment-arguments/README.md index fac953a4b1..ed8ce1e90d 100644 --- a/packages/plugins/fragment-arguments/README.md +++ b/packages/plugins/fragment-arguments/README.md @@ -15,10 +15,15 @@ yarn add @envelop/fragment-arguments ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useFragmentArguments } from '@envelop/fragment-arguments' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useFragmentArguments() diff --git a/packages/plugins/generic-auth/README.md b/packages/plugins/generic-auth/README.md index 3908f0e76b..ba31b63414 100644 --- a/packages/plugins/generic-auth/README.md +++ b/packages/plugins/generic-auth/README.md @@ -78,6 +78,7 @@ This mode offers complete protection for the entire API. It protects your entire To setup this mode, use the following config: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useGenericAuth, ResolveUserFn, ValidateUserFn } from '@envelop/generic-auth' @@ -92,6 +93,10 @@ const validateUser: ValidateUserFn = params => { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useGenericAuth({ diff --git a/packages/plugins/graphql-jit/README.md b/packages/plugins/graphql-jit/README.md index 9997b13bd5..45ec30327b 100644 --- a/packages/plugins/graphql-jit/README.md +++ b/packages/plugins/graphql-jit/README.md @@ -11,10 +11,15 @@ yarn add @envelop/graphql-jit ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useGraphQlJit } from '@envelop/graphql-jit' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useGraphQlJit( diff --git a/packages/plugins/graphql-middleware/README.md b/packages/plugins/graphql-middleware/README.md index 6b285e41f9..85c7503702 100644 --- a/packages/plugins/graphql-middleware/README.md +++ b/packages/plugins/graphql-middleware/README.md @@ -15,6 +15,7 @@ yarn add graphql-middleware @envelop/graphql-middleware You can use any type of middleware defined for `graphql-middleware`, here's an example for doing that with [`graphql-shield`](https://github.com/maticzav/graphql-shield): ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useGraphQLMiddleware } from '@envelop/graphql-middleware' import { rule, shield, and, or, not } from 'graphql-shield' @@ -35,6 +36,10 @@ const permissions = shield({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useSchema(mySchema), diff --git a/packages/plugins/graphql-modules/README.md b/packages/plugins/graphql-modules/README.md index 9b1218cdd2..fe3c256a2a 100644 --- a/packages/plugins/graphql-modules/README.md +++ b/packages/plugins/graphql-modules/README.md @@ -13,6 +13,7 @@ yarn add @envelop/graphql-modules ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { createApplication } from 'graphql-modules' import { useGraphQLModules } from '@envelop/graphql-modules' @@ -24,6 +25,10 @@ const myApp = createApplication({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useGraphQLModules(myApp) diff --git a/packages/plugins/immediate-introspection/README.md b/packages/plugins/immediate-introspection/README.md index 2dca89d0e4..5f3d0f1d07 100644 --- a/packages/plugins/immediate-introspection/README.md +++ b/packages/plugins/immediate-introspection/README.md @@ -1 +1,49 @@ ## `@envelop/immediate-introspection` + +## Getting Started + +``` +yarn add @envelop/immediate-introspection +``` + +## Usage Example + +Context building can be costly and require calling remote services. +For simple GraphQL operations that only select introspection fields building a context is not necessary. + +The `useImmediateIntrospection` can be used to short circuit any further context building if a GraphQL operation selection set only includes introspection fields within the selection set. + +```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop, useImmediateIntrospection } from '@envelop/core' +import { schema } from './schema' + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [ + useSchema(schema), + useImmediateIntrospection() + // additional plugins + ] +}) +``` + +In case you want to authorize that an user is authenticated before allowing introspection the plugin must be placed in front of the `useImmediateIntrospection()` call. + +```ts +import { envelop, useImmediateIntrospection } from '@envelop/core' +import { schema } from './schema' +import { useAuthorization } from './useAuthorization' + +const getEnveloped = envelop({ + plugins: [ + useSchema(schema), + useAuthorization(), // place this before + useImmediateIntrospection() + // additional plugins + ] +}) +``` diff --git a/packages/plugins/live-query/README.md b/packages/plugins/live-query/README.md index 990cf39750..3ba0688d77 100644 --- a/packages/plugins/live-query/README.md +++ b/packages/plugins/live-query/README.md @@ -27,6 +27,7 @@ yarn add @envelop/live-query @n1ru4l/in-memory-live-query-store ### `makeExecutableSchema` from `graphql-tools` ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop, useSchema, useExtendContext } from '@envelop/core' import { useLiveQuery } from '@envelop/live-query' import { InMemoryLiveQueryStore } from '@n1ru4l/in-memory-live-query-store' @@ -59,6 +60,10 @@ setInterval(() => { }, 1000) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(schema), useLiveQuery({ liveQueryStore }), diff --git a/packages/plugins/newrelic/README.md b/packages/plugins/newrelic/README.md index d98218ea10..75867c60ce 100644 --- a/packages/plugins/newrelic/README.md +++ b/packages/plugins/newrelic/README.md @@ -31,10 +31,15 @@ yarn add newrelic @envelop/newrelic ## Basic usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useNewRelic } from '@envelop/newrelic' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useNewRelic({ diff --git a/packages/plugins/opentelemetry/README.md b/packages/plugins/opentelemetry/README.md index 7b94d8dc5f..f46a3ec180 100644 --- a/packages/plugins/opentelemetry/README.md +++ b/packages/plugins/opentelemetry/README.md @@ -15,10 +15,15 @@ yarn add @envelop/opentelemetry By default, this plugin prints the collected telemetry to the console: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useOpenTelemetry } from '@envelop/opentelemetry' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useOpenTelemetry({ @@ -33,6 +38,7 @@ const getEnveloped = envelop({ If you wish to use custom tracer/exporter, create it and pass it. This example integrates Jaeger tracer: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useOpenTelemetry } from '@envelop/opentelemetry' import { JaegerExporter } from '@opentelemetry/exporter-jaeger' @@ -47,6 +53,10 @@ provider.addSpanProcessor(new SimpleSpanProcessor(exporter)) provider.register() const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useOpenTelemetry( diff --git a/packages/plugins/operation-field-permissions/README.md b/packages/plugins/operation-field-permissions/README.md index 6bd673f814..05066c8875 100644 --- a/packages/plugins/operation-field-permissions/README.md +++ b/packages/plugins/operation-field-permissions/README.md @@ -13,10 +13,15 @@ yarn add @envelop/operation-field-permissions ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop, useSchema } from '@envelop/core' import { useOperationFieldPermissions } from '@envelop/operation-field-permissions' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(schema), useOperationFieldPermissions({ diff --git a/packages/plugins/parser-cache/README.md b/packages/plugins/parser-cache/README.md index db0666321f..d39cab4b64 100644 --- a/packages/plugins/parser-cache/README.md +++ b/packages/plugins/parser-cache/README.md @@ -13,10 +13,15 @@ yarn add @envelop/parser-cache ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useParserCache } from '@envelop/parser-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useParserCache({ diff --git a/packages/plugins/persisted-operations/README.md b/packages/plugins/persisted-operations/README.md index 5f25519e52..9536163f7c 100644 --- a/packages/plugins/persisted-operations/README.md +++ b/packages/plugins/persisted-operations/README.md @@ -15,6 +15,7 @@ yarn add @envelop/persisted-operations The most basic implementation can use an in-memory JS `Map` wrapper with a `Store` object: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { usePersistedOperations, InMemoryStore } from '@envelop/persisted-operations' @@ -28,6 +29,10 @@ const store = new InMemoryStore({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePersistedOperations({ @@ -58,6 +63,8 @@ usePersistedOperations({ ## Usage Example with built-in JsonFileStore ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' import { usePersistedOperations, JsonFileStore } from '@envelop/persisted-operations' const persistedOperationsStore = new JsonFilesStore() @@ -70,6 +77,10 @@ persistedOperationsStore.loadFromFileSync(filePath) // load and parse persisted- await persistedOperationsStore.loadFromFile(filePath) // load and parse persisted-operations files const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePersistedOperations({ @@ -84,7 +95,14 @@ const getEnveloped = envelop({ The `store` parameter accepts both a `Store` instance, or a function. If you need to support multiple stores (based on incoming GraphQL operation/HTTP request), you can provide a function to toggle between the stores, based on your needs: ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' + const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePersistedOperations({ diff --git a/packages/plugins/preload-assets/README.md b/packages/plugins/preload-assets/README.md index b3d46d0e36..50fd866652 100644 --- a/packages/plugins/preload-assets/README.md +++ b/packages/plugins/preload-assets/README.md @@ -12,6 +12,7 @@ yarn add @envelop/preload-assets ``` ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { usePreloadAssets } from '@envelop/preload-asset' import { makeExecutableSchema } from 'graphql' @@ -34,6 +35,10 @@ const schema = makeExecutableSchema({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [usePreloadAssets()] }) ``` diff --git a/packages/plugins/prometheus/README.md b/packages/plugins/prometheus/README.md index c182ca0762..6583158ec9 100644 --- a/packages/plugins/prometheus/README.md +++ b/packages/plugins/prometheus/README.md @@ -26,10 +26,15 @@ yarn add prom-client @envelop/prometheus ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { usePrometheus } from '@envelop/prometheus' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePrometheus({ @@ -57,11 +62,17 @@ const getEnveloped = envelop({ You can customize the `prom-client` `Registry` object if you are using a custom one, by passing it along with the configuration object: ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' import { Registry } from 'prom-client' const myRegistry = new Registry() const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePrometheus({ @@ -83,11 +94,16 @@ If you wish to disable introspection logging, you can use `skipIntrospection: tr Each tracing field supports custom `prom-client` objects, and custom `labels` a metadata, you can create a custom extraction function for every `Histogram` / `Summary` / `Counter`: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { Histogram } from 'prom-client' import { envelop } from '@envelop/core' import { createHistogram, usePrometheus } from '@envelop/prometheus' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePrometheus({ diff --git a/packages/plugins/rate-limiter/README.md b/packages/plugins/rate-limiter/README.md index 8aeeed98be..4cb201849c 100644 --- a/packages/plugins/rate-limiter/README.md +++ b/packages/plugins/rate-limiter/README.md @@ -11,6 +11,7 @@ yarn add @envelop/rate-limiter ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useRateLimiter, IdentifyFn } from '@envelop/rate-limiter' @@ -19,6 +20,10 @@ const identifyFn: IdentifyFn = async context => { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useRateLimiter({ diff --git a/packages/plugins/resource-limitations/README.md b/packages/plugins/resource-limitations/README.md index d047f895df..8e72088dad 100644 --- a/packages/plugins/resource-limitations/README.md +++ b/packages/plugins/resource-limitations/README.md @@ -11,10 +11,15 @@ yarn add @envelop/resource-limitations ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResourceLimitations } from '@envelop/resource-limitations' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResourceLimitations({ diff --git a/packages/plugins/response-cache-redis/README.md b/packages/plugins/response-cache-redis/README.md index 98b8f5fb8b..62844e05d2 100644 --- a/packages/plugins/response-cache-redis/README.md +++ b/packages/plugins/response-cache-redis/README.md @@ -22,6 +22,7 @@ In order to use the Redis cache, you need to: - Create an instance of the Redis Cache and set to the `useResponseCache` plugin options ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' import { createRedisCache } from '@envelop/response-cache-redis' @@ -44,6 +45,10 @@ const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652') const cache = createRedisCache({ redis }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ cache }) @@ -54,6 +59,7 @@ const getEnveloped = envelop({ ### Invalidate Cache based on custom logic ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' import { createRedisCache } from '@envelop/response-cache-redis' @@ -66,6 +72,10 @@ const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652') const cache = createRedisCache({ redis }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ diff --git a/packages/plugins/response-cache/README.md b/packages/plugins/response-cache/README.md index c6cc036e70..145b1ee605 100644 --- a/packages/plugins/response-cache/README.md +++ b/packages/plugins/response-cache/README.md @@ -38,10 +38,15 @@ When configuring the `useResponseCache`, you can choose the type of cache: The in-memory LRU cache is used by default. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -55,12 +60,17 @@ const getEnveloped = envelop({ Or, you may create the in-memory LRU cache explicitly. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache, createInMemoryCache } from '@envelop/response-cache' const cache = createInMemoryCache() const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -76,10 +86,15 @@ const getEnveloped = envelop({ ### Cache based on session/user ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -105,6 +120,7 @@ In order to use the Redis cache, you need to: - Create an instance of the Redis Cache and set to the `useResponseCache` plugin options ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' import { createRedisCache } from '@envelop/response-cache-redis' @@ -122,6 +138,10 @@ const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652') const cache = createRedisCache({ redis }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -139,10 +159,15 @@ const getEnveloped = envelop({ ### Cache with maximum TTL ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -158,10 +183,15 @@ const getEnveloped = envelop({ ### Cache with custom TTL per object type ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -178,10 +208,15 @@ const getEnveloped = envelop({ ### Cache with custom TTL per schema coordinate ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -198,10 +233,15 @@ const getEnveloped = envelop({ ### Disable cache based on session/user ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -228,6 +268,7 @@ cache results with certain error types. By default, the `defaultShouldCacheResult` function is used which never caches any query operation execution results that includes any errors (unexpected, EnvelopError, or GraphQLError). ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache, ShouldCacheResultFunction } from '@envelop/response-cache' @@ -238,6 +279,10 @@ export const defaultShouldCacheResult: ShouldCacheResultFunction = (params): Boo } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -255,10 +300,15 @@ By default introspection query operations are not cached. In case you want to ca **Infinite caching** ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -274,10 +324,15 @@ const getEnveloped = envelop({ **TTL caching** ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -293,10 +348,15 @@ const getEnveloped = envelop({ ### Cache with maximum TTL ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -310,10 +370,15 @@ const getEnveloped = envelop({ ### Customize the fields that are used for building the cache ID ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -329,10 +394,15 @@ const getEnveloped = envelop({ ### Disable automatic cache invalidation via mutations ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -348,6 +418,7 @@ const getEnveloped = envelop({ ### Invalidate Cache based on custom logic ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache, createInMemoryCache } from '@envelop/response-cache' import { emitter } from './eventEmitter' @@ -356,6 +427,10 @@ import { emitter } from './eventEmitter' const cache = createInMemoryCache() const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -380,6 +455,7 @@ emitter.on('invalidate', resource => { ### Customize how cache ids are built ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache, createInMemoryCache } from '@envelop/response-cache' import { emitter } from './eventEmitter' @@ -391,6 +467,10 @@ const cache = createInMemoryCache({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -408,7 +488,14 @@ const getEnveloped = envelop({ For debugging or monitoring it might be useful to know whether a response got served from the cache or not. ```ts +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop } from '@envelop/core' + const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ diff --git a/packages/plugins/sentry/README.md b/packages/plugins/sentry/README.md index 9f95bce9fa..c7f882b77d 100644 --- a/packages/plugins/sentry/README.md +++ b/packages/plugins/sentry/README.md @@ -30,12 +30,17 @@ yarn add @sentry/node @sentry/tracing @envelop/sentry ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useSentry } from '@envelop/sentry' // do this only once in you entry file. import '@sentry/tracing' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useSentry({ diff --git a/packages/plugins/statsd/README.md b/packages/plugins/statsd/README.md index 107a203582..581545dd92 100644 --- a/packages/plugins/statsd/README.md +++ b/packages/plugins/statsd/README.md @@ -25,6 +25,7 @@ yarn add hot-shots @envelop/stats ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useStatsD } from '@envelop/statsd' import StatsD from 'hot-shots' @@ -35,6 +36,10 @@ const client = new StatsD({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useStatsD({ diff --git a/packages/plugins/validation-cache/README.md b/packages/plugins/validation-cache/README.md index 9f2fec7501..db52e73d06 100644 --- a/packages/plugins/validation-cache/README.md +++ b/packages/plugins/validation-cache/README.md @@ -13,10 +13,15 @@ yarn add @envelop/validation-cache ## Usage Example ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useValidationCache } from '@envelop/validation-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useValidationCache({ diff --git a/website/docs/README.mdx b/website/docs/README.mdx index de4ac8abd7..8cf5bf7c3d 100644 --- a/website/docs/README.mdx +++ b/website/docs/README.mdx @@ -18,7 +18,6 @@ Plugins allow hooking into all GraphQL phases such as `parse`, `validate`, `exec | Example Plugin | Description | | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | | [`useLogger`](/plugins/use-logger) | Hooks into "before" of all phases, and prints the execution parameters to the console using `console.log` | -| [`useTiming`](/plugins/use-timing) | Hooks into "before" and "after" of all phases, measures times, and then prints them | | [`useParserCache`](/plugins/use-parser-cache) | Hooks into "before" and "after" of the `parse` phase and implements caching based on the operation string | | [`useOpenTelemetry`](/plugins/use-open-telemetry) | Hooks into all phases, execution and resolvers, and creates Spans for OpenTelemetry performance tracing | diff --git a/website/docs/core.mdx b/website/docs/core.mdx index e0e796e5c2..35f23a91d3 100644 --- a/website/docs/core.mdx +++ b/website/docs/core.mdx @@ -12,11 +12,15 @@ This plugin is the simplest plugin for specifying your GraphQL schema. You can s ```ts import { envelop, useSchema } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const mySchema = buildSchema(/* ... */) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(mySchema) // ... other plugins ... @@ -32,13 +36,17 @@ If you are using a framework that creates the schema in an async way, you can ei ```ts import { envelop, useAsyncSchema } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getSchema = async (): Promise => { // return schema when it's ready } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useAsyncSchema(getSchema()) // ... other plugins ... @@ -52,9 +60,13 @@ This plugin invokes a custom function with the every time execution encounters a ```ts import { envelop, useErrorHandler } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useErrorHandler(error => { // This callback is called per each GraphQLError emitted during execution phase @@ -72,9 +84,13 @@ Easily extends the context with custom fields. ```ts import { envelop, useExtendContext } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useExtendContext(async contextSoFar => { return { @@ -94,9 +110,13 @@ Logs parameters and information about the execution phases. You can easily plug ```ts import { envelop, useLogger } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useLogger({ logFn: (eventName, args) => { @@ -117,9 +137,13 @@ The second argument `executionArgs` provides additional information for your for ```ts import { envelop, usePayloadFormatter } from '@envelop/core' -import { buildSchema } from 'graphql' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ usePayloadFormatter((result, executionArgs) => { // Return a modified result here, @@ -130,70 +154,6 @@ const getEnveloped = envelop({ }) ``` -#### useTiming - -It's a simple time metric collection for every phase in your execution. You can easily customize the behavior of each timing measurement. By default, the timing is printed to the log, using `console.log`. - -```ts -import { envelop, useTiming } from '@envelop/core' -import { buildSchema } from 'graphql' - -const getEnveloped = envelop({ - plugins: [ - useTiming({ - // All options are optional. By default it just print it to the log. - // ResultTiming is an object built with { ms, ns } (milliseconds and nanoseconds) - onContextBuildingMeasurement: (timing: ResultTiming) => {}, - onExecutionMeasurement: (args: ExecutionArgs, timing: ResultTiming) => {}, - onSubscriptionMeasurement: (args: SubscriptionArgs, timing: ResultTiming) => {}, - onParsingMeasurement: (source: Source | string, timing: ResultTiming) => {}, - onValidationMeasurement: (document: DocumentNode, timing: ResultTiming) => {}, - onResolverMeasurement: (info: GraphQLResolveInfo, timing: ResultTiming) => {} - }) - // ... other plugins ... - ] -}) -``` - -#### useMaskedErrors - -Prevent unexpected error messages from leaking to the GraphQL API consumers. - -```ts -import { envelop, useSchema, useMaskedErrors, EnvelopError } from '@envelop/core' -import { makeExecutableSchema } from 'graphql' - -const schema = makeExecutableSchema({ - typeDefs: /* GraphQL */ ` - type Query { - something: String! - somethingElse: String! - somethingSpecial: String! - } - `, - resolvers: { - Query: { - something: () => { - throw new EnvelopError('Error that is propagated to the clients.') - }, - somethingElse: () => { - throw new Error("Unsafe error that will be masked as 'Unexpected Error.'.") - }, - somethingSpecial: () => { - throw new EnvelopError('The error will have an extensions field.', { - code: 'ERR_CODE', - randomNumber: 123 - }) - } - } - } -}) - -const getEnveloped = envelop({ - plugins: [useSchema(schema), useMaskedErrors()] -}) -``` - ### Utilities #### enableIf @@ -202,10 +162,15 @@ This utility is helpful when you want to enable a plugin only when a certain con ```ts import { envelop, useMaskedErrors, enableIf } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' const isProd = process.env.NODE_ENV === 'production' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // This plugin is enabled only in production enableIf(isProd, useMaskedErrors()), diff --git a/website/docs/getting-started.mdx b/website/docs/getting-started.mdx index 0674119e46..cc5df19dd3 100644 --- a/website/docs/getting-started.mdx +++ b/website/docs/getting-started.mdx @@ -17,6 +17,7 @@ Start by adding the core of `envelop` and `graphql` to your codebase. After installing the `@envelop/core` package, you can use the `envelop` function for creating your `getEnveloped` function. We use a simple GraphQL schema that we build with the `buildSchema` function from `graphql`. ```ts +import { parse, validate, execute, subcribe } from 'graphql' import { envelop, useSchema } from '@envelop/core' import { buildSchema } from 'graphql' @@ -27,6 +28,10 @@ const schema = buildSchema(/* GraphQL */ ` `) export const getEnveloped = envelop({ + parse, + validate, + execute, + subcribe, plugins: [useSchema(schema)] }) ``` @@ -58,6 +63,7 @@ Let's add a parser and validation cache, so sending the same operation string se ```ts +import { parse, validate, execute, subcribe } from 'graphql' import { envelop, useSchema } from '@envelop/core' import { buildSchema } from 'graphql' import { useParserCache } from '@envelop/parser-cache' @@ -70,6 +76,10 @@ const schema = buildSchema(/* GraphQL */ ` `) const getEnveloped = envelop({ + parse, + validate, + execute, + subcribe, plugins: [ // all enabled plugins useSchema(schema), diff --git a/website/docs/guides/adding-a-graphql-response-cache.mdx b/website/docs/guides/adding-a-graphql-response-cache.mdx index 9675fa83d1..3dc8ec2e05 100644 --- a/website/docs/guides/adding-a-graphql-response-cache.mdx +++ b/website/docs/guides/adding-a-graphql-response-cache.mdx @@ -297,10 +297,15 @@ const getEnveloped = envelop({ Don't want to automatically invalidate based on mutations? Also configurable! ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ @@ -315,9 +320,7 @@ Want a global cache on Redis? Maybe you are in a server-less environment and the In-Memory Cache isn't an option. Also, when having multiple server replicas, you might want to have a shared cache between all the replicas. -```bash -yarn add @envelop/response-cache-redis -``` + First create a Redis database with your favorite hosting provider. @@ -328,6 +331,7 @@ Then, with that instance of the Redis Cache setup, provide it to the `useRespons Here's an example: ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useResponseCache } from '@envelop/response-cache' import { createRedisCache } from '@envelop/response-cache-redis' @@ -344,6 +348,10 @@ const redis = new Redis('rediss://:1234567890@my-redis-db.example.com:30652') const cache = createRedisCache({ redis }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useResponseCache({ cache }) diff --git a/website/docs/guides/adding-authentication-with-auth0.mdx b/website/docs/guides/adding-authentication-with-auth0.mdx index b6ee29c053..cec80c0095 100644 --- a/website/docs/guides/adding-authentication-with-auth0.mdx +++ b/website/docs/guides/adding-authentication-with-auth0.mdx @@ -24,10 +24,15 @@ import { PackageInstall } from '@guild-docs/client' ```ts import { useAuth0 } from '@envelop/auth0' +import { parse, execute, subcribe, validate } from 'graphql' // ... other imports and code const getEnveloped = envelop({ + parse, + execute, + subcribe, + validate, plugins: [ useSchema(schema), useAuth0({ diff --git a/website/docs/guides/integrating-with-databases.md b/website/docs/guides/integrating-with-databases.md index 4d34be690b..908ed8f4c4 100644 --- a/website/docs/guides/integrating-with-databases.md +++ b/website/docs/guides/integrating-with-databases.md @@ -46,6 +46,7 @@ The better way to avoid this is to open only one client per request. With envelo a plugin which adds a client to the context add release it at the end of the request execution. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { isAsyncIterable } from '@envelop/core' import { useSchema } from './use-schema' @@ -73,6 +74,10 @@ const resolvers = { } const getEnvelop = envelop({ + parse, + validate, + execute, + subscribe, plugins: [useSchema(/*...*/), databaseClientPlugin] }) ``` diff --git a/website/docs/guides/monitoring-and-tracing.mdx b/website/docs/guides/monitoring-and-tracing.mdx index b5754b971f..3775ffc014 100644 --- a/website/docs/guides/monitoring-and-tracing.mdx +++ b/website/docs/guides/monitoring-and-tracing.mdx @@ -17,10 +17,15 @@ Sentry is the biggest player regarding error tracking within JavaScript land. Wi As with any other envelop plugin the setup is straight forward! ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useSentry } from '@envelop/sentry' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useSentry() @@ -53,10 +58,15 @@ If you wish to integrate NewRelic for tracing, monitoring and error reporting, y As with any other envelop plugin the setup is straight forward! ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useNewRelic } from '@envelop/newrelic' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useNewRelic({ @@ -73,10 +83,15 @@ const getEnveloped = envelop({ Apollo introduced the apollo-tracing specification and implemented it in apollo-server. With envelop it is possible to use apollo-tracing for tracking down slow resolvers with any server. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useApolloTracing } from '@envelop/apollo-tracing' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloTracing() diff --git a/website/docs/guides/resolving-subscription-data-loader-caching-issues.mdx b/website/docs/guides/resolving-subscription-data-loader-caching-issues.mdx index 6e05565a05..cf50e67bb9 100644 --- a/website/docs/guides/resolving-subscription-data-loader-caching-issues.mdx +++ b/website/docs/guides/resolving-subscription-data-loader-caching-issues.mdx @@ -51,11 +51,16 @@ const GraphQLSubscriptionType = new GraphQLObjectType({ As your project scales this, however, can become a tedious task. With the `useContextValuePerExecuteSubscriptionEvent` plugin we abstracted this away by having a generic solution for extending the original context with a new partial before the subscription event is being executed. ```ts +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useContextValuePerExecuteSubscriptionEvent } from '@envelop/execute-subscription-event' import { createContext, createDataLoaders } from './context' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useContext(() => createContext()), diff --git a/website/docs/guides/securing-your-graphql-api.mdx b/website/docs/guides/securing-your-graphql-api.mdx index 5c81633415..3b36e1a7c9 100644 --- a/website/docs/guides/securing-your-graphql-api.mdx +++ b/website/docs/guides/securing-your-graphql-api.mdx @@ -23,9 +23,15 @@ With the `@envelop/auth0` plugin, you can simply bootstrap the authorization pro ```tsx import { envelop, useExtendContext } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' import { useAuth0 } from '@envelop/auth0' import { schema } from './schema' + const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(schema), useAuth0({ @@ -79,6 +85,7 @@ Libraries such as [`graphql-public-schema-filter`](https://github.com/n1ru4l/gra ```ts import { envelop, useLazyLoadedSchema } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' import { privateSchema, publicSchema } from './schema' const getEnveloped = envelop({ @@ -100,6 +107,10 @@ import { envelop, useSchema } from '@envelop/core' import { useOperationFieldPermissions } from '@envelop/operation-field-permissions' import { schema } from './schema' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins (e.g. useAuth0) useSchema(schema), @@ -148,7 +159,7 @@ In most GraphQL servers any thrown error or rejected promise will result in the ```tsx import { envelop, useSchema, useMaskedErrors, EnvelopError } from '@envelop/core' -import { makeExecutableSchema } from 'graphql' +import { makeExecutableSchema, parse, validate, execute, subscribe } from 'graphql' const schema = makeExecutableSchema({ typeDefs: /* GraphQL */ ` @@ -177,6 +188,10 @@ const schema = makeExecutableSchema({ }) const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [useSchema(schema), useMaskedErrors()] }) ``` @@ -189,9 +204,14 @@ If your schema includes sensitive information that you want to hide from the out ```ts import { envelop } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' import { useDisableIntrospection } from '@envelop/disable-introspection' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [useDisableIntrospection()] }) ``` @@ -264,6 +284,7 @@ With the [`usePersistedOperations`](/plugins/use-persisted-operations) plugin su ```ts import { envelop } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' import { usePersistedOperations, PersistedOperationsStore } from '@envelop/persisted-operations' import persistedOperations from './codegen-artifact' @@ -273,6 +294,10 @@ const store: PersistedOperationsStore = { } const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... usePersistedOperations({ @@ -294,8 +319,13 @@ The [`useDepthLimit`](/plugins/use-depth-limit) allows a maximum nesting level a ```ts import { envelop } from '@envelop/core' import { useDepthLimit } from '@envelop/depth-limit' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useDepthLimit({ @@ -315,6 +345,7 @@ The [`useRateLimiter`](/plugins/use-rate-limiter) to limit access to resources, ```ts import { envelop } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' import { useRateLimiter } from '@envelop/rate-limiter' const getEnveloped = envelop({ diff --git a/website/docs/plugins/testing.mdx b/website/docs/plugins/testing.mdx index 07a2d4056a..b06a7c6992 100644 --- a/website/docs/plugins/testing.mdx +++ b/website/docs/plugins/testing.mdx @@ -9,7 +9,7 @@ The Envelop testkit can also help you to test your envelop plugins in a headless To get stated with the Envelop testkit, make sure to install it in your project (as a `devDependency`): - yarn add -D @envelop/testing + To get started with your `envelop` testing, make sure first to setup your favorite test runner in your project (we use [Jest](https://jestjs.io/)). From d8be66290c849ef59d59f0dbf73f80913cb2e1cb Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Wed, 24 Aug 2022 15:14:58 -0400 Subject: [PATCH 22/61] test matrix for core --- .github/workflows/tests.yml | 33 +++++++++++++++++++++++++++++---- package.json | 1 + 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 27efc754c3..3225816db6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,13 +48,38 @@ jobs: run: yarn ts:check unit: - name: unit / ${{matrix.os}} / node v${{matrix.node-version}} / graphql v${{matrix.graphql_version}} + name: Unit Test + runs-on: ${{matrix.os}} + strategy: + matrix: + os: [ubuntu-latest] # remove windows to speed up the tests + steps: + - name: Checkout Master + uses: actions/checkout@v2 + - name: Setup env + uses: the-guild-org/shared-config/setup@main + with: + nodeVersion: 18 + - name: Install Dependencies + run: yarn install --ignore-engines && git checkout yarn.lock + - name: Cache Jest + uses: actions/cache@v2 + with: + path: .cache/jest + key: ${{ runner.os }}-${{matrix.node-version}}-${{matrix.graphql_version}}-jest-${{ hashFiles('yarn.lock') }}-${{ hashFiles('patches/*.patch') }} + - name: Test + run: yarn test + env: + CI: true + + core: + name: Core Test / ${{matrix.os}} / node v${{matrix.node-version}} / graphql v${{matrix.graphql_version}} runs-on: ${{matrix.os}} strategy: matrix: os: [ubuntu-latest] # remove windows to speed up the tests node-version: [12, 17] - graphql_version: [15, 16] + graphql_version: [15, 16, 'npm:@graphql-tools/graphql@0.1.0-alpha-20220815193214-83898018'] steps: - name: Checkout Master uses: actions/checkout@v2 @@ -71,8 +96,8 @@ jobs: with: path: .cache/jest key: ${{ runner.os }}-${{matrix.node-version}}-${{matrix.graphql_version}}-jest-${{ hashFiles('yarn.lock') }}-${{ hashFiles('patches/*.patch') }} - - name: Test - run: yarn test --ci + - name: Test Core + run: yarn test:core --ci env: CI: true diff --git a/package.json b/package.json index 32a7345e21..df46559c1b 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "build": "bob build", "ts:check": "tsc --noEmit", "test": "jest", + "test:core": "jest ./packages/core --coverage", "test:ci": "jest --coverage", "prerelease": "yarn build", "release": "changeset publish", From c1274cdef10a38f1eeb198bc0e2ad4f4a53a8650 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Wed, 31 Aug 2022 17:11:07 -0400 Subject: [PATCH 23/61] experimenting traced schema (#1501) * experimenting traced schema * Fix * remove comment * do optional chaining since we are not strongly typing * document * cleanup Co-authored-by: Arda TANRIKULU --- packages/core/src/index.ts | 2 +- packages/core/src/orchestrator.ts | 4 +- .../src/traced-schema.ts | 33 +++++- packages/core/test/execute.spec.ts | 102 ++++++++++++++++++ packages/plugins/apollo-tracing/src/index.ts | 4 - packages/plugins/opentelemetry/src/index.ts | 4 - .../opentelemetry/src/traced-schema.ts | 75 ------------- .../test/use-open-telemetry.spec.ts | 5 +- packages/plugins/prometheus/src/index.ts | 2 - .../plugins/prometheus/src/traced-schema.ts | 75 ------------- packages/plugins/rate-limiter/src/index.ts | 4 - .../plugins/rate-limiter/src/traced-schema.ts | 75 ------------- website/docs/plugins/lifecycle.mdx | 6 ++ 13 files changed, 141 insertions(+), 250 deletions(-) rename packages/{plugins/apollo-tracing => core}/src/traced-schema.ts (66%) delete mode 100644 packages/plugins/opentelemetry/src/traced-schema.ts delete mode 100644 packages/plugins/prometheus/src/traced-schema.ts delete mode 100644 packages/plugins/rate-limiter/src/traced-schema.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4ee40cd61f..e72b08d3e6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -8,4 +8,4 @@ export * from './plugins/use-error-handler.js'; export * from './plugins/use-extend-context.js'; export * from './plugins/use-payload-formatter.js'; export * from './enable-if.js'; -export { resolversHooksSymbol } from './orchestrator.js'; +export { resolversHooksSymbol } from './traced-schema.js'; diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index da4b2476d3..714c67676c 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -32,6 +32,7 @@ import { ValidateFunction, ExecutionResult, } from '@envelop/types'; +import { prepareTracedSchema, resolversHooksSymbol } from './traced-schema.js'; import { errorAsyncIterator, finalAsyncIterator, @@ -61,7 +62,6 @@ type EnvelopOrchestratorOptions = { subscribe: SubscribeFunction; validate: ValidateFunction; }; -export const resolversHooksSymbol = Symbol('RESOLVERS_HOOKS'); export function createEnvelopOrchestrator({ plugins, @@ -84,7 +84,7 @@ export function createEnvelopOrchestrator // here not to call the same plugin that initiated the schema switch. const replaceSchema = (newSchema: any, ignorePluginIndex = -1) => { if (onResolversHandlers.length) { - // prepareTracedSchema(newSchema); + prepareTracedSchema(newSchema); } schema = newSchema; diff --git a/packages/plugins/apollo-tracing/src/traced-schema.ts b/packages/core/src/traced-schema.ts similarity index 66% rename from packages/plugins/apollo-tracing/src/traced-schema.ts rename to packages/core/src/traced-schema.ts index 3e33a07888..d1bb33d9de 100644 --- a/packages/plugins/apollo-tracing/src/traced-schema.ts +++ b/packages/core/src/traced-schema.ts @@ -1,10 +1,27 @@ -import { defaultFieldResolver, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; -import { resolversHooksSymbol } from '@envelop/core'; - +export const resolversHooksSymbol = Symbol('RESOLVERS_HOOKS'); const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); -export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): void { +function isObjectLike(value: unknown) { + return typeof value === 'object' && value !== null; +} + +const isIntrospectionType = (type: any) => { + return type?.name?.startsWith('__'); +}; + +/** + * This isn't the best but will get the job done + */ +const isObjectType = (type: any) => { + if (isObjectLike(type) && '_interfaces' in type) { + return true; + } + return false; +}; + +// Note: in future we might have to drop this if there is some implementation which wildly differs +export function prepareTracedSchema(schema: any | null | undefined): void { if (!schema || schema[trackedSchemaSymbol]) { return; } @@ -14,12 +31,18 @@ export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): v for (const type of entries) { if (!isIntrospectionType(type) && isObjectType(type)) { + // @ts-expect-error - we know this is an object type const fields = Object.values(type.getFields()); for (const field of fields) { - let resolverFn: ResolverFn = (field.resolve || defaultFieldResolver) as ResolverFn; + // @ts-expect-error - we hope there is a resolve field + const existingResolver = field.resolve; + // We are not going to wrap any default resolvers + if (!existingResolver) continue; + // @ts-expect-error - we know this is a resolver fn field.resolve = async (root, args, context, info) => { + let resolverFn: ResolverFn = existingResolver; if (context && context[resolversHooksSymbol]) { const hooks: OnResolverCalledHook[] = context[resolversHooksSymbol]; const afterCalls: AfterResolverHook[] = []; diff --git a/packages/core/test/execute.spec.ts b/packages/core/test/execute.spec.ts index 7c5c0c7d31..f8ac840845 100644 --- a/packages/core/test/execute.spec.ts +++ b/packages/core/test/execute.spec.ts @@ -46,6 +46,47 @@ function createDeferred(): Deferred { } describe('execute', () => { + it('Should wrap and trigger events correctly', async () => { + const spiedPlugin = createSpiedPlugin(); + const teskit = createTestkit([spiedPlugin.plugin], schema); + await teskit.execute(query, {}, { test: 1 }); + expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledTimes(1); + expect(spiedPlugin.spies.beforeResolver).toHaveBeenCalledTimes(3); + expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledWith({ + executeFn: expect.any(Function), + setExecuteFn: expect.any(Function), + extendContext: expect.any(Function), + setResultAndStopExecution: expect.any(Function), + args: { + contextValue: expect.objectContaining({ test: 1 }), + rootValue: {}, + schema: expect.any(GraphQLSchema), + operationName: undefined, + fieldResolver: undefined, + typeResolver: undefined, + variableValues: {}, + document: expect.objectContaining({ + definitions: expect.any(Array), + }), + }, + }); + + expect(spiedPlugin.spies.afterResolver).toHaveBeenCalledTimes(3); + expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledTimes(1); + expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledWith({ + args: expect.any(Object), + setResult: expect.any(Function), + result: { + data: { + me: { + id: '1', + name: 'Dotan Simha', + }, + }, + }, + }); + }); + it('Should allow to override execute function', async () => { const altExecute = jest.fn(execute); const teskit = createTestkit( @@ -169,6 +210,67 @@ describe('execute', () => { }); }); + it('Should allow to register to before and after resolver calls', async () => { + const afterResolver = jest.fn(); + const onResolverCalled = jest.fn(() => afterResolver); + + const teskit = createTestkit( + [ + { + onResolverCalled, + }, + ], + schema + ); + + await teskit.execute(query); + expect(onResolverCalled).toHaveBeenCalledTimes(3); + expect(onResolverCalled).toHaveBeenCalledWith({ + root: {}, + args: {}, + context: expect.any(Object), + info: expect.objectContaining({ + fieldName: 'me', + }), + resolverFn: expect.any(Function), + replaceResolverFn: expect.any(Function), + }); + expect(onResolverCalled).toHaveBeenCalledWith({ + root: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, + args: {}, + context: expect.any(Object), + info: expect.objectContaining({ + fieldName: 'id', + }), + resolverFn: expect.any(Function), + replaceResolverFn: expect.any(Function), + }); + expect(onResolverCalled).toHaveBeenCalledWith({ + root: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, + args: {}, + context: expect.any(Object), + info: expect.objectContaining({ + fieldName: 'name', + }), + resolverFn: expect.any(Function), + replaceResolverFn: expect.any(Function), + }); + + expect(afterResolver).toHaveBeenCalledTimes(3); + expect(afterResolver).toHaveBeenCalledWith({ + result: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, + setResult: expect.any(Function), + }); + expect(afterResolver).toHaveBeenCalledWith({ + result: 1, + setResult: expect.any(Function), + }); + expect(afterResolver).toHaveBeenCalledWith({ + result: 'Dotan Simha', + setResult: expect.any(Function), + }); + }); + it('Should be able to manipulate streams', async () => { const streamExecuteFn = async function* () { for (const value of ['a', 'b', 'c', 'd']) { diff --git a/packages/plugins/apollo-tracing/src/index.ts b/packages/plugins/apollo-tracing/src/index.ts index 215a0a21c2..f6d7a3c0a9 100644 --- a/packages/plugins/apollo-tracing/src/index.ts +++ b/packages/plugins/apollo-tracing/src/index.ts @@ -1,7 +1,6 @@ import { Plugin, handleStreamOrSingleExecutionResult } from '@envelop/core'; import { TracingFormat } from 'apollo-tracing'; import { GraphQLType, ResponsePath, responsePathAsArray } from 'graphql'; -import { prepareTracedSchema } from './traced-schema.js'; const HR_TO_NS = 1e9; const NS_TO_MS = 1e6; @@ -42,9 +41,6 @@ type TracingContextObject = { export const useApolloTracing = (): Plugin => { return { - onSchemaChange: ({ schema }) => { - prepareTracedSchema(schema); - }, onResolverCalled: ({ info, context }) => { const ctx = context[apolloTracingSymbol] as TracingContextObject; // Taken from https://github.com/apollographql/apollo-server/blob/main/packages/apollo-tracing/src/index.ts diff --git a/packages/plugins/opentelemetry/src/index.ts b/packages/plugins/opentelemetry/src/index.ts index 77ba73d8ca..2e2a6b56fb 100644 --- a/packages/plugins/opentelemetry/src/index.ts +++ b/packages/plugins/opentelemetry/src/index.ts @@ -3,7 +3,6 @@ import { SpanAttributes, SpanKind } from '@opentelemetry/api'; import * as opentelemetry from '@opentelemetry/api'; import { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; import { print } from 'graphql'; -import { prepareTracedSchema } from './traced-schema.js'; export enum AttributeName { EXECUTION_ERROR = 'graphql.execute.error', @@ -46,9 +45,6 @@ export const useOpenTelemetry = ( const tracer = tracingProvider.getTracer(serviceName); return { - onSchemaChange: async ({ schema }) => { - prepareTracedSchema(schema); - }, onResolverCalled: options.resolvers ? ({ info, context, args }) => { if (context && typeof context === 'object' && context[tracingSpanSymbol]) { diff --git a/packages/plugins/opentelemetry/src/traced-schema.ts b/packages/plugins/opentelemetry/src/traced-schema.ts deleted file mode 100644 index 3e33a07888..0000000000 --- a/packages/plugins/opentelemetry/src/traced-schema.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { defaultFieldResolver, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; -import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; -import { resolversHooksSymbol } from '@envelop/core'; - -const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); - -export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): void { - if (!schema || schema[trackedSchemaSymbol]) { - return; - } - - schema[trackedSchemaSymbol] = true; - const entries = Object.values(schema.getTypeMap()); - - for (const type of entries) { - if (!isIntrospectionType(type) && isObjectType(type)) { - const fields = Object.values(type.getFields()); - - for (const field of fields) { - let resolverFn: ResolverFn = (field.resolve || defaultFieldResolver) as ResolverFn; - - field.resolve = async (root, args, context, info) => { - if (context && context[resolversHooksSymbol]) { - const hooks: OnResolverCalledHook[] = context[resolversHooksSymbol]; - const afterCalls: AfterResolverHook[] = []; - - for (const hook of hooks) { - const afterFn = await hook({ - root, - args, - context, - info, - resolverFn, - replaceResolverFn: newFn => { - resolverFn = newFn as ResolverFn; - }, - }); - afterFn && afterCalls.push(afterFn); - } - - try { - let result = await resolverFn(root, args, context, info); - - for (const afterFn of afterCalls) { - afterFn({ - result, - setResult: newResult => { - result = newResult; - }, - }); - } - - return result; - } catch (e) { - let resultErr = e; - - for (const afterFn of afterCalls) { - afterFn({ - result: resultErr, - setResult: newResult => { - resultErr = newResult; - }, - }); - } - - throw resultErr; - } - } else { - return resolverFn(root, args, context, info); - } - }; - } - } - } -} diff --git a/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts b/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts index fecebd3d8f..6d67b89c30 100644 --- a/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts +++ b/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts @@ -67,8 +67,7 @@ describe('useOpenTelemetry', () => { await testInstance.execute(query); const actual = exporter.getFinishedSpans(); - expect(actual.length).toBe(2); - expect(actual[0].name).toBe('Query.ping'); - expect(actual[1].name).toBe('Anonymous Operation'); + expect(actual.length).toBe(1); + expect(actual[0].name).toBe('Anonymous Operation'); }); }); diff --git a/packages/plugins/prometheus/src/index.ts b/packages/plugins/prometheus/src/index.ts index f0e82db7d4..97c7faee0e 100644 --- a/packages/plugins/prometheus/src/index.ts +++ b/packages/plugins/prometheus/src/index.ts @@ -22,7 +22,6 @@ import { createSummary, } from './utils.js'; import { PrometheusTracingPluginConfig } from './config.js'; -import { prepareTracedSchema } from './traced-schema.js'; export { PrometheusTracingPluginConfig, createCounter, createHistogram, createSummary, FillLabelsFnParams }; @@ -351,7 +350,6 @@ export const usePrometheus = (config: PrometheusTracingPluginConfig = {}): Plugi }); }, onSchemaChange({ schema }) { - prepareTracedSchema(schema); typeInfo = new TypeInfo(schema); }, onParse, diff --git a/packages/plugins/prometheus/src/traced-schema.ts b/packages/plugins/prometheus/src/traced-schema.ts deleted file mode 100644 index 3e33a07888..0000000000 --- a/packages/plugins/prometheus/src/traced-schema.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { defaultFieldResolver, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; -import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; -import { resolversHooksSymbol } from '@envelop/core'; - -const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); - -export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): void { - if (!schema || schema[trackedSchemaSymbol]) { - return; - } - - schema[trackedSchemaSymbol] = true; - const entries = Object.values(schema.getTypeMap()); - - for (const type of entries) { - if (!isIntrospectionType(type) && isObjectType(type)) { - const fields = Object.values(type.getFields()); - - for (const field of fields) { - let resolverFn: ResolverFn = (field.resolve || defaultFieldResolver) as ResolverFn; - - field.resolve = async (root, args, context, info) => { - if (context && context[resolversHooksSymbol]) { - const hooks: OnResolverCalledHook[] = context[resolversHooksSymbol]; - const afterCalls: AfterResolverHook[] = []; - - for (const hook of hooks) { - const afterFn = await hook({ - root, - args, - context, - info, - resolverFn, - replaceResolverFn: newFn => { - resolverFn = newFn as ResolverFn; - }, - }); - afterFn && afterCalls.push(afterFn); - } - - try { - let result = await resolverFn(root, args, context, info); - - for (const afterFn of afterCalls) { - afterFn({ - result, - setResult: newResult => { - result = newResult; - }, - }); - } - - return result; - } catch (e) { - let resultErr = e; - - for (const afterFn of afterCalls) { - afterFn({ - result: resultErr, - setResult: newResult => { - resultErr = newResult; - }, - }); - } - - throw resultErr; - } - } else { - return resolverFn(root, args, context, info); - } - }; - } - } - } -} diff --git a/packages/plugins/rate-limiter/src/index.ts b/packages/plugins/rate-limiter/src/index.ts index b27e799f32..25dac08c4c 100644 --- a/packages/plugins/rate-limiter/src/index.ts +++ b/packages/plugins/rate-limiter/src/index.ts @@ -2,7 +2,6 @@ import { Plugin } from '@envelop/core'; import { IntValueNode, StringValueNode, GraphQLResolveInfo } from 'graphql'; import { getDirective } from './utils.js'; import { getGraphQLRateLimiter } from 'graphql-rate-limit'; -import { prepareTracedSchema } from './traced-schema.js'; export * from './utils.js'; export class UnauthenticatedError extends Error {} @@ -28,9 +27,6 @@ export const useRateLimiter = ( const rateLimiterFn = getGraphQLRateLimiter({ identifyContext: options.identifyFn }); return { - onSchemaChange: ({ schema }) => { - prepareTracedSchema(schema); - }, async onContextBuilding({ extendContext }) { extendContext({ rateLimiterFn, diff --git a/packages/plugins/rate-limiter/src/traced-schema.ts b/packages/plugins/rate-limiter/src/traced-schema.ts deleted file mode 100644 index 3e33a07888..0000000000 --- a/packages/plugins/rate-limiter/src/traced-schema.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { defaultFieldResolver, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; -import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; -import { resolversHooksSymbol } from '@envelop/core'; - -const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); - -export function prepareTracedSchema(schema: GraphQLSchema | null | undefined): void { - if (!schema || schema[trackedSchemaSymbol]) { - return; - } - - schema[trackedSchemaSymbol] = true; - const entries = Object.values(schema.getTypeMap()); - - for (const type of entries) { - if (!isIntrospectionType(type) && isObjectType(type)) { - const fields = Object.values(type.getFields()); - - for (const field of fields) { - let resolverFn: ResolverFn = (field.resolve || defaultFieldResolver) as ResolverFn; - - field.resolve = async (root, args, context, info) => { - if (context && context[resolversHooksSymbol]) { - const hooks: OnResolverCalledHook[] = context[resolversHooksSymbol]; - const afterCalls: AfterResolverHook[] = []; - - for (const hook of hooks) { - const afterFn = await hook({ - root, - args, - context, - info, - resolverFn, - replaceResolverFn: newFn => { - resolverFn = newFn as ResolverFn; - }, - }); - afterFn && afterCalls.push(afterFn); - } - - try { - let result = await resolverFn(root, args, context, info); - - for (const afterFn of afterCalls) { - afterFn({ - result, - setResult: newResult => { - result = newResult; - }, - }); - } - - return result; - } catch (e) { - let resultErr = e; - - for (const afterFn of afterCalls) { - afterFn({ - result: resultErr, - setResult: newResult => { - resultErr = newResult; - }, - }); - } - - throw resultErr; - } - } else { - return resolverFn(root, args, context, info); - } - }; - } - } - } -} diff --git a/website/docs/plugins/lifecycle.mdx b/website/docs/plugins/lifecycle.mdx index 37156a43af..934d61617f 100644 --- a/website/docs/plugins/lifecycle.mdx +++ b/website/docs/plugins/lifecycle.mdx @@ -3,6 +3,8 @@ title: Plugin Lifecycle sidebar_label: Plugin Lifecycle --- +import { Callout } from '@theguild/components' + ## Plugins Lifecycle Plugins are executed in order of their usage, and inject functionality serially, so aim to keep your plugins simple and standalone as much as possible. @@ -243,6 +245,10 @@ Some plugins (like gateway implementations) could potentially change the schema Triggered when a resolver is called during the execution of the operation. + + This hook will not call the default field resolver. + + - `params` - an object with `{ root, args, context, info }` that was originally passed to the resolver. You can also return a function to run after the resolver is done, with the following API: From 2647f7f95185343affa9bb353abc522266f5fbde Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Thu, 1 Sep 2022 13:25:06 -0400 Subject: [PATCH 24/61] feat: drop node 12 (#1505) --- .github/workflows/tests.yml | 2 +- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3225816db6..d50fe456fe 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -78,7 +78,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] # remove windows to speed up the tests - node-version: [12, 17] + node-version: [14, 16, 17, 18] graphql_version: [15, 16, 'npm:@graphql-tools/graphql@0.1.0-alpha-20220815193214-83898018'] steps: - name: Checkout Master diff --git a/tsconfig.json b/tsconfig.json index b9fd0057a7..0ecbe64115 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "importHelpers": true, "experimentalDecorators": true, "module": "node16", - "target": "es2018", + "target": "ES2020", "lib": ["es6", "esnext", "es2015", "dom"], "suppressImplicitAnyIndexErrors": true, "moduleResolution": "node", From 5ab6cc6a85d309e6e2bc3af50b6bc5284321314d Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Fri, 2 Sep 2022 13:28:55 -0400 Subject: [PATCH 25/61] feat: remove `enableIf` utility (#1504) * feat: remove enableIf utility * make types happpy * make types happy * add changeset --- .changeset/quiet-mice-jam.md | 27 ++++++++++++++++++++++++++ packages/core/src/create.ts | 25 ++++++++++++++++-------- packages/core/src/enable-if.ts | 27 -------------------------- packages/core/src/index.ts | 1 - packages/core/test/utils.spec.ts | 31 ------------------------------ packages/testing/src/index.ts | 6 +++--- packages/testing/test/test.spec.ts | 3 +-- packages/types/src/utils.ts | 2 +- website/docs/core.mdx | 27 -------------------------- 9 files changed, 49 insertions(+), 100 deletions(-) create mode 100644 .changeset/quiet-mice-jam.md delete mode 100644 packages/core/src/enable-if.ts delete mode 100644 packages/core/test/utils.spec.ts diff --git a/.changeset/quiet-mice-jam.md b/.changeset/quiet-mice-jam.md new file mode 100644 index 0000000000..fe54fc024d --- /dev/null +++ b/.changeset/quiet-mice-jam.md @@ -0,0 +1,27 @@ +--- +'@envelop/core': major +--- + +Remove `enableIf` utility in favor of more type safe way to conditionally enable plugins. It wasn't a great experience to have a utility + +We can easily replace usage like this: + +```diff +- import { envelop, useMaskedErrors, enableIf } from '@envelop/core' ++ import { envelop, useMaskedErrors } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' + +const isProd = process.env.NODE_ENV === 'production' + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [ + // This plugin is enabled only in production +- enableIf(isProd, useMaskedErrors()) ++ isProd && useMaskedErrors() + ] +}) +``` diff --git a/packages/core/src/create.ts b/packages/core/src/create.ts index 6740a5ceb1..498a3c4996 100644 --- a/packages/core/src/create.ts +++ b/packages/core/src/create.ts @@ -7,20 +7,26 @@ import { SubscribeFunction, ParseFunction, ValidateFunction, + Optional, } from '@envelop/types'; -import { isPluginEnabled, PluginOrDisabledPlugin } from './enable-if.js'; import { createEnvelopOrchestrator, EnvelopOrchestrator } from './orchestrator.js'; -export function envelop[]>(options: { - plugins: Array; +type ExcludeFalsy = Exclude[]; + +function notEmpty(value: Optional): value is T { + return value != null; +} + +export function envelop>[]>(options: { + plugins: PluginsType; enableInternalTracing?: boolean; parse: ParseFunction; execute: ExecuteFunction; validate: ValidateFunction; subscribe: SubscribeFunction; -}): GetEnvelopedFn> { - const plugins = options.plugins.filter(isPluginEnabled); - const orchestrator = createEnvelopOrchestrator>({ +}): GetEnvelopedFn>> { + const plugins = options.plugins.filter(notEmpty); + const orchestrator = createEnvelopOrchestrator>>({ plugins, parse: options.parse, execute: options.execute, @@ -31,7 +37,10 @@ export function envelop[]>(options: { const getEnveloped = ( initialContext: TInitialContext = {} as TInitialContext ) => { - const typedOrchestrator = orchestrator as EnvelopOrchestrator>; + const typedOrchestrator = orchestrator as EnvelopOrchestrator< + TInitialContext, + ComposeContext> + >; typedOrchestrator.init(initialContext); return { @@ -46,5 +55,5 @@ export function envelop[]>(options: { getEnveloped._plugins = plugins; - return getEnveloped as GetEnvelopedFn>; + return getEnveloped as GetEnvelopedFn>>; } diff --git a/packages/core/src/enable-if.ts b/packages/core/src/enable-if.ts deleted file mode 100644 index 5c1607c9c2..0000000000 --- a/packages/core/src/enable-if.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Plugin } from '@envelop/types'; - -/** - * This enum is used only internally in order to create nominal type for the disabled plugin - */ -enum EnableIfBranded { - DisabledPlugin, -} - -export type PluginOrDisabledPlugin = Plugin | EnableIfBranded.DisabledPlugin; - -export function isPluginEnabled(t: PluginOrDisabledPlugin): t is Plugin { - return t !== EnableIfBranded.DisabledPlugin && t !== null; -} - -/** - * Utility function to enable a plugin. - */ -export function enableIf = {}>( - condition: boolean, - plugin: Plugin | (() => Plugin) -): PluginOrDisabledPlugin { - if (condition) { - return typeof plugin === 'function' ? plugin() : plugin; - } - return EnableIfBranded.DisabledPlugin; -} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e72b08d3e6..3ffbbdbc63 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,5 +7,4 @@ export * from './plugins/use-schema.js'; export * from './plugins/use-error-handler.js'; export * from './plugins/use-extend-context.js'; export * from './plugins/use-payload-formatter.js'; -export * from './enable-if.js'; export { resolversHooksSymbol } from './traced-schema.js'; diff --git a/packages/core/test/utils.spec.ts b/packages/core/test/utils.spec.ts deleted file mode 100644 index f6831d08bc..0000000000 --- a/packages/core/test/utils.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useLogger, enableIf } from '@envelop/core'; -import { createTestkit, createSpiedPlugin } from '@envelop/testing'; -import { query, schema } from './common.js'; - -describe('Utils', () => { - describe('enableIf', () => { - it('Should return a plugin', () => { - const plugin = enableIf(true, useLogger()); - expect(plugin).toBeTruthy(); - }); - - it('Should return null', () => { - const plugin = enableIf(false, useLogger()); - expect(plugin).toBeFalsy(); - }); - - it('Should not init plugin', async () => { - const spiedPlugin = createSpiedPlugin(); - const testkit = createTestkit([enableIf(false, spiedPlugin.plugin)], schema); - await testkit.execute(query); - expect(spiedPlugin.spies.beforeExecute).not.toHaveBeenCalled(); - }); - - it('Should init plugin', async () => { - const spiedPlugin = createSpiedPlugin(); - const testkit = createTestkit([enableIf(true, spiedPlugin.plugin)], schema); - await testkit.execute(query); - expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalled(); - }); - }); -}); diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index 4b21137908..316a89ac6c 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -10,8 +10,8 @@ import { subscribe, validate, } from 'graphql'; -import { useSchema, envelop, PluginOrDisabledPlugin, isAsyncIterable } from '@envelop/core'; -import { GetEnvelopedFn, Plugin } from '@envelop/types'; +import { useSchema, envelop, isAsyncIterable } from '@envelop/core'; +import { GetEnvelopedFn, Optional, Plugin } from '@envelop/types'; import { mapSchema as cloneSchema, isDocumentNode } from '@graphql-tools/utils'; export type ModifyPluginsFn = (plugins: Plugin[]) => Plugin[]; @@ -97,7 +97,7 @@ export type TestkitInstance = { }; export function createTestkit( - pluginsOrEnvelop: GetEnvelopedFn | Array, + pluginsOrEnvelop: GetEnvelopedFn | Parameters['0']['plugins'], schema?: GraphQLSchema ): TestkitInstance { const toGraphQLErrorOrThrow = (thrownThing: unknown): GraphQLError => { diff --git a/packages/testing/test/test.spec.ts b/packages/testing/test/test.spec.ts index 42d3de89a6..2b11ceddbd 100644 --- a/packages/testing/test/test.spec.ts +++ b/packages/testing/test/test.spec.ts @@ -1,4 +1,3 @@ -import { enableIf } from '@envelop/core'; import { assertSingleExecutionValue, createTestkit } from '@envelop/testing'; import { Plugin } from '@envelop/types'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -98,7 +97,7 @@ describe('Test the testkit', () => { onValidate: jest.fn().mockReturnValue(undefined), }; - const testkit = createTestkit([plugin1, enableIf(false, plugin2)], createSchema()); + const testkit = createTestkit([plugin1, false && plugin2], createSchema()); const result = await testkit.execute('query test { foo }'); assertSingleExecutionValue(result); expect(plugin1.onParse).toBeCalled(); diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts index bd2af53904..0faa5ef955 100644 --- a/packages/types/src/utils.ts +++ b/packages/types/src/utils.ts @@ -29,7 +29,7 @@ export type ArbitraryObject = Record; export type PromiseOrValue = T | Promise; export type AsyncIterableIteratorOrValue = T | AsyncIterableIterator; export type Maybe = T | null | undefined; - +export type Optional = T | Maybe | false; export interface ObjMap { [key: string]: T; } diff --git a/website/docs/core.mdx b/website/docs/core.mdx index 35f23a91d3..9097ad4b09 100644 --- a/website/docs/core.mdx +++ b/website/docs/core.mdx @@ -153,30 +153,3 @@ const getEnveloped = envelop({ ] }) ``` - -### Utilities - -#### enableIf - -This utility is helpful when you want to enable a plugin only when a certain condition is met. - -```ts -import { envelop, useMaskedErrors, enableIf } from '@envelop/core' -import { parse, validate, execute, subscribe } from 'graphql' - -const isProd = process.env.NODE_ENV === 'production' - -const getEnveloped = envelop({ - parse, - validate, - execute, - subscribe, - plugins: [ - // This plugin is enabled only in production - enableIf(isProd, useMaskedErrors()), - // you can also pass function - enableIf(isProd, () => useMaskedErrors()) - // ... other plugins ... - ] -}) -``` From 6e27fdd4578acff8e6ef5fa237d966717a747cfe Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Mon, 5 Sep 2022 13:09:14 -0400 Subject: [PATCH 26/61] feat: remove async schema plugin and rename lazy loaded schema plugin (#1506) * remove useAsyncSchema * rename to useSchemaByContext --- .changeset/curvy-bottles-repeat.md | 5 ++++ .changeset/nervous-seas-own.md | 5 ++++ packages/core/src/plugins/use-schema.ts | 12 +-------- website/docs/core.mdx | 26 ------------------- .../docs/guides/securing-your-graphql-api.mdx | 6 ++--- website/src/lib/plugins.ts | 13 +--------- 6 files changed, 15 insertions(+), 52 deletions(-) create mode 100644 .changeset/curvy-bottles-repeat.md create mode 100644 .changeset/nervous-seas-own.md diff --git a/.changeset/curvy-bottles-repeat.md b/.changeset/curvy-bottles-repeat.md new file mode 100644 index 0000000000..1514400798 --- /dev/null +++ b/.changeset/curvy-bottles-repeat.md @@ -0,0 +1,5 @@ +--- +'@envelop/core': major +--- + +Remove async schema loading plugin. This was a mistake from beginning as we cannot asynchronously `validate` and `parse` since with GraphQL.js are synchronous in nature. diff --git a/.changeset/nervous-seas-own.md b/.changeset/nervous-seas-own.md new file mode 100644 index 0000000000..4e156a5b6d --- /dev/null +++ b/.changeset/nervous-seas-own.md @@ -0,0 +1,5 @@ +--- +'@envelop/core': major +--- + +Renmae `useLazyLoadedSchema` to `useSchemaByContext` since the original name was vert misleading. diff --git a/packages/core/src/plugins/use-schema.ts b/packages/core/src/plugins/use-schema.ts index aab02040ab..f7246567d3 100644 --- a/packages/core/src/plugins/use-schema.ts +++ b/packages/core/src/plugins/use-schema.ts @@ -8,20 +8,10 @@ export const useSchema = (schema: any): Plugin => { }; }; -export const useLazyLoadedSchema = (schemaLoader: (context: Maybe) => any): Plugin => { +export const useSchemaByContext = (schemaLoader: (context: Maybe) => any): Plugin => { return { onEnveloped({ setSchema, context }) { setSchema(schemaLoader(context)); }, }; }; - -export const useAsyncSchema = (schemaPromise: Promise): Plugin => { - return { - onPluginInit({ setSchema }) { - schemaPromise.then(schemaObj => { - setSchema(schemaObj); - }); - }, - }; -}; diff --git a/website/docs/core.mdx b/website/docs/core.mdx index 9097ad4b09..d1532842c3 100644 --- a/website/docs/core.mdx +++ b/website/docs/core.mdx @@ -28,32 +28,6 @@ const getEnveloped = envelop({ }) ``` -#### useAsyncSchema - -This plugin is the simplest plugin for specifying your GraphQL schema, but in an async way. - -If you are using a framework that creates the schema in an async way, you can either use this plugin, or `await` for the schema and then use `useSchema`. - -```ts -import { envelop, useAsyncSchema } from '@envelop/core' -import { parse, validate, execute, subscribe } from 'graphql' - -const getSchema = async (): Promise => { - // return schema when it's ready -} - -const getEnveloped = envelop({ - parse, - validate, - execute, - subscribe, - plugins: [ - useAsyncSchema(getSchema()) - // ... other plugins ... - ] -}) -``` - #### useErrorHandler This plugin invokes a custom function with the every time execution encounters an error. diff --git a/website/docs/guides/securing-your-graphql-api.mdx b/website/docs/guides/securing-your-graphql-api.mdx index 3b36e1a7c9..beb5ec9c17 100644 --- a/website/docs/guides/securing-your-graphql-api.mdx +++ b/website/docs/guides/securing-your-graphql-api.mdx @@ -79,19 +79,19 @@ Right now envelop ships with two plugins that allow applying authorization befor ### Schema based on context -With the `useLazyLoadedSchema` plugin it is possible to dynamically select a schema for execution based on the context object. This is handy if you have a public schema (e.g. for third-party API consumers) and a private schema (for in-house API consumers). +With the `useSchemaByContext` plugin it is possible to dynamically select a schema for execution based on the context object. This is handy if you have a public schema (e.g. for third-party API consumers) and a private schema (for in-house API consumers). Libraries such as [`graphql-public-schema-filter`](https://github.com/n1ru4l/graphql-public-schema-filter) can be used for generating a schema with only access to a sub part of the original schema using either SDL directives or schema field extensions. ```ts -import { envelop, useLazyLoadedSchema } from '@envelop/core' +import { envelop, useSchemaByContext } from '@envelop/core' import { parse, validate, execute, subscribe } from 'graphql' import { privateSchema, publicSchema } from './schema' const getEnveloped = envelop({ plugins: [ // ... other plugins (e.g. useAuth0) - useLazyLoadedSchema(context => (context.isPrivateApiUser ? privateSchema : publicSchema)) + useSchemaByContext(context => (context.isPrivateApiUser ? privateSchema : publicSchema)) ] }) ``` diff --git a/website/src/lib/plugins.ts b/website/src/lib/plugins.ts index 11aeb24a06..ab82d8de79 100644 --- a/website/src/lib/plugins.ts +++ b/website/src/lib/plugins.ts @@ -44,20 +44,9 @@ export const pluginsArr: Package[] = [ iconUrl: '/logo.png', tags: ['core', 'schema'], }, - { - identifier: 'use-async-schema', - title: 'useAsyncSchema', - githubReadme: { - repo: 'n1ru4l/envelop', - path: 'packages/core/docs/use-async-schema.md', - }, - npmPackage: '@envelop/core', - iconUrl: '/logo.png', - tags: ['core', 'schema'], - }, { identifier: 'use-lazy-loaded-schema', - title: 'useLazyLoadedSchema', + title: 'useSchemaByContext', githubReadme: { repo: 'n1ru4l/envelop', path: 'packages/core/docs/use-lazy-loaded-schema.md', From 59d8a788a7797e0ad8fe446517fffd86d81ee3e0 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Wed, 7 Sep 2022 08:18:59 -0400 Subject: [PATCH 27/61] add eslint rule (#1509) --- .eslintrc.json | 10 ++++++++-- package.json | 3 ++- yarn.lock | 51 ++++++++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 5 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index cdc921ea6a..f7367a5eda 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,13 @@ { "parser": "@typescript-eslint/parser", - "extends": ["eslint:recommended", "standard", "prettier", "plugin:@typescript-eslint/recommended"], - "plugins": ["@typescript-eslint", "unicorn"], + "extends": [ + "eslint:recommended", + "standard", + "prettier", + "plugin:@typescript-eslint/recommended", + "plugin:package-json/recommended" + ], + "plugins": ["@typescript-eslint", "unicorn", "package-json"], "rules": { "unicorn/filename-case": "error", "no-lonely-if": "error", diff --git a/package.json b/package.json index ee28152412..9cb4b9297d 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "@babel/plugin-proposal-decorators": "7.17.2", "@babel/preset-env": "7.16.11", "@babel/preset-typescript": "7.16.7", - "@changesets/cli": "2.24.2", "@changesets/changelog-github": "0.4.6", + "@changesets/cli": "2.24.2", "@graphql-tools/schema": "8.5.0", "@theguild/prettier-config": "0.0.2", "@types/benchmark": "2.1.1", @@ -65,6 +65,7 @@ "eslint-config-standard": "17.0.0", "eslint-plugin-import": "2.26.0", "eslint-plugin-n": "15.2.1", + "eslint-plugin-package-json": "^0.1.4", "eslint-plugin-promise": "6.0.0", "eslint-plugin-unicorn": "43.0.0", "faker": "5.5.3", diff --git a/yarn.lock b/yarn.lock index a4203ac142..d21baba98b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4459,7 +4459,7 @@ ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -ansi-styles@^4.0.0, ansi-styles@^4.1.0: +ansi-styles@^4.0.0, ansi-styles@^4.1.0, ansi-styles@^4.2.1: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== @@ -6714,7 +6714,7 @@ diff-sequences@^27.5.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" integrity sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ== -diff@^4.0.1: +diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== @@ -6731,6 +6731,14 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +disparity@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/disparity/-/disparity-3.2.0.tgz#7198eaf7a873a130f8098c93061c1df8934500f2" + integrity sha512-8cl9ouncFYE7OQsYwJNiy2e15S0xN80X1Jj/N/YkoiM+VGWSyg1YzPToecKyYx2DQiJapt5IC8yi43GW23TUHQ== + dependencies: + ansi-styles "^4.2.1" + diff "^4.0.2" + dlv@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" @@ -7249,6 +7257,15 @@ eslint-plugin-n@15.2.1: resolve "^1.10.1" semver "^7.3.7" +eslint-plugin-package-json@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-package-json/-/eslint-plugin-package-json-0.1.4.tgz#88ad9ec30f28795c51e9001d4d8294c78889ad7a" + integrity sha512-qdb9LUBFR3tM9OZM1AaYCkxoZnRx7PX2xqvLm49D0JbUu+EkbDur9ug+tCS2xlA1Lbt12Wff5qES81ttc/VBdg== + dependencies: + disparity "^3.0.0" + package-json-validator "^0.6.3" + requireindex "^1.2.0" + eslint-plugin-promise@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz#017652c07c9816413a41e11c30adc42c3d55ff18" @@ -11476,6 +11493,11 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +minimist@~0.0.1: + version "0.0.10" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" + integrity sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw== + minipass-collect@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" @@ -12051,6 +12073,14 @@ open@^8.2.0: is-docker "^2.1.1" is-wsl "^2.2.0" +optimist@~0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g== + dependencies: + minimist "~0.0.1" + wordwrap "~0.0.2" + optionator@^0.8.1: version "0.8.3" resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" @@ -12178,6 +12208,13 @@ p-try@^2.0.0: resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== +package-json-validator@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/package-json-validator/-/package-json-validator-0.6.3.tgz#aa01a888688b81facf4f83cd7e57e8b6ad1017a1" + integrity sha512-juKiFboV4UKUvWQ+OSxstnyukhuluyuEoFmgZw1Rx21XzmwlgDWLcbl3qzjA3789IRORYhVFs7cmAO0YFGwHCg== + dependencies: + optimist "~0.6.0" + param-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" @@ -13266,6 +13303,11 @@ require-main-filename@^2.0.0: resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== +requireindex@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/requireindex/-/requireindex-1.2.0.tgz#3463cdb22ee151902635aa6c9535d4de9c2ef1ef" + integrity sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww== + resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" @@ -15615,6 +15657,11 @@ word-wrap@^1.2.3, word-wrap@~1.2.3: resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw== + worktop@0.7.0: version "0.7.0" resolved "https://registry.npmjs.org/worktop/-/worktop-0.7.0.tgz#d88fd3dcc894715f656d3b80d24433652f455a55" From 49cf4201c09882732d76c3d349929d2d0006cf00 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Wed, 7 Sep 2022 14:23:40 +0200 Subject: [PATCH 28/61] feat: `@envelop/on-resolve` plugin for hooking into schema resolvers (#1500) * on resolve plugin * changeset * no more onResolverCalled * unused import * args is a record * integrate and useOnResolve * resolversHooksSymbol does not exist * plugincontext for OnPluginInit * on-resolve uses addPlugin * onresolvercalled is no more * refactor for new on-resolve * fix open-telemetry tests * fix newrelic * opentelemetry graphql as peer dep * tests * addPlugin doesnt need to be used * reorder * respects onPluginInit context * drop unused import * fixes false positive eslint warnings Co-authored-by: Dimitri POSTOLOV --- .changeset/neat-spoons-play.md | 5 + .eslintrc.json | 19 +++- package.json | 4 +- packages/core/src/index.ts | 1 - packages/core/src/orchestrator.ts | 23 +---- packages/core/src/traced-schema.ts | 98 ------------------ packages/core/test/execute.spec.ts | 63 ------------ packages/plugins/apollo-tracing/package.json | 1 + packages/plugins/apollo-tracing/src/index.ts | 34 ++++--- .../plugins/extended-validation/src/plugin.ts | 5 +- packages/plugins/newrelic/package.json | 1 + packages/plugins/newrelic/src/index.ts | 99 +++++++++---------- packages/plugins/on-resolve/README.md | 98 ++++++++++++++++++ packages/plugins/on-resolve/package.json | 66 +++++++++++++ packages/plugins/on-resolve/src/index.ts | 85 ++++++++++++++++ .../on-resolve/test/use-on-resolve.spec.ts | 63 ++++++++++++ packages/plugins/opentelemetry/package.json | 4 +- packages/plugins/opentelemetry/src/index.ts | 73 +++++++------- .../test/use-open-telemetry.spec.ts | 5 +- .../operation-field-permissions/src/index.ts | 4 +- packages/plugins/prometheus/package.json | 1 + packages/plugins/prometheus/src/index.ts | 45 +++++---- packages/plugins/rate-limiter/package.json | 1 + packages/plugins/rate-limiter/src/index.ts | 98 +++++++++--------- packages/plugins/sentry/package.json | 1 + packages/plugins/sentry/src/index.ts | 18 ++-- packages/testing/src/index.ts | 4 +- packages/types/src/hooks.ts | 45 ++------- packages/types/src/plugin.ts | 8 +- website/algolia-lockfile.json | 5 - website/docs/plugins/lifecycle.mdx | 15 --- yarn.lock | 97 +++++++++--------- 32 files changed, 603 insertions(+), 486 deletions(-) create mode 100644 .changeset/neat-spoons-play.md delete mode 100644 packages/core/src/traced-schema.ts create mode 100644 packages/plugins/on-resolve/README.md create mode 100644 packages/plugins/on-resolve/package.json create mode 100644 packages/plugins/on-resolve/src/index.ts create mode 100644 packages/plugins/on-resolve/test/use-on-resolve.spec.ts diff --git a/.changeset/neat-spoons-play.md b/.changeset/neat-spoons-play.md new file mode 100644 index 0000000000..bb69f87659 --- /dev/null +++ b/.changeset/neat-spoons-play.md @@ -0,0 +1,5 @@ +--- +'@envelop/on-resolve': major +--- + +Plugin allowing you to hook into resolves of every field in the GraphQL schema. diff --git a/.eslintrc.json b/.eslintrc.json index f7367a5eda..03707bae19 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -15,9 +15,19 @@ "no-empty": "off", "no-console": "error", "no-prototype-builtins": "off", - "prefer-arrow-callback": ["error", { "allowNamedFunctions": true }], + "prefer-arrow-callback": [ + "error", + { + "allowNamedFunctions": true + } + ], "no-useless-constructor": "off", - "@typescript-eslint/no-unused-vars": ["warn", { "args": "none" }], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "args": "none" + } + ], "@typescript-eslint/no-use-before-define": "off", "@typescript-eslint/no-namespace": "off", "@typescript-eslint/no-empty-interface": "off", @@ -32,10 +42,11 @@ "unicorn/no-useless-fallback-in-spread": "error", "import/no-extraneous-dependencies": [ "error", - { "devDependencies": ["**/*.test.ts", "**/*.spec.ts", "**/test/**/*.ts"] } + { + "devDependencies": ["**/*.test.ts", "**/*.spec.ts", "**/test/**/*.ts"] + } ] }, - "env": { "es6": true, "node": true diff --git a/package.json b/package.json index 9cb4b9297d..f5ba53f559 100644 --- a/package.json +++ b/package.json @@ -54,8 +54,8 @@ "@types/jest": "27.4.1", "@types/k6": "0.36.0", "@types/node": "16.11.26", - "@typescript-eslint/eslint-plugin": "5.27.0", - "@typescript-eslint/parser": "5.27.0", + "@typescript-eslint/eslint-plugin": "5.36.2", + "@typescript-eslint/parser": "5.36.2", "apollo-server": "3.5.0", "benchmark": "2.1.4", "bob-the-bundler": "4.0.0", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3ffbbdbc63..0789738c90 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,4 +7,3 @@ export * from './plugins/use-schema.js'; export * from './plugins/use-error-handler.js'; export * from './plugins/use-extend-context.js'; export * from './plugins/use-payload-formatter.js'; -export { resolversHooksSymbol } from './traced-schema.js'; diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 714c67676c..f3a7f2a9a5 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -10,7 +10,6 @@ import { OnExecuteDoneHook, OnExecuteHook, OnParseHook, - OnResolverCalledHook, OnSubscribeHook, OnValidateHook, Plugin, @@ -32,7 +31,6 @@ import { ValidateFunction, ExecutionResult, } from '@envelop/types'; -import { prepareTracedSchema, resolversHooksSymbol } from './traced-schema.js'; import { errorAsyncIterator, finalAsyncIterator, @@ -72,20 +70,11 @@ export function createEnvelopOrchestrator }: EnvelopOrchestratorOptions): EnvelopOrchestrator { let schema: any | undefined | null = null; let initDone = false; - const onResolversHandlers: OnResolverCalledHook[] = []; - for (const plugin of plugins) { - if (plugin.onResolverCalled) { - onResolversHandlers.push(plugin.onResolverCalled); - } - } // Define the initial method for replacing the GraphQL schema, this is needed in order // to allow setting the schema from the onPluginInit callback. We also need to make sure // here not to call the same plugin that initiated the schema switch. const replaceSchema = (newSchema: any, ignorePluginIndex = -1) => { - if (onResolversHandlers.length) { - prepareTracedSchema(newSchema); - } schema = newSchema; if (initDone) { @@ -341,7 +330,7 @@ export function createEnvelopOrchestrator } : initialContext => orchestratorCtx => orchestratorCtx ? { ...initialContext, ...orchestratorCtx } : initialContext; - const useCustomSubscribe = beforeCallbacks.subscribe.length || onResolversHandlers.length; + const useCustomSubscribe = beforeCallbacks.subscribe.length; const customSubscribe = useCustomSubscribe ? makeSubscribe(async args => { @@ -382,10 +371,6 @@ export function createEnvelopOrchestrator } } - if (onResolversHandlers.length) { - context[resolversHooksSymbol] = onResolversHandlers; - } - if (result === undefined) { result = await subscribeFn({ ...args, @@ -458,7 +443,7 @@ export function createEnvelopOrchestrator }) : makeSubscribe(subscribe as any); - const useCustomExecute = beforeCallbacks.execute.length || onResolversHandlers.length; + const useCustomExecute = beforeCallbacks.execute.length; const customExecute = useCustomExecute ? makeExecute(async args => { @@ -503,10 +488,6 @@ export function createEnvelopOrchestrator } } - if (onResolversHandlers.length) { - context[resolversHooksSymbol] = onResolversHandlers; - } - if (result === undefined) { result = (await executeFn({ ...args, diff --git a/packages/core/src/traced-schema.ts b/packages/core/src/traced-schema.ts deleted file mode 100644 index d1bb33d9de..0000000000 --- a/packages/core/src/traced-schema.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { AfterResolverHook, OnResolverCalledHook, ResolverFn } from '@envelop/types'; -export const resolversHooksSymbol = Symbol('RESOLVERS_HOOKS'); -const trackedSchemaSymbol = Symbol('TRACKED_SCHEMA'); - -function isObjectLike(value: unknown) { - return typeof value === 'object' && value !== null; -} - -const isIntrospectionType = (type: any) => { - return type?.name?.startsWith('__'); -}; - -/** - * This isn't the best but will get the job done - */ -const isObjectType = (type: any) => { - if (isObjectLike(type) && '_interfaces' in type) { - return true; - } - return false; -}; - -// Note: in future we might have to drop this if there is some implementation which wildly differs -export function prepareTracedSchema(schema: any | null | undefined): void { - if (!schema || schema[trackedSchemaSymbol]) { - return; - } - - schema[trackedSchemaSymbol] = true; - const entries = Object.values(schema.getTypeMap()); - - for (const type of entries) { - if (!isIntrospectionType(type) && isObjectType(type)) { - // @ts-expect-error - we know this is an object type - const fields = Object.values(type.getFields()); - - for (const field of fields) { - // @ts-expect-error - we hope there is a resolve field - const existingResolver = field.resolve; - // We are not going to wrap any default resolvers - if (!existingResolver) continue; - - // @ts-expect-error - we know this is a resolver fn - field.resolve = async (root, args, context, info) => { - let resolverFn: ResolverFn = existingResolver; - if (context && context[resolversHooksSymbol]) { - const hooks: OnResolverCalledHook[] = context[resolversHooksSymbol]; - const afterCalls: AfterResolverHook[] = []; - - for (const hook of hooks) { - const afterFn = await hook({ - root, - args, - context, - info, - resolverFn, - replaceResolverFn: newFn => { - resolverFn = newFn as ResolverFn; - }, - }); - afterFn && afterCalls.push(afterFn); - } - - try { - let result = await resolverFn(root, args, context, info); - - for (const afterFn of afterCalls) { - afterFn({ - result, - setResult: newResult => { - result = newResult; - }, - }); - } - - return result; - } catch (e) { - let resultErr = e; - - for (const afterFn of afterCalls) { - afterFn({ - result: resultErr, - setResult: newResult => { - resultErr = newResult; - }, - }); - } - - throw resultErr; - } - } else { - return resolverFn(root, args, context, info); - } - }; - } - } - } -} diff --git a/packages/core/test/execute.spec.ts b/packages/core/test/execute.spec.ts index f8ac840845..f0019dbd46 100644 --- a/packages/core/test/execute.spec.ts +++ b/packages/core/test/execute.spec.ts @@ -51,7 +51,6 @@ describe('execute', () => { const teskit = createTestkit([spiedPlugin.plugin], schema); await teskit.execute(query, {}, { test: 1 }); expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledTimes(1); - expect(spiedPlugin.spies.beforeResolver).toHaveBeenCalledTimes(3); expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledWith({ executeFn: expect.any(Function), setExecuteFn: expect.any(Function), @@ -71,7 +70,6 @@ describe('execute', () => { }, }); - expect(spiedPlugin.spies.afterResolver).toHaveBeenCalledTimes(3); expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledWith({ args: expect.any(Object), @@ -210,67 +208,6 @@ describe('execute', () => { }); }); - it('Should allow to register to before and after resolver calls', async () => { - const afterResolver = jest.fn(); - const onResolverCalled = jest.fn(() => afterResolver); - - const teskit = createTestkit( - [ - { - onResolverCalled, - }, - ], - schema - ); - - await teskit.execute(query); - expect(onResolverCalled).toHaveBeenCalledTimes(3); - expect(onResolverCalled).toHaveBeenCalledWith({ - root: {}, - args: {}, - context: expect.any(Object), - info: expect.objectContaining({ - fieldName: 'me', - }), - resolverFn: expect.any(Function), - replaceResolverFn: expect.any(Function), - }); - expect(onResolverCalled).toHaveBeenCalledWith({ - root: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, - args: {}, - context: expect.any(Object), - info: expect.objectContaining({ - fieldName: 'id', - }), - resolverFn: expect.any(Function), - replaceResolverFn: expect.any(Function), - }); - expect(onResolverCalled).toHaveBeenCalledWith({ - root: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, - args: {}, - context: expect.any(Object), - info: expect.objectContaining({ - fieldName: 'name', - }), - resolverFn: expect.any(Function), - replaceResolverFn: expect.any(Function), - }); - - expect(afterResolver).toHaveBeenCalledTimes(3); - expect(afterResolver).toHaveBeenCalledWith({ - result: { _id: 1, firstName: 'Dotan', lastName: 'Simha' }, - setResult: expect.any(Function), - }); - expect(afterResolver).toHaveBeenCalledWith({ - result: 1, - setResult: expect.any(Function), - }); - expect(afterResolver).toHaveBeenCalledWith({ - result: 'Dotan Simha', - setResult: expect.any(Function), - }); - }); - it('Should be able to manipulate streams', async () => { const streamExecuteFn = async function* () { for (const value of ['a', 'b', 'c', 'd']) { diff --git a/packages/plugins/apollo-tracing/package.json b/packages/plugins/apollo-tracing/package.json index 215ba19963..c844e92801 100644 --- a/packages/plugins/apollo-tracing/package.json +++ b/packages/plugins/apollo-tracing/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "apollo-tracing": "^0.15.0", "tslib": "^2.4.0" }, diff --git a/packages/plugins/apollo-tracing/src/index.ts b/packages/plugins/apollo-tracing/src/index.ts index f6d7a3c0a9..fd6fcb74b8 100644 --- a/packages/plugins/apollo-tracing/src/index.ts +++ b/packages/plugins/apollo-tracing/src/index.ts @@ -1,4 +1,5 @@ import { Plugin, handleStreamOrSingleExecutionResult } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { TracingFormat } from 'apollo-tracing'; import { GraphQLType, ResponsePath, responsePathAsArray } from 'graphql'; @@ -41,21 +42,24 @@ type TracingContextObject = { export const useApolloTracing = (): Plugin => { return { - onResolverCalled: ({ info, context }) => { - const ctx = context[apolloTracingSymbol] as TracingContextObject; - // Taken from https://github.com/apollographql/apollo-server/blob/main/packages/apollo-tracing/src/index.ts - const resolverCall: ResolverCall = { - path: info.path, - fieldName: info.fieldName, - parentType: info.parentType, - returnType: info.returnType, - startOffset: process.hrtime(ctx.hrtime), - }; - - return () => { - resolverCall.endOffset = process.hrtime(ctx.hrtime); - ctx.resolversTiming.push(resolverCall); - }; + onPluginInit({ addPlugin }) { + addPlugin( + useOnResolve(({ info, context }) => { + const ctx = context[apolloTracingSymbol] as TracingContextObject; + // Taken from https://github.com/apollographql/apollo-server/blob/main/packages/apollo-tracing/src/index.ts + const resolverCall: ResolverCall = { + path: info.path, + fieldName: info.fieldName, + parentType: info.parentType, + returnType: info.returnType, + startOffset: process.hrtime(ctx.hrtime), + }; + return () => { + resolverCall.endOffset = process.hrtime(ctx.hrtime); + ctx.resolversTiming.push(resolverCall); + }; + }) + ); }, onExecute(onExecuteContext) { const ctx: TracingContextObject = { diff --git a/packages/plugins/extended-validation/src/plugin.ts b/packages/plugins/extended-validation/src/plugin.ts index a21b6efaaf..1dfecb7721 100644 --- a/packages/plugins/extended-validation/src/plugin.ts +++ b/packages/plugins/extended-validation/src/plugin.ts @@ -24,13 +24,13 @@ type OnValidationFailedCallback = (params: { setResult: (result: ExecutionResult) => void; }) => void; -export const useExtendedValidation = (options: { +export const useExtendedValidation = = {}>(options: { rules: Array; /** * Callback that is invoked if the extended validation yields any errors. */ onValidationFailed?: OnValidationFailedCallback; -}): Plugin => { +}): Plugin => { let schemaTypeInfo: TypeInfo; function getTypeInfo(): TypeInfo | undefined { @@ -50,6 +50,7 @@ export const useExtendedValidation = (options: { didRun: false, }; extendContext({ + ...context, [symbolExtendedValidationRules]: validationRulesContext, }); } diff --git a/packages/plugins/newrelic/package.json b/packages/plugins/newrelic/package.json index 541df1537a..013a0b0d61 100644 --- a/packages/plugins/newrelic/package.json +++ b/packages/plugins/newrelic/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "tslib": "^2.4.0" }, "devDependencies": { diff --git a/packages/plugins/newrelic/src/index.ts b/packages/plugins/newrelic/src/index.ts index 8dddaa8a3c..840512bc42 100644 --- a/packages/plugins/newrelic/src/index.ts +++ b/packages/plugins/newrelic/src/index.ts @@ -1,4 +1,5 @@ -import { Plugin, OnResolverCalledHook, Path, isAsyncIterable, DefaultContext } from '@envelop/core'; +import { Plugin, Path, isAsyncIterable, DefaultContext } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { print, FieldNode, Kind, OperationDefinitionNode, ExecutionResult, GraphQLError } from 'graphql'; enum AttributeName { @@ -77,6 +78,53 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => { }); return { + onPluginInit({ addPlugin }) { + if (options.trackResolvers) { + addPlugin( + useOnResolve(async ({ args: resolversArgs, info }) => { + const instrumentationApi = await instrumentationApi$; + const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState; + const delimiter = transactionNameState.delimiter; + + const logger = await logger$; + const { returnType, path, parentType } = info; + const formattedPath = flattenPath(path, delimiter); + const currentSegment = instrumentationApi.getActiveSegment(); + if (!currentSegment) { + logger.trace('No active segment found at resolver call. Not recording resolver (%s).', formattedPath); + return () => {}; + } + + const resolverSegment = instrumentationApi.createSegment( + `resolver${delimiter}${formattedPath}`, + null, + currentSegment + ); + if (!resolverSegment) { + logger.trace('Resolver segment was not created (%s).', formattedPath); + return () => {}; + } + resolverSegment.start(); + resolverSegment.addAttribute(AttributeName.RESOLVER_FIELD_PATH, formattedPath); + resolverSegment.addAttribute(AttributeName.RESOLVER_TYPE_NAME, parentType.toString()); + resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT_TYPE, returnType.toString()); + if (options.includeResolverArgs) { + const rawArgs = resolversArgs || {}; + const resolverArgsToTrack = options.isResolverArgsRegex + ? filterPropertiesByRegex(rawArgs, options.includeResolverArgs as RegExp) + : rawArgs; + resolverSegment.addAttribute(AttributeName.RESOLVER_ARGS, JSON.stringify(resolverArgsToTrack)); + } + return ({ result }) => { + if (options.includeRawResult) { + resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT, JSON.stringify(result)); + } + resolverSegment.end(); + }; + }) + ); + } + }, async onExecute({ args }) { const instrumentationApi = await instrumentationApi$; const transactionNameState = instrumentationApi.agent.tracer.getTransaction().nameState; @@ -125,56 +173,7 @@ export const useNewRelic = (rawOptions?: UseNewRelicOptions): Plugin => { const operationSegment = instrumentationApi.getActiveSegment(); - const onResolverCalled: OnResolverCalledHook | undefined = options.trackResolvers - ? async ({ args: resolversArgs, info }) => { - const logger = await logger$; - const { returnType, path, parentType } = info; - const formattedPath = flattenPath(path, delimiter); - const currentSegment = instrumentationApi.getActiveSegment(); - - if (!currentSegment) { - logger.trace('No active segment found at resolver call. Not recording resolver (%s).', formattedPath); - return () => {}; - } - - const resolverSegment = instrumentationApi.createSegment( - `resolver${delimiter}${formattedPath}`, - null, - operationSegment - ); - - if (!resolverSegment) { - logger.trace('Resolver segment was not created (%s).', formattedPath); - return () => {}; - } - - resolverSegment.start(); - - resolverSegment.addAttribute(AttributeName.RESOLVER_FIELD_PATH, formattedPath); - resolverSegment.addAttribute(AttributeName.RESOLVER_TYPE_NAME, parentType.toString()); - resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT_TYPE, returnType.toString()); - - if (options.includeResolverArgs) { - const rawArgs = resolversArgs || {}; - const resolverArgsToTrack = options.isResolverArgsRegex - ? filterPropertiesByRegex(rawArgs, options.includeResolverArgs as RegExp) - : rawArgs; - - resolverSegment.addAttribute(AttributeName.RESOLVER_ARGS, JSON.stringify(resolverArgsToTrack)); - } - - return ({ result }) => { - if (options.includeRawResult) { - resolverSegment.addAttribute(AttributeName.RESOLVER_RESULT, JSON.stringify(result)); - } - - resolverSegment.end(); - }; - } - : undefined; - return { - onResolverCalled, onExecuteDone({ result }) { const sendResult = (singularResult: ExecutionResult) => { if (singularResult.data && options.includeRawResult) { diff --git a/packages/plugins/on-resolve/README.md b/packages/plugins/on-resolve/README.md new file mode 100644 index 0000000000..326065b65b --- /dev/null +++ b/packages/plugins/on-resolve/README.md @@ -0,0 +1,98 @@ +## `@envelop/on-resolve` + +This plugin allows you to hook into resolves of every field in the GraphQL schema. + +Useful for tracing or augmenting resolvers (and their results) with custom logic. + +## Getting Started + +``` +yarn add @envelop/on-resolve +``` + +## Usage Example + +### Custom field resolutions + +```ts +import { envelop } from '@envelop/core' +import { useOnResolve } from '@envelop/on-resolve' +import { specialResolver } from './my-resolvers' + +const getEnveloped = envelop({ + plugins: [ + // ... other plugins ... + useOnResolve(async function onResolve({ context, root, args, info, replaceResolver }) { + // replace special field's resolver + if (info.fieldName === 'special') { + replaceResolver(specialResolver) + } + + // replace field's result + if (info.fieldName === 'alwaysHello') { + return ({ setResult }) => { + setResult('hello') + } + } + }) + ] +}) +``` + +### Tracing + +```ts +import { envelop, Plugin } from '@envelop/core' +import { useOnResolve } from '@envelop/on-resolve' + +interface FieldTracingPluginContext { + tracerUrl: string +} + +function useFieldTracing() { + return { + onPluginInit({ addPlugin }) { + addPlugin( + useOnResolve(async function onResolve({ context, root, args, info }) { + await fetch(context.tracerUrl, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + startedResolving: { + ...info, + parent: root, + args + } + }) + }) + + return async () => { + await fetch(context.tracerUrl, { + method: 'POST', + headers: { + 'content-type': 'application/json' + }, + body: JSON.stringify({ + endedResolving: { + ...info, + parent: root, + args + } + }) + }) + } + }) + ) + } + } +} + +const getEnveloped = envelop({ + plugins: [ + // ... other plugins ... + useSpecialResolve() + ] +}) +``` diff --git a/packages/plugins/on-resolve/package.json b/packages/plugins/on-resolve/package.json new file mode 100644 index 0000000000..1e931c13d2 --- /dev/null +++ b/packages/plugins/on-resolve/package.json @@ -0,0 +1,66 @@ +{ + "name": "@envelop/on-resolve", + "version": "1.0.0", + "author": "Denis Badurina ", + "license": "MIT", + "sideEffects": false, + "repository": { + "type": "git", + "url": "https://github.com/n1ru4l/envelop.git", + "directory": "packages/plugins/on-resolve" + }, + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "require": { + "types": "./dist/typings/index.d.cts", + "default": "./dist/cjs/index.js" + }, + "import": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + }, + "default": { + "types": "./dist/typings/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "./*": { + "require": { + "types": "./dist/typings/*.d.cts", + "default": "./dist/cjs/*.js" + }, + "import": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + }, + "default": { + "types": "./dist/typings/*.d.ts", + "default": "./dist/esm/*.js" + } + }, + "./package.json": "./package.json" + }, + "typings": "dist/typings/index.d.ts", + "typescript": { + "definition": "dist/typings/index.d.ts" + }, + "dependencies": {}, + "devDependencies": { + "graphql": "16.3.0", + "typescript": "4.7.4" + }, + "peerDependencies": { + "@envelop/core": "^2.5.0", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + }, + "type": "module" +} diff --git a/packages/plugins/on-resolve/src/index.ts b/packages/plugins/on-resolve/src/index.ts new file mode 100644 index 0000000000..73b2ed9976 --- /dev/null +++ b/packages/plugins/on-resolve/src/index.ts @@ -0,0 +1,85 @@ +import { defaultFieldResolver, GraphQLResolveInfo, GraphQLSchema, isIntrospectionType, isObjectType } from 'graphql'; +import { Plugin, PromiseOrValue } from '@envelop/core'; + +export type Resolver = ( + root: unknown, + args: Record, + context: Context, + info: GraphQLResolveInfo +) => PromiseOrValue; + +export type AfterResolver = (options: { + result: unknown; + setResult: (newResult: unknown) => void; +}) => PromiseOrValue; + +export interface OnResolveOptions = {}> { + context: PluginContext; + root: unknown; + args: Record; + info: GraphQLResolveInfo; + resolver: Resolver; + replaceResolver: (newResolver: Resolver) => void; +} + +export type OnResolve = {}> = ( + options: OnResolveOptions +) => PromiseOrValue; + +/** + * Wraps the provided schema by hooking into the resolvers of every field. + * + * Use the `onResolve` argument to manipulate the resolver and its results/errors. + */ +export function useOnResolve = {}>( + onResolve: OnResolve +): Plugin { + return { + onSchemaChange({ schema: _schema }) { + const schema = _schema as GraphQLSchema; + if (!schema) return; // nothing to do if schema is missing + + for (const type of Object.values(schema.getTypeMap())) { + if (!isIntrospectionType(type) && isObjectType(type)) { + for (const field of Object.values(type.getFields())) { + let resolver = (field.resolve || defaultFieldResolver) as Resolver; + + field.resolve = async (root, args, context, info) => { + const afterResolve = await onResolve({ + root, + args, + context, + info, + resolver, + replaceResolver: newResolver => { + resolver = newResolver; + }, + }); + + let result; + try { + result = await resolver(root, args, context, info); + } catch (err) { + result = err as Error; + } + + if (typeof afterResolve === 'function') { + await afterResolve({ + result, + setResult: newResult => { + result = newResult; + }, + }); + } + + if (result instanceof Error) { + throw result; + } + return result; + }; + } + } + } + }, + }; +} diff --git a/packages/plugins/on-resolve/test/use-on-resolve.spec.ts b/packages/plugins/on-resolve/test/use-on-resolve.spec.ts new file mode 100644 index 0000000000..7b6954f8ff --- /dev/null +++ b/packages/plugins/on-resolve/test/use-on-resolve.spec.ts @@ -0,0 +1,63 @@ +import { OnResolveOptions, useOnResolve } from '@envelop/on-resolve'; +import { assertSingleExecutionValue, createTestkit } from '@envelop/testing'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +describe('useOnResolve', () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + value1: String! + value2: String! + } + `, + resolvers: { + Query: { + value1: () => 'value1', + value2: () => 'value2', + }, + }, + }); + + it('should invoke the callback for each resolver', async () => { + const onResolveDoneFn = jest.fn(); + const onResolveFn = jest.fn((_opts: OnResolveOptions) => onResolveDoneFn); + const testkit = createTestkit([useOnResolve(onResolveFn)], schema); + + await testkit.execute('{ value1, value2 }'); + + expect(onResolveFn).toBeCalledTimes(2); + expect(onResolveDoneFn).toBeCalledTimes(2); + + let i = 0; + for (const field of ['value1', 'value2']) { + expect(onResolveFn.mock.calls[i][0].context).toBeDefined(); + expect(onResolveFn.mock.calls[i][0].root).toBeDefined(); + expect(onResolveFn.mock.calls[i][0].args).toBeDefined(); + expect(onResolveFn.mock.calls[i][0].info).toBeDefined(); + expect(onResolveFn.mock.calls[i][0].info.fieldName).toBe(field); + expect(onResolveFn.mock.calls[i][0].resolver).toBeInstanceOf(Function); + expect(onResolveFn.mock.calls[i][0].replaceResolver).toBeInstanceOf(Function); + + expect(onResolveDoneFn.mock.calls[i][0].result).toBe(field); + expect(onResolveDoneFn.mock.calls[i][0].setResult).toBeInstanceOf(Function); + + i++; + } + }); + + it('should replace the result using the after hook', async () => { + const testkit = createTestkit( + [ + useOnResolve(() => ({ setResult }) => { + setResult('value2'); + }), + ], + schema + ); + + const result = await testkit.execute('{ value1 }'); + assertSingleExecutionValue(result); + + expect(result.data?.value1).toBe('value2'); + }); +}); diff --git a/packages/plugins/opentelemetry/package.json b/packages/plugins/opentelemetry/package.json index 01c8d6a6b6..2d784dd4f6 100644 --- a/packages/plugins/opentelemetry/package.json +++ b/packages/plugins/opentelemetry/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "@opentelemetry/api": "^1.0.0", "@opentelemetry/tracing": "^0.24.0", "tslib": "^2.4.0" @@ -56,7 +57,8 @@ "typescript": "4.7.4" }, "peerDependencies": { - "@envelop/core": "^2.6.0" + "@envelop/core": "^2.6.0", + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "buildOptions": { "input": "./src/index.ts" diff --git a/packages/plugins/opentelemetry/src/index.ts b/packages/plugins/opentelemetry/src/index.ts index 2e2a6b56fb..362d34c47e 100644 --- a/packages/plugins/opentelemetry/src/index.ts +++ b/packages/plugins/opentelemetry/src/index.ts @@ -1,4 +1,5 @@ import { Plugin, OnExecuteHookResult, isAsyncIterable } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { SpanAttributes, SpanKind } from '@opentelemetry/api'; import * as opentelemetry from '@opentelemetry/api'; import { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; @@ -45,41 +46,45 @@ export const useOpenTelemetry = ( const tracer = tracingProvider.getTracer(serviceName); return { - onResolverCalled: options.resolvers - ? ({ info, context, args }) => { - if (context && typeof context === 'object' && context[tracingSpanSymbol]) { - tracer.getActiveSpanProcessor(); - const ctx = opentelemetry.trace.setSpan(opentelemetry.context.active(), context[tracingSpanSymbol]); - const { fieldName, returnType, parentType } = info; - - const resolverSpan = tracer.startSpan( - `${parentType.name}.${fieldName}`, - { - attributes: { - [AttributeName.RESOLVER_FIELD_NAME]: fieldName, - [AttributeName.RESOLVER_TYPE_NAME]: parentType.toString(), - [AttributeName.RESOLVER_RESULT_TYPE]: returnType.toString(), - [AttributeName.RESOLVER_ARGS]: JSON.stringify(args || {}), + onPluginInit({ addPlugin }) { + if (options.resolvers) { + addPlugin( + useOnResolve(({ info, context, args }) => { + if (context && typeof context === 'object' && context[tracingSpanSymbol]) { + tracer.getActiveSpanProcessor(); + const ctx = opentelemetry.trace.setSpan(opentelemetry.context.active(), context[tracingSpanSymbol]); + const { fieldName, returnType, parentType } = info; + + const resolverSpan = tracer.startSpan( + `${parentType.name}.${fieldName}`, + { + attributes: { + [AttributeName.RESOLVER_FIELD_NAME]: fieldName, + [AttributeName.RESOLVER_TYPE_NAME]: parentType.toString(), + [AttributeName.RESOLVER_RESULT_TYPE]: returnType.toString(), + [AttributeName.RESOLVER_ARGS]: JSON.stringify(args || {}), + }, }, - }, - ctx - ); - - return ({ result }) => { - if (result instanceof Error) { - resolverSpan.recordException({ - name: AttributeName.RESOLVER_EXCEPTION, - message: JSON.stringify(result), - }); - } else { - resolverSpan.end(); - } - }; - } - - return () => {}; - } - : undefined, + ctx + ); + + return ({ result }) => { + if (result instanceof Error) { + resolverSpan.recordException({ + name: AttributeName.RESOLVER_EXCEPTION, + message: JSON.stringify(result), + }); + } else { + resolverSpan.end(); + } + }; + } + + return () => {}; + }) + ); + } + }, onExecute({ args, extendContext }) { const executionSpan = tracer.startSpan(`${args.operationName || 'Anonymous Operation'}`, { kind: spanKind, diff --git a/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts b/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts index 6d67b89c30..fecebd3d8f 100644 --- a/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts +++ b/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts @@ -67,7 +67,8 @@ describe('useOpenTelemetry', () => { await testInstance.execute(query); const actual = exporter.getFinishedSpans(); - expect(actual.length).toBe(1); - expect(actual[0].name).toBe('Anonymous Operation'); + expect(actual.length).toBe(2); + expect(actual[0].name).toBe('Query.ping'); + expect(actual[1].name).toBe('Anonymous Operation'); }); }); diff --git a/packages/plugins/operation-field-permissions/src/index.ts b/packages/plugins/operation-field-permissions/src/index.ts index ea93be2675..dc2473c2b3 100644 --- a/packages/plugins/operation-field-permissions/src/index.ts +++ b/packages/plugins/operation-field-permissions/src/index.ts @@ -124,7 +124,9 @@ type OperationScopeOptions = { const defaultFormatError = (schemaCoordinate: string) => `Insufficient permissions for selecting '${schemaCoordinate}'.`; -export const useOperationFieldPermissions = (opts: OperationScopeOptions): Plugin => { +export const useOperationFieldPermissions = ( + opts: OperationScopeOptions +): Plugin<{ [OPERATION_PERMISSIONS_SYMBOL]: ScopeContext }> => { return { onPluginInit({ addPlugin }) { addPlugin( diff --git a/packages/plugins/prometheus/package.json b/packages/plugins/prometheus/package.json index af0c9644ff..83809478c7 100644 --- a/packages/plugins/prometheus/package.json +++ b/packages/plugins/prometheus/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "tslib": "^2.4.0" }, "devDependencies": { diff --git a/packages/plugins/prometheus/src/index.ts b/packages/plugins/prometheus/src/index.ts index 97c7faee0e..73c72857cb 100644 --- a/packages/plugins/prometheus/src/index.ts +++ b/packages/plugins/prometheus/src/index.ts @@ -9,6 +9,7 @@ import { isIntrospectionOperationString, isAsyncIterable, } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { TypeInfo } from 'graphql'; import { Summary, Counter, Histogram, register as defaultRegistry } from 'prom-client'; import { @@ -324,31 +325,35 @@ export const usePrometheus = (config: PrometheusTracingPluginConfig = {}): Plugi : undefined; return { - onResolverCalled: resolversHistogram - ? ({ info, context }) => { - const shouldTrace = shouldTraceFieldResolver(info, config.resolversWhitelist); - - if (!shouldTrace) { - return undefined; - } - - const startTime = Date.now(); - - return () => { - const totalTime = (Date.now() - startTime) / 1000; - const paramsCtx = { - ...context[promPluginContext], - info, - }; - resolversHistogram.histogram.observe(resolversHistogram.fillLabelsFn(paramsCtx, context), totalTime); - }; - } - : undefined, onEnveloped({ extendContext }) { extendContext({ [promPluginExecutionStartTimeSymbol]: Date.now(), }); }, + onPluginInit({ addPlugin }) { + if (resolversHistogram) { + addPlugin( + useOnResolve(({ info, context }) => { + const shouldTrace = shouldTraceFieldResolver(info, config.resolversWhitelist); + + if (!shouldTrace) { + return undefined; + } + + const startTime = Date.now(); + + return () => { + const totalTime = (Date.now() - startTime) / 1000; + const paramsCtx = { + ...context[promPluginContext], + info, + }; + resolversHistogram.histogram.observe(resolversHistogram.fillLabelsFn(paramsCtx, context), totalTime); + }; + }) + ); + } + }, onSchemaChange({ schema }) { typeInfo = new TypeInfo(schema); }, diff --git a/packages/plugins/rate-limiter/package.json b/packages/plugins/rate-limiter/package.json index 72a572bb88..e90d106e05 100644 --- a/packages/plugins/rate-limiter/package.json +++ b/packages/plugins/rate-limiter/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "graphql-rate-limit": "3.3.0", "tslib": "^2.4.0" }, diff --git a/packages/plugins/rate-limiter/src/index.ts b/packages/plugins/rate-limiter/src/index.ts index 25dac08c4c..3ee6023d5c 100644 --- a/packages/plugins/rate-limiter/src/index.ts +++ b/packages/plugins/rate-limiter/src/index.ts @@ -1,4 +1,5 @@ import { Plugin } from '@envelop/core'; +import { useOnResolve } from '@envelop/on-resolve'; import { IntValueNode, StringValueNode, GraphQLResolveInfo } from 'graphql'; import { getDirective } from './utils.js'; import { getGraphQLRateLimiter } from 'graphql-rate-limit'; @@ -19,61 +20,66 @@ export type RateLimiterPluginOptions = { onRateLimitError?: (event: { error: string; identifier: string; context: unknown; info: GraphQLResolveInfo }) => void; }; -export const useRateLimiter = ( - options: RateLimiterPluginOptions -): Plugin<{ +interface RateLimiterContext { rateLimiterFn: ReturnType; -}> => { +} + +export const useRateLimiter = (options: RateLimiterPluginOptions): Plugin => { const rateLimiterFn = getGraphQLRateLimiter({ identifyContext: options.identifyFn }); return { - async onContextBuilding({ extendContext }) { - extendContext({ - rateLimiterFn, - }); - }, - async onResolverCalled({ args, root, context, info }) { - const rateLimitDirectiveNode = getDirective(info, options.rateLimitDirectiveName || 'rateLimit'); + onPluginInit({ addPlugin }) { + addPlugin( + useOnResolve(async ({ args, root, context, info }) => { + const rateLimitDirectiveNode = getDirective(info, options.rateLimitDirectiveName || 'rateLimit'); - if (rateLimitDirectiveNode && rateLimitDirectiveNode.arguments) { - const maxNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'max')?.value as IntValueNode; - const windowNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'window') - ?.value as StringValueNode; - const messageNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'message') - ?.value as IntValueNode; + if (rateLimitDirectiveNode && rateLimitDirectiveNode.arguments) { + const maxNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'max') + ?.value as IntValueNode; + const windowNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'window') + ?.value as StringValueNode; + const messageNode = rateLimitDirectiveNode.arguments.find(arg => arg.name.value === 'message') + ?.value as IntValueNode; - const message = messageNode.value; - const max = parseInt(maxNode.value); - const window = windowNode.value; - const id = options.identifyFn(context); + const message = messageNode.value; + const max = parseInt(maxNode.value); + const window = windowNode.value; + const id = options.identifyFn(context); - const errorMessage = await context.rateLimiterFn( - { parent: root, args, context, info }, - { - max, - window, - message: interpolate(message, { - id, - }), - } - ); - if (errorMessage) { - if (options.onRateLimitError) { - options.onRateLimitError({ - error: errorMessage, - identifier: id, - context, - info, - }); - } + const errorMessage = await context.rateLimiterFn( + { parent: root, args, context, info }, + { + max, + window, + message: interpolate(message, { + id, + }), + } + ); + if (errorMessage) { + if (options.onRateLimitError) { + options.onRateLimitError({ + error: errorMessage, + identifier: id, + context, + info, + }); + } - if (options.transformError) { - throw options.transformError(errorMessage); - } + if (options.transformError) { + throw options.transformError(errorMessage); + } - throw new Error(errorMessage); - } - } + throw new Error(errorMessage); + } + } + }) + ); + }, + async onContextBuilding({ extendContext }) { + extendContext({ + rateLimiterFn, + }); }, }; }; diff --git a/packages/plugins/sentry/package.json b/packages/plugins/sentry/package.json index fdd89fce42..2d32cc473e 100644 --- a/packages/plugins/sentry/package.json +++ b/packages/plugins/sentry/package.json @@ -47,6 +47,7 @@ "definition": "dist/typings/index.d.ts" }, "dependencies": { + "@envelop/on-resolve": "^1.0.0", "tslib": "^2.4.0" }, "devDependencies": { diff --git a/packages/plugins/sentry/src/index.ts b/packages/plugins/sentry/src/index.ts index a7d236db89..c3843585aa 100644 --- a/packages/plugins/sentry/src/index.ts +++ b/packages/plugins/sentry/src/index.ts @@ -1,9 +1,5 @@ -import { - Plugin, - OnResolverCalledHook, - handleStreamOrSingleExecutionResult, - OnExecuteDoneHookResultOnNextHook, -} from '@envelop/core'; +import { Plugin, handleStreamOrSingleExecutionResult, OnExecuteDoneHookResultOnNextHook } from '@envelop/core'; +import { OnResolve, useOnResolve } from '@envelop/on-resolve'; import * as Sentry from '@sentry/node'; import type { Span, TraceparentData } from '@sentry/types'; import { ExecutionArgs, GraphQLError, Kind, OperationDefinitionNode, print, responsePathAsArray } from 'graphql'; @@ -122,7 +118,7 @@ export const useSentry = (options: SentryPluginOptions = {}): Plugin => { }); } - const onResolverCalled: OnResolverCalledHook | undefined = trackResolvers + const onResolve: OnResolve | undefined = trackResolvers ? ({ args: resolversArgs, info, context }) => { const { rootSpan, opName, operationType } = context[sentryTracingSymbol] as SentryTracingContext; if (rootSpan) { @@ -168,7 +164,11 @@ export const useSentry = (options: SentryPluginOptions = {}): Plugin => { : undefined; return { - onResolverCalled, + onPluginInit({ addPlugin }) { + if (onResolve) { + addPlugin(useOnResolve(onResolve)); + } + }, onExecute({ args, extendContext }) { if (skipOperation(args)) { return; @@ -244,7 +244,7 @@ export const useSentry = (options: SentryPluginOptions = {}): Plugin => { Sentry.configureScope(scope => options.configureScope!(args, scope)); } - if (onResolverCalled) { + if (onResolve) { const sentryContext: SentryTracingContext = { rootSpan, opName, diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index 316a89ac6c..ffcec3835b 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -11,7 +11,7 @@ import { validate, } from 'graphql'; import { useSchema, envelop, isAsyncIterable } from '@envelop/core'; -import { GetEnvelopedFn, Optional, Plugin } from '@envelop/types'; +import { GetEnvelopedFn, Plugin } from '@envelop/types'; import { mapSchema as cloneSchema, isDocumentNode } from '@graphql-tools/utils'; export type ModifyPluginsFn = (plugins: Plugin[]) => Plugin[]; @@ -59,7 +59,6 @@ export function createSpiedPlugin() { beforeExecute: jest.fn(() => ({ onExecuteDone: baseSpies.afterExecute, })), - onResolverCalled: baseSpies.beforeResolver, }; return { @@ -75,7 +74,6 @@ export function createSpiedPlugin() { onValidate: spies.beforeValidate, onExecute: spies.beforeExecute, onContextBuilding: spies.beforeContextBuilding, - onResolverCalled: spies.beforeResolver, }, }; } diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index a31e7f6d85..1172893b55 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -1,5 +1,4 @@ import { Maybe, PromiseOrValue, AsyncIterableIteratorOrValue } from './utils.js'; -import { DefaultContext } from './context-types.js'; import { ExecuteFunction, ParseFunction, @@ -43,15 +42,15 @@ export type RegisterContextErrorHandler = (handler: OnContextErrorHandler) => vo /** * Payload forwarded to the onPluginInit hook. */ -export type OnPluginInitEventPayload = { +export type OnPluginInitEventPayload> = { /** * Register a new plugin. */ - addPlugin: (newPlugin: Plugin) => void; + addPlugin: (newPlugin: Plugin) => void; /** * A list of all currently active plugins. */ - plugins: Plugin[]; + plugins: Plugin[]; /** * Set the GraphQL schema. */ @@ -65,7 +64,9 @@ export type OnPluginInitEventPayload = { /** * Invoked when a plugin is initialized. */ -export type OnPluginInitHook = (options: OnPluginInitEventPayload) => void; +export type OnPluginInitHook> = ( + options: OnPluginInitEventPayload +) => void; /** onPluginInit */ export type OnEnvelopedHookEventPayload = { @@ -262,40 +263,6 @@ export type OnContextBuildingHook = ( options: OnContextBuildingEventPayload ) => PromiseOrValue>; -export type ResolverFn = ( - root: ParentType, - args: ArgsType, - context: ContextType, - info: any -) => PromiseOrValue; - -export type OnBeforeResolverCalledEventPayload< - ParentType = unknown, - ArgsType = DefaultArgs, - ContextType = unknown, - ResultType = unknown -> = { - root: ParentType; - args: ArgsType; - context: ContextType; - info: any; - resolverFn: ResolverFn; - replaceResolverFn: (newResolver: ResolverFn) => void; -}; - -export type AfterResolverEventPayload = { result: unknown | Error; setResult: (newResult: unknown) => void }; - -export type AfterResolverHook = (options: AfterResolverEventPayload) => void; - -export type OnResolverCalledHook< - ParentType = unknown, - ArgsType = DefaultArgs, - ContextType = DefaultContext, - ResultType = unknown -> = ( - options: OnBeforeResolverCalledEventPayload -) => PromiseOrValue; - /** * Execution arguments with inferred context value type. */ diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts index 37941b362d..7deb4d5df8 100644 --- a/packages/types/src/plugin.ts +++ b/packages/types/src/plugin.ts @@ -7,8 +7,6 @@ import { OnSchemaChangeHook, OnSubscribeHook, OnValidateHook, - OnResolverCalledHook, - DefaultArgs, } from './hooks.js'; export interface Plugin = {}> { @@ -23,7 +21,7 @@ export interface Plugin = {}> { /** * Invoked when a plugin is initialized. */ - onPluginInit?: OnPluginInitHook; + onPluginInit?: OnPluginInitHook; /** * Invoked for each execute call. */ @@ -44,8 +42,4 @@ export interface Plugin = {}> { * Invoked for each time the context is builded. */ onContextBuilding?: OnContextBuildingHook; - /** - * Invoked before each resolver has been invoked during the execution phase. - */ - onResolverCalled?: OnResolverCalledHook; } diff --git a/website/algolia-lockfile.json b/website/algolia-lockfile.json index 9e359f524a..d5281857e2 100644 --- a/website/algolia-lockfile.json +++ b/website/algolia-lockfile.json @@ -361,11 +361,6 @@ "children": [], "title": "`onSchemaChange(api)`", "anchor": "onschemachangeapi" - }, - { - "children": [], - "title": "`onResolverCalled(api)`", - "anchor": "onresolvercalledapi" } ], "title": "`onPluginInit(api)`", diff --git a/website/docs/plugins/lifecycle.mdx b/website/docs/plugins/lifecycle.mdx index 934d61617f..ac4598b227 100644 --- a/website/docs/plugins/lifecycle.mdx +++ b/website/docs/plugins/lifecycle.mdx @@ -240,18 +240,3 @@ Some plugins (like gateway implementations) could potentially change the schema - `schema` - the `GraphQLSchema` - `replaceSchema` - replaces the schema. Calling this will trigger `onSchemaChange` for all other plugins (except for the one that initiated the change); - -### `onResolverCalled(api)` - -Triggered when a resolver is called during the execution of the operation. - - - This hook will not call the default field resolver. - - -- `params` - an object with `{ root, args, context, info }` that was originally passed to the resolver. - -You can also return a function to run after the resolver is done, with the following API: - -- `result` - the resolver return value. -- `setResult` - replaces the resolver return value. diff --git a/yarn.lock b/yarn.lock index d21baba98b..0e595f92c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4174,14 +4174,14 @@ resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.29.11.tgz#d654a112973f5e004bf8438122bd7e56a8e5cd7e" integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== -"@typescript-eslint/eslint-plugin@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.27.0.tgz#23d82a4f21aaafd8f69dbab7e716323bb6695cc8" - integrity sha512-DDrIA7GXtmHXr1VCcx9HivA39eprYBIFxbQEHI6NyraRDxCGpxAFiYQAT/1Y0vh1C+o2vfBiy4IuPoXxtTZCAQ== - dependencies: - "@typescript-eslint/scope-manager" "5.27.0" - "@typescript-eslint/type-utils" "5.27.0" - "@typescript-eslint/utils" "5.27.0" +"@typescript-eslint/eslint-plugin@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.36.2.tgz#6df092a20e0f9ec748b27f293a12cb39d0c1fe4d" + integrity sha512-OwwR8LRwSnI98tdc2z7mJYgY60gf7I9ZfGjN5EjCwwns9bdTuQfAXcsjSB2wSQ/TVNYSGKf4kzVXbNGaZvwiXw== + dependencies: + "@typescript-eslint/scope-manager" "5.36.2" + "@typescript-eslint/type-utils" "5.36.2" + "@typescript-eslint/utils" "5.36.2" debug "^4.3.4" functional-red-black-tree "^1.0.1" ignore "^5.2.0" @@ -4189,69 +4189,70 @@ semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/parser@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.27.0.tgz#62bb091ed5cf9c7e126e80021bb563dcf36b6b12" - integrity sha512-8oGjQF46c52l7fMiPPvX4It3u3V3JipssqDfHQ2hcR0AeR8Zge+OYyKUCm5b70X72N1qXt0qgHenwN6Gc2SXZA== +"@typescript-eslint/parser@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.36.2.tgz#3ddf323d3ac85a25295a55fcb9c7a49ab4680ddd" + integrity sha512-qS/Kb0yzy8sR0idFspI9Z6+t7mqk/oRjnAYfewG+VN73opAUvmYL3oPIMmgOX6CnQS6gmVIXGshlb5RY/R22pA== dependencies: - "@typescript-eslint/scope-manager" "5.27.0" - "@typescript-eslint/types" "5.27.0" - "@typescript-eslint/typescript-estree" "5.27.0" + "@typescript-eslint/scope-manager" "5.36.2" + "@typescript-eslint/types" "5.36.2" + "@typescript-eslint/typescript-estree" "5.36.2" debug "^4.3.4" -"@typescript-eslint/scope-manager@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.27.0.tgz#a272178f613050ed62f51f69aae1e19e870a8bbb" - integrity sha512-VnykheBQ/sHd1Vt0LJ1JLrMH1GzHO+SzX6VTXuStISIsvRiurue/eRkTqSrG0CexHQgKG8shyJfR4o5VYioB9g== +"@typescript-eslint/scope-manager@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.36.2.tgz#a75eb588a3879ae659514780831370642505d1cd" + integrity sha512-cNNP51L8SkIFSfce8B1NSUBTJTu2Ts4nWeWbFrdaqjmn9yKrAaJUBHkyTZc0cL06OFHpb+JZq5AUHROS398Orw== dependencies: - "@typescript-eslint/types" "5.27.0" - "@typescript-eslint/visitor-keys" "5.27.0" + "@typescript-eslint/types" "5.36.2" + "@typescript-eslint/visitor-keys" "5.36.2" -"@typescript-eslint/type-utils@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.27.0.tgz#36fd95f6747412251d79c795b586ba766cf0974b" - integrity sha512-vpTvRRchaf628Hb/Xzfek+85o//zEUotr1SmexKvTfs7czXfYjXVT/a5yDbpzLBX1rhbqxjDdr1Gyo0x1Fc64g== +"@typescript-eslint/type-utils@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.36.2.tgz#752373f4babf05e993adf2cd543a763632826391" + integrity sha512-rPQtS5rfijUWLouhy6UmyNquKDPhQjKsaKH0WnY6hl/07lasj8gPaH2UD8xWkePn6SC+jW2i9c2DZVDnL+Dokw== dependencies: - "@typescript-eslint/utils" "5.27.0" + "@typescript-eslint/typescript-estree" "5.36.2" + "@typescript-eslint/utils" "5.36.2" debug "^4.3.4" tsutils "^3.21.0" -"@typescript-eslint/types@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.27.0.tgz#c3f44b9dda6177a9554f94a74745ca495ba9c001" - integrity sha512-lY6C7oGm9a/GWhmUDOs3xAVRz4ty/XKlQ2fOLr8GAIryGn0+UBOoJDWyHer3UgrHkenorwvBnphhP+zPmzmw0A== +"@typescript-eslint/types@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.36.2.tgz#a5066e500ebcfcee36694186ccc57b955c05faf9" + integrity sha512-9OJSvvwuF1L5eS2EQgFUbECb99F0mwq501w0H0EkYULkhFa19Qq7WFbycdw1PexAc929asupbZcgjVIe6OK/XQ== -"@typescript-eslint/typescript-estree@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.27.0.tgz#7965f5b553c634c5354a47dcce0b40b94611e995" - integrity sha512-QywPMFvgZ+MHSLRofLI7BDL+UczFFHyj0vF5ibeChDAJgdTV8k4xgEwF0geFhVlPc1p8r70eYewzpo6ps+9LJQ== +"@typescript-eslint/typescript-estree@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.36.2.tgz#0c93418b36c53ba0bc34c61fe9405c4d1d8fe560" + integrity sha512-8fyH+RfbKc0mTspfuEjlfqA4YywcwQK2Amcf6TDOwaRLg7Vwdu4bZzyvBZp4bjt1RRjQ5MDnOZahxMrt2l5v9w== dependencies: - "@typescript-eslint/types" "5.27.0" - "@typescript-eslint/visitor-keys" "5.27.0" + "@typescript-eslint/types" "5.36.2" + "@typescript-eslint/visitor-keys" "5.36.2" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" semver "^7.3.7" tsutils "^3.21.0" -"@typescript-eslint/utils@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.27.0.tgz#d0021cbf686467a6a9499bd0589e19665f9f7e71" - integrity sha512-nZvCrkIJppym7cIbP3pOwIkAefXOmfGPnCM0LQfzNaKxJHI6VjI8NC662uoiPlaf5f6ymkTy9C3NQXev2mdXmA== +"@typescript-eslint/utils@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.36.2.tgz#b01a76f0ab244404c7aefc340c5015d5ce6da74c" + integrity sha512-uNcopWonEITX96v9pefk9DC1bWMdkweeSsewJ6GeC7L6j2t0SJywisgkr9wUTtXk90fi2Eljj90HSHm3OGdGRg== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.27.0" - "@typescript-eslint/types" "5.27.0" - "@typescript-eslint/typescript-estree" "5.27.0" + "@typescript-eslint/scope-manager" "5.36.2" + "@typescript-eslint/types" "5.36.2" + "@typescript-eslint/typescript-estree" "5.36.2" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.27.0": - version "5.27.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.27.0.tgz#97aa9a5d2f3df8215e6d3b77f9d214a24db269bd" - integrity sha512-46cYrteA2MrIAjv9ai44OQDUoCZyHeGIc4lsjCUX2WT6r4C+kidz1bNiR4017wHOPUythYeH+Sc7/cFP97KEAA== +"@typescript-eslint/visitor-keys@5.36.2": + version "5.36.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.36.2.tgz#2f8f78da0a3bad3320d2ac24965791ac39dace5a" + integrity sha512-BtRvSR6dEdrNt7Net2/XDjbYKU5Ml6GqJgVfXT0CxTCJlnIqK7rAGreuWKMT2t8cFUT2Msv5oxw0GMRD7T5J7A== dependencies: - "@typescript-eslint/types" "5.27.0" + "@typescript-eslint/types" "5.36.2" eslint-visitor-keys "^3.3.0" "@tyriar/fibonacci-heap@^2.0.7": From 050d3170ee616f6a3cf130e5381833fdca6be4ae Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Wed, 7 Sep 2022 08:25:48 -0400 Subject: [PATCH 29/61] docs: order of plugins matter (#1513) --- website/docs/plugins/README.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/website/docs/plugins/README.mdx b/website/docs/plugins/README.mdx index 7ddc15b0aa..9ba7443d46 100644 --- a/website/docs/plugins/README.mdx +++ b/website/docs/plugins/README.mdx @@ -22,3 +22,9 @@ Plugins can also change the GraphQL schema during execution - so if your server ## What plugins are available? You can find a list of all plugins, their documentation and installation instructions on the [Envelop Plugin Hub](/plugins). + +## Does the order of plugins matter? + +The plugin order specifies the order in which the handlers of each plugin will be invoked. E.g. the `onExecute` hook of the plugin at the array index 0 will always be invoked before the plugin at the index 1. + +Plugins have the option to stop the execution completly and stop calling further plugins `onExecute` hooks. The `useResponseCache` plugin is such a plugin. Once a response is served from the cache all further `onExecute` are never called and all `onExecuteDone` hooks are never called at all. From b1bf2e9596c60abc11307b16eabc6cdfc9062acb Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Thu, 8 Sep 2022 14:09:39 -0400 Subject: [PATCH 30/61] feat: remove handler for validation and parse errors (#1510) * feat: remove hanlder for validation and parse errors * tests * make it work * Add docs * Fix serialization issue * Go * .. * update docs * update test * make it work * feat: add originalError in dev mode (#1514) * remove Fn appendix * name graphql error * make ts happy * make toJSON required Co-authored-by: Arda TANRIKULU Co-authored-by: Laurin Quast --- .changeset/rude-cats-peel.md | 10 + packages/core/docs/use-masked-errors.md | 64 +++ .../core/src/plugins/use-masked-errors.ts | 113 ++++ .../test/plugins/use-masked-errors.spec.ts | 483 ++++++++++++++++++ 4 files changed, 670 insertions(+) create mode 100644 .changeset/rude-cats-peel.md create mode 100644 packages/core/docs/use-masked-errors.md create mode 100644 packages/core/src/plugins/use-masked-errors.ts create mode 100644 packages/core/test/plugins/use-masked-errors.spec.ts diff --git a/.changeset/rude-cats-peel.md b/.changeset/rude-cats-peel.md new file mode 100644 index 0000000000..c0a8ef5543 --- /dev/null +++ b/.changeset/rude-cats-peel.md @@ -0,0 +1,10 @@ +--- +'@envelop/core': major +--- + +Remove `handleValidationErrors` and `handleParseErrors` options from `useMaskedErrors`. + +> ONLY masking validation errors OR ONLY disabling introspection errors does not make sense, as both can be abused for reverse-engineering the GraphQL schema (see https://github.com/nikitastupin/clairvoyance for reverse-engineering the schema based on validation error suggestions). +> https://github.com/n1ru4l/envelop/issues/1482#issue-1340015060 + +Rename `formatError` function option to `maskErrorFn` diff --git a/packages/core/docs/use-masked-errors.md b/packages/core/docs/use-masked-errors.md new file mode 100644 index 0000000000..979a62dd76 --- /dev/null +++ b/packages/core/docs/use-masked-errors.md @@ -0,0 +1,64 @@ +#### `useMaskedErrors` + +Prevent unexpected error messages from leaking to the GraphQL clients. + +```ts +import { envelop, useSchema, useMaskedErrors } from '@envelop/core' +import { makeExecutableSchema, GraphQLError } from 'graphql' + +const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + something: String! + somethingElse: String! + somethingSpecial: String! + } + `, + resolvers: { + Query: { + something: () => { + throw new GraphQLError('Error that is propagated to the clients.') + }, + somethingElse: () => { + throw new Error("Unsafe error that will be masked as 'Unexpected Error.'.") + }, + somethingSpecial: () => { + throw new GraphQLError('The error will have an extensions field.', { + code: 'ERR_CODE', + randomNumber: 123 + }) + } + } + } +}) + +const getEnveloped = envelop({ + plugins: [useSchema(schema), useMaskedErrors()] +}) +``` + +You may customize the default error message `Unexpected error.` with your own `errorMessage`: + +```ts +const getEnveloped = envelop({ + plugins: [useSchema(schema), useMaskedErrors({ errorMessage: 'Something went wrong.' })] +}) +``` + +Or provide a custom formatter when masking the output: + +```ts +import { isGraphQLError, MaskErrorFn } from '@envelop/core' + +export const customFormatError: MaskErrorFn = err => { + if (isGraphQLError(err)) { + return new GraphQLError('Sorry, something went wrong.') + } + + return err +} + +const getEnveloped = envelop({ + plugins: [useSchema(schema), useMaskedErrors({ maskErrorFn: customFormatError })] +}) +``` diff --git a/packages/core/src/plugins/use-masked-errors.ts b/packages/core/src/plugins/use-masked-errors.ts new file mode 100644 index 0000000000..ebc59ca740 --- /dev/null +++ b/packages/core/src/plugins/use-masked-errors.ts @@ -0,0 +1,113 @@ +import { Plugin, ExecutionResult } from '@envelop/types'; +import { handleStreamOrSingleExecutionResult } from '../utils.js'; + +export const DEFAULT_ERROR_MESSAGE = 'Unexpected error.'; + +export type MaskError = (error: unknown, message: string) => Error; + +export type SerializableGraphQLErrorLike = Error & { + name: 'GraphQLError'; + toJSON(): { message: string }; + extensions?: Record; +}; + +export function isGraphQLError(error: unknown): error is Error & { originalError?: Error } { + return error instanceof Error && error.name === 'GraphQLError'; +} + +function createSerializableGraphQLError( + message: string, + originalError: unknown, + isDev: boolean +): SerializableGraphQLErrorLike { + const error = new Error(message) as SerializableGraphQLErrorLike; + error.name = 'GraphQLError'; + if (isDev) { + const extensions = + originalError instanceof Error + ? { message: originalError.message, stack: originalError.stack } + : { message: String(originalError) }; + + Object.defineProperty(error, 'extensions', { + get() { + return extensions; + }, + }); + } + + Object.defineProperty(error, 'toJSON', { + value() { + return { + message: error.message, + extensions: error.extensions, + }; + }, + }); + + return error as SerializableGraphQLErrorLike; +} + +export const createDefaultMaskError = + (isDev: boolean): MaskError => + (error, message) => { + if (isGraphQLError(error)) { + if (error?.originalError) { + if (isGraphQLError(error.originalError)) { + return error; + } + return createSerializableGraphQLError(message, error, isDev); + } + return error; + } + return createSerializableGraphQLError(message, error, isDev); + }; + +const isDev = globalThis.process?.env?.NODE_ENV === 'development'; + +export const defaultMaskError: MaskError = createDefaultMaskError(isDev); + +export type UseMaskedErrorsOpts = { + /** The function used for identify and mask errors. */ + maskError?: MaskError; + /** The error message that shall be used for masked errors. */ + errorMessage?: string; +}; + +const makeHandleResult = + (maskError: MaskError, message: string) => + ({ result, setResult }: { result: ExecutionResult; setResult: (result: ExecutionResult) => void }) => { + if (result.errors != null) { + setResult({ ...result, errors: result.errors.map(error => maskError(error, message)) }); + } + }; + +export const useMaskedErrors = (opts?: UseMaskedErrorsOpts): Plugin => { + const maskError = opts?.maskError ?? defaultMaskError; + const message = opts?.errorMessage || DEFAULT_ERROR_MESSAGE; + const handleResult = makeHandleResult(maskError, message); + + return { + onPluginInit(context) { + context.registerContextErrorHandler(({ error, setError }) => { + setError(maskError(error, message)); + }); + }, + onExecute() { + return { + onExecuteDone(payload) { + return handleStreamOrSingleExecutionResult(payload, handleResult); + }, + }; + }, + onSubscribe() { + return { + onSubscribeResult(payload) { + return handleStreamOrSingleExecutionResult(payload, handleResult); + }, + onSubscribeError({ error, setError }) { + setError(maskError(error, message)); + }, + }; + }, + }; +}; diff --git a/packages/core/test/plugins/use-masked-errors.spec.ts b/packages/core/test/plugins/use-masked-errors.spec.ts new file mode 100644 index 0000000000..13a31ad45e --- /dev/null +++ b/packages/core/test/plugins/use-masked-errors.spec.ts @@ -0,0 +1,483 @@ +import { makeExecutableSchema } from '@graphql-tools/schema'; +import { + assertSingleExecutionValue, + assertStreamExecutionValue, + collectAsyncIteratorValues, + createTestkit, +} from '@envelop/testing'; +import { + useMaskedErrors, + DEFAULT_ERROR_MESSAGE, + MaskError, + createDefaultMaskError, +} from '../../src/plugins/use-masked-errors.js'; +import { useExtendContext } from '@envelop/core'; +import { useAuth0 } from '../../../plugins/auth0/src/index.js'; +import { GraphQLError } from 'graphql'; +import { createGraphQLError } from '@graphql-tools/utils'; + +describe('useMaskedErrors', () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + secret: String! + secretEnvelop: String! + secretWithExtensions: String! + } + type Subscription { + instantError: String + streamError: String + streamResolveError: String + instantGraphQLError: String + streamGraphQLError: String + streamResolveGraphQLError: String + } + `, + resolvers: { + Query: { + secret: () => { + throw new Error('Secret sauce that should not leak.'); + }, + secretEnvelop: () => { + throw createGraphQLError('This message goes to all the clients out there!', { extensions: { foo: 1 } }); + }, + secretWithExtensions: () => { + throw createGraphQLError('This message goes to all the clients out there!', { + extensions: { + code: 'Foo', + message: 'Bar', + }, + }); + }, + }, + Subscription: { + instantError: { + subscribe: async function () { + throw new Error('Noop'); + }, + resolve: _ => _, + }, + streamError: { + subscribe: async function* () { + throw new Error('Noop'); + }, + resolve: _ => _, + }, + streamResolveError: { + subscribe: async function* () { + yield '1'; + }, + resolve: _ => { + throw new Error('Noop'); + }, + }, + instantGraphQLError: { + subscribe: async function () { + throw createGraphQLError('Noop'); + }, + resolve: _ => _, + }, + streamGraphQLError: { + subscribe: async function* () { + throw createGraphQLError('Noop'); + }, + resolve: _ => _, + }, + streamResolveGraphQLError: { + subscribe: async function* () { + yield '1'; + }, + resolve: _ => { + throw createGraphQLError('Noop'); + }, + }, + }, + }, + }); + + it('Should mask non GraphQLErrors', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const result = await testInstance.execute(`query { secret }`); + assertSingleExecutionValue(result); + expect(result.errors).toBeDefined(); + expect(result.errors).toHaveLength(1); + const [error] = result.errors!; + expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); + }); + + it('Should not mask expected errors', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const result = await testInstance.execute(`query { secretEnvelop }`); + assertSingleExecutionValue(result); + expect(result.errors).toBeDefined(); + expect(result.errors).toHaveLength(1); + const [error] = result.errors!; + expect(error.message).toEqual('This message goes to all the clients out there!'); + expect(error.extensions).toEqual({ foo: 1 }); + }); + + it('Should not mask GraphQL operation syntax errors (of course it does not since we are only hooking in after execute, but just to be sure)', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const result = await testInstance.execute(`query { idonotexist }`); + assertSingleExecutionValue(result); + expect(result.errors).toBeDefined(); + expect(result.errors).toHaveLength(1); + const [error] = result.errors!; + expect(error.message).toEqual('Cannot query field "idonotexist" on type "Query".'); + }); + + it('Should forward extensions from GraphQLError to final GraphQLError in errors array', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const result = await testInstance.execute(`query { secretWithExtensions }`); + assertSingleExecutionValue(result); + expect(result.errors).toBeDefined(); + expect(result.errors).toHaveLength(1); + const [error] = result.errors!; + expect(error.extensions).toEqual({ + code: 'Foo', + message: 'Bar', + }); + expect(JSON.stringify(result)).toMatchInlineSnapshot( + `"{\\"errors\\":[{\\"message\\":\\"This message goes to all the clients out there!\\",\\"locations\\":[{\\"line\\":1,\\"column\\":9}],\\"path\\":[\\"secretWithExtensions\\"],\\"extensions\\":{\\"code\\":\\"Foo\\",\\"message\\":\\"Bar\\"}}],\\"data\\":null}"` + ); + }); + + it('Should properly mask context creation errors with a custom error message', async () => { + expect.assertions(1); + const testInstance = createTestkit( + [ + useExtendContext((): {} => { + throw new Error('No context for you!'); + }), + useMaskedErrors({ errorMessage: 'My Custom Error Message.' }), + ], + schema + ); + try { + await testInstance.execute(`query { secretWithExtensions }`); + } catch (err) { + expect(err).toMatchInlineSnapshot(`[GraphQLError: My Custom Error Message.]`); + } + }); + it('Should properly mask context creation errors', async () => { + expect.assertions(1); + const testInstance = createTestkit( + [ + useExtendContext((): {} => { + throw new Error('No context for you!'); + }), + useMaskedErrors(), + ], + schema + ); + try { + await testInstance.execute(`query { secretWithExtensions }`); + } catch (err: any) { + expect(err.message).toEqual(DEFAULT_ERROR_MESSAGE); + } + }); + + it('Should not mask expected context creation errors', async () => { + expect.assertions(2); + const testInstance = createTestkit( + [ + useExtendContext((): {} => { + throw createGraphQLError('No context for you!', { extensions: { foo: 1 } }); + }), + useMaskedErrors(), + ], + schema + ); + try { + await testInstance.execute(`query { secretWithExtensions }`); + } catch (err) { + if (err instanceof GraphQLError) { + expect(err.message).toEqual(`No context for you!`); + expect(err.extensions).toEqual({ foo: 1 }); + } else { + throw err; + } + } + }); + + it('Should mask subscribe (sync/promise) subscription errors', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const result = await testInstance.execute(`subscription { instantError }`); + assertSingleExecutionValue(result); + expect(result.errors).toBeDefined(); + expect(result.errors).toHaveLength(1); + const [error] = result.errors!; + expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); + }); + + it('Should mask subscribe (sync/promise) subscription errors with a custom error message', async () => { + const testInstance = createTestkit( + [useMaskedErrors({ errorMessage: 'My Custom subscription error message.' })], + schema + ); + const result = await testInstance.execute(`subscription { instantError }`); + assertSingleExecutionValue(result); + expect(result.errors).toBeDefined(); + expect(result.errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: My Custom subscription error message.], + ] + `); + }); + + it('Should not mask subscribe (sync/promise) subscription GraphQL errors', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const result = await testInstance.execute(`subscription { instantGraphQLError }`); + assertSingleExecutionValue(result); + expect(result.errors).toBeDefined(); + expect(result.errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: Noop], + ] + `); + }); + + it('Should mask subscribe (AsyncIterable) subscription errors', async () => { + expect.assertions(1); + const testInstance = createTestkit([useMaskedErrors()], schema); + const resultStream = await testInstance.execute(`subscription { streamError }`); + assertStreamExecutionValue(resultStream); + try { + await collectAsyncIteratorValues(resultStream); + } catch (err: any) { + expect(err.message).toEqual(DEFAULT_ERROR_MESSAGE); + } + }); + + it('Should mask subscribe (AsyncIterable) subscription errors with a custom error message', async () => { + expect.assertions(1); + const testInstance = createTestkit( + [useMaskedErrors({ errorMessage: 'My AsyncIterable Custom Error Message.' })], + schema + ); + const resultStream = await testInstance.execute(`subscription { streamError }`); + assertStreamExecutionValue(resultStream); + try { + await collectAsyncIteratorValues(resultStream); + } catch (err) { + expect(err).toMatchInlineSnapshot(`[GraphQLError: My AsyncIterable Custom Error Message.]`); + } + }); + + it('Should not mask subscribe (AsyncIterable) subscription envelop errors', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const resultStream = await testInstance.execute(`subscription { streamGraphQLError }`); + assertStreamExecutionValue(resultStream); + try { + await collectAsyncIteratorValues(resultStream); + } catch (err) { + expect(err).toMatchInlineSnapshot(`[GraphQLError: Noop]`); + } + }); + + it('Should mask resolve subscription errors', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const resultStream = await testInstance.execute(`subscription { streamResolveError }`); + assertStreamExecutionValue(resultStream); + const allResults = await collectAsyncIteratorValues(resultStream); + expect(allResults).toHaveLength(1); + const [result] = allResults; + expect(result.errors).toBeDefined(); + expect(result.errors).toHaveLength(1); + const [error] = result.errors!; + expect(error.message).toEqual(DEFAULT_ERROR_MESSAGE); + }); + + it('Should mask resolve subscription errors with a custom error message', async () => { + const testInstance = createTestkit( + [useMaskedErrors({ errorMessage: 'Custom resolve subscription errors.' })], + schema + ); + const resultStream = await testInstance.execute(`subscription { streamResolveError }`); + assertStreamExecutionValue(resultStream); + const allResults = await collectAsyncIteratorValues(resultStream); + expect(allResults).toHaveLength(1); + const [result] = allResults; + expect(JSON.stringify(result)).toMatchInlineSnapshot( + `"{\\"errors\\":[{\\"message\\":\\"Custom resolve subscription errors.\\"}],\\"data\\":{\\"streamResolveError\\":null}}"` + ); + }); + + it('Should not mask resolve subscription envelop errors', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const resultStream = await testInstance.execute(`subscription { streamResolveGraphQLError }`); + assertStreamExecutionValue(resultStream); + const allResults = await collectAsyncIteratorValues(resultStream); + expect(allResults).toHaveLength(1); + const [result] = allResults; + expect(result.errors).toBeDefined(); + expect(result.errors).toMatchInlineSnapshot(` + Array [ + [GraphQLError: Noop], + ] + `); + }); + + it('Should not mask auth0 header errors', async () => { + expect.assertions(2); + const auto0Options = { + domain: 'domain.com', + audience: 'audience', + headerName: 'authorization', + preventUnauthenticatedAccess: false, + extendContextField: 'auth0', + tokenType: 'Bearer', + }; + const testInstance = createTestkit([useMaskedErrors(), useAuth0(auto0Options)], schema); + try { + await testInstance.execute(`query { secret }`, {}, { request: { headers: { authorization: 'Something' } } }); + } catch (err) { + expect(err).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`); + } + + try { + await testInstance.execute(`query { secret }`, {}, { request: { headers: { authorization: 'Something else' } } }); + } catch (err) { + expect(err).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`); + } + }); + + it('should not mask parse errors', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const result = await testInstance.execute(`query { a `, {}); + assertSingleExecutionValue(result); + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Syntax Error: Expected Name, found .], + ], + } + `); + }); + + it('should not mask validation errors', async () => { + const testInstance = createTestkit([useMaskedErrors()], schema); + const result = await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); + assertSingleExecutionValue(result); + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Cannot query field "iDoNotExistsMyGuy" on type "Query".], + ], + } + `); + }); + + it('should use custom error mask function for execution errors', async () => { + const customErrorMaskFn: MaskError = e => + new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { + custom: true, + }); + const testInstance = createTestkit([useMaskedErrors({ maskError: customErrorMaskFn })], schema); + const result = await testInstance.execute(`query { secret }`); + assertSingleExecutionValue(result); + expect(result).toMatchInlineSnapshot(` + Object { + "data": null, + "errors": Array [ + [GraphQLError: Custom error message for Secret sauce that should not leak. + + GraphQL request:1:9 + 1 | query { secret } + | ^], + ], + } + `); + expect(JSON.stringify(result)).toMatchInlineSnapshot( + `"{\\"errors\\":[{\\"message\\":\\"Custom error message for Secret sauce that should not leak.\\\\n\\\\nGraphQL request:1:9\\\\n1 | query { secret }\\\\n | ^\\",\\"extensions\\":{\\"custom\\":true}}],\\"data\\":null}"` + ); + }); + + it('should use custom error mask function for subscribe (AsyncIterable) subscription errors', async () => { + const customErrorMaskFn: MaskError = e => + new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { + custom: true, + }); + expect.assertions(2); + const testInstance = createTestkit([useMaskedErrors({ maskError: customErrorMaskFn })], schema); + const resultStream = await testInstance.execute(`subscription { streamError }`); + assertStreamExecutionValue(resultStream); + try { + await collectAsyncIteratorValues(resultStream); + } catch (err: any) { + expect(err.message).toEqual('Custom error message for Error: Noop'); + expect(err.extensions.custom).toBe(true); + } + }); + + it('should use custom error mask function for errors while building the context', async () => { + const customErrorMaskFn: MaskError = e => + new GraphQLError('Custom error message for ' + e, null, null, null, null, null, { + custom: true, + }); + const testInstance = createTestkit( + [ + useMaskedErrors({ maskError: customErrorMaskFn }), + useExtendContext(() => { + throw createGraphQLError('Custom error'); + return {}; + }), + ], + schema + ); + try { + await testInstance.execute(`query { secret }`, {}, {}); + } catch (e) { + expect((e as GraphQLError).message).toEqual('Custom error message for Custom error'); + } + expect.assertions(1); + }); + + it('should include the original error message stack in the extensions in development mode', async () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + foo: String + } + `, + resolvers: { + Query: { + foo: () => { + throw new Error("I'm a teapot"); + }, + }, + }, + }); + const testInstance = createTestkit([useMaskedErrors({ maskError: createDefaultMaskError(true) })], schema); + const result = await testInstance.execute(`query { foo }`, {}, {}); + assertSingleExecutionValue(result); + expect(result.errors?.[0].extensions).toEqual({ + message: "I'm a teapot", + stack: expect.stringMatching(/^Error: I'm a teapot/), + }); + }); + + it('should include the original thrown thing in the extensions in development mode', async () => { + const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + foo: String + } + `, + resolvers: { + Query: { + foo: () => { + throw "I'm a teapot"; + }, + }, + }, + }); + const testInstance = createTestkit([useMaskedErrors({ maskError: createDefaultMaskError(true) })], schema); + const result = await testInstance.execute(`query { foo }`, {}, {}); + assertSingleExecutionValue(result); + expect(result.errors?.[0].extensions).toEqual({ + message: 'Unexpected error value: "I\'m a teapot"', + stack: expect.stringMatching(/NonErrorThrown: Unexpected error value: \"I'm a teapot/), + }); + }); +}); From 503846a86995815f8aa95d7d805ed2010387b988 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 17:29:13 +0200 Subject: [PATCH 31/61] begin --- packages/core/src/create.ts | 1 + packages/core/src/orchestrator.ts | 36 +++++++++++++++++++++++++++++ packages/core/test/perform.spec.ts | 25 ++++++++++++++++++++ packages/types/src/get-enveloped.ts | 2 ++ packages/types/src/hooks.ts | 31 +++++++++++++++++++++++++ packages/types/src/plugin.ts | 5 ++++ 6 files changed, 100 insertions(+) create mode 100644 packages/core/test/perform.spec.ts diff --git a/packages/core/src/create.ts b/packages/core/src/create.ts index 498a3c4996..6e5d5e0955 100644 --- a/packages/core/src/create.ts +++ b/packages/core/src/create.ts @@ -50,6 +50,7 @@ export function envelop>[]>(options: { execute: typedOrchestrator.execute, subscribe: typedOrchestrator.subscribe, schema: typedOrchestrator.getCurrentSchema(), + perform: typedOrchestrator.perform(initialContext), }; }; diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index f3a7f2a9a5..288f838237 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -30,6 +30,7 @@ import { ParseFunction, ValidateFunction, ExecutionResult, + PerformFunction, } from '@envelop/types'; import { errorAsyncIterator, @@ -51,6 +52,7 @@ export type EnvelopOrchestrator< subscribe: ReturnType>['subscribe']; contextFactory: EnvelopContextFnWrapper>['contextFactory'], PluginsContext>; getCurrentSchema: () => Maybe; + perform: EnvelopContextFnWrapper; }; type EnvelopOrchestratorOptions = { @@ -556,6 +558,39 @@ export function createEnvelopOrchestrator } } + const customPerform: EnvelopContextFnWrapper = initialContext => { + const parse = customParse(initialContext); + const validate = customValidate(initialContext); + const contextFactory = customContextFactory(initialContext); + + return async params => { + let document; + try { + document = parse(params.query); + } catch (err) { + return { errors: [err] }; + } + + const validationErrors = validate(schema, document); + if (validationErrors.length) { + return { errors: validationErrors }; + } + + // TODO: add context to perform + const context = await contextFactory(); + + // TODO: handle subscriptions + const result = await customExecute({ + document, + schema, + variableValues: params.variables, + contextValue: context, + }); + + return result; + }; + }; + return { getCurrentSchema() { return schema; @@ -566,5 +601,6 @@ export function createEnvelopOrchestrator execute: customExecute as ExecuteFunction, subscribe: customSubscribe, contextFactory: customContextFactory, + perform: customPerform, }; } diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts new file mode 100644 index 0000000000..a96c6f8d58 --- /dev/null +++ b/packages/core/test/perform.spec.ts @@ -0,0 +1,25 @@ +import { assertSingleExecutionValue, createSpiedPlugin, createTestkit } from '@envelop/testing'; +import { parse, validate, execute, subscribe } from 'graphql'; +import { envelop, useSchema } from '../src/index.js'; +import { schema, query } from './common.js'; + +const graphqlFuncs = { parse, validate, execute, subscribe }; + +describe('perform', () => { + it('should parse, validate, assemble context and execute', async () => { + const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: '{ me { id } }' }); + assertSingleExecutionValue(result); + + expect(result.data).toMatchInlineSnapshot(` + Object { + "me": Object { + "id": "1", + }, + } + `); + }); +}); diff --git a/packages/types/src/get-enveloped.ts b/packages/types/src/get-enveloped.ts index b1505b8551..15626babc6 100644 --- a/packages/types/src/get-enveloped.ts +++ b/packages/types/src/get-enveloped.ts @@ -1,6 +1,7 @@ import { Plugin } from './plugin.js'; import { ExecuteFunction, ParseFunction, SubscribeFunction, ValidateFunction } from './graphql.js'; import { ArbitraryObject, Spread, PromiseOrValue } from './utils.js'; +import { PerformFunction } from './hooks.js'; export { ArbitraryObject } from './utils.js'; export type EnvelopContextFnWrapper = ( @@ -17,6 +18,7 @@ export type GetEnvelopedFn = { contextExtension?: ContextExtension ) => PromiseOrValue>; schema: any; + perform: PerformFunction; }; _plugins: Plugin[]; }; diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index 1172893b55..ef398b759c 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -502,3 +502,34 @@ export type OnSubscribeHookResult = { export type OnSubscribeHook = ( options: OnSubscribeEventPayload ) => PromiseOrValue>; + +export interface PerformParams { + operationName?: string; + query: string; + variables?: Record; +} + +/** + * Performs the parsing, validation, context assembly and execution/subscription. + * + * Will never throw GraphQL errors, they will be constructed accordingly and placed in the result. + */ +export type PerformFunction = (params: PerformParams) => Promise>; + +export type OnPerformEventPayload = { + params: PerformParams; + setParams: (newParams: PerformParams) => void; +}; + +export type OnPerformDoneEventPayload = { + result: AsyncIterableIteratorOrValue; + setResult: (newResult: AsyncIterableIteratorOrValue) => void; +}; + +export type OnPerformDoneHook = (options: OnPerformDoneEventPayload) => PromiseOrValue; + +export type OnPerformHookResult = { + onPerformDone?: OnPerformDoneHook; +}; + +export type OnPerformHook = (options: OnPerformEventPayload) => PromiseOrValue; diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts index 7deb4d5df8..c88df508ee 100644 --- a/packages/types/src/plugin.ts +++ b/packages/types/src/plugin.ts @@ -7,6 +7,7 @@ import { OnSchemaChangeHook, OnSubscribeHook, OnValidateHook, + OnPerformHook, } from './hooks.js'; export interface Plugin = {}> { @@ -42,4 +43,8 @@ export interface Plugin = {}> { * Invoked for each time the context is builded. */ onContextBuilding?: OnContextBuildingHook; + /** + * Invoked for each perform call. + */ + onPerform?: OnPerformHook; } From 19cd80c1a5654690c707dcbf3faccd10ed96f939 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 17:30:50 +0200 Subject: [PATCH 32/61] organize imports --- packages/core/test/perform.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index a96c6f8d58..70b30943ac 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -1,7 +1,7 @@ -import { assertSingleExecutionValue, createSpiedPlugin, createTestkit } from '@envelop/testing'; import { parse, validate, execute, subscribe } from 'graphql'; import { envelop, useSchema } from '../src/index.js'; -import { schema, query } from './common.js'; +import { assertSingleExecutionValue } from '@envelop/testing'; +import { schema } from './common.js'; const graphqlFuncs = { parse, validate, execute, subscribe }; From 823b3186ceda9297cf5a02d0e8ab2807c3b0057a Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 17:57:44 +0200 Subject: [PATCH 33/61] support subscriptions --- packages/core/src/orchestrator.ts | 15 ++++++-- packages/core/src/utils.ts | 12 ++++++ packages/core/test/perform.spec.ts | 59 +++++++++++++++++++++++++++--- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 288f838237..53bcd338a2 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -39,6 +39,7 @@ import { makeSubscribe, mapAsyncIterator, isAsyncIterable, + isSubscriptionOperation, } from './utils.js'; export type EnvelopOrchestrator< @@ -579,15 +580,21 @@ export function createEnvelopOrchestrator // TODO: add context to perform const context = await contextFactory(); - // TODO: handle subscriptions - const result = await customExecute({ + if (isSubscriptionOperation(document, params.operationName)) { + return await customSubscribe({ + document, + schema, + variableValues: params.variables, + contextValue: context, + }); + } + + return await customExecute({ document, schema, variableValues: params.variables, contextValue: context, }); - - return result; }; }; diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index fe0ebd1bd6..8126e59a0d 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -219,3 +219,15 @@ export function errorAsyncIterator( return stream; } + +export function isSubscriptionOperation(document: any, operationName?: string): boolean { + if (operationName) { + return document.definitions.some( + (def: any) => + def.kind === 'OperationDefinition' && def.name?.value === operationName && def.operation === 'subscription' + ); + } + return document.definitions.some( + (def: any) => def.kind === 'OperationDefinition' && def.operation === 'subscription' + ); +} diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index 70b30943ac..a847eb0ed8 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -1,25 +1,72 @@ import { parse, validate, execute, subscribe } from 'graphql'; import { envelop, useSchema } from '../src/index.js'; -import { assertSingleExecutionValue } from '@envelop/testing'; -import { schema } from './common.js'; +import { assertSingleExecutionValue, assertStreamExecutionValue } from '@envelop/testing'; +import { makeExecutableSchema } from '@graphql-tools/schema'; const graphqlFuncs = { parse, validate, execute, subscribe }; +const greetings = ['Hello', 'Bonjour', 'Ciao']; +const schema = makeExecutableSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String! + } + type Subscription { + greetings: String! + } + `, + resolvers: { + Query: { + hello() { + return 'world'; + }, + }, + Subscription: { + greetings: { + async *subscribe() { + for (const greet of greetings) { + yield { greetings: greet }; + } + }, + }, + }, + }, +}); + describe('perform', () => { it('should parse, validate, assemble context and execute', async () => { const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); const { perform } = getEnveloped(); - const result = await perform({ query: '{ me { id } }' }); + const result = await perform({ query: '{ hello }' }); assertSingleExecutionValue(result); - expect(result.data).toMatchInlineSnapshot(` + expect(result).toMatchInlineSnapshot(` Object { - "me": Object { - "id": "1", + "data": Object { + "hello": "world", }, } `); }); + + it('should parse, validate, assemble context and subscribe', async () => { + const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: 'subscription { greetings }' }); + assertStreamExecutionValue(result); + + let i = 0; + for await (const part of result) { + expect(part).toEqual({ + data: { + greetings: greetings[i], + }, + }); + i++; + } + }); }); From 8b3de5eefa4701d9e8896da0a4d06a292fdb0949 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 18:04:08 +0200 Subject: [PATCH 34/61] with context extension --- packages/core/src/orchestrator.ts | 5 ++--- packages/types/src/get-enveloped.ts | 11 +++++++---- packages/types/src/hooks.ts | 5 ++++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 53bcd338a2..b37f756fdb 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -564,7 +564,7 @@ export function createEnvelopOrchestrator const validate = customValidate(initialContext); const contextFactory = customContextFactory(initialContext); - return async params => { + return async (params, contextExtension) => { let document; try { document = parse(params.query); @@ -577,8 +577,7 @@ export function createEnvelopOrchestrator return { errors: validationErrors }; } - // TODO: add context to perform - const context = await contextFactory(); + const context = await contextFactory(contextExtension); if (isSubscriptionOperation(document, params.operationName)) { return await customSubscribe({ diff --git a/packages/types/src/get-enveloped.ts b/packages/types/src/get-enveloped.ts index 15626babc6..a3826076d3 100644 --- a/packages/types/src/get-enveloped.ts +++ b/packages/types/src/get-enveloped.ts @@ -1,7 +1,7 @@ import { Plugin } from './plugin.js'; -import { ExecuteFunction, ParseFunction, SubscribeFunction, ValidateFunction } from './graphql.js'; -import { ArbitraryObject, Spread, PromiseOrValue } from './utils.js'; -import { PerformFunction } from './hooks.js'; +import { ExecuteFunction, ExecutionResult, ParseFunction, SubscribeFunction, ValidateFunction } from './graphql.js'; +import { ArbitraryObject, Spread, PromiseOrValue, AsyncIterableIteratorOrValue } from './utils.js'; +import { PerformParams } from './hooks.js'; export { ArbitraryObject } from './utils.js'; export type EnvelopContextFnWrapper = ( @@ -18,7 +18,10 @@ export type GetEnvelopedFn = { contextExtension?: ContextExtension ) => PromiseOrValue>; schema: any; - perform: PerformFunction; + perform: ( + params: PerformParams, + contextExtension?: ContextExtension + ) => Promise>; }; _plugins: Plugin[]; }; diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index ef398b759c..a4e6ce05a0 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -514,7 +514,10 @@ export interface PerformParams { * * Will never throw GraphQL errors, they will be constructed accordingly and placed in the result. */ -export type PerformFunction = (params: PerformParams) => Promise>; +export type PerformFunction = ( + params: PerformParams, + contextExtension?: ContextExtension +) => Promise>; export type OnPerformEventPayload = { params: PerformParams; From dbfb294b04dd251a8f6d66710562e41d2d31ed8b Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 18:07:17 +0200 Subject: [PATCH 35/61] should include parsing and validation errors in result --- packages/core/test/perform.spec.ts | 34 ++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index a847eb0ed8..82024c45b9 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -69,4 +69,38 @@ describe('perform', () => { i++; } }); + + it('should include parsing errors in result', async () => { + const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: '{' }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Syntax Error: Expected Name, found .], + ], + } + `); + }); + + it('should include validation errors in result', async () => { + const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: '{ idontexist }' }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Cannot query field "idontexist" on type "Query".], + ], + } + `); + }); }); From 63929b13468f5460921d2e6e55cb4012a8df261f Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 18:38:24 +0200 Subject: [PATCH 36/61] on perform plugin --- packages/core/src/orchestrator.ts | 53 ++++++++++++++++++++++++------ packages/core/test/perform.spec.ts | 38 ++++++++++++++++++++- packages/types/src/hooks.ts | 8 +++-- packages/types/src/plugin.ts | 2 +- 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index b37f756fdb..e3b2050ad0 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -31,6 +31,9 @@ import { ValidateFunction, ExecutionResult, PerformFunction, + OnPerformHook, + OnPerformHookResult, + OnPerformDoneHook, } from '@envelop/types'; import { errorAsyncIterator, @@ -118,15 +121,17 @@ export function createEnvelopOrchestrator subscribe: [] as OnSubscribeHook[], execute: [] as OnExecuteHook[], context: [] as OnContextBuildingHook[], + perform: [] as OnPerformHook[], }; - for (const { onContextBuilding, onExecute, onParse, onSubscribe, onValidate, onEnveloped } of plugins) { + for (const { onContextBuilding, onExecute, onParse, onSubscribe, onValidate, onEnveloped, onPerform } of plugins) { onEnveloped && beforeCallbacks.init.push(onEnveloped); onContextBuilding && beforeCallbacks.context.push(onContextBuilding); onExecute && beforeCallbacks.execute.push(onExecute); onParse && beforeCallbacks.parse.push(onParse); onSubscribe && beforeCallbacks.subscribe.push(onSubscribe); onValidate && beforeCallbacks.validate.push(onValidate); + onPerform && beforeCallbacks.perform.push(onPerform); } const init: EnvelopOrchestrator['init'] = initialContext => { @@ -565,6 +570,24 @@ export function createEnvelopOrchestrator const contextFactory = customContextFactory(initialContext); return async (params, contextExtension) => { + const context = await contextFactory(contextExtension); + + const doneFns: OnPerformDoneHook[] = []; + for (const onPerform of beforeCallbacks.perform) { + const result = await onPerform({ + context, + extendContext: extension => { + Object.assign(context, extension); + }, + params, + setParams: newParams => { + params = newParams; + }, + }); + + result?.onPerformDone && doneFns.push(result.onPerformDone); + } + let document; try { document = parse(params.query); @@ -577,10 +600,16 @@ export function createEnvelopOrchestrator return { errors: validationErrors }; } - const context = await contextFactory(contextExtension); - + let result; if (isSubscriptionOperation(document, params.operationName)) { - return await customSubscribe({ + result = await customSubscribe({ + document, + schema, + variableValues: params.variables, + contextValue: context, + }); + } else { + result = await customExecute({ document, schema, variableValues: params.variables, @@ -588,12 +617,16 @@ export function createEnvelopOrchestrator }); } - return await customExecute({ - document, - schema, - variableValues: params.variables, - contextValue: context, - }); + for (const doneFn of doneFns) { + doneFn({ + result, + setResult: newResult => { + result = newResult; + }, + }); + } + + return result; }; }; diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index 82024c45b9..f38398f15e 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -1,5 +1,5 @@ import { parse, validate, execute, subscribe } from 'graphql'; -import { envelop, useSchema } from '../src/index.js'; +import { envelop, OnPerformDoneHook, OnPerformHook, useSchema } from '../src/index.js'; import { assertSingleExecutionValue, assertStreamExecutionValue } from '@envelop/testing'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -103,4 +103,40 @@ describe('perform', () => { } `); }); + + it('should invoke onPerform plugin hooks', async () => { + const onPerformDoneFn = jest.fn((() => { + // noop + }) as OnPerformDoneHook); + const onPerformFn = jest.fn((() => ({ + onPerformDone: onPerformDoneFn, + })) as OnPerformHook); + + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: onPerformFn, + }, + ], + }); + + const params = { query: '{ hello }' }; + const { perform } = getEnveloped({ initial: 'context' }); + await perform(params, { extension: 'context' }); + + expect(onPerformFn).toBeCalled(); + expect(onPerformFn.mock.calls[0][0].context).toEqual({ initial: 'context', extension: 'context' }); + expect(onPerformFn.mock.calls[0][0].params).toBe(params); + + expect(onPerformDoneFn).toBeCalled(); + expect(onPerformDoneFn.mock.calls[0][0].result).toMatchInlineSnapshot(` + Object { + "data": Object { + "hello": "world", + }, + } + `); + }); }); diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index a4e6ce05a0..e0d1cd3277 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -519,7 +519,9 @@ export type PerformFunction = ( contextExtension?: ContextExtension ) => Promise>; -export type OnPerformEventPayload = { +export type OnPerformEventPayload = { + context: Readonly; + extendContext: (contextExtension: Partial) => void; params: PerformParams; setParams: (newParams: PerformParams) => void; }; @@ -535,4 +537,6 @@ export type OnPerformHookResult = { onPerformDone?: OnPerformDoneHook; }; -export type OnPerformHook = (options: OnPerformEventPayload) => PromiseOrValue; +export type OnPerformHook = ( + options: OnPerformEventPayload +) => PromiseOrValue; diff --git a/packages/types/src/plugin.ts b/packages/types/src/plugin.ts index c88df508ee..5da2899c38 100644 --- a/packages/types/src/plugin.ts +++ b/packages/types/src/plugin.ts @@ -46,5 +46,5 @@ export interface Plugin = {}> { /** * Invoked for each perform call. */ - onPerform?: OnPerformHook; + onPerform?: OnPerformHook; } From cba0e698e80c7b28168ed0948a6ccecffbd541e9 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 18:46:12 +0200 Subject: [PATCH 37/61] test replacing params and result --- packages/core/test/perform.spec.ts | 50 ++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index f38398f15e..9553bac30c 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -139,4 +139,54 @@ describe('perform', () => { } `); }); + + it('should replace params in onPerform plugin', async () => { + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: ({ setParams }) => { + setParams({ query: '{ hello }' }); + }, + }, + ], + }); + + const { perform } = getEnveloped(); + const result = await perform({ query: 'subscribe { greetings }' }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "data": Object { + "hello": "world", + }, + } + `); + }); + + it('should replace result in onPerformDone plugin', async () => { + const replacedResult = { data: { something: 'else' } }; + + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: () => ({ + onPerformDone: ({ setResult }) => { + setResult(replacedResult); + }, + }), + }, + ], + }); + + const { perform } = getEnveloped(); + const result = await perform({ query: '{ hello }' }); + assertSingleExecutionValue(result); + + expect(result).toBe(replacedResult); + }); }); From 2e2127f70d4a15c98c43fe1f476da7a860fe8fa1 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 19:01:21 +0200 Subject: [PATCH 38/61] early result --- packages/core/src/orchestrator.ts | 63 ++++++++++++++++++------------ packages/core/test/perform.spec.ts | 22 +++++++++++ packages/types/src/hooks.ts | 4 ++ 3 files changed, 63 insertions(+), 26 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index e3b2050ad0..1154defb1d 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -572,9 +572,11 @@ export function createEnvelopOrchestrator return async (params, contextExtension) => { const context = await contextFactory(contextExtension); + let result: AsyncIterableIteratorOrValue | null = null; + const doneFns: OnPerformDoneHook[] = []; for (const onPerform of beforeCallbacks.perform) { - const result = await onPerform({ + const after = await onPerform({ context, extendContext: extension => { Object.assign(context, extension); @@ -583,38 +585,47 @@ export function createEnvelopOrchestrator setParams: newParams => { params = newParams; }, + setResult: earlyResult => { + result = earlyResult; + }, }); - result?.onPerformDone && doneFns.push(result.onPerformDone); + after?.onPerformDone && doneFns.push(after.onPerformDone); } - let document; - try { - document = parse(params.query); - } catch (err) { - return { errors: [err] }; - } + if (!result) { + let document; + try { + document = parse(params.query); + } catch (err) { + return { errors: [err] }; + } + + const validationErrors = validate(schema, document); + if (validationErrors.length) { + return { errors: validationErrors }; + } - const validationErrors = validate(schema, document); - if (validationErrors.length) { - return { errors: validationErrors }; + if (isSubscriptionOperation(document, params.operationName)) { + result = await customSubscribe({ + document, + schema, + variableValues: params.variables, + contextValue: context, + }); + } else { + result = await customExecute({ + document, + schema, + variableValues: params.variables, + contextValue: context, + }); + } } - let result; - if (isSubscriptionOperation(document, params.operationName)) { - result = await customSubscribe({ - document, - schema, - variableValues: params.variables, - contextValue: context, - }); - } else { - result = await customExecute({ - document, - schema, - variableValues: params.variables, - contextValue: context, - }); + if (!result) { + // should never happen + throw new Error('Result not available'); } for (const doneFn of doneFns) { diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index 9553bac30c..65bd3a4ffa 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -189,4 +189,26 @@ describe('perform', () => { expect(result).toBe(replacedResult); }); + + it('should early result in onPerform plugin', async () => { + const earlyResult = { data: { hi: 'hello' } }; + + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: ({ setResult }) => { + setResult(earlyResult); + }, + }, + ], + }); + + const { perform } = getEnveloped(); + const result = await perform({ query: '{ hello }' }); + assertSingleExecutionValue(result); + + expect(result).toBe(earlyResult); + }); }); diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index e0d1cd3277..fd2874044f 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -524,6 +524,10 @@ export type OnPerformEventPayload = { extendContext: (contextExtension: Partial) => void; params: PerformParams; setParams: (newParams: PerformParams) => void; + /** + * Set an early result which will be immediatelly returned. Useful for cached results. + */ + setResult: (newResult: AsyncIterableIteratorOrValue) => void; }; export type OnPerformDoneEventPayload = { From d072fe3277ea118dae1716ce6e6f8e2ae9ffd92d Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 19:10:02 +0200 Subject: [PATCH 39/61] refactor and invoke onDone always --- packages/core/src/orchestrator.ts | 85 +++++++++++++++--------------- packages/core/test/perform.spec.ts | 30 +++++++++++ 2 files changed, 72 insertions(+), 43 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 1154defb1d..cfb62517e7 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -572,9 +572,8 @@ export function createEnvelopOrchestrator return async (params, contextExtension) => { const context = await contextFactory(contextExtension); - let result: AsyncIterableIteratorOrValue | null = null; - - const doneFns: OnPerformDoneHook[] = []; + let earlyResult: AsyncIterableIteratorOrValue | null = null; + const onDones: OnPerformDoneHook[] = []; for (const onPerform of beforeCallbacks.perform) { const after = await onPerform({ context, @@ -585,59 +584,59 @@ export function createEnvelopOrchestrator setParams: newParams => { params = newParams; }, - setResult: earlyResult => { - result = earlyResult; + setResult: result => { + earlyResult = result; }, }); + after?.onPerformDone && onDones.push(after.onPerformDone); + } + const done = (result: AsyncIterableIteratorOrValue) => { + for (const onDone of onDones) { + onDone({ + result, + setResult: newResult => { + result = newResult; + }, + }); + } + return result; + }; - after?.onPerformDone && doneFns.push(after.onPerformDone); + if (earlyResult) { + return done(earlyResult); } - if (!result) { - let document; - try { - document = parse(params.query); - } catch (err) { - return { errors: [err] }; - } + let document; + try { + document = parse(params.query); + } catch (err) { + return done({ errors: [err] }); + } - const validationErrors = validate(schema, document); - if (validationErrors.length) { - return { errors: validationErrors }; - } + const validationErrors = validate(schema, document); + if (validationErrors.length) { + return done({ errors: validationErrors }); + } - if (isSubscriptionOperation(document, params.operationName)) { - result = await customSubscribe({ + if (isSubscriptionOperation(document, params.operationName)) { + return done( + await customSubscribe({ document, schema, variableValues: params.variables, contextValue: context, - }); - } else { - result = await customExecute({ - document, - schema, - variableValues: params.variables, - contextValue: context, - }); - } + }) + ); } - if (!result) { - // should never happen - throw new Error('Result not available'); - } - - for (const doneFn of doneFns) { - doneFn({ - result, - setResult: newResult => { - result = newResult; - }, - }); - } - - return result; + return done( + await customExecute({ + document, + schema, + variableValues: params.variables, + contextValue: context, + }) + ); }; }; diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index 65bd3a4ffa..7c9c37d688 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -211,4 +211,34 @@ describe('perform', () => { expect(result).toBe(earlyResult); }); + + it('should provide result with parsing errors to onPerformDone hook', async () => { + const onPerformDoneFn = jest.fn((() => { + // noop + }) as OnPerformDoneHook); + + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onPerform: () => ({ + onPerformDone: onPerformDoneFn, + }), + }, + ], + }); + + const { perform } = getEnveloped(); + await perform({ query: '{' }); + + expect(onPerformDoneFn).toBeCalled(); + expect(onPerformDoneFn.mock.calls[0][0].result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Syntax Error: Expected Name, found .], + ], + } + `); + }); }); From b052f604ba69a738bcece1106a0d34ccb37ac891 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Fri, 9 Sep 2022 19:29:14 +0200 Subject: [PATCH 40/61] changeset --- .changeset/four-masks-jam.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/four-masks-jam.md diff --git a/.changeset/four-masks-jam.md b/.changeset/four-masks-jam.md new file mode 100644 index 0000000000..6a619903d1 --- /dev/null +++ b/.changeset/four-masks-jam.md @@ -0,0 +1,5 @@ +--- +'@envelop/core': minor +--- + +`perform` function that does parsing, validation, context assembly and execution/subscription From 7b7b530b9651ab0334928dd6481bd2a937e5d458 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Mon, 12 Sep 2022 11:10:50 -0400 Subject: [PATCH 41/61] add changeset --- .changeset/lovely-clocks-type.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .changeset/lovely-clocks-type.md diff --git a/.changeset/lovely-clocks-type.md b/.changeset/lovely-clocks-type.md new file mode 100644 index 0000000000..52166356be --- /dev/null +++ b/.changeset/lovely-clocks-type.md @@ -0,0 +1,12 @@ +--- +'@envelop/core': major +--- + +We have built the new `envelop` to be engine agnostic. `graphql-js` is no longer a peer dependency. Now you can use any spec compliant GraphQL engine with `envelop` and get the benefit of building a plugin system. + +```diff ++ import { parse, validate, execute, subscribe } from 'graphql'; + +- const getEnveloped = envelop([ ... ]) ++ const getEnveloped = envelop({ parse, validate, execute, subscribe, plugins: [ ... ] }) +``` From cdbfbb2784f0129fec2376fbd0ed7b88c21e3d9d Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Mon, 12 Sep 2022 12:29:59 -0400 Subject: [PATCH 42/61] Update .changeset/nervous-seas-own.md --- .changeset/nervous-seas-own.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/nervous-seas-own.md b/.changeset/nervous-seas-own.md index 4e156a5b6d..88250d82b7 100644 --- a/.changeset/nervous-seas-own.md +++ b/.changeset/nervous-seas-own.md @@ -2,4 +2,4 @@ '@envelop/core': major --- -Renmae `useLazyLoadedSchema` to `useSchemaByContext` since the original name was vert misleading. +Rename `useLazyLoadedSchema` to `useSchemaByContext` since the original name was vert misleading. From ebf9e1a6e342dc4527edbcba4e9ef45007b2a11a Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Mon, 12 Sep 2022 12:30:06 -0400 Subject: [PATCH 43/61] Update .changeset/rude-cats-peel.md --- .changeset/rude-cats-peel.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rude-cats-peel.md b/.changeset/rude-cats-peel.md index c0a8ef5543..77ab8534ac 100644 --- a/.changeset/rude-cats-peel.md +++ b/.changeset/rude-cats-peel.md @@ -7,4 +7,4 @@ Remove `handleValidationErrors` and `handleParseErrors` options from `useMaskedE > ONLY masking validation errors OR ONLY disabling introspection errors does not make sense, as both can be abused for reverse-engineering the GraphQL schema (see https://github.com/nikitastupin/clairvoyance for reverse-engineering the schema based on validation error suggestions). > https://github.com/n1ru4l/envelop/issues/1482#issue-1340015060 -Rename `formatError` function option to `maskErrorFn` +Rename `formatError` function option to `maskError` From 7bc641236fb08ba9bd5cb27b61b0bde0bcc94dd2 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Tue, 13 Sep 2022 11:40:00 +0200 Subject: [PATCH 44/61] no-use-before-define (#1522) --- .eslintrc.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.json b/.eslintrc.json index 03707bae19..c09453752d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -22,6 +22,7 @@ } ], "no-useless-constructor": "off", + "no-use-before-define": "off", "@typescript-eslint/no-unused-vars": [ "warn", { From e923bce9cbeb2c93df88c9e99889d276736c2229 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Tue, 13 Sep 2022 12:15:18 +0200 Subject: [PATCH 45/61] document and recommend perform --- README.md | 22 +++++----------------- examples/simple-http/index.ts | 22 ++-------------------- website/algolia-lockfile.json | 5 +++++ website/docs/getting-started.mdx | 29 ++++++++--------------------- website/docs/plugins/lifecycle.mdx | 17 +++++++++++++++++ 5 files changed, 37 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 040b035334..3f15726aff 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ const getEnveloped = envelop({ }) ``` -The result of `envelop` is a function that allows you to get everything you need for the GraphQL execution: `parse`, `validate`, `contextBuilder` and `execute`. Use that to run the client's GraphQL queries. Here's a pseudo-code example of how it should look like: +The result of `envelop` is a function that allows you to get everything you need for the GraphQL execution. It's recommended to use the `perform` function which does parsing, validation, context assembly and execution/subscription, and returns a ready result. Here's a pseudo-code example of how it should look like: ```ts const httpServer = createServer() @@ -54,25 +54,13 @@ const httpServer = createServer() httpServer.on('request', async (req, res) => { // Here you get the alternative methods that are bundled with your plugins // You can also pass the "req" to make it available for your plugins or GraphQL context. - const { parse, validate, contextFactory, execute, schema } = getEnveloped({ req }) + const { perform } = getEnveloped({ req }) - // Parse the initial request and validate it + // Parse the initial request const { query, variables } = JSON.parse(req.payload) - const document = parse(query) - const validationErrors = validate(schema, document) - if (validationErrors.length > 0) { - return res.end(JSON.stringify({ errors: validationErrors })) - } - - // Build the context and execute - const context = await contextFactory(req) - const result = await execute({ - document, - schema, - variableValues: variables, - contextValue: context - }) + // Perform the GraphQL operation + const result = await perform({ query, variables }) // Send the response res.end(JSON.stringify(result)) diff --git a/examples/simple-http/index.ts b/examples/simple-http/index.ts index cdeed847de..197a76fb7b 100644 --- a/examples/simple-http/index.ts +++ b/examples/simple-http/index.ts @@ -20,7 +20,7 @@ const getEnveloped = envelop({ }); const server = createServer((req, res) => { - const { parse, validate, contextFactory, execute, schema } = getEnveloped({ req }); + const { perform } = getEnveloped({ req }); let payload = ''; req.on('data', chunk => { @@ -29,26 +29,8 @@ const server = createServer((req, res) => { req.on('end', async () => { const { query, variables } = JSON.parse(payload); - const document = parse(query); - const validationErrors = validate(schema, document); - if (validationErrors.length > 0) { - res.end( - JSON.stringify({ - errors: validationErrors, - }) - ); - - return; - } - - const context = await contextFactory(); - const result = await execute({ - document, - schema, - variableValues: variables, - contextValue: context, - }); + const result = await perform({ query, variables }); res.end(JSON.stringify(result)); }); diff --git a/website/algolia-lockfile.json b/website/algolia-lockfile.json index d5281857e2..61eb96b190 100644 --- a/website/algolia-lockfile.json +++ b/website/algolia-lockfile.json @@ -361,6 +361,11 @@ "children": [], "title": "`onSchemaChange(api)`", "anchor": "onschemachangeapi" + }, + { + "children": [], + "title": "`onPerform(api)`", + "anchor": "onperformapi" } ], "title": "`onPluginInit(api)`", diff --git a/website/docs/getting-started.mdx b/website/docs/getting-started.mdx index cc5df19dd3..a13cf7f0a5 100644 --- a/website/docs/getting-started.mdx +++ b/website/docs/getting-started.mdx @@ -38,10 +38,12 @@ export const getEnveloped = envelop({ ## Use your envelop -The result of `envelop` is a factory function that allows you to get everything you need for the GraphQL execution: `parse`, `validate`, `contextBuilder`, `execute` and `subscribe`. It is usually named `getEnveloped`. +The result of `envelop` is a factory function that allows you to get everything you need for the GraphQL execution: `parse`, `validate`, `contextBuilder`, `execute`, `subscribe` and `perform`. It is usually named `getEnveloped`. By calling the `getEnveloped` function you will get all the primitive functions required for the GraphQL execution layer. +It's recommended to use the `perform` function which does parsing, validation, context assembly and execution/subscription, and returns a ready result. This function will NEVER throw GraphQL errors, it will instead place them in the result. + ```ts // prettier-ignore const { @@ -51,6 +53,7 @@ const { execute, subscribe, schema, + perform, } = getEnveloped() ``` @@ -115,32 +118,16 @@ const httpServer = http.createServer(async (req, res) => { }) const { - // Get the GraphQL execution functions with attached plugin handlers - parse, - validate, - contextFactory, - execute, - schema + // Get the perform function that does parsing, validation, context assembly and execution/subscription, and returns a ready GraphQL operation result. + perform // pass in an initial context that all plugins can consume and extend } = getEnveloped({ req }) // Parse request body JSON const { query, variables } = JSON.parse(req.body) - const document = parse(query) - const validationErrors = validate(schema, document) - - if (validationErrors.length > 0) { - return res.end(JSON.stringify({ errors: validationErrors })) - } - // Build the context and execute - const contextValue = await contextFactory() - const result = await execute({ - document, - schema, - variableValues: variables, - contextValue - }) + // Perform the GraphQL operation. This function will NEVER throw GraphQL errors, it will instead place them in the result. + const result = await perform({ query, variables }) // Send the response res.end(JSON.stringify(result)) diff --git a/website/docs/plugins/lifecycle.mdx b/website/docs/plugins/lifecycle.mdx index ac4598b227..1153ad5036 100644 --- a/website/docs/plugins/lifecycle.mdx +++ b/website/docs/plugins/lifecycle.mdx @@ -240,3 +240,20 @@ Some plugins (like gateway implementations) could potentially change the schema - `schema` - the `GraphQLSchema` - `replaceSchema` - replaces the schema. Calling this will trigger `onSchemaChange` for all other plugins (except for the one that initiated the change); + +### `onPerform(api)` + +Called every time the `perform` function is invoked. Used for augmenting GraphQL parameters and/or setting an early result. + +**`before` API**: + +- `context` - the context object built so far by other plugins. +- `extendContext` - extends the context object with additional fields. +- `params` - GraphQL parameters passed to `perform`. +- `setParams` - replace GraphQL parameters before performing. +- `setResult` - sets an early result for immediate response. + +**`onPerformDone` API**: + +- `result` - the execution result, or AsyncIterable in case of stream response. +- `setResult` - replaces the result. From b87758d95b113ea6c94b2aca16bb5f568714fc34 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Tue, 13 Sep 2022 12:18:36 +0200 Subject: [PATCH 46/61] comment on perform func --- packages/types/src/get-enveloped.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/types/src/get-enveloped.ts b/packages/types/src/get-enveloped.ts index a3826076d3..cd59da407f 100644 --- a/packages/types/src/get-enveloped.ts +++ b/packages/types/src/get-enveloped.ts @@ -18,6 +18,13 @@ export type GetEnvelopedFn = { contextExtension?: ContextExtension ) => PromiseOrValue>; schema: any; + /** + * Parse, validate, assemble context and execute/subscribe. + * + * Returns a ready-to-use GraphQL response. + * + * This function will NEVER throw GraphQL errors, it will instead place them in the result. + */ perform: ( params: PerformParams, contextExtension?: ContextExtension From 9a59c62d738caa9256cdfdb8ef31af7a3a6253af Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Tue, 13 Sep 2022 12:54:39 +0200 Subject: [PATCH 47/61] perform query param can be a graphql document --- packages/core/src/orchestrator.ts | 13 ++++++++----- packages/core/test/perform.spec.ts | 17 +++++++++++++++++ packages/types/src/hooks.ts | 5 ++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index cfb62517e7..31ab04a537 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -606,11 +606,14 @@ export function createEnvelopOrchestrator return done(earlyResult); } - let document; - try { - document = parse(params.query); - } catch (err) { - return done({ errors: [err] }); + let document = params.query; + if (typeof document === 'string') { + // query parameter can be a graphql document already, skip parsing in that case + try { + document = parse(params.query); + } catch (err) { + return done({ errors: [err] }); + } } const validationErrors = validate(schema, document); diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index 7c9c37d688..1673a0f4df 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -70,6 +70,23 @@ describe('perform', () => { } }); + it('should accept graphql document as the query', async () => { + const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: parse('{ hello }') }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "data": Object { + "hello": "world", + }, + } + `); + }); + it('should include parsing errors in result', async () => { const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index fd2874044f..c3c7a4d483 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -505,7 +505,10 @@ export type OnSubscribeHook = ( export interface PerformParams { operationName?: string; - query: string; + /** + * Can either be a string query or a GraphQL document. + */ + query: string | Record; variables?: Record; } From 827fb179a91e8ec89b7c8460c09bc650eb112b72 Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Tue, 13 Sep 2022 13:04:53 +0200 Subject: [PATCH 48/61] update apollo-server example --- examples/apollo-server/index.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/examples/apollo-server/index.ts b/examples/apollo-server/index.ts index 5ef3d636e8..95d056c486 100644 --- a/examples/apollo-server/index.ts +++ b/examples/apollo-server/index.ts @@ -24,13 +24,11 @@ const getEnveloped = envelop({ const server = new ApolloServer({ schema, executor: async requestContext => { - const { schema, execute, contextFactory } = getEnveloped({ req: requestContext.request.http }); + const { perform } = getEnveloped({ req: requestContext.request.http }); - return execute({ - schema, - document: requestContext.document, - contextValue: await contextFactory(), - variableValues: requestContext.request.variables, + return perform({ + query: requestContext.document, + variables: requestContext.request.variables, operationName: requestContext.operationName, }); }, From 1770063406bf65bd0f6bda41659f271450b80268 Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Tue, 13 Sep 2022 10:18:41 -0400 Subject: [PATCH 49/61] feat: trigger on context, validate and parse errors (#1511) * feat: trigger on context, validate and parse errors * trying * make it work * pass in phase * add phase details * feedback --- .changeset/silent-impalas-retire.md | 5 ++ .../core/src/plugins/use-error-handler.ts | 37 +++++++++- .../test/plugins/use-error-handler.spec.ts | 69 ++++++++++++++++--- 3 files changed, 101 insertions(+), 10 deletions(-) create mode 100644 .changeset/silent-impalas-retire.md diff --git a/.changeset/silent-impalas-retire.md b/.changeset/silent-impalas-retire.md new file mode 100644 index 0000000000..303ae2e14d --- /dev/null +++ b/.changeset/silent-impalas-retire.md @@ -0,0 +1,5 @@ +--- +'@envelop/core': minor +--- + +respond to context, parse and validate errors in `useErrorHandler` plugin diff --git a/packages/core/src/plugins/use-error-handler.ts b/packages/core/src/plugins/use-error-handler.ts index 742005ade1..bbc33b884c 100644 --- a/packages/core/src/plugins/use-error-handler.ts +++ b/packages/core/src/plugins/use-error-handler.ts @@ -1,7 +1,16 @@ import { Plugin, DefaultContext, TypedExecutionArgs, ExecutionResult } from '@envelop/types'; import { handleStreamOrSingleExecutionResult } from '../utils.js'; +import { isGraphQLError, SerializableGraphQLErrorLike } from './use-masked-errors.js'; -export type ErrorHandler = (errors: readonly Error[] | any[], context: Readonly) => void; +export type ErrorHandler = ({ + errors, + context, + phase, +}: { + errors: readonly Error[] | readonly SerializableGraphQLErrorLike[]; + context: Readonly; + phase: 'parse' | 'validate' | 'context' | 'execution'; +}) => void; type ErrorHandlerCallback = { result: ExecutionResult; @@ -12,7 +21,7 @@ const makeHandleResult = >(errorHandler: ErrorHandler) => ({ result, args }: ErrorHandlerCallback) => { if (result.errors?.length) { - errorHandler(result.errors, args); + errorHandler({ errors: result.errors, context: args, phase: 'execution' }); } }; @@ -21,6 +30,30 @@ export const useErrorHandler = >( ): Plugin => { const handleResult = makeHandleResult(errorHandler); return { + onParse() { + return function onParseEnd({ result, context }) { + if (result instanceof Error) { + errorHandler({ errors: [result], context, phase: 'parse' }); + } + }; + }, + onValidate() { + return function onValidateEnd({ valid, result, context }) { + if (valid === false && result.length > 0) { + errorHandler({ errors: result as Error[], context, phase: 'validate' }); + } + }; + }, + onPluginInit(context) { + context.registerContextErrorHandler(({ error }) => { + if (isGraphQLError(error)) { + errorHandler({ errors: [error], context, phase: 'context' }); + } else { + // @ts-expect-error its not an error at this point so we just create a new one - can we handle this better? + errorHandler({ errors: [new Error(error)], context, phase: 'context' }); + } + }); + }, onExecute() { return { onExecuteDone(payload) { diff --git a/packages/core/test/plugins/use-error-handler.spec.ts b/packages/core/test/plugins/use-error-handler.spec.ts index 77b4c73e71..aa731b2456 100644 --- a/packages/core/test/plugins/use-error-handler.spec.ts +++ b/packages/core/test/plugins/use-error-handler.spec.ts @@ -1,7 +1,11 @@ import { useErrorHandler } from '../../src/plugins/use-error-handler.js'; import { assertStreamExecutionValue, collectAsyncIteratorValues, createTestkit } from '@envelop/testing'; +import { Plugin } from '@envelop/types'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { Repeater } from '@repeaterjs/repeater'; +import { createGraphQLError } from '@graphql-tools/utils'; +import { schema } from '../common.js'; +import { useExtendContext } from '@envelop/core'; describe('useErrorHandler', () => { it('should invoke error handler when error happens during execution', async () => { @@ -26,16 +30,67 @@ describe('useErrorHandler', () => { const testInstance = createTestkit([useErrorHandler(mockHandler)], schema); await testInstance.execute(`query { foo }`, {}, { foo: 'bar' }); + expect(mockHandler).toHaveBeenCalledWith(expect.objectContaining({ phase: 'execution' })); + }); + + it('should invoke error handler when error happens during parse', async () => { + expect.assertions(2); + const mockHandler = jest.fn(); + const testInstance = createTestkit([useErrorHandler(mockHandler)], schema); + await testInstance.execute(`query { me `, {}); + expect(mockHandler).toHaveBeenCalledTimes(1); expect(mockHandler).toHaveBeenCalledWith( - [testError], expect.objectContaining({ - contextValue: expect.objectContaining({ - foo: 'bar', - }), + phase: 'parse', + }) + ); + }); + + it('should invoke error handler on validation error', async () => { + expect.assertions(2); + const useMyFailingValidator: Plugin = { + onValidate(payload) { + payload.setValidationFn(() => { + return [createGraphQLError('Failure!')]; + }); + }, + }; + const mockHandler = jest.fn(); + const testInstance = createTestkit([useMyFailingValidator, useErrorHandler(mockHandler)], schema); + await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); + expect(mockHandler).toHaveBeenCalledTimes(1); + expect(mockHandler).toHaveBeenCalledWith( + expect.objectContaining({ + phase: 'validate', }) ); }); + it('should invoke error handle for context errors', async () => { + expect.assertions(2); + const mockHandler = jest.fn(); + const testInstance = createTestkit( + [ + useExtendContext((): {} => { + throw new Error('No context for you!'); + }), + useErrorHandler(mockHandler), + ], + schema + ); + + try { + await testInstance.execute(`query { me { name } }`); + } catch { + expect(mockHandler).toHaveBeenCalledWith( + expect.objectContaining({ + phase: 'context', + }) + ); + expect(mockHandler).toHaveBeenCalledTimes(1); + } + }); + it('should invoke error handler when error happens during subscription resolver call', async () => { const testError = new Error('Foobar'); @@ -71,11 +126,9 @@ describe('useErrorHandler', () => { await collectAsyncIteratorValues(result); expect(mockHandler).toHaveBeenCalledWith( - [testError], expect.objectContaining({ - contextValue: expect.objectContaining({ - foo: 'bar', - }), + errors: expect.objectContaining([testError]), + phase: 'execution', }) ); }); From 3dcdd0328d7a8c9cff5d87f57cb6acf3b5dc340c Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 14 Sep 2022 16:51:18 +0200 Subject: [PATCH 50/61] Revert "perform query param can be a graphql document" This reverts commit 9a59c62d738caa9256cdfdb8ef31af7a3a6253af. --- packages/core/src/orchestrator.ts | 13 +++++-------- packages/core/test/perform.spec.ts | 17 ----------------- packages/types/src/hooks.ts | 5 +---- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 31ab04a537..cfb62517e7 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -606,14 +606,11 @@ export function createEnvelopOrchestrator return done(earlyResult); } - let document = params.query; - if (typeof document === 'string') { - // query parameter can be a graphql document already, skip parsing in that case - try { - document = parse(params.query); - } catch (err) { - return done({ errors: [err] }); - } + let document; + try { + document = parse(params.query); + } catch (err) { + return done({ errors: [err] }); } const validationErrors = validate(schema, document); diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index 1673a0f4df..7c9c37d688 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -70,23 +70,6 @@ describe('perform', () => { } }); - it('should accept graphql document as the query', async () => { - const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); - - const { perform } = getEnveloped(); - - const result = await perform({ query: parse('{ hello }') }); - assertSingleExecutionValue(result); - - expect(result).toMatchInlineSnapshot(` - Object { - "data": Object { - "hello": "world", - }, - } - `); - }); - it('should include parsing errors in result', async () => { const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index c3c7a4d483..fd2874044f 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -505,10 +505,7 @@ export type OnSubscribeHook = ( export interface PerformParams { operationName?: string; - /** - * Can either be a string query or a GraphQL document. - */ - query: string | Record; + query: string; variables?: Record; } From cdb23b51f16a458cb9733c8cf6e36d172a1c2c3e Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 14 Sep 2022 16:51:25 +0200 Subject: [PATCH 51/61] drop unused import --- packages/core/src/orchestrator.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index cfb62517e7..fa3c4b4394 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -32,7 +32,6 @@ import { ExecutionResult, PerformFunction, OnPerformHook, - OnPerformHookResult, OnPerformDoneHook, } from '@envelop/types'; import { From d542d3e29d3da5578416c7017161f285fe9f0c40 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 14 Sep 2022 16:53:15 +0200 Subject: [PATCH 52/61] perform params query is optional (persisted queries) --- packages/types/src/hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index fd2874044f..b0186a0895 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -505,7 +505,7 @@ export type OnSubscribeHook = ( export interface PerformParams { operationName?: string; - query: string; + query?: string; variables?: Record; } From 3ccdd020eb1a4f9be85244e041d4670bf72efe5b Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 14 Sep 2022 16:54:50 +0200 Subject: [PATCH 53/61] perform takes apollo-server request --- examples/apollo-server/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/examples/apollo-server/index.ts b/examples/apollo-server/index.ts index 95d056c486..ca0e1aa8fb 100644 --- a/examples/apollo-server/index.ts +++ b/examples/apollo-server/index.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { ApolloServer } from 'apollo-server'; -import { envelop, useSchema, useTiming } from '@envelop/core'; +import { envelop, isAsyncIterable, useSchema, useTiming } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'; @@ -25,12 +25,11 @@ const server = new ApolloServer({ schema, executor: async requestContext => { const { perform } = getEnveloped({ req: requestContext.request.http }); - - return perform({ - query: requestContext.document, - variables: requestContext.request.variables, - operationName: requestContext.operationName, - }); + const result = await perform(requestContext.request); + if (isAsyncIterable(result)) { + throw new Error('Unsupported streaming result'); + } + return result; }, plugins: [ApolloServerPluginLandingPageGraphQLPlayground({ endpoint: '/graphql' })], }); From 40f760efb38ac13b5694c49f58ab644bad876074 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 14 Sep 2022 17:12:08 +0200 Subject: [PATCH 54/61] revert apollo-server example --- examples/apollo-server/index.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/examples/apollo-server/index.ts b/examples/apollo-server/index.ts index ca0e1aa8fb..5ef3d636e8 100644 --- a/examples/apollo-server/index.ts +++ b/examples/apollo-server/index.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { ApolloServer } from 'apollo-server'; -import { envelop, isAsyncIterable, useSchema, useTiming } from '@envelop/core'; +import { envelop, useSchema, useTiming } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'; @@ -24,12 +24,15 @@ const getEnveloped = envelop({ const server = new ApolloServer({ schema, executor: async requestContext => { - const { perform } = getEnveloped({ req: requestContext.request.http }); - const result = await perform(requestContext.request); - if (isAsyncIterable(result)) { - throw new Error('Unsupported streaming result'); - } - return result; + const { schema, execute, contextFactory } = getEnveloped({ req: requestContext.request.http }); + + return execute({ + schema, + document: requestContext.document, + contextValue: await contextFactory(), + variableValues: requestContext.request.variables, + operationName: requestContext.operationName, + }); }, plugins: [ApolloServerPluginLandingPageGraphQLPlayground({ endpoint: '/graphql' })], }); From 2e2209b04dc0bbd6f12d5bad1021fcccbc55e0d8 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 14 Sep 2022 20:37:53 +0200 Subject: [PATCH 55/61] perform should catch thrown validation errors too --- packages/core/src/orchestrator.ts | 10 +++++++--- packages/core/test/perform.spec.ts | 31 +++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index fa3c4b4394..caf10bc3a2 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -612,9 +612,13 @@ export function createEnvelopOrchestrator return done({ errors: [err] }); } - const validationErrors = validate(schema, document); - if (validationErrors.length) { - return done({ errors: validationErrors }); + try { + const validationErrors = validate(schema, document); + if (validationErrors.length) { + return done({ errors: validationErrors }); + } + } catch (err) { + return done({ errors: [err] }); } if (isSubscriptionOperation(document, params.operationName)) { diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index 7c9c37d688..d26b7ead0e 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -1,4 +1,4 @@ -import { parse, validate, execute, subscribe } from 'graphql'; +import { parse, validate, execute, subscribe, GraphQLError } from 'graphql'; import { envelop, OnPerformDoneHook, OnPerformHook, useSchema } from '../src/index.js'; import { assertSingleExecutionValue, assertStreamExecutionValue } from '@envelop/testing'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -104,6 +104,35 @@ describe('perform', () => { `); }); + it('should include thrown validation errors in result', async () => { + const getEnveloped = envelop({ + ...graphqlFuncs, + plugins: [ + useSchema(schema), + { + onValidate: ({ addValidationRule }) => { + addValidationRule(() => { + throw new GraphQLError('Invalid!'); + }); + }, + }, + ], + }); + + const { perform } = getEnveloped(); + + const result = await perform({ query: '{ hello }' }); + assertSingleExecutionValue(result); + + expect(result).toMatchInlineSnapshot(` + Object { + "errors": Array [ + [GraphQLError: Invalid!], + ], + } + `); + }); + it('should invoke onPerform plugin hooks', async () => { const onPerformDoneFn = jest.fn((() => { // noop From 0dd0915f3542da503d2a5c378953e2827304fd2d Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 14 Sep 2022 21:23:37 +0200 Subject: [PATCH 56/61] context factory after parse and validate --- packages/core/src/orchestrator.ts | 9 ++++++--- packages/core/test/perform.spec.ts | 7 ++++--- packages/types/src/hooks.ts | 11 ++++++----- website/docs/plugins/lifecycle.mdx | 1 + 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index caf10bc3a2..4ffe16618d 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -563,16 +563,16 @@ export function createEnvelopOrchestrator } } - const customPerform: EnvelopContextFnWrapper = initialContext => { + const customPerform: EnvelopContextFnWrapper = initialContext => { const parse = customParse(initialContext); const validate = customValidate(initialContext); const contextFactory = customContextFactory(initialContext); return async (params, contextExtension) => { - const context = await contextFactory(contextExtension); + let context = initialContext; let earlyResult: AsyncIterableIteratorOrValue | null = null; - const onDones: OnPerformDoneHook[] = []; + const onDones: OnPerformDoneHook[] = []; for (const onPerform of beforeCallbacks.perform) { const after = await onPerform({ context, @@ -592,6 +592,7 @@ export function createEnvelopOrchestrator const done = (result: AsyncIterableIteratorOrValue) => { for (const onDone of onDones) { onDone({ + context, // either the initial or factory context, depenends when done result, setResult: newResult => { result = newResult; @@ -621,6 +622,8 @@ export function createEnvelopOrchestrator return done({ errors: [err] }); } + context = await contextFactory(contextExtension); + if (isSubscriptionOperation(document, params.operationName)) { return done( await customSubscribe({ diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index d26b7ead0e..9010ff163f 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -136,7 +136,7 @@ describe('perform', () => { it('should invoke onPerform plugin hooks', async () => { const onPerformDoneFn = jest.fn((() => { // noop - }) as OnPerformDoneHook); + }) as OnPerformDoneHook); const onPerformFn = jest.fn((() => ({ onPerformDone: onPerformDoneFn, })) as OnPerformHook); @@ -156,10 +156,11 @@ describe('perform', () => { await perform(params, { extension: 'context' }); expect(onPerformFn).toBeCalled(); - expect(onPerformFn.mock.calls[0][0].context).toEqual({ initial: 'context', extension: 'context' }); + expect(onPerformFn.mock.calls[0][0].context).toEqual({ initial: 'context' }); expect(onPerformFn.mock.calls[0][0].params).toBe(params); expect(onPerformDoneFn).toBeCalled(); + expect(onPerformDoneFn.mock.calls[0][0].context).toEqual({ initial: 'context', extension: 'context' }); expect(onPerformDoneFn.mock.calls[0][0].result).toMatchInlineSnapshot(` Object { "data": Object { @@ -244,7 +245,7 @@ describe('perform', () => { it('should provide result with parsing errors to onPerformDone hook', async () => { const onPerformDoneFn = jest.fn((() => { // noop - }) as OnPerformDoneHook); + }) as OnPerformDoneHook); const getEnveloped = envelop({ ...graphqlFuncs, diff --git a/packages/types/src/hooks.ts b/packages/types/src/hooks.ts index b0186a0895..8dc370f0d8 100644 --- a/packages/types/src/hooks.ts +++ b/packages/types/src/hooks.ts @@ -530,17 +530,18 @@ export type OnPerformEventPayload = { setResult: (newResult: AsyncIterableIteratorOrValue) => void; }; -export type OnPerformDoneEventPayload = { +export type OnPerformDoneEventPayload = { + context: Readonly; result: AsyncIterableIteratorOrValue; setResult: (newResult: AsyncIterableIteratorOrValue) => void; }; -export type OnPerformDoneHook = (options: OnPerformDoneEventPayload) => PromiseOrValue; +export type OnPerformDoneHook = (options: OnPerformDoneEventPayload) => PromiseOrValue; -export type OnPerformHookResult = { - onPerformDone?: OnPerformDoneHook; +export type OnPerformHookResult = { + onPerformDone?: OnPerformDoneHook; }; export type OnPerformHook = ( options: OnPerformEventPayload -) => PromiseOrValue; +) => PromiseOrValue>; diff --git a/website/docs/plugins/lifecycle.mdx b/website/docs/plugins/lifecycle.mdx index 1153ad5036..5fc996f3d8 100644 --- a/website/docs/plugins/lifecycle.mdx +++ b/website/docs/plugins/lifecycle.mdx @@ -255,5 +255,6 @@ Called every time the `perform` function is invoked. Used for augmenting GraphQL **`onPerformDone` API**: +- `context` - the context object built so far by other plugins. - `result` - the execution result, or AsyncIterable in case of stream response. - `setResult` - replaces the result. From 3b098bff4fcaf72a07511aff42976376144557d0 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 14 Sep 2022 20:06:41 +0200 Subject: [PATCH 57/61] perform in testkit --- packages/testing/src/index.ts | 34 +++++++++++++++++++++++++++++- packages/testing/test/test.spec.ts | 15 +++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/testing/src/index.ts b/packages/testing/src/index.ts index ffcec3835b..370c0f76c4 100644 --- a/packages/testing/src/index.ts +++ b/packages/testing/src/index.ts @@ -11,7 +11,7 @@ import { validate, } from 'graphql'; import { useSchema, envelop, isAsyncIterable } from '@envelop/core'; -import { GetEnvelopedFn, Plugin } from '@envelop/types'; +import { GetEnvelopedFn, Plugin, PerformFunction } from '@envelop/types'; import { mapSchema as cloneSchema, isDocumentNode } from '@graphql-tools/utils'; export type ModifyPluginsFn = (plugins: Plugin[]) => Plugin[]; @@ -84,12 +84,19 @@ type MaybeAsyncIterableIterator = T | AsyncIterableIterator; type ExecutionReturn = MaybeAsyncIterableIterator; export type TestkitInstance = { + perform: PerformFunction; + /** @deprecated Consider using `perform` instead. */ execute: ( operation: DocumentNode | string, variables?: Record, initialContext?: any ) => MaybePromise; modifyPlugins: (modifyPluginsFn: ModifyPluginsFn) => void; + /** + * Works only when used with `execute`, will NOT work with `perform`. + * + * @deprecated Consider using plugins for mocking. + */ mockPhase: (phaseReplacement: PhaseReplacementParams) => void; wait: (ms: number) => Promise; }; @@ -131,6 +138,31 @@ export function createTestkit( phasesReplacements.push(phaseReplacement); }, wait: ms => new Promise(resolve => setTimeout(resolve, ms)), + perform: (params, initialContext) => { + const { perform } = getEnveloped(initialContext as any); + return perform(params, { + request: { + headers: {}, + method: 'POST', + query: '', + body: { + query: params.query, + variables: params.variables, + }, + }, + // TODO: how important is the document object? + get document() { + try { + return parse(params.query!); + } catch { + return {}; + } + }, + operation: params.query, + variables: params.variables, + // ...initialContext unnecessary spread + }); + }, execute: async (operation, variableValues = {}, initialContext = {}) => { const proxy = getEnveloped(initialContext); diff --git a/packages/testing/test/test.spec.ts b/packages/testing/test/test.spec.ts index 2b11ceddbd..8730c9740b 100644 --- a/packages/testing/test/test.spec.ts +++ b/packages/testing/test/test.spec.ts @@ -82,7 +82,7 @@ describe('Test the testkit', () => { }; const testkit = createTestkit([], createSchema()); testkit.modifyPlugins(plugins => [addedPlugin]); - const result = await testkit.execute('query test { foo }'); + const result = await testkit.perform({ query: 'query test { foo }' }); assertSingleExecutionValue(result); expect(addedPlugin.onParse).toBeCalled(); expect(addedPlugin.onValidate).toBeCalled(); @@ -98,10 +98,21 @@ describe('Test the testkit', () => { }; const testkit = createTestkit([plugin1, false && plugin2], createSchema()); - const result = await testkit.execute('query test { foo }'); + const result = await testkit.perform({ query: 'query test { foo }' }); assertSingleExecutionValue(result); expect(plugin1.onParse).toBeCalled(); expect(plugin2.onValidate).not.toBeCalled(); expect(result.data).toBeDefined(); }); + + it('Should use perform', async () => { + const testkit = createTestkit([], createSchema()); + const result = await testkit.perform({ query: 'query test { foo }' }); + assertSingleExecutionValue(result); + expect(result.data).toMatchInlineSnapshot(` + Object { + "foo": "1", + } + `); + }); }); From 288fd29812c7974658248782aa28ccd8512b16af Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 15 Sep 2022 11:21:47 +0200 Subject: [PATCH 58/61] tests use perform --- packages/core/test/context.spec.ts | 12 +- packages/core/test/execute.spec.ts | 75 ++-- packages/core/test/extends.spec.ts | 2 +- packages/core/test/parse.spec.ts | 8 +- .../test/plugins/use-error-handler.spec.ts | 4 +- .../test/plugins/use-masked-errors.spec.ts | 68 ++-- packages/core/test/subscribe.spec.ts | 24 +- packages/core/test/validate.spec.ts | 12 +- .../test/use-apollo-datasources.spec.ts | 7 +- .../apollo-federation/test/federation.spec.ts | 6 +- .../test/use-apollo-tracing.spec.ts | 2 +- .../dataloader/test/dataloader.spec.ts | 2 +- ...ntext-value-per-subscription-event.spec.ts | 4 +- .../test/extended-validation.spec.ts | 8 +- .../extended-validation/test/one-of.spec.ts | 4 +- .../tests/use-generic-auth.spec.ts | 32 +- .../graphql-jit/test/graphql-jit.spec.ts | 14 +- .../test/graphql-middleware.spec.ts | 2 +- .../test/use-graphql-modules.spec.ts | 2 +- .../test/use-immediate-introspection.spec.ts | 92 +++-- .../live-query/test/use-live-query.spec.ts | 30 +- .../on-resolve/test/use-on-resolve.spec.ts | 5 +- .../test/use-open-telemetry.spec.ts | 6 +- .../test/use-operation-permissions.spec.ts | 66 +-- .../parser-cache/test/parser-cache.spec.ts | 24 +- .../tests/persisted-operations.spec.ts | 28 +- .../test/use-preload-assets.spec.ts | 6 +- .../tests/use-rate-limiter.spec.ts | 22 +- .../test/use-resource-limitations.spec.ts | 376 ++++++++++-------- .../test/response-redis-cache.spec.ts | 109 +++-- .../test/response-cache.spec.ts | 229 +++++------ .../test/validation-cache.spec.ts | 24 +- 32 files changed, 686 insertions(+), 619 deletions(-) diff --git a/packages/core/test/context.spec.ts b/packages/core/test/context.spec.ts index 740f6f75c8..57d3179895 100644 --- a/packages/core/test/context.spec.ts +++ b/packages/core/test/context.spec.ts @@ -6,7 +6,7 @@ describe('contextFactory', () => { it('Should call before parse and after parse correctly', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query); + await teskit.perform({ query }); expect(spiedPlugin.spies.beforeContextBuilding).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeContextBuilding).toHaveBeenCalledWith({ context: expect.any(Object), @@ -24,7 +24,7 @@ describe('contextFactory', () => { it('Should set initial `createProxy` arguments as initial context', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query, {}, { test: true }); + await teskit.perform({ query }, { test: true }); expect(spiedPlugin.spies.beforeContextBuilding).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeContextBuilding).toHaveBeenCalledWith({ context: expect.objectContaining({ @@ -55,7 +55,7 @@ describe('contextFactory', () => { schema ); - await teskit.execute(query, {}, {}); + await teskit.perform({ query }); expect(afterContextSpy).toHaveBeenCalledWith({ context: expect.objectContaining({ test: true, @@ -94,7 +94,7 @@ describe('contextFactory', () => { ], schema ); - await teskit.execute(query, {}, {}); + await teskit.perform({ query }); expect(afterContextSpy).toHaveBeenCalledWith( expect.objectContaining({ context: expect.objectContaining({ @@ -140,7 +140,7 @@ describe('contextFactory', () => { schema ); - const execution = teskit.execute(query, {}, { test: true }); + const execution = teskit.perform({ query, variables: {} }, { test: true }); return new Promise((resolve, reject) => { if (execution instanceof Promise) { return execution.then().catch(() => { @@ -165,7 +165,7 @@ describe('contextFactory', () => { return resolve(); }); } else { - return reject('Expected result of testkit.execute to return a promise'); + return reject('Expected result of testkit.perform to return a promise'); } }); }); diff --git a/packages/core/test/execute.spec.ts b/packages/core/test/execute.spec.ts index f0019dbd46..79cedc03f3 100644 --- a/packages/core/test/execute.spec.ts +++ b/packages/core/test/execute.spec.ts @@ -49,7 +49,7 @@ describe('execute', () => { it('Should wrap and trigger events correctly', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query, {}, { test: 1 }); + await teskit.perform({ query, variables: {} }, { test: 1 }); expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledWith({ executeFn: expect.any(Function), @@ -58,7 +58,6 @@ describe('execute', () => { setResultAndStopExecution: expect.any(Function), args: { contextValue: expect.objectContaining({ test: 1 }), - rootValue: {}, schema: expect.any(GraphQLSchema), operationName: undefined, fieldResolver: undefined, @@ -97,7 +96,7 @@ describe('execute', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(altExecute).toHaveBeenCalledTimes(1); }); @@ -113,7 +112,7 @@ describe('execute', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); }); describe('setResultAndStopExecution', () => { @@ -147,7 +146,7 @@ describe('execute', () => { ], schema ); - const result = await teskit.execute(query); + const result = await teskit.perform({ query }); assertSingleExecutionValue(result); expect(onExecuteCalled).toEqual(true); expect(onExecuteDoneCalled).toEqual(true); @@ -192,7 +191,7 @@ describe('execute', () => { ], schema ); - const result = await teskit.execute(query); + const result = await teskit.perform({ query }); assertSingleExecutionValue(result); expect(onExecuteCalled).toEqual(false); expect(onExecuteDoneCalled).toEqual(false); @@ -236,11 +235,13 @@ describe('execute', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - query { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + query { + alphabet + } + `, + }); assertStreamExecutionValue(result); const values = await collectAsyncIteratorValues(result); expect(values).toEqual([ @@ -284,11 +285,13 @@ describe('execute', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - query { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + query { + alphabet + } + `, + }); assertStreamExecutionValue(result); // run AsyncGenerator await collectAsyncIteratorValues(result); @@ -343,11 +346,13 @@ describe('execute', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - query { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + query { + alphabet + } + `, + }); assertStreamExecutionValue(result); const iterator = result[Symbol.asyncIterator](); await iterator.next(); @@ -405,11 +410,13 @@ describe('execute', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - query { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + query { + alphabet + } + `, + }); assertStreamExecutionValue(result); const iterator = result[Symbol.asyncIterator](); const nextPromise = iterator.next(); @@ -444,7 +451,7 @@ describe('execute', () => { schema ); - expect(await testkit.execute(query)).toEqual({ data: { test: 'test' } }); + expect(await testkit.perform({ query })).toEqual({ data: { test: 'test' } }); }); it('hook into subscription phases with proper cleanup on the source', async () => { @@ -520,7 +527,7 @@ describe('execute', () => { } `; - const result = await testkit.execute(document); + const result = await testkit.perform({ query: document }); assertStreamExecutionValue(result); await result.next(); await result.next(); @@ -597,11 +604,13 @@ it.each([ schema ); - const result = await teskit.execute(/* GraphQL */ ` - subscription { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + subscription { + alphabet + } + `, + }); assertStreamExecutionValue(result); const iterator = result[Symbol.asyncIterator](); const nextPromise = iterator.next(); diff --git a/packages/core/test/extends.spec.ts b/packages/core/test/extends.spec.ts index d325e7f524..3380e0d298 100644 --- a/packages/core/test/extends.spec.ts +++ b/packages/core/test/extends.spec.ts @@ -33,7 +33,7 @@ describe('extending envelops', () => { }); const teskit = createTestkit(instance); - await teskit.execute(query, {}); + await teskit.perform({ query }); expect(onExecuteChildSpy).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeExecute).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.afterExecute).toHaveBeenCalledTimes(1); diff --git a/packages/core/test/parse.spec.ts b/packages/core/test/parse.spec.ts index d0c7d85624..405f445de3 100644 --- a/packages/core/test/parse.spec.ts +++ b/packages/core/test/parse.spec.ts @@ -6,7 +6,7 @@ describe('parse', () => { it('Should call before parse and after parse correctly', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query); + await teskit.perform({ query }); expect(spiedPlugin.spies.beforeParse).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeParse).toHaveBeenCalledWith({ context: expect.any(Object), @@ -41,7 +41,7 @@ describe('parse', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(replacementFn).toHaveBeenCalledTimes(1); expect(replacementFn).toHaveBeenCalledWith(query, undefined); }); @@ -63,7 +63,7 @@ describe('parse', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(replacementFn).toHaveBeenCalledTimes(0); expect(afterFn).toHaveBeenCalledTimes(1); expect(afterFn).toHaveBeenCalledWith({ @@ -103,7 +103,7 @@ describe('parse', () => { ], schema ); - const result = await teskit.execute(query); + const result = await teskit.perform({ query }); assertSingleExecutionValue(result); expect(afterFn).toHaveBeenCalledTimes(1); expect(result.data?.currentUser).toBeDefined(); diff --git a/packages/core/test/plugins/use-error-handler.spec.ts b/packages/core/test/plugins/use-error-handler.spec.ts index 77b4c73e71..3e6ea51f7f 100644 --- a/packages/core/test/plugins/use-error-handler.spec.ts +++ b/packages/core/test/plugins/use-error-handler.spec.ts @@ -24,7 +24,7 @@ describe('useErrorHandler', () => { const mockHandler = jest.fn(); const testInstance = createTestkit([useErrorHandler(mockHandler)], schema); - await testInstance.execute(`query { foo }`, {}, { foo: 'bar' }); + await testInstance.perform({ query: `query { foo }` }, { foo: 'bar' }); expect(mockHandler).toHaveBeenCalledWith( [testError], @@ -66,7 +66,7 @@ describe('useErrorHandler', () => { const mockHandler = jest.fn(); const testInstance = createTestkit([useErrorHandler(mockHandler)], schema); - const result = await testInstance.execute(`subscription { foo }`, {}, { foo: 'bar' }); + const result = await testInstance.perform({ query: `subscription { foo }` }, { foo: 'bar' }); assertStreamExecutionValue(result); await collectAsyncIteratorValues(result); diff --git a/packages/core/test/plugins/use-masked-errors.spec.ts b/packages/core/test/plugins/use-masked-errors.spec.ts index 13a31ad45e..2e8bd441e2 100644 --- a/packages/core/test/plugins/use-masked-errors.spec.ts +++ b/packages/core/test/plugins/use-masked-errors.spec.ts @@ -97,7 +97,7 @@ describe('useMaskedErrors', () => { it('Should mask non GraphQLErrors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { secret }`); + const result = await testInstance.perform({ query: `query { secret }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -107,7 +107,7 @@ describe('useMaskedErrors', () => { it('Should not mask expected errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { secretEnvelop }`); + const result = await testInstance.perform({ query: `query { secretEnvelop }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -118,7 +118,7 @@ describe('useMaskedErrors', () => { it('Should not mask GraphQL operation syntax errors (of course it does not since we are only hooking in after execute, but just to be sure)', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { idonotexist }`); + const result = await testInstance.perform({ query: `query { idonotexist }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -128,7 +128,7 @@ describe('useMaskedErrors', () => { it('Should forward extensions from GraphQLError to final GraphQLError in errors array', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { secretWithExtensions }`); + const result = await testInstance.perform({ query: `query { secretWithExtensions }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -154,7 +154,7 @@ describe('useMaskedErrors', () => { schema ); try { - await testInstance.execute(`query { secretWithExtensions }`); + await testInstance.perform({ query: `query { secretWithExtensions }` }); } catch (err) { expect(err).toMatchInlineSnapshot(`[GraphQLError: My Custom Error Message.]`); } @@ -171,7 +171,7 @@ describe('useMaskedErrors', () => { schema ); try { - await testInstance.execute(`query { secretWithExtensions }`); + await testInstance.perform({ query: `query { secretWithExtensions }` }); } catch (err: any) { expect(err.message).toEqual(DEFAULT_ERROR_MESSAGE); } @@ -189,7 +189,7 @@ describe('useMaskedErrors', () => { schema ); try { - await testInstance.execute(`query { secretWithExtensions }`); + await testInstance.perform({ query: `query { secretWithExtensions }` }); } catch (err) { if (err instanceof GraphQLError) { expect(err.message).toEqual(`No context for you!`); @@ -202,7 +202,7 @@ describe('useMaskedErrors', () => { it('Should mask subscribe (sync/promise) subscription errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`subscription { instantError }`); + const result = await testInstance.perform({ query: `subscription { instantError }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toHaveLength(1); @@ -215,7 +215,7 @@ describe('useMaskedErrors', () => { [useMaskedErrors({ errorMessage: 'My Custom subscription error message.' })], schema ); - const result = await testInstance.execute(`subscription { instantError }`); + const result = await testInstance.perform({ query: `subscription { instantError }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toMatchInlineSnapshot(` @@ -227,7 +227,7 @@ describe('useMaskedErrors', () => { it('Should not mask subscribe (sync/promise) subscription GraphQL errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`subscription { instantGraphQLError }`); + const result = await testInstance.perform({ query: `subscription { instantGraphQLError }` }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors).toMatchInlineSnapshot(` @@ -240,7 +240,7 @@ describe('useMaskedErrors', () => { it('Should mask subscribe (AsyncIterable) subscription errors', async () => { expect.assertions(1); const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamError }` }); assertStreamExecutionValue(resultStream); try { await collectAsyncIteratorValues(resultStream); @@ -255,7 +255,7 @@ describe('useMaskedErrors', () => { [useMaskedErrors({ errorMessage: 'My AsyncIterable Custom Error Message.' })], schema ); - const resultStream = await testInstance.execute(`subscription { streamError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamError }` }); assertStreamExecutionValue(resultStream); try { await collectAsyncIteratorValues(resultStream); @@ -266,7 +266,7 @@ describe('useMaskedErrors', () => { it('Should not mask subscribe (AsyncIterable) subscription envelop errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamGraphQLError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamGraphQLError }` }); assertStreamExecutionValue(resultStream); try { await collectAsyncIteratorValues(resultStream); @@ -277,7 +277,7 @@ describe('useMaskedErrors', () => { it('Should mask resolve subscription errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamResolveError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamResolveError }` }); assertStreamExecutionValue(resultStream); const allResults = await collectAsyncIteratorValues(resultStream); expect(allResults).toHaveLength(1); @@ -293,7 +293,7 @@ describe('useMaskedErrors', () => { [useMaskedErrors({ errorMessage: 'Custom resolve subscription errors.' })], schema ); - const resultStream = await testInstance.execute(`subscription { streamResolveError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamResolveError }` }); assertStreamExecutionValue(resultStream); const allResults = await collectAsyncIteratorValues(resultStream); expect(allResults).toHaveLength(1); @@ -305,7 +305,7 @@ describe('useMaskedErrors', () => { it('Should not mask resolve subscription envelop errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const resultStream = await testInstance.execute(`subscription { streamResolveGraphQLError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamResolveGraphQLError }` }); assertStreamExecutionValue(resultStream); const allResults = await collectAsyncIteratorValues(resultStream); expect(allResults).toHaveLength(1); @@ -329,22 +329,24 @@ describe('useMaskedErrors', () => { tokenType: 'Bearer', }; const testInstance = createTestkit([useMaskedErrors(), useAuth0(auto0Options)], schema); - try { - await testInstance.execute(`query { secret }`, {}, { request: { headers: { authorization: 'Something' } } }); - } catch (err) { - expect(err).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`); - } + let result = await testInstance.perform( + { query: `query { secret }` }, + { request: { headers: { authorization: 'Something' } } } + ); + assertSingleExecutionValue(result); + expect(result.errors?.[0]).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`); - try { - await testInstance.execute(`query { secret }`, {}, { request: { headers: { authorization: 'Something else' } } }); - } catch (err) { - expect(err).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`); - } + result = await testInstance.perform( + { query: `query { secret }` }, + { request: { headers: { authorization: 'Something else' } } } + ); + assertSingleExecutionValue(result); + expect(result.errors?.[0]).toMatchInlineSnapshot(`[GraphQLError: Unexpected error.]`); }); it('should not mask parse errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { a `, {}); + const result = await testInstance.perform({ query: `query { a ` }); assertSingleExecutionValue(result); expect(result).toMatchInlineSnapshot(` Object { @@ -357,7 +359,7 @@ describe('useMaskedErrors', () => { it('should not mask validation errors', async () => { const testInstance = createTestkit([useMaskedErrors()], schema); - const result = await testInstance.execute(`query { iDoNotExistsMyGuy }`, {}); + const result = await testInstance.perform({ query: `query { iDoNotExistsMyGuy }` }); assertSingleExecutionValue(result); expect(result).toMatchInlineSnapshot(` Object { @@ -374,7 +376,7 @@ describe('useMaskedErrors', () => { custom: true, }); const testInstance = createTestkit([useMaskedErrors({ maskError: customErrorMaskFn })], schema); - const result = await testInstance.execute(`query { secret }`); + const result = await testInstance.perform({ query: `query { secret }` }); assertSingleExecutionValue(result); expect(result).toMatchInlineSnapshot(` Object { @@ -400,7 +402,7 @@ describe('useMaskedErrors', () => { }); expect.assertions(2); const testInstance = createTestkit([useMaskedErrors({ maskError: customErrorMaskFn })], schema); - const resultStream = await testInstance.execute(`subscription { streamError }`); + const resultStream = await testInstance.perform({ query: `subscription { streamError }` }); assertStreamExecutionValue(resultStream); try { await collectAsyncIteratorValues(resultStream); @@ -426,7 +428,7 @@ describe('useMaskedErrors', () => { schema ); try { - await testInstance.execute(`query { secret }`, {}, {}); + await testInstance.perform({ query: `query { secret }` }); } catch (e) { expect((e as GraphQLError).message).toEqual('Custom error message for Custom error'); } @@ -449,7 +451,7 @@ describe('useMaskedErrors', () => { }, }); const testInstance = createTestkit([useMaskedErrors({ maskError: createDefaultMaskError(true) })], schema); - const result = await testInstance.execute(`query { foo }`, {}, {}); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors?.[0].extensions).toEqual({ message: "I'm a teapot", @@ -473,7 +475,7 @@ describe('useMaskedErrors', () => { }, }); const testInstance = createTestkit([useMaskedErrors({ maskError: createDefaultMaskError(true) })], schema); - const result = await testInstance.execute(`query { foo }`, {}, {}); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors?.[0].extensions).toEqual({ message: 'Unexpected error value: "I\'m a teapot"', diff --git a/packages/core/test/subscribe.spec.ts b/packages/core/test/subscribe.spec.ts index 9c1709aa60..ad4865b65d 100644 --- a/packages/core/test/subscribe.spec.ts +++ b/packages/core/test/subscribe.spec.ts @@ -31,11 +31,13 @@ describe('subscribe', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - subscription { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + subscription { + alphabet + } + `, + }); assertStreamExecutionValue(result); const values = await collectAsyncIteratorValues(result); expect(values).toEqual([ @@ -79,11 +81,13 @@ describe('subscribe', () => { schema ); - const result = await teskit.execute(/* GraphQL */ ` - subscription { - alphabet - } - `); + const result = await teskit.perform({ + query: /* GraphQL */ ` + subscription { + alphabet + } + `, + }); assertStreamExecutionValue(result); await collectAsyncIteratorValues(result); }); diff --git a/packages/core/test/validate.spec.ts b/packages/core/test/validate.spec.ts index b5f0a99074..89b895332b 100644 --- a/packages/core/test/validate.spec.ts +++ b/packages/core/test/validate.spec.ts @@ -6,7 +6,7 @@ describe('validate', () => { it('Should call before validate and after validate correctly', async () => { const spiedPlugin = createSpiedPlugin(); const teskit = createTestkit([spiedPlugin.plugin], schema); - await teskit.execute(query); + await teskit.perform({ query }); expect(spiedPlugin.spies.beforeValidate).toHaveBeenCalledTimes(1); expect(spiedPlugin.spies.beforeValidate).toHaveBeenCalledWith({ context: expect.any(Object), @@ -46,7 +46,7 @@ describe('validate', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(replacementFn).toHaveBeenCalledTimes(1); expect(replacementFn).toHaveBeenCalledWith( expect.any(GraphQLSchema), @@ -70,7 +70,7 @@ describe('validate', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(replacementFn).toHaveBeenCalledTimes(0); }); @@ -92,7 +92,7 @@ describe('validate', () => { ], schema ); - await teskit.execute(query); + await teskit.perform({ query }); expect(after).toHaveBeenCalledTimes(1); expect(after).toHaveBeenCalledWith({ valid: false, @@ -118,7 +118,7 @@ describe('validate', () => { schema ); - const r = await teskit.execute(query); + const r = await teskit.perform({ query }); assertSingleExecutionValue(r); expect(r.errors).toBeDefined(); @@ -140,7 +140,7 @@ describe('validate', () => { schema ); - const r = await teskit.execute(query); + const r = await teskit.perform({ query }); assertSingleExecutionValue(r); expect(r.errors).toBeDefined(); diff --git a/packages/plugins/apollo-datasources/test/use-apollo-datasources.spec.ts b/packages/plugins/apollo-datasources/test/use-apollo-datasources.spec.ts index 609a85b60c..fab9a8bf25 100644 --- a/packages/plugins/apollo-datasources/test/use-apollo-datasources.spec.ts +++ b/packages/plugins/apollo-datasources/test/use-apollo-datasources.spec.ts @@ -31,9 +31,8 @@ describe('useApolloDataSources', () => { ], schema ); - const result = await testInstance.execute( - `query { foo }`, - {}, + const result = await testInstance.perform( + { query: `query { foo }` }, { initialContextValue: true, } @@ -80,7 +79,7 @@ describe('useApolloDataSources', () => { ], schema ); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data).toBeDefined(); diff --git a/packages/plugins/apollo-federation/test/federation.spec.ts b/packages/plugins/apollo-federation/test/federation.spec.ts index 823ea5bbc6..b1f8aabcec 100644 --- a/packages/plugins/apollo-federation/test/federation.spec.ts +++ b/packages/plugins/apollo-federation/test/federation.spec.ts @@ -62,7 +62,7 @@ describe('useApolloFederation', () => { }, ]); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(onExecuteSpy).toHaveBeenCalledTimes(1); expect(onExecuteSpy.mock.calls[0][0].executeFn).not.toBe(execute); @@ -71,7 +71,7 @@ describe('useApolloFederation', () => { it('Should execute document string correctly', async () => { const testInstance = createTestkit([useTestFederation()]); - const result = await testInstance.execute(query); + const result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toBeFalsy(); expect(result.data).toMatchInlineSnapshot(` @@ -101,7 +101,7 @@ Object { it('Should execute parsed document correctly', async () => { const testInstance = createTestkit([useTestFederation()]); - const result = await testInstance.execute(parse(query)); + const result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toBeFalsy(); expect(result.data).toMatchInlineSnapshot(` diff --git a/packages/plugins/apollo-tracing/test/use-apollo-tracing.spec.ts b/packages/plugins/apollo-tracing/test/use-apollo-tracing.spec.ts index 2f0ca667b5..8502982e0a 100644 --- a/packages/plugins/apollo-tracing/test/use-apollo-tracing.spec.ts +++ b/packages/plugins/apollo-tracing/test/use-apollo-tracing.spec.ts @@ -15,7 +15,7 @@ describe('useApolloTracing', () => { it('should measure execution times and return it as extension', async () => { const testInstance = createTestkit([useApolloTracing()], schema); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data).toBeDefined(); diff --git a/packages/plugins/dataloader/test/dataloader.spec.ts b/packages/plugins/dataloader/test/dataloader.spec.ts index 297a79bb72..e8c84c601b 100644 --- a/packages/plugins/dataloader/test/dataloader.spec.ts +++ b/packages/plugins/dataloader/test/dataloader.spec.ts @@ -27,7 +27,7 @@ describe('useDataLoader', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.data?.test).toBe('myValue'); }); diff --git a/packages/plugins/execute-subscription-event/test/use-extend-context-value-per-subscription-event.spec.ts b/packages/plugins/execute-subscription-event/test/use-extend-context-value-per-subscription-event.spec.ts index f921d2113a..7aa94118b3 100644 --- a/packages/plugins/execute-subscription-event/test/use-extend-context-value-per-subscription-event.spec.ts +++ b/packages/plugins/execute-subscription-event/test/use-extend-context-value-per-subscription-event.spec.ts @@ -27,7 +27,7 @@ describe('useContextValuePerExecuteSubscriptionEvent', () => { schema ); - const result = await testInstance.execute(subscriptionOperationString); + const result = await testInstance.perform({ query: subscriptionOperationString }); assertStreamExecutionValue(result); pushValue({}); @@ -63,7 +63,7 @@ describe('useContextValuePerExecuteSubscriptionEvent', () => { schema ); - const result = await testInstance.execute(subscriptionOperationString); + const result = await testInstance.perform({ query: subscriptionOperationString }); assertStreamExecutionValue(result); pushValue({}); diff --git a/packages/plugins/extended-validation/test/extended-validation.spec.ts b/packages/plugins/extended-validation/test/extended-validation.spec.ts index 7cf58c0410..f9d45ca3b8 100644 --- a/packages/plugins/extended-validation/test/extended-validation.spec.ts +++ b/packages/plugins/extended-validation/test/extended-validation.spec.ts @@ -45,7 +45,7 @@ describe('useExtendedValidation', () => { schema ); - const result = await testInstance.execute(operation); + const result = await testInstance.perform({ query: operation }); expect(result).toMatchInlineSnapshot(` Object { "data": null, @@ -94,7 +94,7 @@ describe('useExtendedValidation', () => { schema ); - await testInstance.execute(operation); + await testInstance.perform({ query: operation }); expect(extendedValidationRunCount).toEqual(1); }); it('execute throws an error if "contextFactory" has not been invoked', async () => { @@ -169,7 +169,7 @@ describe('useExtendedValidation', () => { ], schema ); - const result = await testkit.execute(operation); + const result = await testkit.perform({ query: operation }); expect(calledExtendedValidationRule).toEqual(true); }); it('subscribe does result in extended validation phase errors', async () => { @@ -210,7 +210,7 @@ describe('useExtendedValidation', () => { ], schema ); - const result = await testkit.execute(operation); + const result = await testkit.perform({ query: operation }); assertSingleExecutionValue(result); expect(result).toMatchInlineSnapshot(` Object { diff --git a/packages/plugins/extended-validation/test/one-of.spec.ts b/packages/plugins/extended-validation/test/one-of.spec.ts index 4d55b435ca..1ba0a24f02 100644 --- a/packages/plugins/extended-validation/test/one-of.spec.ts +++ b/packages/plugins/extended-validation/test/one-of.spec.ts @@ -529,7 +529,7 @@ describe('oneOf', () => { testSchema ); - const result = await testInstance.execute(document, variables); + const result = await testInstance.perform({ query: document, variables }); assertSingleExecutionValue(result); if (expectedError) { expect(result.errors).toBeDefined(); @@ -631,7 +631,7 @@ describe('oneOf', () => { testSchema ); - const result = await testInstance.execute(document, variables); + const result = await testInstance.perform({ query: document, variables }); assertSingleExecutionValue(result); if (expectedError) { expect(result.errors).toBeDefined(); diff --git a/packages/plugins/generic-auth/tests/use-generic-auth.spec.ts b/packages/plugins/generic-auth/tests/use-generic-auth.spec.ts index 3c0b706c0a..397d62256f 100644 --- a/packages/plugins/generic-auth/tests/use-generic-auth.spec.ts +++ b/packages/plugins/generic-auth/tests/use-generic-auth.spec.ts @@ -70,7 +70,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(getIntrospectionQuery()); + const result = await testInstance.perform({ query: getIntrospectionQuery() }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); }); @@ -86,7 +86,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.test).toBe('Dotan'); @@ -104,7 +104,7 @@ describe('useGenericAuth', () => { ); try { - await testInstance.execute(`query { test }`); + await testInstance.perform({ query: `query { test }` }); } catch (err) { expect(err).toMatchInlineSnapshot(`[GraphQLError: Unauthenticated!]`); } @@ -125,7 +125,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(spyFn).toHaveBeenCalledWith( @@ -153,7 +153,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { public }`); + const result = await testInstance.perform({ query: `query { public }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.public).toBe('public'); @@ -170,7 +170,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { public }`); + const result = await testInstance.perform({ query: `query { public }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.public).toBe('public'); @@ -189,7 +189,7 @@ describe('useGenericAuth', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.test).toBe('Dotan'); @@ -206,7 +206,7 @@ describe('useGenericAuth', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.test).toBe(''); @@ -227,7 +227,7 @@ describe('useGenericAuth', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(spyFn).toHaveBeenCalledWith( @@ -259,7 +259,7 @@ describe('useGenericAuth', () => { schema ); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(spyFn).toHaveBeenCalledWith( @@ -304,7 +304,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { protected }`); + const result = await testInstance.perform({ query: `query { protected }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.protected).toBe('Dotan'); @@ -321,7 +321,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { public }`); + const result = await testInstance.perform({ query: `query { public }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.public).toBe('public'); @@ -338,7 +338,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { public }`); + const result = await testInstance.perform({ query: `query { public }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.public).toBe('public'); @@ -355,7 +355,7 @@ describe('useGenericAuth', () => { schemaWithDirective ); - const result = await testInstance.execute(`query { protected }`); + const result = await testInstance.perform({ query: `query { protected }` }); assertSingleExecutionValue(result); expect(result.errors?.length).toBe(1); expect(result.errors?.[0].message).toBe(`Accessing 'Query.protected' requires authentication.`); @@ -442,7 +442,7 @@ describe('useGenericAuth', () => { schemaWithDirectiveWithRole ); - const result = await testInstance.execute(`query { admin }`); + const result = await testInstance.perform({ query: `query { admin }` }); assertSingleExecutionValue(result); expect(result.errors?.length).toBe(1); expect(result.errors?.[0].message).toBe( @@ -462,7 +462,7 @@ describe('useGenericAuth', () => { schemaWithDirectiveWithRole ); - const result = await testInstance.execute(`query { admin }`); + const result = await testInstance.perform({ query: `query { admin }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.admin).toBe('admin'); diff --git a/packages/plugins/graphql-jit/test/graphql-jit.spec.ts b/packages/plugins/graphql-jit/test/graphql-jit.spec.ts index 1fdc81f9c8..c357835cb6 100644 --- a/packages/plugins/graphql-jit/test/graphql-jit.spec.ts +++ b/packages/plugins/graphql-jit/test/graphql-jit.spec.ts @@ -48,7 +48,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`query { test }`); + await testInstance.perform({ query: `query { test }` }); expect(onExecuteSpy).toHaveBeenCalledTimes(1); expect(onExecuteSpy.mock.calls[0][0].executeFn).not.toBe(execute); @@ -67,7 +67,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`subscription { count }`); + await testInstance.perform({ query: `subscription { count }` }); expect(onSubscribeSpy).toHaveBeenCalledTimes(1); expect(onSubscribeSpy.mock.calls[0][0].subscribeFn).not.toBe(subscribe); @@ -91,7 +91,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`query { test }`); + await testInstance.perform({ query: `query { test }` }); expect(onExecuteSpy).toHaveBeenCalledTimes(1); expect(onExecuteSpy.mock.calls[0][0].executeFn).toBe(execute); @@ -116,7 +116,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`subscription { count }`); + await testInstance.perform({ query: `subscription { count }` }); expect(onSubscribeSpy).toHaveBeenCalledTimes(1); expect(onSubscribeSpy.mock.calls[0][0].subscribeFn).toBe(subscribe); @@ -125,14 +125,14 @@ describe('useGraphQlJit', () => { it('Should execute correctly', async () => { const testInstance = createTestkit([useGraphQlJit()], schema); - const result = await testInstance.execute(`query { test }`); + const result = await testInstance.perform({ query: `query { test }` }); assertSingleExecutionValue(result); expect(result.data?.test).toBe('boop'); }); it('Should subscribe correctly', async () => { const testInstance = createTestkit([useGraphQlJit()], schema); - const result = await testInstance.execute(`subscription { count }`); + const result = await testInstance.perform({ query: `subscription { count }` }); assertStreamExecutionValue(result); const values = await collectAsyncIteratorValues(result); for (let i = 0; i < 10; i++) { @@ -157,7 +157,7 @@ describe('useGraphQlJit', () => { schema ); - await testInstance.execute(`query { test }`); + await testInstance.perform({ query: `query { test }` }); expect(cache.get).toHaveBeenCalled(); expect(cache.set).toHaveBeenCalled(); }); diff --git a/packages/plugins/graphql-middleware/test/graphql-middleware.spec.ts b/packages/plugins/graphql-middleware/test/graphql-middleware.spec.ts index 5f4253d258..6e89062c80 100644 --- a/packages/plugins/graphql-middleware/test/graphql-middleware.spec.ts +++ b/packages/plugins/graphql-middleware/test/graphql-middleware.spec.ts @@ -28,6 +28,6 @@ describe('useGraphQlJit', () => { schema ); - await testkit.execute(`{ __typename}`); + await testkit.perform({ query: '{ __typename}' }); }); }); diff --git a/packages/plugins/graphql-modules/test/use-graphql-modules.spec.ts b/packages/plugins/graphql-modules/test/use-graphql-modules.spec.ts index 2302f06406..777ebd6097 100644 --- a/packages/plugins/graphql-modules/test/use-graphql-modules.spec.ts +++ b/packages/plugins/graphql-modules/test/use-graphql-modules.spec.ts @@ -38,7 +38,7 @@ describe('useGraphQLModules', () => { it('Should work correctly and init all providers at the right time', async () => { const testInstance = createTestkit([useGraphQLModules(app)]); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.data?.foo).toBe('testFoo'); }); diff --git a/packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts b/packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts index 99ede68cba..72318c9400 100644 --- a/packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts +++ b/packages/plugins/immediate-introspection/test/use-immediate-introspection.spec.ts @@ -11,11 +11,13 @@ describe('useImmediateIntrospection', () => { schema ); - await testInstance.execute(/* GraphQL */ ` - query { - __typename - } - `); + await testInstance.perform({ + query: /* GraphQL */ ` + query { + __typename + } + `, + }); }); it('skips context building for introspection only operation (alias)', async () => { const testInstance = createTestkit( @@ -23,11 +25,13 @@ describe('useImmediateIntrospection', () => { schema ); - await testInstance.execute(/* GraphQL */ ` - query { - some: __typename - } - `); + await testInstance.perform({ + query: /* GraphQL */ ` + query { + some: __typename + } + `, + }); }); it('runs context building for operation containing non introspection fields', async () => { const testInstance = createTestkit( @@ -36,16 +40,18 @@ describe('useImmediateIntrospection', () => { ); try { - await testInstance.execute(/* GraphQL */ ` - query { - __schema { - aaa: __typename - } - me { - id + await testInstance.perform({ + query: /* GraphQL */ ` + query { + __schema { + aaa: __typename + } + me { + id + } } - } - `); + `, + }); throw new Error('Should throw.'); } catch (err) { if (err === 'This should reject') { @@ -62,13 +68,15 @@ describe('useImmediateIntrospection', () => { ); try { - await testInstance.execute(/* GraphQL */ ` - mutation { - createUser { - id + await testInstance.perform({ + query: /* GraphQL */ ` + mutation { + createUser { + id + } } - } - `); + `, + }); throw new Error('Should throw.'); } catch (err) { if (err === 'This should reject') { @@ -85,11 +93,13 @@ describe('useImmediateIntrospection', () => { ); try { - await testInstance.execute(/* GraphQL */ ` - subscription { - message - } - `); + await testInstance.perform({ + query: /* GraphQL */ ` + subscription { + message + } + `, + }); throw new Error('Should throw.'); } catch (err) { if (err === 'This should reject') { @@ -106,16 +116,18 @@ describe('useImmediateIntrospection', () => { ); try { - await testInstance.execute(/* GraphQL */ ` - query { - __schema { - aaa: __typename - } - me { - id + await testInstance.perform({ + query: /* GraphQL */ ` + query { + __schema { + aaa: __typename + } + me { + id + } } - } - `); + `, + }); throw new Error('Should throw.'); } catch (err) { if (err === 'This should reject') { @@ -134,6 +146,6 @@ describe('useImmediateIntrospection', () => { schema ); - await testInstance.execute(getIntrospectionQuery()); + await testInstance.perform({ query: getIntrospectionQuery() }); }); }); diff --git a/packages/plugins/live-query/test/use-live-query.spec.ts b/packages/plugins/live-query/test/use-live-query.spec.ts index 64dd1cf0de..4697afecef 100644 --- a/packages/plugins/live-query/test/use-live-query.spec.ts +++ b/packages/plugins/live-query/test/use-live-query.spec.ts @@ -27,13 +27,14 @@ describe('useLiveQuery', () => { const contextValue = { greetings: ['Hi', 'Sup', 'Ola'], }; - const result = await testKit.execute( - /* GraphQL */ ` - query @live { - greetings - } - `, - undefined, + const result = await testKit.perform( + { + query: /* GraphQL */ ` + query @live { + greetings + } + `, + }, contextValue ); assertStreamExecutionValue(result); @@ -77,13 +78,14 @@ describe('useLiveQuery', () => { const contextValue = { greetings: ['Hi', 'Sup', 'Ola'], }; - const result = await testKit.execute( - /* GraphQL */ ` - query @live { - greetings - } - `, - undefined, + const result = await testKit.perform( + { + query: /* GraphQL */ ` + query @live { + greetings + } + `, + }, contextValue ); assertStreamExecutionValue(result); diff --git a/packages/plugins/on-resolve/test/use-on-resolve.spec.ts b/packages/plugins/on-resolve/test/use-on-resolve.spec.ts index 7b6954f8ff..f16f165d68 100644 --- a/packages/plugins/on-resolve/test/use-on-resolve.spec.ts +++ b/packages/plugins/on-resolve/test/use-on-resolve.spec.ts @@ -23,7 +23,7 @@ describe('useOnResolve', () => { const onResolveFn = jest.fn((_opts: OnResolveOptions) => onResolveDoneFn); const testkit = createTestkit([useOnResolve(onResolveFn)], schema); - await testkit.execute('{ value1, value2 }'); + await testkit.perform({ query: '{ value1, value2 }' }); expect(onResolveFn).toBeCalledTimes(2); expect(onResolveDoneFn).toBeCalledTimes(2); @@ -31,7 +31,6 @@ describe('useOnResolve', () => { let i = 0; for (const field of ['value1', 'value2']) { expect(onResolveFn.mock.calls[i][0].context).toBeDefined(); - expect(onResolveFn.mock.calls[i][0].root).toBeDefined(); expect(onResolveFn.mock.calls[i][0].args).toBeDefined(); expect(onResolveFn.mock.calls[i][0].info).toBeDefined(); expect(onResolveFn.mock.calls[i][0].info.fieldName).toBe(field); @@ -55,7 +54,7 @@ describe('useOnResolve', () => { schema ); - const result = await testkit.execute('{ value1 }'); + const result = await testkit.perform({ query: '{ value1 }' }); assertSingleExecutionValue(result); expect(result.data?.value1).toBe('value2'); diff --git a/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts b/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts index fecebd3d8f..9f51fadf28 100644 --- a/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts +++ b/packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts @@ -46,7 +46,7 @@ describe('useOpenTelemetry', () => { schema ); - const result = await testInstance.execute(query); + const result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(onExecuteSpy).toHaveBeenCalledTimes(1); }); @@ -55,7 +55,7 @@ describe('useOpenTelemetry', () => { const exporter = new InMemorySpanExporter(); const testInstance = createTestkit([useTestOpenTelemetry(exporter)], schema); - await testInstance.execute(query); + await testInstance.perform({ query }); const actual = exporter.getFinishedSpans(); expect(actual.length).toBe(1); expect(actual[0].name).toBe('Anonymous Operation'); @@ -65,7 +65,7 @@ describe('useOpenTelemetry', () => { const exporter = new InMemorySpanExporter(); const testInstance = createTestkit([useTestOpenTelemetry(exporter, { resolvers: true })], schema); - await testInstance.execute(query); + await testInstance.perform({ query }); const actual = exporter.getFinishedSpans(); expect(actual.length).toBe(2); expect(actual[0].name).toBe('Query.ping'); diff --git a/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts b/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts index 4811e67682..8c84615e59 100644 --- a/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts +++ b/packages/plugins/operation-field-permissions/test/use-operation-permissions.spec.ts @@ -52,7 +52,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(getIntrospectionQuery()); + const result = await kit.perform({ query: getIntrospectionQuery() }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); }); @@ -67,14 +67,16 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(/* GraphQL */ ` - query { - __schema { - __typename + const result = await kit.perform({ + query: /* GraphQL */ ` + query { + __schema { + __typename + } + greetings } - greetings - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -93,7 +95,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(query); + const result = await kit.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); }); @@ -108,7 +110,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(query); + const result = await kit.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -128,7 +130,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(query); + const result = await kit.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -146,7 +148,7 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(query); + const result = await kit.perform({ query }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); }); @@ -160,13 +162,15 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(/* GraphQL */ ` - query { - postOrUser { - __typename + const result = await kit.perform({ + query: /* GraphQL */ ` + query { + postOrUser { + __typename + } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -187,13 +191,15 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(/* GraphQL */ ` - query { - node { - __typename + const result = await kit.perform({ + query: /* GraphQL */ ` + query { + node { + __typename + } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toMatchInlineSnapshot(` Array [ @@ -214,11 +220,13 @@ describe('useOperationPermissions', () => { schema ); - const result = await kit.execute(/* GraphQL */ ` - query { - __typename - } - `); + const result = await kit.perform({ + query: /* GraphQL */ ` + query { + __typename + } + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); const [error] = result.errors!; diff --git a/packages/plugins/parser-cache/test/parser-cache.spec.ts b/packages/plugins/parser-cache/test/parser-cache.spec.ts index 68deb1f71e..eb3606f6fc 100644 --- a/packages/plugins/parser-cache/test/parser-cache.spec.ts +++ b/packages/plugins/parser-cache/test/parser-cache.spec.ts @@ -30,23 +30,23 @@ describe('useParserCache', () => { it('Should call original parse when cache is empty', async () => { const testInstance = createTestkit([useTestPlugin, useParserCache()], testSchema); - await testInstance.execute(`query { foo }`); + await testInstance.perform({ query: `query { foo }` }); expect(testParser).toHaveBeenCalledTimes(1); }); it('Should call parse once once when operation is cached', async () => { const testInstance = createTestkit([useTestPlugin, useParserCache()], testSchema); - await testInstance.execute(`query { foo }`); - await testInstance.execute(`query { foo }`); - await testInstance.execute(`query { foo }`); + await testInstance.perform({ query: `query { foo }` }); + await testInstance.perform({ query: `query { foo }` }); + await testInstance.perform({ query: `query { foo }` }); expect(testParser).toHaveBeenCalledTimes(1); }); it('Should call parse once once when operation is cached and errored', async () => { const testInstance = createTestkit([useTestPlugin, useParserCache()], testSchema); - const r1 = await testInstance.execute(`FAILED\ { foo }`); + const r1 = await testInstance.perform({ query: `FAILED\ { foo }` }); assertSingleExecutionValue(r1); - const r2 = await testInstance.execute(`FAILED\ { foo }`); + const r2 = await testInstance.perform({ query: `FAILED\ { foo }` }); assertSingleExecutionValue(r2); expect(testParser).toHaveBeenCalledTimes(1); expect(r1.errors![0].message).toBe(`Syntax Error: Unexpected Name "FAILED".`); @@ -56,8 +56,8 @@ describe('useParserCache', () => { it('Should call parse multiple times on different operations', async () => { const testInstance = createTestkit([useTestPlugin, useParserCache()], testSchema); - await testInstance.execute(`query t { foo }`); - await testInstance.execute(`query t2 { foo }`); + await testInstance.perform({ query: `query t { foo }` }); + await testInstance.perform({ query: `query t2 { foo }` }); expect(testParser).toHaveBeenCalledTimes(2); }); @@ -75,9 +75,9 @@ describe('useParserCache', () => { ], testSchema ); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); await testInstance.wait(10); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); expect(testParser).toHaveBeenCalledTimes(2); }); @@ -95,7 +95,7 @@ describe('useParserCache', () => { testSchema ); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); expect(documentCache.get).toHaveBeenCalled(); expect(documentCache.set).toHaveBeenCalled(); }); @@ -114,7 +114,7 @@ describe('useParserCache', () => { testSchema ); - await testInstance.execute(`FAILED\ { foo }`); + await testInstance.perform({ query: `FAILED\ { foo }` }); expect(errorCache.get).toHaveBeenCalled(); expect(errorCache.set).toHaveBeenCalled(); }); diff --git a/packages/plugins/persisted-operations/tests/persisted-operations.spec.ts b/packages/plugins/persisted-operations/tests/persisted-operations.spec.ts index 75ce4f6f49..517d1d6634 100644 --- a/packages/plugins/persisted-operations/tests/persisted-operations.spec.ts +++ b/packages/plugins/persisted-operations/tests/persisted-operations.spec.ts @@ -30,7 +30,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`persisted_1`, {}, {}); + const result = await testInstance.perform({ query: `persisted_1` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.foo).toBe('test'); @@ -49,7 +49,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`persisted_1`); + const result = await testInstance.perform({ query: `persisted_1` }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe(`Unable to match operation with id 'persisted_1'`); }); @@ -67,7 +67,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`persisted_2`); + const result = await testInstance.perform({ query: `persisted_2` }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe(`Unable to match operation with id 'persisted_2'`); }); @@ -85,7 +85,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`invalid`); + const result = await testInstance.perform({ query: `invalid` }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe(`Syntax Error: Unexpected Name "invalid".`); }); @@ -103,7 +103,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.foo).toBe('test'); @@ -122,7 +122,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`query { foo }`); + const result = await testInstance.perform({ query: `query { foo }` }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe('Must provide operation id'); }); @@ -139,7 +139,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1'); + const result = await testInstance.perform({ query: 'persisted_1' }); assertSingleExecutionValue(result); expect(result.errors![0].message).toBe('Must provide store for persisted-operations!'); }); @@ -158,7 +158,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1', {}, initialContext); + const result = await testInstance.perform({ query: 'persisted_1' }, initialContext); assertSingleExecutionValue(result); expect(result.data?.foo).toBe('test'); }); @@ -177,7 +177,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('invalid', {}, initialContext); + const result = await testInstance.perform({ query: 'invalid' }, initialContext); assertSingleExecutionValue(result); expect(result.data?.foo).toBe('test'); }); @@ -196,7 +196,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute(`query { bar }`, {}, initialContext); + const result = await testInstance.perform({ query: `query { bar }` }, initialContext); assertSingleExecutionValue(result); expect(result.data?.foo).toBe('test'); }); @@ -216,7 +216,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1', {}, initialContext); + const result = await testInstance.perform({ query: 'persisted_1' }, initialContext); assertSingleExecutionValue(result); expect(mockOnMissingMatch).toHaveBeenCalledTimes(1); expect(mockOnMissingMatch).toHaveBeenCalledWith(initialContext, 'persisted_1'); @@ -237,7 +237,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1', {}, initialContext); + const result = await testInstance.perform({ query: 'persisted_1' }, initialContext); assertSingleExecutionValue(result); expect(mockOnMissingMatch).toHaveBeenCalledTimes(1); expect(mockOnMissingMatch).toHaveBeenCalledWith(initialContext, 'persisted_1'); @@ -257,7 +257,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1'); + const result = await testInstance.perform({ query: 'persisted_1' }); assertSingleExecutionValue(result); expect(mockOnMissingMatch).not.toHaveBeenCalled(); }); @@ -276,7 +276,7 @@ describe('usePersistedOperations', () => { testSchema ); - const result = await testInstance.execute('persisted_1'); + const result = await testInstance.perform({ query: 'persisted_1' }); assertSingleExecutionValue(result); expect(mockOnMissingMatch).not.toHaveBeenCalled(); }); diff --git a/packages/plugins/preload-assets/test/use-preload-assets.spec.ts b/packages/plugins/preload-assets/test/use-preload-assets.spec.ts index 5287e86a1f..1adb816b66 100644 --- a/packages/plugins/preload-assets/test/use-preload-assets.spec.ts +++ b/packages/plugins/preload-assets/test/use-preload-assets.spec.ts @@ -20,7 +20,7 @@ describe('usePreloadAssets', () => { it('Should include assets to preload', async () => { const testInstance = createTestkit([usePreloadAssets()], schema); - const result = await testInstance.execute(`query { imageUrl }`); + const result = await testInstance.perform({ query: `query { imageUrl }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -30,7 +30,7 @@ describe('usePreloadAssets', () => { it('Should not include the preload extension if no asset should be preloaded', async () => { const testInstance = createTestkit([usePreloadAssets()], schema); - const result = await testInstance.execute(`query { noAsset }`); + const result = await testInstance.perform({ query: `query { noAsset }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toBeUndefined(); @@ -44,7 +44,7 @@ describe('usePreloadAssets', () => { ], schema ); - const result = await testInstance.execute(`query { imageUrl }`); + const result = await testInstance.perform({ query: `query { imageUrl }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toBeUndefined(); diff --git a/packages/plugins/rate-limiter/tests/use-rate-limiter.spec.ts b/packages/plugins/rate-limiter/tests/use-rate-limiter.spec.ts index 1fe9f8a41e..8a09cb6e19 100644 --- a/packages/plugins/rate-limiter/tests/use-rate-limiter.spec.ts +++ b/packages/plugins/rate-limiter/tests/use-rate-limiter.spec.ts @@ -11,7 +11,7 @@ describe('useRateLimiter', () => { const schemaWithDirective = makeExecutableSchema({ typeDefs: ` ${DIRECTIVE_SDL} - + type Query { limited: String @rateLimit( max: 1, @@ -39,9 +39,9 @@ describe('useRateLimiter', () => { schemaWithDirective ); - testInstance.execute(`query { unlimited }`); - await testInstance.execute(`query { unlimited }`); - const result = await testInstance.execute(`query { unlimited }`); + testInstance.perform({ query: `query { unlimited }` }); + await testInstance.perform({ query: `query { unlimited }` }); + const result = await testInstance.perform({ query: `query { unlimited }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.unlimited).toBe('unlimited'); @@ -57,9 +57,9 @@ describe('useRateLimiter', () => { schemaWithDirective ); - await testInstance.execute(`query { limited }`); + await testInstance.perform({ query: `query { limited }` }); await delay(300); - const result = await testInstance.execute(`query { limited }`); + const result = await testInstance.perform({ query: `query { limited }` }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.data?.limited).toBe('limited'); @@ -74,8 +74,8 @@ describe('useRateLimiter', () => { ], schemaWithDirective ); - await testInstance.execute(`query { limited }`); - const result = await testInstance.execute(`query { limited }`); + await testInstance.perform({ query: `query { limited }` }); + const result = await testInstance.perform({ query: `query { limited }` }); assertSingleExecutionValue(result); expect(result.errors!.length).toBe(1); expect(result.errors![0].message).toBe('too many calls'); @@ -86,7 +86,7 @@ describe('useRateLimiter', () => { const schema = makeExecutableSchema({ typeDefs: ` ${DIRECTIVE_SDL} - + type Query { limited: String @rateLimit( max: 1, @@ -112,8 +112,8 @@ describe('useRateLimiter', () => { ], schema ); - await testInstance.execute(`query { limited }`); - const result = await testInstance.execute(`query { limited }`); + await testInstance.perform({ query: `query { limited }` }); + const result = await testInstance.perform({ query: `query { limited }` }); assertSingleExecutionValue(result); diff --git a/packages/plugins/resource-limitations/test/use-resource-limitations.spec.ts b/packages/plugins/resource-limitations/test/use-resource-limitations.spec.ts index e9bd777487..b69dd11b2a 100644 --- a/packages/plugins/resource-limitations/test/use-resource-limitations.spec.ts +++ b/packages/plugins/resource-limitations/test/use-resource-limitations.spec.ts @@ -57,19 +57,21 @@ const schema = makeExecutableSchema({ describe('useResourceLimitations', () => { it('requires the usage of either the first or last field on fields that resolve to a Connection type.', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -79,19 +81,21 @@ describe('useResourceLimitations', () => { }); it('requires the usage of either the first or last field on fields that resolve to a Connection type (other argument provided).', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(after: "abc") { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(after: "abc") { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -101,19 +105,21 @@ describe('useResourceLimitations', () => { }); it('requires the first field to be at least 1', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(first: 0) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(first: 0) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -123,19 +129,21 @@ describe('useResourceLimitations', () => { }); it('requires the first field to be at least a custom minimum value', async () => { const testkit = createTestkit([useResourceLimitations({ paginationArgumentMinimum: 2, extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(first: 1) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(first: 1) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -145,19 +153,21 @@ describe('useResourceLimitations', () => { }); it('requires the first field to be not higher than 100', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(first: 101) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(first: 101) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -170,19 +180,21 @@ describe('useResourceLimitations', () => { [useResourceLimitations({ paginationArgumentMaximum: 99, extensions: true })], schema ); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(first: 100) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(first: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -192,19 +204,21 @@ describe('useResourceLimitations', () => { }); it('requires the last field to be at least 1', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 0) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 0) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -214,19 +228,21 @@ describe('useResourceLimitations', () => { }); it('requires the last field to be at least a custom minimum value', async () => { const testkit = createTestkit([useResourceLimitations({ paginationArgumentMinimum: 2, extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 1) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 1) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -236,19 +252,21 @@ describe('useResourceLimitations', () => { }); it('requires the last field to be not higher than 100', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 101) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 101) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -261,19 +279,21 @@ describe('useResourceLimitations', () => { [useResourceLimitations({ paginationArgumentMaximum: 99, extensions: true })], schema ); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 100) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeDefined(); expect(result.errors?.length).toEqual(1); @@ -283,19 +303,21 @@ describe('useResourceLimitations', () => { }); it('calculates node cost (single)', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 100) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -309,19 +331,21 @@ describe('useResourceLimitations', () => { [useResourceLimitations({ paginationArgumentScalars: ['ConnectionInt'], extensions: true })], schema ); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositoriesCustom(first: 100) { - edges { - node { - name + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositoriesCustom(first: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -332,18 +356,20 @@ describe('useResourceLimitations', () => { }); it('calculates node cost (nested)', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 100) { - edges { - node { - name - issues(first: 10) { - edges { - node { - title - bodyHTML + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 100) { + edges { + node { + name + issues(first: 10) { + edges { + node { + title + bodyHTML + } } } } @@ -351,8 +377,8 @@ describe('useResourceLimitations', () => { } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -363,50 +389,52 @@ describe('useResourceLimitations', () => { }); it('calculates node cost (multiple nested)', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 100) { - edges { - node { - name - issues(first: 10) { - edges { - node { - title - bodyHTML + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 100) { + edges { + node { + name + issues(first: 10) { + edges { + node { + title + bodyHTML + } } } } } } - } - more: repositories(last: 1) { - edges { - node { - name - issues(first: 2) { - edges { - node { - title - bodyHTML + more: repositories(last: 1) { + edges { + node { + name + issues(first: 2) { + edges { + node { + title + bodyHTML + } } } } } } - } - # These should not count towards the total due to invalid argument types - repositoriesCustom(first: 100) { - edges { - node { - name + # These should not count towards the total due to invalid argument types + repositoriesCustom(first: 100) { + edges { + node { + name + } } } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.errors).toBeUndefined(); expect(result.extensions).toEqual({ @@ -417,18 +445,20 @@ describe('useResourceLimitations', () => { }); it('stops execution if node cost limit is exceeded', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true, nodeCostLimit: 20 })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - repositories(last: 19) { - edges { - node { - name - issues(first: 2) { - edges { - node { - title - bodyHTML + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + repositories(last: 19) { + edges { + node { + name + issues(first: 2) { + edges { + node { + title + bodyHTML + } } } } @@ -436,8 +466,8 @@ describe('useResourceLimitations', () => { } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ resourceLimitations: { @@ -451,13 +481,15 @@ describe('useResourceLimitations', () => { }); it('minimum cost is always 1', async () => { const testkit = createTestkit([useResourceLimitations({ extensions: true, nodeCostLimit: 20 })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - viewer { - id + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + viewer { + id + } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ resourceLimitations: { diff --git a/packages/plugins/response-cache-redis/test/response-redis-cache.spec.ts b/packages/plugins/response-cache-redis/test/response-redis-cache.spec.ts index b80feb87f2..d96cec08a9 100644 --- a/packages/plugins/response-cache-redis/test/response-redis-cache.spec.ts +++ b/packages/plugins/response-cache-redis/test/response-redis-cache.spec.ts @@ -87,8 +87,8 @@ describe('useResponseCache with Redis cache', () => { } } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); }); @@ -166,24 +166,24 @@ describe('useResponseCache with Redis cache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute( - /* GraphQL */ ` + await testInstance.perform({ + query: /* GraphQL */ ` mutation test($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -262,9 +262,9 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // get from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // so queried just once expect(spy).toHaveBeenCalledTimes(1); @@ -312,11 +312,11 @@ describe('useResponseCache with Redis cache', () => { expect(await redis.exists('Comment:2')).toBeFalsy(); // query and cache since ws invalidated - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // but since we've queried once before when we started above, we've now actually queried twice expect(spy).toHaveBeenCalledTimes(2); }); @@ -396,9 +396,9 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // queried once expect(spy).toHaveBeenCalledTimes(1); @@ -419,16 +419,16 @@ describe('useResponseCache with Redis cache', () => { expect(await redis.smembers('Comment')).toHaveLength(0); // we've invalidated so, now query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // so have queried twice expect(spy).toHaveBeenCalledTimes(2); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // still just queried twice expect(spy).toHaveBeenCalledTimes(2); @@ -512,29 +512,29 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - const queryResult = await testInstance.execute(query); + const queryResult = await testInstance.perform({ query }); let cacheHitMaybe = queryResult['extensions']['responseCache']['hit']; expect(cacheHitMaybe).toBeFalsy(); // get from cache - const cachedResult = await testInstance.execute(query); + const cachedResult = await testInstance.perform({ query }); cacheHitMaybe = cachedResult['extensions']['responseCache']['hit']; expect(cacheHitMaybe).toBeTruthy(); - const mutationResult = await testInstance.execute( - /* GraphQL */ ` + const mutationResult = await testInstance.perform({ + query: /* GraphQL */ ` mutation test($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); cacheHitMaybe = mutationResult['extensions']['responseCache']['hit']; expect(cacheHitMaybe).toBeFalsy(); @@ -604,18 +604,18 @@ describe('useResponseCache with Redis cache', () => { schema ); - const result = await testInstance.execute( - /* GraphQL */ ` + const result = await testInstance.perform({ + query: /* GraphQL */ ` mutation test($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); const responseCache = result['extensions']['responseCache']; const invalidatedEntities = responseCache['invalidatedEntities']; @@ -695,9 +695,9 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache 2 users - await testInstance.execute(query, { limit: 2 }); + await testInstance.perform({ query, variables: { limit: 2 } }); // fetch 2 users from cache - await testInstance.execute(query, { limit: 2 }); + await testInstance.perform({ query, variables: { limit: 2 } }); // so just one query expect(spy).toHaveBeenCalledTimes(1); @@ -705,7 +705,7 @@ describe('useResponseCache with Redis cache', () => { expect(await redis.keys('operations:*')).toHaveLength(1); // query just one user - await testInstance.execute(query, { limit: 1 }); + await testInstance.perform({ query, variables: { limit: 1 } }); // since 2 users are in cache, we query again for the 1 as a response expect(spy).toHaveBeenCalledTimes(2); @@ -778,9 +778,9 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); // so queried just once expect(spy).toHaveBeenCalledTimes(1); @@ -788,7 +788,7 @@ describe('useResponseCache with Redis cache', () => { jest.advanceTimersByTime(150); // since the cache has expired, now when we query - await testInstance.execute(query); + await testInstance.perform({ query }); // we query again so now twice expect(spy).toHaveBeenCalledTimes(2); }); @@ -867,16 +867,14 @@ describe('useResponseCache with Redis cache', () => { } `; - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 1, } ); - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 1, } @@ -886,9 +884,8 @@ describe('useResponseCache with Redis cache', () => { // we should have one response for that sessionId of 1 expect(await redis.keys('operations:*')).toHaveLength(1); - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 2, } @@ -967,7 +964,7 @@ describe('useResponseCache with Redis cache', () => { `; // query but don't cache - await testInstance.execute(query); + await testInstance.perform({ query }); // none of the queries entities are cached because contains Comment expect(await redis.exists('User')).toBeFalsy(); @@ -976,7 +973,7 @@ describe('useResponseCache with Redis cache', () => { expect(await redis.exists('Comment:2')).toBeFalsy(); // since not cached - await testInstance.execute(query); + await testInstance.perform({ query }); // still none of the queries entities are cached because contains Comment expect(await redis.exists('User')).toBeFalsy(); @@ -1065,15 +1062,15 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); // wait so User expires jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); // now we've queried twice expect(spy).toHaveBeenCalledTimes(2); }); @@ -1156,15 +1153,15 @@ describe('useResponseCache with Redis cache', () => { `; // query and cache - await testInstance.execute(query); + await testInstance.perform({ query }); // from cache - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); // wait so User expires jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); // now we've queried twice expect(spy).toHaveBeenCalledTimes(2); }); diff --git a/packages/plugins/response-cache/test/response-cache.spec.ts b/packages/plugins/response-cache/test/response-cache.spec.ts index 5aefe450b6..c8f9717295 100644 --- a/packages/plugins/response-cache/test/response-cache.spec.ts +++ b/packages/plugins/response-cache/test/response-cache.spec.ts @@ -120,13 +120,13 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -186,8 +186,8 @@ describe('useResponseCache', () => { } } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); }); @@ -269,30 +269,30 @@ describe('useResponseCache', () => { } `; - let result = await testInstance.execute(query); + let result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: false, didCache: true, ttl: Infinity }); - result = await testInstance.execute(query); + result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: true }); expect(spy).toHaveBeenCalledTimes(1); - result = await testInstance.execute( - /* GraphQL */ ` + result = await testInstance.perform({ + query: /* GraphQL */ ` mutation it($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); assertSingleExecutionValue(result); expect(result?.extensions?.responseCache).toEqual({ invalidatedEntities: [{ id: '1', typename: 'User' }] }); - result = await testInstance.execute(query); + result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: false, didCache: true, ttl: Infinity }); expect(spy).toHaveBeenCalledTimes(2); @@ -382,30 +382,30 @@ describe('useResponseCache', () => { } `; - let result = await testInstance.execute(query); + let result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: false, didCache: true, ttl: Infinity }); - result = await testInstance.execute(query); + result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: true }); expect(spy).toHaveBeenCalledTimes(1); - result = await testInstance.execute( - /* GraphQL */ ` + result = await testInstance.perform({ + query: /* GraphQL */ ` mutation it($id: ID!) { updateUser(id: $id) { id } } `, - { + variables: { id: 1, - } - ); + }, + }); assertSingleExecutionValue(result); expect(result?.extensions?.responseCache).toEqual({ invalidatedEntities: [{ id: '1', typename: 'User' }] }); - result = await testInstance.execute(query); + result = await testInstance.perform({ query }); assertSingleExecutionValue(result); expect(result.extensions?.responseCache).toEqual({ hit: false, didCache: true, ttl: Infinity }); expect(spy).toHaveBeenCalledTimes(2); @@ -486,13 +486,13 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); cache.invalidate([{ typename: 'Comment', id: 2 }]); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -571,13 +571,13 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); cache.invalidate([{ typename: 'Comment' }]); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -647,10 +647,10 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query, { limit: 2 }); - await testInstance.execute(query, { limit: 2 }); + await testInstance.perform({ query, variables: { limit: 2 } }); + await testInstance.perform({ query, variables: { limit: 2 } }); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute(query, { limit: 1 }); + await testInstance.perform({ query, variables: { limit: 1 } }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -720,14 +720,14 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); // let's travel in time beyond the ttl of 100 jest.advanceTimersByTime(150); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -804,24 +804,21 @@ describe('useResponseCache', () => { } `; - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 1, } ); - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 1, } ); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute( - query, - {}, + await testInstance.perform( + { query }, { sessionId: 2, } @@ -893,8 +890,8 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -974,11 +971,11 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -1058,11 +1055,11 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); jest.advanceTimersByTime(201); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -1130,8 +1127,8 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -1257,7 +1254,7 @@ describe('useResponseCache', () => { } `; - let result = await testInstance.execute(userQuery); + let result = await testInstance.perform({ query: userQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1266,7 +1263,7 @@ describe('useResponseCache', () => { ttl: 200, }, }); - result = await testInstance.execute(orderQuery); + result = await testInstance.perform({ query: orderQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1277,12 +1274,12 @@ describe('useResponseCache', () => { }); jest.advanceTimersByTime(2); - await testInstance.execute(userQuery); - await testInstance.execute(orderQuery); + await testInstance.perform({ query: userQuery }); + await testInstance.perform({ query: orderQuery }); expect(userSpy).toHaveBeenCalledTimes(1); expect(orderSpy).toHaveBeenCalledTimes(2); jest.advanceTimersByTime(201); - await testInstance.execute(userQuery); + await testInstance.perform({ query: userQuery }); expect(userSpy).toHaveBeenCalledTimes(2); }); @@ -1363,7 +1360,7 @@ describe('useResponseCache', () => { } `; - let result = await testInstance.execute(userQuery); + let result = await testInstance.perform({ query: userQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1374,7 +1371,7 @@ describe('useResponseCache', () => { }); jest.advanceTimersByTime(2); - result = await testInstance.execute(userQuery); + result = await testInstance.perform({ query: userQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1384,7 +1381,7 @@ describe('useResponseCache', () => { jest.advanceTimersByTime(200); - result = await testInstance.execute(userQuery); + result = await testInstance.perform({ query: userQuery }); assertSingleExecutionValue(result); expect(result.extensions).toEqual({ responseCache: { @@ -1428,10 +1425,10 @@ describe('useResponseCache', () => { } } `; - await testInstance.execute(query); - await testInstance.execute(query); - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(4); }); @@ -1477,10 +1474,10 @@ describe('useResponseCache', () => { } } `; - await testInstance.execute(query); - await testInstance.execute(query); - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); + await testInstance.perform({ query }); + await testInstance.perform({ query }); // the resolver is only called once as all following executions hit the cache expect(spy).toHaveBeenCalledTimes(1); }); @@ -1568,12 +1565,12 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); - await testInstance.execute(query); + await testInstance.perform({ query }); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(1); - await testInstance.execute( - /* GraphQL */ ` + await testInstance.perform({ + query: /* GraphQL */ ` mutation it($id: ID!) { updateUser(id: $id) { id @@ -1581,14 +1578,14 @@ describe('useResponseCache', () => { } } `, - { + variables: { id: 1, - } - ); + }, + }); expect(errorSpy).toHaveBeenCalledTimes(1); - await testInstance.execute(query); + await testInstance.perform({ query }); expect(spy).toHaveBeenCalledTimes(2); }); @@ -1634,10 +1631,10 @@ describe('useResponseCache', () => { // after each execution the introspectionCounter should be incremented by 1 // as we never cache the introspection - await testInstance.execute(introspectionQuery); + await testInstance.perform({ query: introspectionQuery }); expect(introspectionCounter).toEqual(1); - await testInstance.execute(introspectionQuery); + await testInstance.perform({ query: introspectionQuery }); expect(introspectionCounter).toEqual(2); }); @@ -1683,11 +1680,11 @@ describe('useResponseCache', () => { schema ); - await testInstance.execute(introspectionQuery); + await testInstance.perform({ query: introspectionQuery }); // after the first execution the introspectionCounter should be incremented by 1 expect(introspectionCounter).toEqual(1); - await testInstance.execute(introspectionQuery); + await testInstance.perform({ query: introspectionQuery }); // as we now cache the introspection the resolver shall not be called for further introspections expect(introspectionCounter).toEqual(1); }); @@ -1728,11 +1725,11 @@ describe('useResponseCache', () => { } `; - await testInstance.execute(query); + await testInstance.perform({ query }); expect(usersResolverInvocationCount).toEqual(1); const testInstance2 = createTestkit([useResponseCache({ session: () => null, cache })], schema); - await testInstance2.execute(query); + await testInstance2.perform({ query }); expect(usersResolverInvocationCount).toEqual(2); }); @@ -1758,7 +1755,7 @@ describe('useResponseCache', () => { } `; - let result = await testkit.execute(document); + let result = await testkit.perform({ query: document }); expect(result).toMatchInlineSnapshot(` Object { "data": Object { @@ -1766,7 +1763,7 @@ describe('useResponseCache', () => { }, } `); - result = await testkit.execute(document); + result = await testkit.perform({ query: document }); expect(result).toMatchInlineSnapshot(` Object { "data": Object { @@ -1801,9 +1798,9 @@ describe('useResponseCache', () => { foo } `; - const result1 = await testkit.execute(operation); + const result1 = await testkit.perform({ query: operation }); assertSingleExecutionValue(result1); - const result2 = await testkit.execute(operation); + const result2 = await testkit.perform({ query: operation }); assertSingleExecutionValue(result2); // ensure the response is served from the cache expect(result1).toBe(result2); @@ -1831,18 +1828,20 @@ describe('useResponseCache', () => { }, }); const testkit = createTestkit([useResponseCache({ session: () => null })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - user { - __typename - id - friends { + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + user { __typename id + friends { + __typename + id + } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result).toEqual({ data: { @@ -1877,16 +1876,18 @@ describe('useResponseCache', () => { }, }); const testkit = createTestkit([useResponseCache({ session: () => null })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - user { - id - friends { + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + user { id + friends { + id + } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result).toEqual({ data: { @@ -1917,17 +1918,19 @@ describe('useResponseCache', () => { }, }); const testkit = createTestkit([useResponseCache({ session: () => null })], schema); - const result = await testkit.execute(/* GraphQL */ ` - query { - user { - foo: __typename - id - friends { + const result = await testkit.perform({ + query: /* GraphQL */ ` + query { + user { + foo: __typename id + friends { + id + } } } - } - `); + `, + }); assertSingleExecutionValue(result); expect(result).toEqual({ data: { @@ -1983,7 +1986,7 @@ describe('useResponseCache', () => { schema ); - let result = await testkit.execute(operation); + let result = await testkit.perform({ query: operation }); assertSingleExecutionValue(result); expect(result).toEqual({ data: { @@ -1999,13 +2002,13 @@ describe('useResponseCache', () => { }, }, }); - result = await testkit.execute(operation); + result = await testkit.perform({ query: operation }); assertSingleExecutionValue(result); expect(result.extensions?.['responseCache']).toEqual({ hit: true, }); await cache.invalidate([{ typename: 'Cat', id: '1' }]); - result = await testkit.execute(operation); + result = await testkit.perform({ query: operation }); assertSingleExecutionValue(result); expect(result.extensions?.['responseCache']).toEqual({ didCache: true, diff --git a/packages/plugins/validation-cache/test/validation-cache.spec.ts b/packages/plugins/validation-cache/test/validation-cache.spec.ts index dee90b7b9f..a3a460e4ff 100644 --- a/packages/plugins/validation-cache/test/validation-cache.spec.ts +++ b/packages/plugins/validation-cache/test/validation-cache.spec.ts @@ -30,30 +30,30 @@ describe('useValidationCache', () => { it('Should call original validate when cache is empty', async () => { const testInstance = createTestkit([useTestPlugin, useValidationCache()], testSchema); - await testInstance.execute(`query { foo }`); + await testInstance.perform({ query: `query { foo }` }); expect(testValidator).toHaveBeenCalledTimes(1); }); it('Should call validate once once when operation is cached', async () => { const testInstance = createTestkit([useTestPlugin, useValidationCache()], testSchema); - await testInstance.execute(`query { foo }`); - await testInstance.execute(`query { foo }`); - await testInstance.execute(`query { foo }`); + await testInstance.perform({ query: `query { foo }` }); + await testInstance.perform({ query: `query { foo }` }); + await testInstance.perform({ query: `query { foo }` }); expect(testValidator).toHaveBeenCalledTimes(1); }); it('Should call validate once once when operation is cached and errored', async () => { const testInstance = createTestkit([useTestPlugin, useValidationCache()], testSchema); - const r1 = await testInstance.execute(`query { foo2 }`); - const r2 = await testInstance.execute(`query { foo2 }`); + const r1 = await testInstance.perform({ query: `query { foo2 }` }); + const r2 = await testInstance.perform({ query: `query { foo2 }` }); expect(testValidator).toHaveBeenCalledTimes(1); expect(r1).toEqual(r2); }); it('Should call validate multiple times on different operations', async () => { const testInstance = createTestkit([useTestPlugin, useValidationCache()], testSchema); - await testInstance.execute(`query t { foo }`); - await testInstance.execute(`query t2 { foo }`); + await testInstance.perform({ query: `query t { foo }` }); + await testInstance.perform({ query: `query t2 { foo }` }); expect(testValidator).toHaveBeenCalledTimes(2); }); @@ -71,9 +71,9 @@ describe('useValidationCache', () => { ], testSchema ); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); await testInstance.wait(10); - await testInstance.execute(`query t { foo }`); + await testInstance.perform({ query: `query t { foo }` }); expect(testValidator).toHaveBeenCalledTimes(2); }); @@ -90,8 +90,8 @@ describe('useValidationCache', () => { ], testSchema ); - await testInstance.execute(`query { foo2 }`); - await testInstance.execute(`query { foo2 }`); + await testInstance.perform({ query: `query { foo2 }` }); + await testInstance.perform({ query: `query { foo2 }` }); expect(cache.get).toHaveBeenCalled(); expect(cache.set).toHaveBeenCalled(); }); From 7cab2a336d2867a224a95d2480b83b10a3b8efed Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Thu, 15 Sep 2022 14:21:21 +0200 Subject: [PATCH 59/61] only GraphQL errors are a part of the result --- packages/core/src/orchestrator.ts | 14 +++++++++++-- packages/core/test/perform.spec.ts | 32 +++++++++++++++++++++++++++-- packages/types/src/get-enveloped.ts | 3 ++- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/packages/core/src/orchestrator.ts b/packages/core/src/orchestrator.ts index 4ffe16618d..a12154491b 100644 --- a/packages/core/src/orchestrator.ts +++ b/packages/core/src/orchestrator.ts @@ -610,7 +610,12 @@ export function createEnvelopOrchestrator try { document = parse(params.query); } catch (err) { - return done({ errors: [err] }); + if (err instanceof Error && err.name === 'GraphQLError') { + // only graphql errors can be a part of the result + return done({ errors: [err] }); + } + // everything else bubble + throw err; } try { @@ -619,7 +624,12 @@ export function createEnvelopOrchestrator return done({ errors: validationErrors }); } } catch (err) { - return done({ errors: [err] }); + if (err instanceof Error && err.name === 'GraphQLError') { + // only graphql errors can be a part of the result + return done({ errors: [err] }); + } + // everything else bubble + throw err; } context = await contextFactory(contextExtension); diff --git a/packages/core/test/perform.spec.ts b/packages/core/test/perform.spec.ts index 9010ff163f..c3346452d7 100644 --- a/packages/core/test/perform.spec.ts +++ b/packages/core/test/perform.spec.ts @@ -70,7 +70,7 @@ describe('perform', () => { } }); - it('should include parsing errors in result', async () => { + it('should include parsing GraphQL errors in result', async () => { const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); const { perform } = getEnveloped(); @@ -87,7 +87,21 @@ describe('perform', () => { `); }); - it('should include validation errors in result', async () => { + it('should throw parsing non-GraphQL errors', async () => { + const getEnveloped = envelop({ + ...graphqlFuncs, + parse: () => { + throw new Error('Oops!'); + }, + plugins: [useSchema(schema)], + }); + + const { perform } = getEnveloped(); + + expect(perform({ query: '{' })).rejects.toBeInstanceOf(Error); + }); + + it('should include validation GraphQL errors in result', async () => { const getEnveloped = envelop({ ...graphqlFuncs, plugins: [useSchema(schema)] }); const { perform } = getEnveloped(); @@ -104,6 +118,20 @@ describe('perform', () => { `); }); + it('should throw validation non-GraphQL errors', async () => { + const getEnveloped = envelop({ + ...graphqlFuncs, + validate: () => { + throw new Error('Oops!'); + }, + plugins: [useSchema(schema)], + }); + + const { perform } = getEnveloped(); + + expect(perform({ query: '{ idontexist }' })).rejects.toBeInstanceOf(Error); + }); + it('should include thrown validation errors in result', async () => { const getEnveloped = envelop({ ...graphqlFuncs, diff --git a/packages/types/src/get-enveloped.ts b/packages/types/src/get-enveloped.ts index cd59da407f..ad90b8b97f 100644 --- a/packages/types/src/get-enveloped.ts +++ b/packages/types/src/get-enveloped.ts @@ -23,7 +23,8 @@ export type GetEnvelopedFn = { * * Returns a ready-to-use GraphQL response. * - * This function will NEVER throw GraphQL errors, it will instead place them in the result. + * This function will NEVER throw GraphQL errors, it will instead place them + * in the result. However, non-GraphQL errors WILL bubble if thrown. */ perform: ( params: PerformParams, From 9399a33376818c537930d5253ee971aac7efbe7d Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Thu, 15 Sep 2022 09:01:52 -0400 Subject: [PATCH 60/61] docs: migration guide (#1520) * docs: migration guide * remove slashes * Update website/docs/guides/migrating-from-v2-to-v3.mdx Co-authored-by: Denis Badurina * feedback * document removing of orchestrated tracer * more feedback * update docs * update examples * async schema example Co-authored-by: Denis Badurina --- examples/apollo-server/index.ts | 9 +- examples/apollo-server/package.json | 3 +- examples/azure-functions/index.ts | 9 +- examples/azure-functions/package.json | 3 +- examples/cloudflare-workers/index.ts | 9 +- examples/cloudflare-workers/package.json | 3 +- examples/express-graphql/index.ts | 9 +- examples/express-graphql/package.json | 3 +- examples/google-cloud-functions/index.ts | 9 +- examples/google-cloud-functions/package.json | 3 +- examples/graphql-helix-auth0/index.ts | 5 + examples/graphql-helix-auth0/package.json | 3 +- examples/graphql-helix-defer-stream/index.ts | 9 +- examples/graphql-helix/index.ts | 9 +- examples/graphql-helix/package.json | 3 +- examples/graphql-socket.io/index.ts | 7 +- examples/graphql-socket.io/package.json | 3 +- examples/graphql-sse/index.ts | 9 +- examples/graphql-sse/package.json | 3 +- examples/graphql-ws/index.ts | 9 +- examples/graphql-ws/package.json | 3 +- examples/lambda-aws/index.ts | 9 +- examples/lambda-aws/package.json | 3 +- examples/nexus/index.ts | 9 +- examples/simple-http/index.ts | 9 +- examples/simple-http/package.json | 3 +- examples/typegraphql/index.ts | 15 ++- examples/with-esm/package.json | 3 +- examples/with-esm/src/index.ts | 9 +- packages/core/README.md | 2 - packages/core/docs/use-async-schema.md | 23 ---- packages/core/docs/use-masked-errors.md | 4 +- ...ded-schema.md => use-schema-by-context.md} | 4 +- packages/plugins/apollo-federation/README.md | 5 + .../plugins/apollo-server-errors/README.md | 2 +- .../docs/guides/migrating-from-v2-to-v3.mdx | 118 ++++++++++++++++++ website/docs/plugins/custom-plugin.mdx | 7 ++ website/docs/plugins/lifecycle.mdx | 5 + website/routes.ts | 1 + website/src/lib/plugins.ts | 15 +-- yarn.lock | 2 +- 41 files changed, 278 insertions(+), 93 deletions(-) delete mode 100644 packages/core/docs/use-async-schema.md rename packages/core/docs/{use-lazy-loaded-schema.md => use-schema-by-context.md} (86%) create mode 100644 website/docs/guides/migrating-from-v2-to-v3.mdx diff --git a/examples/apollo-server/index.ts b/examples/apollo-server/index.ts index 5ef3d636e8..0deb297fd6 100644 --- a/examples/apollo-server/index.ts +++ b/examples/apollo-server/index.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import { ApolloServer } from 'apollo-server'; -import { envelop, useSchema, useTiming } from '@envelop/core'; +import { envelop, useSchema } from '@envelop/core'; +import { parse, validate, subscribe, execute } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'; @@ -18,7 +19,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useTiming()], + parse, + validate, + subscribe, + execute, + plugins: [useSchema(schema)], }); const server = new ApolloServer({ diff --git a/examples/apollo-server/package.json b/examples/apollo-server/package.json index 48f6af6ca7..90e82f1572 100644 --- a/examples/apollo-server/package.json +++ b/examples/apollo-server/package.json @@ -9,7 +9,8 @@ "@envelop/core": "*", "apollo-server": "3.5.0", "apollo-server-core": "3.5.0", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/azure-functions/index.ts b/examples/azure-functions/index.ts index 87ff811d9c..9b7b0ef44a 100644 --- a/examples/azure-functions/index.ts +++ b/examples/azure-functions/index.ts @@ -1,4 +1,5 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, subscribe, execute } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { AzureFunction, Context, HttpRequest } from '@azure/functions'; import { getGraphQLParameters, processRequest, Response } from 'graphql-helix'; @@ -17,7 +18,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); export const index: AzureFunction = async (context: Context, req: HttpRequest): Promise => { diff --git a/examples/azure-functions/package.json b/examples/azure-functions/package.json index db521ad1e5..89532bb99d 100644 --- a/examples/azure-functions/package.json +++ b/examples/azure-functions/package.json @@ -8,7 +8,8 @@ "dependencies": { "graphql-helix": "1.8.3", "@envelop/core": "*", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@azure/functions": "1.2.3", diff --git a/examples/cloudflare-workers/index.ts b/examples/cloudflare-workers/index.ts index 41fac195fe..bf2becd792 100644 --- a/examples/cloudflare-workers/index.ts +++ b/examples/cloudflare-workers/index.ts @@ -1,4 +1,5 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { getGraphQLParameters, processRequest, Response } from 'graphql-helix'; import { Router } from 'worktop'; @@ -20,7 +21,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); router.add('POST', '/graphql', async (req, res) => { diff --git a/examples/cloudflare-workers/package.json b/examples/cloudflare-workers/package.json index 702ff47665..607d107be4 100644 --- a/examples/cloudflare-workers/package.json +++ b/examples/cloudflare-workers/package.json @@ -9,7 +9,8 @@ "graphql-helix": "1.8.3", "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", - "worktop": "0.7.0" + "worktop": "0.7.0", + "graphql": "16.6.0" }, "devDependencies": { "@cloudflare/workers-types": "2.2.2", diff --git a/examples/express-graphql/index.ts b/examples/express-graphql/index.ts index e06d88c2df..31994634e0 100644 --- a/examples/express-graphql/index.ts +++ b/examples/express-graphql/index.ts @@ -1,4 +1,5 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import express from 'express'; import { graphqlHTTP } from 'express-graphql'; @@ -17,7 +18,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = express(); diff --git a/examples/express-graphql/package.json b/examples/express-graphql/package.json index 7c14c6c5f8..a0787a1a60 100644 --- a/examples/express-graphql/package.json +++ b/examples/express-graphql/package.json @@ -9,7 +9,8 @@ "express": "3.14.0", "express-graphql": "0.12.0", "@envelop/core": "*", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/google-cloud-functions/index.ts b/examples/google-cloud-functions/index.ts index 33fc5c7d21..fad779a9fa 100644 --- a/examples/google-cloud-functions/index.ts +++ b/examples/google-cloud-functions/index.ts @@ -1,4 +1,5 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import * as functions from 'firebase-functions'; import { getGraphQLParameters, processRequest, Response } from 'graphql-helix'; @@ -17,7 +18,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); // https://firebase.google.com/docs/functions/typescript diff --git a/examples/google-cloud-functions/package.json b/examples/google-cloud-functions/package.json index 9cc92dd45f..2affb5b9b8 100644 --- a/examples/google-cloud-functions/package.json +++ b/examples/google-cloud-functions/package.json @@ -9,7 +9,8 @@ "@envelop/core": "*", "firebase-admin": "9.9.0", "firebase-functions": "3.14.1", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "main": "lib/index.js", "devDependencies": { diff --git a/examples/graphql-helix-auth0/index.ts b/examples/graphql-helix-auth0/index.ts index 43a0b818ef..f0940177fe 100644 --- a/examples/graphql-helix-auth0/index.ts +++ b/examples/graphql-helix-auth0/index.ts @@ -2,6 +2,7 @@ import fastify from 'fastify'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; import { envelop, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { useAuth0 } from '@envelop/auth0'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -40,6 +41,10 @@ const auth0Config = { }; const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ useSchema(schema), useAuth0({ diff --git a/examples/graphql-helix-auth0/package.json b/examples/graphql-helix-auth0/package.json index fce86e90d9..ce79381df0 100644 --- a/examples/graphql-helix-auth0/package.json +++ b/examples/graphql-helix-auth0/package.json @@ -10,7 +10,8 @@ "@envelop/core": "*", "@envelop/auth0": "*", "graphql-helix": "1.8.3", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/graphql-helix-defer-stream/index.ts b/examples/graphql-helix-defer-stream/index.ts index ccb20bf76b..c071bf4557 100644 --- a/examples/graphql-helix-defer-stream/index.ts +++ b/examples/graphql-helix-defer-stream/index.ts @@ -1,7 +1,8 @@ /* eslint-disable no-console */ import fastify from 'fastify'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; +import { envelop, useLogger, useSchema } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; const sleep = (t = 1000) => new Promise(resolve => setTimeout(resolve, t)); @@ -140,7 +141,11 @@ const graphiQLContent = /* GraphQL */ ` `; const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = fastify(); diff --git a/examples/graphql-helix/index.ts b/examples/graphql-helix/index.ts index 20d936390a..d64beb126a 100644 --- a/examples/graphql-helix/index.ts +++ b/examples/graphql-helix/index.ts @@ -1,7 +1,8 @@ /* eslint-disable no-console */ import fastify from 'fastify'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ @@ -18,7 +19,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = fastify(); diff --git a/examples/graphql-helix/package.json b/examples/graphql-helix/package.json index e73837b229..b3c4d0bf35 100644 --- a/examples/graphql-helix/package.json +++ b/examples/graphql-helix/package.json @@ -9,7 +9,8 @@ "fastify": "3.14.0", "@envelop/core": "*", "graphql-helix": "1.8.3", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/graphql-socket.io/index.ts b/examples/graphql-socket.io/index.ts index a167653500..57b4e6969f 100644 --- a/examples/graphql-socket.io/index.ts +++ b/examples/graphql-socket.io/index.ts @@ -1,7 +1,8 @@ import { Server } from 'socket.io'; import * as http from 'http'; import { registerSocketIOGraphQLServer } from '@n1ru4l/socket-io-graphql-server'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ @@ -17,9 +18,7 @@ const schema = makeExecutableSchema({ }, }); -const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], -}); +const getEnveloped = envelop({ parse, validate, execute, subscribe, plugins: [useSchema(schema), useLogger()] }); const httpServer = http.createServer(); const socketServer = new Server(httpServer); diff --git a/examples/graphql-socket.io/package.json b/examples/graphql-socket.io/package.json index bcb8db5b51..2e2c4a0f48 100644 --- a/examples/graphql-socket.io/package.json +++ b/examples/graphql-socket.io/package.json @@ -13,7 +13,8 @@ "graphql-ws": "^4.4.2", "ws": "7.4.5", "socket.io": "4.1.2", - "socket.io-client": "4.1.2" + "socket.io-client": "4.1.2", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/graphql-sse/index.ts b/examples/graphql-sse/index.ts index ae9f95fde4..0a0090724e 100644 --- a/examples/graphql-sse/index.ts +++ b/examples/graphql-sse/index.ts @@ -1,5 +1,6 @@ import http from 'http'; -import { envelop, useSchema, useLogger, useTiming } from '@envelop/core'; +import { envelop, useSchema, useLogger } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { createHandler } from 'graphql-sse'; @@ -30,7 +31,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const handler = createHandler({ diff --git a/examples/graphql-sse/package.json b/examples/graphql-sse/package.json index da4933b4c3..4e86e72622 100644 --- a/examples/graphql-sse/package.json +++ b/examples/graphql-sse/package.json @@ -8,7 +8,8 @@ "dependencies": { "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", - "graphql-sse": "^1.0.1" + "graphql-sse": "^1.0.1", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/graphql-ws/index.ts b/examples/graphql-ws/index.ts index 87020b79c3..2cf47ad193 100644 --- a/examples/graphql-ws/index.ts +++ b/examples/graphql-ws/index.ts @@ -1,5 +1,6 @@ -import { envelop, useSchema, useLogger, useTiming } from '@envelop/core'; +import { envelop, useSchema, useLogger } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { parse, validate, execute, subscribe } from 'graphql'; import ws from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; @@ -30,7 +31,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); useServer( diff --git a/examples/graphql-ws/package.json b/examples/graphql-ws/package.json index 65e0ac8c2e..66f5c1d33e 100644 --- a/examples/graphql-ws/package.json +++ b/examples/graphql-ws/package.json @@ -9,7 +9,8 @@ "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", "graphql-ws": "^4.4.2", - "ws": "7.4.5" + "ws": "7.4.5", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/lambda-aws/index.ts b/examples/lambda-aws/index.ts index 4d86131d65..ae790dc82e 100644 --- a/examples/lambda-aws/index.ts +++ b/examples/lambda-aws/index.ts @@ -1,5 +1,6 @@ -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { parse, validate, execute, subscribe } from 'graphql'; import { APIGatewayProxyHandler } from 'aws-lambda'; import { getGraphQLParameters, processRequest, Response } from 'graphql-helix'; @@ -17,7 +18,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); export const lambdaHandler: APIGatewayProxyHandler = async event => { diff --git a/examples/lambda-aws/package.json b/examples/lambda-aws/package.json index 9a5781a5d6..fbbd4a80b3 100644 --- a/examples/lambda-aws/package.json +++ b/examples/lambda-aws/package.json @@ -9,7 +9,8 @@ "graphql-helix": "1.8.3", "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", - "aws-sdk": "2.918.0" + "aws-sdk": "2.918.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/aws-lambda": "8.10.76", diff --git a/examples/nexus/index.ts b/examples/nexus/index.ts index 36ffe618ff..5b68b07c2e 100644 --- a/examples/nexus/index.ts +++ b/examples/nexus/index.ts @@ -1,7 +1,8 @@ /* eslint-disable no-console */ import 'reflect-metadata'; import fastify from 'fastify'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; import { arg, enumType, intArg, interfaceType, makeSchema, objectType, queryType, stringArg, list } from 'nexus'; @@ -51,7 +52,11 @@ const schema = makeSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = fastify(); diff --git a/examples/simple-http/index.ts b/examples/simple-http/index.ts index cdeed847de..fae397fc39 100644 --- a/examples/simple-http/index.ts +++ b/examples/simple-http/index.ts @@ -1,5 +1,6 @@ import { createServer } from 'http'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; const schema = makeExecutableSchema({ @@ -16,7 +17,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const server = createServer((req, res) => { diff --git a/examples/simple-http/package.json b/examples/simple-http/package.json index 61e4919a9a..c7e6746631 100644 --- a/examples/simple-http/package.json +++ b/examples/simple-http/package.json @@ -7,7 +7,8 @@ "license": "MIT", "dependencies": { "@envelop/core": "*", - "@graphql-tools/schema": "8.5.0" + "@graphql-tools/schema": "8.5.0", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.6.1", diff --git a/examples/typegraphql/index.ts b/examples/typegraphql/index.ts index 1561ee72ed..3ca665bdb1 100644 --- a/examples/typegraphql/index.ts +++ b/examples/typegraphql/index.ts @@ -1,8 +1,9 @@ /* eslint-disable no-console */ import 'reflect-metadata'; import fastify from 'fastify'; -import { envelop, useLogger, useAsyncSchema, useTiming } from '@envelop/core'; -import { Field, ObjectType, buildSchema, ID, Resolver, Query, Arg } from 'type-graphql'; +import { envelop, useLogger, useSchema } from '@envelop/core'; +import { parse, validate, execute, subscribe } from 'graphql'; +import { Field, ObjectType, buildSchemaSync, ID, Resolver, Query, Arg } from 'type-graphql'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; @ObjectType() @@ -40,16 +41,18 @@ class RecipeResolver { } } -// You can also use `buildSchemaSync` and `useSchema` plugin const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ - useAsyncSchema( - buildSchema({ + useSchema( + buildSchemaSync({ resolvers: [RecipeResolver], }) ), useLogger(), - useTiming(), ], }); diff --git a/examples/with-esm/package.json b/examples/with-esm/package.json index 179779f7af..d6980c3849 100644 --- a/examples/with-esm/package.json +++ b/examples/with-esm/package.json @@ -10,7 +10,8 @@ "@envelop/core": "*", "@graphql-tools/schema": "8.5.0", "fastify": "3.14.0", - "graphql-helix": "1.8.3" + "graphql-helix": "1.8.3", + "graphql": "16.6.0" }, "devDependencies": { "@types/node": "15.12.2", diff --git a/examples/with-esm/src/index.ts b/examples/with-esm/src/index.ts index 699cee7e1a..b4ab0ab96a 100644 --- a/examples/with-esm/src/index.ts +++ b/examples/with-esm/src/index.ts @@ -1,8 +1,9 @@ /* eslint-disable no-console */ import fastify from 'fastify'; import { getGraphQLParameters, processRequest, renderGraphiQL, sendResult, shouldRenderGraphiQL } from 'graphql-helix'; -import { envelop, useLogger, useSchema, useTiming } from '@envelop/core'; +import { envelop, useLogger, useSchema } from '@envelop/core'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { parse, validate, execute, subscribe } from 'graphql'; const schema = makeExecutableSchema({ typeDefs: /* GraphQL */ ` @@ -18,7 +19,11 @@ const schema = makeExecutableSchema({ }); const getEnveloped = envelop({ - plugins: [useSchema(schema), useLogger(), useTiming()], + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema), useLogger()], }); const app = fastify(); diff --git a/packages/core/README.md b/packages/core/README.md index cc86e2f198..e35719ce8d 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -5,11 +5,9 @@ This is the core package for Envelop. You can find a complete documentation here ### Built-in plugins - [`useSchema`](./docs/use-schema.md) -- [`useAsyncSchema`](./docs/use-async-schema.md) - [`useLazyLoadedSchema`](./docs/use-lazy-loaded-schema.md) - [`useErrorHandler`](./docs/use-error-handler.md) - [`useExtendContext`](./docs/use-extend-context.md) - [`useLogger`](./docs/use-logger.md) - [`useMaskedErrors`](./docs/use-masked-errors.md) - [`usePayloadFormatter`](./docs/use-payload-formatter.md) -- [`useTiming`](./docs/use-timing.md) diff --git a/packages/core/docs/use-async-schema.md b/packages/core/docs/use-async-schema.md deleted file mode 100644 index e44b847de0..0000000000 --- a/packages/core/docs/use-async-schema.md +++ /dev/null @@ -1,23 +0,0 @@ -#### `useAsyncSchema` - -This plugin is the simplest plugin for specifying your GraphQL schema. You can specify a schema created from any tool that emits `Promise` object. - -```ts -import { envelop, useAsyncSchema } from '@envelop/core' -import { parse, validate, execute, subscribe } from 'graphql' - -const getSchema = async (): Promise => { - // return schema when it's ready -} - -const getEnveloped = envelop({ - parse, - validate, - execute, - subscribe, - plugins: [ - useAsyncSchema(getSchema()) - // ... other plugins ... - ] -}) -``` diff --git a/packages/core/docs/use-masked-errors.md b/packages/core/docs/use-masked-errors.md index 979a62dd76..0bb09e383a 100644 --- a/packages/core/docs/use-masked-errors.md +++ b/packages/core/docs/use-masked-errors.md @@ -48,9 +48,9 @@ const getEnveloped = envelop({ Or provide a custom formatter when masking the output: ```ts -import { isGraphQLError, MaskErrorFn } from '@envelop/core' +import { isGraphQLError, MaskError } from '@envelop/core' -export const customFormatError: MaskErrorFn = err => { +export const customFormatError: MaskError = err => { if (isGraphQLError(err)) { return new GraphQLError('Sorry, something went wrong.') } diff --git a/packages/core/docs/use-lazy-loaded-schema.md b/packages/core/docs/use-schema-by-context.md similarity index 86% rename from packages/core/docs/use-lazy-loaded-schema.md rename to packages/core/docs/use-schema-by-context.md index e363fc8205..cff850017d 100644 --- a/packages/core/docs/use-lazy-loaded-schema.md +++ b/packages/core/docs/use-schema-by-context.md @@ -3,7 +3,7 @@ This plugin is the simplest plugin for specifying your GraphQL schema. You can specify a schema created from any tool that emits `GraphQLSchema` object, and you can choose to load the schema based on the initial context (or the incoming request). ```ts -import { envelop, useLazyLoadedSchema } from '@envelop/core' +import { envelop, useSchemaByContext } from '@envelop/core' import { parse, validate, execute, subscribe } from 'graphql' async function getSchema({ req }): GraphQLSchema { @@ -20,7 +20,7 @@ const getEnveloped = envelop({ execute, subscribe, plugins: [ - useLazyLoadedSchema(getSchema) + useSchemaByContext(getSchema) // ... other plugins ... ] }) diff --git a/packages/plugins/apollo-federation/README.md b/packages/plugins/apollo-federation/README.md index a5efe100bb..45a015c36b 100644 --- a/packages/plugins/apollo-federation/README.md +++ b/packages/plugins/apollo-federation/README.md @@ -12,6 +12,7 @@ yarn add @envelop/apollo-federation ```ts import { envelop } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' import { ApolloGateway } from '@apollo/gateway' import { useApolloFederation } from '@envelop/apollo-federation' @@ -29,6 +30,10 @@ await gateway.load() // Then pass it to the plugin configuration const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ // ... other plugins ... useApolloFederation({ gateway }) diff --git a/packages/plugins/apollo-server-errors/README.md b/packages/plugins/apollo-server-errors/README.md index a598ac3c32..e82b24f7a0 100644 --- a/packages/plugins/apollo-server-errors/README.md +++ b/packages/plugins/apollo-server-errors/README.md @@ -11,7 +11,7 @@ yarn add @envelop/apollo-server-errors ## Usage Example ```ts -import { parse, validate, execute, subscribe } from 'grapqhl' +import { parse, validate, execute, subscribe } from 'graphql' import { envelop } from '@envelop/core' import { useApolloServerErrors } from '@envelop/apollo-server-errors' diff --git a/website/docs/guides/migrating-from-v2-to-v3.mdx b/website/docs/guides/migrating-from-v2-to-v3.mdx new file mode 100644 index 0000000000..9e43467453 --- /dev/null +++ b/website/docs/guides/migrating-from-v2-to-v3.mdx @@ -0,0 +1,118 @@ +# Migrating from `v2` to `v3` + +With [new major version](https://github.com/n1ru4l/envelop/pull/1487) comes breaking changes. This section will guide you through the process of migrating from `v2` to `v3`. + +### 1. Remove `graphql` as a peer dependency + +We have designed the new `envelop` to be engine agnostic. This allows you to use any GraphQL engine you want. This means that `graphql` is no longer a peer dependency and `envelop` simply just wraps the `parse`, `validate`, `execute` and `subscribe` functions that you provide. + +```diff +import { envelop } from '@envelop/core'; ++ import { parse, validate, execute, subscribe } from 'graphql'; + +- const getEnveloped = envelop([ ... ]) ++ const getEnveloped = envelop({ parse, validate, execute, subscribe, plugins: [ ... ] }) +``` + +### 2. Removed orchestrator tracing + +We were wrapping the `GraphQLSchema` object but with the new version we no longer want to depend on a specific implementation. + +#### 1. Remove `onResolverCalled` + +We decided to drop this and instead [provide a new plugin](https://github.com/n1ru4l/envelop/pull/1500) that will let you hook into this phase. + +```diff +import { parse, validate, execute, subscribe } from 'graphql' +import { envelop, Plugin } from '@envelop/core' ++ import { useOnResolve } from '@envelop/on-resolve' + +import { onResolverCalled } from './my-resolver' + +function useResolve(): Plugin { + return { +- onResolverCalled: onResolverCalled, ++ onPluginInit: ({ addPlugin }) => { ++ addPlugin(useOnResolve(onResolverCalled)) ++ }, + } +} + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [ + // ... other plugins ... + useResolve(), + ], +}); +``` + +#### 2. Drop `useTiming` plugin + +This plugin dependended on tracing the schema so we no longer support this out of the box. At this moment we do not have any alternative. We recommned using more advanced tracing solution. If you want to use this feel free to send in a pull request for new plugin! + +#### 3. Remove `EnvelopError` + +To keep the core agnostic from a specific implementation we no longer provide the `EnvelopError` class. To ensure an error is `GraphQLError` envelop check if it an `instanceOf Error` and the name of error is `GraphQLError`. We provide a helper utility `isGraphQLError` to check if an error is a `GraphQLError`. + +### 3. Remove `useAsyncSchema` plugin + +This was a mistake from beginning as we cannot asynchronously `validate` and `parse` since with [`graphql`](https://github.com/graphql/graphql-js) these functions are synchronous in nature. + +You should first load your schema and then create the envelop instance and pass the schema to it. + +```ts +import { envelop, useSchema } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' + +// this assume you are running latest node js version where top-level await is supported +const schema = await loadSchema() + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [useSchema(schema)] +}) +``` + +### 4. Rename `useLazyLoadedSchema` to `useSchemaByContext` + +Oringinal name was very misleading since lazy loading could mean it can be asynchronous in nature. This plugin was renamed to better reflect its purpose. It is now called `useSchemaByContext` + +### 5. Remove `enableIf` utility + +This utility was used to enable plugins conditionally. For a better developer experience we have dropped this utility and favor more type safe way to conditionally enable plugins. + +```diff +- import { envelop, useMaskedErrors, enableIf } from '@envelop/core' ++ import { envelop, useMaskedErrors } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' + +const isProd = process.env.NODE_ENV === 'production' + +const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, + plugins: [ + // This plugin is enabled only in production +- enableIf(isProd, useMaskedErrors()) ++ isProd && useMaskedErrors() + ] +}) +``` + +### 6. Update options for `useMaskedErrors` plugin + +- We _removed_ `handleValidationErrors` and `handleParseErrors` options since ONLY masking validation errors OR ONLY disabling introspection errors does not make sense, as both can be abused for reverse-engineering the GraphQL schema (see https://github.com/nikitastupin/clairvoyance for reverse-engineering the schema based on validation error suggestions). Instead you should use `useErrorHandler` plugin where you can access each phase and decide what to do with the error. +- Renamed `formatError` to `maskError` + +### 7. Drop support for Node.js v12 + +Node.js v12 is no longer supported by the Node.js team. https://github.com/nodejs/Release/#end-of-life-releases diff --git a/website/docs/plugins/custom-plugin.mdx b/website/docs/plugins/custom-plugin.mdx index f7f17a9985..58288be6eb 100644 --- a/website/docs/plugins/custom-plugin.mdx +++ b/website/docs/plugins/custom-plugin.mdx @@ -56,6 +56,9 @@ const getEnveloped = envelop({ Often plugins require additional configurartion. A common pattern for doing this is by creating a factor function that returns a `Plugin`. ```ts +import { envelop } from '@envelop/core' +import { parse, validate, subscribe, execute } from 'graphql' + const myPlugin = (shouldPrintResult: boolean): Plugin => { return { onExecute({ args }) { @@ -73,6 +76,10 @@ const myPlugin = (shouldPrintResult: boolean): Plugin => { } const getEnveloped = envelop({ + parse, + validate, + subscribe, + execute, plugins: [ /// ... other plugins ..., myPlugin(true) diff --git a/website/docs/plugins/lifecycle.mdx b/website/docs/plugins/lifecycle.mdx index ac4598b227..a9fce2f09f 100644 --- a/website/docs/plugins/lifecycle.mdx +++ b/website/docs/plugins/lifecycle.mdx @@ -38,8 +38,13 @@ In most cases, you'll pass the incoming HTTP request (or, just the relevant part ```ts import { envelop } from '@envelop/core' +import { parse, validate, execute, subscribe } from 'graphql' const getEnveloped = envelop({ + parse, + validate, + execute, + subscribe, plugins: [ /* ... plugins ... */ ] diff --git a/website/routes.ts b/website/routes.ts index 8c0c7add3e..31a82ebe68 100644 --- a/website/routes.ts +++ b/website/routes.ts @@ -25,6 +25,7 @@ export function getRoutes(): IRoutes { 'docs/guides': { $name: 'Guides', $routes: [ + ['migrating-from-v2-to-v3', 'Migrating from `v2` to `v3`'], ['securing-your-graphql-api', 'Securing Your GraphQL API'], ['adding-authentication-with-auth0', 'Authentication with Auth0'], ['monitoring-and-tracing', 'Monitoring and Tracing'], diff --git a/website/src/lib/plugins.ts b/website/src/lib/plugins.ts index 846da7f5c7..de1d2ae141 100644 --- a/website/src/lib/plugins.ts +++ b/website/src/lib/plugins.ts @@ -47,11 +47,11 @@ export const pluginsArr: Package[] = [ tags: ['core', 'schema'], }, { - identifier: 'use-lazy-loaded-schema', + identifier: 'use-schema-by-context', title: 'useSchemaByContext', githubReadme: { repo: 'n1ru4l/envelop', - path: 'packages/core/docs/use-lazy-loaded-schema.md', + path: 'packages/core/docs/use-schema-by-context.md', }, npmPackage: '@envelop/core', iconUrl: '/logo.png', @@ -123,17 +123,6 @@ export const pluginsArr: Package[] = [ iconUrl: '/logo.png', tags: ['core', 'utilities'], }, - { - identifier: 'use-timing', - title: 'useTiming', - githubReadme: { - repo: 'n1ru4l/envelop', - path: 'packages/core/docs/use-timing.md', - }, - npmPackage: '@envelop/core', - iconUrl: '/logo.png', - tags: ['core', 'tracing', 'utilities'], - }, { identifier: 'use-graphql-jit', title: 'useGraphQLJit', diff --git a/yarn.lock b/yarn.lock index 0e595f92c1..cfd1585d5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8524,7 +8524,7 @@ graphql-ws@^4.4.2: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-4.4.2.tgz#f2d83f1863ba3069117199311d664fd28f4aaa8e" integrity sha512-Cz+t1w+8+tiHIKzcz45tMwrxJpPSQ7KNjQrfN8ADAELECkkBB7aSvAgpShWz0Tne8teH/UxzJsULObLVq5eMvQ== -graphql@15.5.1, graphql@16.3.0, graphql@^14.5.3: +graphql@15.5.1, graphql@16.3.0, graphql@16.6.0, graphql@^14.5.3: version "16.3.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.3.0.tgz#a91e24d10babf9e60c706919bb182b53ccdffc05" integrity sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A== From 172017613e3b4e16a5a49a9bbb134b3da77f3dbb Mon Sep 17 00:00:00 2001 From: Saihajpreet Singh Date: Thu, 15 Sep 2022 09:15:35 -0400 Subject: [PATCH 61/61] Add redirects --- website/next.config.mjs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/website/next.config.mjs b/website/next.config.mjs index 39adc44ed9..8d0c3d7fe3 100644 --- a/website/next.config.mjs +++ b/website/next.config.mjs @@ -51,5 +51,20 @@ export default withGuildDocs({ destination: '/plugins/graphql-armor-max-depth', permanent: true, }, + { + source: '/plugins/use-async-schema', + destination: '/docs/guides/migrating-from-v2-to-v3#3-remove-useasyncschema-plugin', + permanent: true, + }, + { + source: '/plugins/use-timing', + destination: '/docs/guides/migrating-from-v2-to-v3#2-drop-usetiming-plugin', + permanent: true, + }, + { + source: '/plugins/use-lazy-loaded-schema', + destination: '/docs/guides/migrating-from-v2-to-v3#4-rename-uselazyloadedschema-to-useschemabycontext', + permanent: true, + }, ], });