Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions packages/supermassive/src/__tests__/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -677,4 +677,126 @@ describe("executeWithoutSchema - regression tests", () => {
const events = await drainExecution(result);
expect(events).toMatchSnapshot();
});

interface MissingInputTypeTestCase {
name: string;
document: string;
vars?: Record<string, unknown>;
partialSchema: string;
fullSchema: string;
}

const partialDownloadFileSchema = `
type Mutation {
downloadFile(input: FileCoordinates!): DownloadFileResult
}

type DownloadFileResult {
downloadUrl: String!
}`;

const fullDownloadFileSchema = `
type Mutation {
downloadFile(input: FileCoordinates!): DownloadFileResult
}

input FileCoordinates {
name: String!
options: FileDownloadOptions
}

input FileDownloadOptions {
compression: CompressionMode
}

enum CompressionMode {
NONE
RAR
ZIP
}

type DownloadFileResult {
downloadUrl: String!
}`;

const missingInputTypeCases: MissingInputTypeTestCase[] = [
{
name: "missing input variable type",
document: `mutation DownloadFile($input: FileCoordinates!) {
downloadFile(input: $input) { downloadUrl }
}`,
vars: { input: { name: "test-file" } },
partialSchema: partialDownloadFileSchema,
fullSchema: fullDownloadFileSchema,
},
{
name: "missing nested input variable type",
document: `mutation DownloadFile($input: FileCoordinates!) {
downloadFile(input: $input) { downloadUrl }
}`,
vars: { input: { name: "test-file", options: { compression: "NONE" } } },
partialSchema: `
type Mutation {
downloadFile(input: FileCoordinates!): DownloadFileResult
}

input FileCoordinates {
name: String!
options: FileDownloadOptions
}

type DownloadFileResult {
downloadUrl: String!
}`,
fullSchema: fullDownloadFileSchema,
},
{
name: "missing inline argument type",
document: `mutation DownloadFile {
downloadFile(input: {name: "bar-file", options:{ compression: "RAR" }}) { downloadUrl }
}`,
partialSchema: partialDownloadFileSchema,
fullSchema: fullDownloadFileSchema,
},
];

