Skip to content

Commit

Permalink
Improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Jan 2, 2024
1 parent 974df8a commit b798b3b
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 46 deletions.
5 changes: 5 additions & 0 deletions .changeset/fast-mice-listen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/executor-http': patch
---

Memoize the print result automatically, and able to accept a custom print function
5 changes: 5 additions & 0 deletions .changeset/modern-moles-remain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphql-tools/executor-envelop': patch
---

Skip validation if the schema is not provided
6 changes: 6 additions & 0 deletions .changeset/silver-eggs-deliver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-tools/executor-envelop': patch
'@graphql-tools/executor-yoga': patch
---

Move schema introspection logic to Envelop
1 change: 1 addition & 0 deletions packages/executors/envelop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
},
"dependencies": {
"@graphql-tools/utils": "^10.0.0",
"@graphql-tools/wrap": "^10.0.0",
"tslib": "^2.3.1"
},
"devDependencies": {
Expand Down
122 changes: 118 additions & 4 deletions packages/executors/envelop/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,29 @@
import { ExecutionArgs, Plugin } from '@envelop/core';
import { Executor } from '@graphql-tools/utils';
import { Executor, isPromise, MaybePromise } from '@graphql-tools/utils';
import { schemaFromExecutor } from '@graphql-tools/wrap';

export function useExecutor(executor: Executor): Plugin {
type GraphQLSchema = any;

export interface ExecutorPluginContext {
schema$?: MaybePromise<GraphQLSchema>;
schema?: GraphQLSchema;
schemaSetPromise$?: PromiseLike<void>;
skipIntrospection: boolean;
}

export type ExecutorPluginOpts = Parameters<typeof schemaFromExecutor>[2] & {
polling?: number;
};

export function useExecutor(
executor: Executor,
opts?: ExecutorPluginOpts,
): Plugin & {
invalidateSupergraph: () => void;
pluginCtx: ExecutorPluginContext;
ensureSchema(ctx?: any): void;
} {
const EMPTY_ARRAY = Object.freeze([]);
function executorToExecuteFn(executionArgs: ExecutionArgs) {
return executor({
document: executionArgs.document,
Expand All @@ -11,12 +33,104 @@ export function useExecutor(executor: Executor): Plugin {
operationName: executionArgs.operationName,
});
}
const pluginCtx: ExecutorPluginContext = {
schema$: undefined,
schema: undefined,
schemaSetPromise$: undefined,
skipIntrospection: false,
};
if (opts?.polling) {
setInterval(() => {
pluginCtx.schema$ = undefined;
pluginCtx.schema = undefined;
}, opts.polling);
}
function ensureSchema(ctx?: any) {
if (pluginCtx.skipIntrospection) {
return;
}
try {
if (!pluginCtx.schema && !pluginCtx.schemaSetPromise$) {
pluginCtx.schema$ ||= schemaFromExecutor(executor, ctx, opts);
if (isPromise(pluginCtx.schema$)) {
pluginCtx.schemaSetPromise$ = (
pluginCtx.schema$.then(newSchema => {
pluginCtx.schema = newSchema;
}) as Promise<void>
).catch?.(err => {
console.warn(
`Introspection failed, skipping introspection due to the following errors;\n`,
err,
);
pluginCtx.skipIntrospection = true;
});
} else {
pluginCtx.schema = pluginCtx.schema$;
}
}
} catch (err) {
pluginCtx.skipIntrospection = true;
console.warn(
`Introspection failed, skipping introspection due to the following errors;\n`,
err,
);
}
}
return {
onExecute({ setExecuteFn }) {
onEnveloped({ context, setSchema }) {
ensureSchema(context);
if (pluginCtx.schema) {
setSchema(pluginCtx.schema);
}
},
onContextBuilding() {
ensureSchema();
if (pluginCtx.schemaSetPromise$) {
return pluginCtx.schemaSetPromise$ as Promise<void>;
}
},
onExecute({ args, setExecuteFn }) {
if (args.schema) {
pluginCtx.schema = args.schema;
pluginCtx.schema$ = pluginCtx.schema;
}
ensureSchema(args.contextValue);
if (isPromise(pluginCtx.schemaSetPromise$)) {
return pluginCtx.schemaSetPromise$.then(() => {
setExecuteFn(executorToExecuteFn);
}) as Promise<void>;
}
setExecuteFn(executorToExecuteFn);
},
onSubscribe({ setSubscribeFn }) {
onSubscribe({ args, setSubscribeFn }) {
if (args.schema) {
pluginCtx.schema = args.schema;
pluginCtx.schema$ = pluginCtx.schema;
}
ensureSchema(args.contextValue);
if (isPromise(pluginCtx.schemaSetPromise$)) {
return pluginCtx.schemaSetPromise$.then(() => {
setSubscribeFn(executorToExecuteFn);
}) as Promise<void>;
}
setSubscribeFn(executorToExecuteFn);
},
onValidate({ params, context, setResult }) {
if (params.schema) {
pluginCtx.schema = params.schema;
pluginCtx.schema$ = pluginCtx.schema;
}
ensureSchema(context);
if (pluginCtx.schema?.[Symbol.toStringTag] !== 'GraphQLSchema') {
setResult(EMPTY_ARRAY);
}
},
pluginCtx,
ensureSchema,
invalidateSupergraph() {
pluginCtx.schema$ = undefined;
pluginCtx.schema = undefined;
pluginCtx.skipIntrospection = false;
},
};
}
31 changes: 30 additions & 1 deletion packages/executors/envelop/tests/envelop.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ describe('Envelop', () => {
hello: 'Hello World!',
},
});
expect(executor).toBeCalledWith({
expect(executor).toHaveBeenCalledWith({
document,
context,
});
Expand Down Expand Up @@ -88,4 +88,33 @@ describe('Envelop', () => {
context,
});
});
it('should skip validation if schema is not provided', async () => {
const executor: Executor = jest.fn().mockImplementation(() => {
return {
data: {
hello: 'Hello World!',
},
};
});
const getEnveloped = envelop({
plugins: [useExecutor(executor)],
});
const context = {};
const { validate, execute } = getEnveloped(context);
const validationResult = validate({}, document);
expect(validationResult).toHaveLength(0);
const result = await execute({
schema: {},
document,
});
expect(result).toEqual({
data: {
hello: 'Hello World!',
},
});
expect(executor).toHaveBeenCalledWith({
document,
context,
});
});
});
12 changes: 12 additions & 0 deletions packages/executors/http/src/defaultPrintFn.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { DocumentNode, print } from 'graphql';

const printCache = new WeakMap<DocumentNode, string>();

export function defaultPrintFn(document: DocumentNode) {
let printed = printCache.get(document);
if (!printed) {
printed = print(document);
printCache.set(document, printed);
}
return printed;
}
10 changes: 8 additions & 2 deletions packages/executors/http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GraphQLResolveInfo, print } from 'graphql';
import { DocumentNode, GraphQLResolveInfo } from 'graphql';
import { ValueOrPromise } from 'value-or-promise';
import {
AsyncExecutor,
Expand All @@ -11,6 +11,7 @@ import {
} from '@graphql-tools/utils';
import { fetch as defaultFetch } from '@whatwg-node/fetch';
import { createFormDataFromVariables } from './createFormDataFromVariables.js';
import { defaultPrintFn } from './defaultPrintFn.js';
import { handleEventStreamResponse } from './handleEventStreamResponse.js';
import { handleMultipartMixedResponse } from './handleMultipartMixedResponse.js';
import { isLiveQueryOperationDefinitionNode } from './isLiveQueryOperationDefinitionNode.js';
Expand Down Expand Up @@ -79,6 +80,10 @@ export interface HTTPExecutorOptions {
* @see https://developer.mozilla.org/en-US/docs/Web/API/FormData
*/
FormData?: typeof FormData;
/**
* Print function for DocumentNode
*/
print?: (doc: DocumentNode) => string;
}

export type HeadersConfig = Record<string, string>;
Expand All @@ -102,6 +107,7 @@ export function buildHTTPExecutor(
export function buildHTTPExecutor(
options?: HTTPExecutorOptions,
): Executor<any, HTTPExecutorOptions> {
const printFn = options?.print ?? defaultPrintFn;
const executor = (request: ExecutionRequest<any, any, any, HTTPExecutorOptions>) => {
const fetchFn = request.extensions?.fetch ?? options?.fetch ?? defaultFetch;
let controller: AbortController | undefined;
Expand Down Expand Up @@ -141,7 +147,7 @@ export function buildHTTPExecutor(
request.extensions = restExtensions;
}

const query = print(request.document);
const query = printFn(request.document);

let timeoutId: any;
if (options?.timeout) {
Expand Down
1 change: 0 additions & 1 deletion packages/executors/yoga/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
"dependencies": {
"@graphql-tools/executor-envelop": "^2.0.2",
"@graphql-tools/utils": "^10.0.1",
"@graphql-tools/wrap": "^10.0.0",
"tslib": "^2.3.1"
},
"devDependencies": {
Expand Down
51 changes: 13 additions & 38 deletions packages/executors/yoga/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,32 @@
import { GraphQLSchema } from 'graphql';
import { Plugin } from 'graphql-yoga';
import { useExecutor as useEnvelopExecutor } from '@graphql-tools/executor-envelop';
import { Executor, isPromise, MaybePromise } from '@graphql-tools/utils';
import { schemaFromExecutor } from '@graphql-tools/wrap';

type Opts = Parameters<typeof schemaFromExecutor>[2] & {
polling?: number;
};
import type { Plugin } from 'graphql-yoga';
import {
ExecutorPluginOpts,
useExecutor as useEnvelopExecutor,
} from '@graphql-tools/executor-envelop';
import { Executor } from '@graphql-tools/utils';

export function useExecutor(
executor: Executor,
opts?: Opts,
opts?: ExecutorPluginOpts,
): Plugin & { invalidateSupergraph: () => void } {
let schema$: MaybePromise<GraphQLSchema> | undefined;
let schema: GraphQLSchema | undefined;
if (opts?.polling) {
setInterval(() => {
schema$ = undefined;
schema = undefined;
}, opts.polling);
}
const envelopPlugin = useEnvelopExecutor(executor, opts);
return {
onPluginInit({ addPlugin }) {
addPlugin(
// @ts-expect-error TODO: fix typings
useEnvelopExecutor(executor),
envelopPlugin,
);
},
onRequestParse({ serverContext }) {
return {
onRequestParseDone() {
if (!schema$) {
schema$ ||= schemaFromExecutor(executor, serverContext, opts);
if (isPromise(schema$)) {
return schema$.then(newSchema => {
schema = newSchema;
}) as Promise<void>;
}
envelopPlugin.ensureSchema(serverContext);
if (envelopPlugin.pluginCtx.schemaSetPromise$) {
return envelopPlugin.pluginCtx.schemaSetPromise$ as Promise<void>;
}
},
};
},
onEnveloped({ setSchema }) {
if (!schema) {
throw new Error(
`You provide a promise of a schema but it hasn't been resolved yet. Make sure you use this plugin with GraphQL Yoga.`,
);
}
setSchema(schema);
},
invalidateSupergraph() {
schema$ = undefined;
schema = undefined;
},
invalidateSupergraph: envelopPlugin.invalidateSupergraph,
};
}

0 comments on commit b798b3b

Please sign in to comment.