test.each(missingInputTypeCases)(
"$name",
async ({
document,
vars,
fullSchema,
partialSchema,
}: MissingInputTypeTestCase) => {
expect.assertions(1);

const fullDefinitions = encodeASTSchema(parse(fullSchema))[0];
const partialDefinitions = encodeASTSchema(parse(partialSchema))[0];

const schemaFragment = {
schemaId: "test",
definitions: partialDefinitions,
resolvers: {
Mutation: {
downloadFile: () => {
return { downloadUrl: "hi" };
},
},
},
};

const parsedDocument = parse(document);
const result = await executeWithoutSchema({
document: parsedDocument,
variableValues: vars,
schemaFragment,
schemaFragmentLoader: (currentFragment, _context, req) => {
currentFragment.definitions = fullDefinitions;
return Promise.resolve({ mergedFragment: currentFragment });
},
});

expect(result).toEqual({ data: { downloadFile: { downloadUrl: "hi" } } });
},
);
});
101 changes: 58 additions & 43 deletions packages/supermassive/src/executeWithoutSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import type {
IncrementalExecutionResult,
SchemaFragment,
SchemaFragmentLoader,
SchemaFragmentRequest,
} from "./types";
import {
getArgumentValues,
Expand All @@ -52,6 +51,7 @@ import type { ExecutionHooks } from "./hooks/types";
import { arraysAreEqual } from "./utilities/array";
import { isAsyncIterable } from "./jsutils/isAsyncIterable";
import { mapAsyncIterator } from "./utilities/mapAsyncIterator";
import { requestSchemaFragment } from "./utilities/requestSchemaFragment";
import { GraphQLStreamDirective } from "./schema/directives";
import { memoize3 } from "./jsutils/memoize3";
import {
Expand Down Expand Up @@ -140,6 +140,13 @@ export function executeWithoutSchema(
// If a valid execution context cannot be created due to incorrect arguments,
// a "Response" with only errors is returned.
const exeContext = buildExecutionContext(args);
if (isPromise(exeContext)) {
return exeContext.then((ctx) =>
"schemaFragment" in ctx
? executeOperationWithBeforeHook(ctx)
: { errors: ctx },
);
}

// Return early errors if execution context failed.
if (!("schemaFragment" in exeContext)) {
Expand Down Expand Up @@ -178,13 +185,13 @@ export function assertValidExecutionArguments(
*/
function buildExecutionContext(
args: ExecutionWithoutSchemaArgs,
): Array<GraphQLError> | ExecutionContext {
): PromiseOrValue<Array<GraphQLError> | ExecutionContext> {
const {
schemaFragment,
schemaFragmentLoader,
document,
rootValue,
contextValue,
contextValue: initialContextValue,
buildContextValue,
variableValues,
operationName,
Expand Down Expand Up @@ -232,29 +239,25 @@ function buildExecutionContext(

// istanbul ignore next (See: 'https://github.com/graphql/graphql-js/issues/2203')
const variableDefinitions = operation.variableDefinitions ?? [];
const contextValue = buildContextValue
? buildContextValue(initialContextValue)
: initialContextValue;

const coercedVariableValues = getVariableValues(
schemaFragment,
{ contextValue, schemaFragment, schemaFragmentLoader },
variableDefinitions,
variableValues ?? {},
{ maxErrors: 50 },
);

if (coercedVariableValues.errors) {
return coercedVariableValues.errors;
}

return {
const exeContext: Omit<ExecutionContext, "variableValues"> = {
schemaFragment,
schemaFragmentLoader,
fragments,
rootValue,
contextValue: buildContextValue
? buildContextValue(contextValue)
: contextValue,
contextValue,
buildContextValue,
operation,
variableValues: coercedVariableValues.coerced,
fieldResolver: fieldResolver ?? defaultFieldResolver,
typeResolver: typeResolver ?? defaultTypeResolver,
subscribeFieldResolver: subscribeFieldResolver ?? defaultFieldResolver,
Expand All @@ -263,6 +266,28 @@ function buildExecutionContext(
subsequentPayloads: new Set(),
enablePerEventContext: enablePerEventContext ?? true,
};

if (isPromise(coercedVariableValues)) {
return coercedVariableValues.then((r) => {
if (r.errors) {
return r.errors;
}

return {
...exeContext,
variableValues: r.coerced,
};
});
}

if (coercedVariableValues.errors) {
return coercedVariableValues.errors;
}

return {
...exeContext,
variableValues: coercedVariableValues.coerced,
};
}

function buildPerEventExecutionContext(
Expand Down Expand Up @@ -609,32 +634,6 @@ function executeField(
});
}

function requestSchemaFragment(
exeContext: ExecutionContext,
request: SchemaFragmentRequest,
): PromiseOrValue<void> {
if (!exeContext.schemaFragmentLoader) {
return;
}
const currentSchemaId = exeContext.schemaFragment.schemaId;
return exeContext
.schemaFragmentLoader(
exeContext.schemaFragment,
exeContext.contextValue,
request,
)
.then(({ mergedFragment, mergedContextValue }) => {
if (currentSchemaId !== mergedFragment.schemaId) {
throw new Error(
`Cannot use new schema fragment: old and new fragments describe different schemas:` +
` ${currentSchemaId} vs. ${mergedFragment.schemaId}`,
);
}
exeContext.contextValue = mergedContextValue ?? exeContext.contextValue;
exeContext.schemaFragment = mergedFragment;
});
}

/**
* Implements the "CreateSourceEventStream" algorithm described in the
* GraphQL specification, resolving the subscription source event stream.
Expand Down Expand Up @@ -822,15 +821,23 @@ function runSubscriptionResolver(
result = hookContext.then((context) => {
hookContext = context;

return resolveFn(rootValue, args, contextValue, info);
return isPromise(args)
? args.then((resolvedArgs) =>
resolveFn(rootValue, resolvedArgs, contextValue, info),
)
: resolveFn(rootValue, args, contextValue, info);
});
}
}

// Call the `subscribe()` resolver or the default resolver to produce an
// AsyncIterable yielding raw payloads.
if (result === undefined) {
result = resolveFn(rootValue, args, contextValue, info);
result = isPromise(args)
? args.then((resolvedArgs) =>
resolveFn(rootValue, resolvedArgs, contextValue, info),
)
: resolveFn(rootValue, args, contextValue, info);
}

if (isPromise(result)) {
Expand Down Expand Up @@ -1150,10 +1157,18 @@ function resolveAndCompleteField(
return null;
}

return resolveFn(source, args, contextValue, info);
return isPromise(args)
? args.then((resolvedArgs) =>
resolveFn(source, resolvedArgs, contextValue, info),
)
: resolveFn(source, args, contextValue, info);
});
} else {
result = resolveFn(source, args, contextValue, info);
result = isPromise(args)
? args.then((resolvedArgs) =>
resolveFn(source, resolvedArgs, contextValue, info),
)
: resolveFn(source, args, contextValue, info);
}

let completed;
Expand Down
7 changes: 6 additions & 1 deletion packages/supermassive/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,10 @@ export type SchemaFragment = {
operationTypes?: OperationTypes;
};

export type SchemaFragmentForInputTypeRequest = {
kind: "InputType";
typeName: string;
};
export type SchemaFragmentForReturnTypeRequest = {
kind: "ReturnType";
parentTypeName: TypeName;
Expand All @@ -290,7 +294,8 @@ export type SchemaFragmentForRuntimeTypeRequest = {

export type SchemaFragmentRequest =
| SchemaFragmentForReturnTypeRequest
| SchemaFragmentForRuntimeTypeRequest;
| SchemaFragmentForRuntimeTypeRequest
| SchemaFragmentForInputTypeRequest;

export type SchemaFragmentLoaderResult = {
mergedFragment: SchemaFragment;
Expand Down
38 changes: 38 additions & 0 deletions packages/supermassive/src/utilities/requestSchemaFragment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {
SchemaFragment,
SchemaFragmentLoader,
SchemaFragmentRequest,
} from "../types";
import type { PromiseOrValue } from "../jsutils/PromiseOrValue";

export interface SchemaFragmentLoaderContext {
contextValue: unknown;
schemaFragment: SchemaFragment;
schemaFragmentLoader?: SchemaFragmentLoader;
}

export function requestSchemaFragment(
exeContext: SchemaFragmentLoaderContext,
request: SchemaFragmentRequest,
): PromiseOrValue<void> {
if (!exeContext.schemaFragmentLoader) {
return;
}
const currentSchemaId = exeContext.schemaFragment.schemaId;
return exeContext
.schemaFragmentLoader(
exeContext.schemaFragment,
exeContext.contextValue,
request,
)
.then(({ mergedFragment, mergedContextValue }) => {
if (currentSchemaId !== mergedFragment.schemaId) {
throw new Error(
`Cannot use new schema fragment: old and new fragments describe different schemas:` +
` ${currentSchemaId} vs. ${mergedFragment.schemaId}`,
);
}
exeContext.contextValue = mergedContextValue ?? exeContext.contextValue;
exeContext.schemaFragment = mergedFragment;
});
}
Loading
Loading