Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

jsutils: Add generic Path implementation #2053

Merged
merged 1 commit into from Jul 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 12 additions & 36 deletions src/execution/execute.js
Expand Up @@ -14,6 +14,7 @@ import promiseForObject from '../jsutils/promiseForObject';
import promiseReduce from '../jsutils/promiseReduce';
import { type ObjMap } from '../jsutils/ObjMap';
import { type PromiseOrValue } from '../jsutils/PromiseOrValue';
import { type Path, addPath, pathToArray } from '../jsutils/Path';

import { getOperationRootType } from '../utilities/getOperationRootType';
import { typeFromAST } from '../utilities/typeFromAST';
Expand All @@ -32,7 +33,6 @@ import {
type GraphQLFieldResolver,
type GraphQLResolveInfo,
type GraphQLTypeResolver,
type ResponsePath,
type GraphQLList,
isObjectType,
isAbstractType,
Expand Down Expand Up @@ -235,30 +235,6 @@ function buildResponse(
: { errors: exeContext.errors, data };
}

/**
* Given a ResponsePath (found in the `path` entry in the information provided
* as the last argument to a field resolver), return an Array of the path keys.
*/
export function responsePathAsArray(
path: ResponsePath,
): $ReadOnlyArray<string | number> {
const flattened = [];
let curr = path;
while (curr) {
flattened.push(curr.key);
curr = curr.prev;
}
return flattened.reverse();
}

/**
* Given a ResponsePath and a key, return a new ResponsePath containing the
* new key.
*/
export function addPath(prev: ResponsePath | void, key: string | number) {
return { prev, key };
}

/**
* Essential assertions before executing to provide developer feedback for
* improper use of the GraphQL library.
Expand Down Expand Up @@ -420,7 +396,7 @@ function executeFieldsSerially(
exeContext: ExecutionContext,
parentType: GraphQLObjectType,
sourceValue: mixed,
path: ResponsePath | void,
path: Path | void,
fields: ObjMap<Array<FieldNode>>,
): PromiseOrValue<ObjMap<mixed>> {
return promiseReduce(
Expand Down Expand Up @@ -459,7 +435,7 @@ function executeFields(
exeContext: ExecutionContext,
parentType: GraphQLObjectType,
sourceValue: mixed,
path: ResponsePath | void,
path: Path | void,
fields: ObjMap<Array<FieldNode>>,
): PromiseOrValue<ObjMap<mixed>> {
const results = Object.create(null);
Expand Down Expand Up @@ -639,7 +615,7 @@ function resolveField(
parentType: GraphQLObjectType,
source: mixed,
fieldNodes: $ReadOnlyArray<FieldNode>,
path: ResponsePath,
path: Path,
): PromiseOrValue<mixed> {
const fieldNode = fieldNodes[0];
const fieldName = fieldNode.name.value;
Expand Down Expand Up @@ -685,7 +661,7 @@ export function buildResolveInfo(
fieldDef: GraphQLField<mixed, mixed>,
fieldNodes: $ReadOnlyArray<FieldNode>,
parentType: GraphQLObjectType,
path: ResponsePath,
path: Path,
): GraphQLResolveInfo {
// The resolve function's optional fourth argument is a collection of
// information about the current execution state.
Expand Down Expand Up @@ -751,7 +727,7 @@ function completeValueCatchingError(
returnType: GraphQLOutputType,
fieldNodes: $ReadOnlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: ResponsePath,
path: Path,
result: mixed,
): PromiseOrValue<mixed> {
try {
Expand Down Expand Up @@ -788,7 +764,7 @@ function handleFieldError(rawError, fieldNodes, path, returnType, exeContext) {
const error = locatedError(
asErrorInstance(rawError),
fieldNodes,
responsePathAsArray(path),
pathToArray(path),
);

// If the field type is non-nullable, then it is resolved without any
Expand Down Expand Up @@ -829,7 +805,7 @@ function completeValue(
returnType: GraphQLOutputType,
fieldNodes: $ReadOnlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: ResponsePath,
path: Path,
result: mixed,
): PromiseOrValue<mixed> {
// If result is an Error, throw a located error.
Expand Down Expand Up @@ -922,7 +898,7 @@ function completeListValue(
returnType: GraphQLList<GraphQLOutputType>,
fieldNodes: $ReadOnlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: ResponsePath,
path: Path,
result: mixed,
): PromiseOrValue<$ReadOnlyArray<mixed>> {
invariant(
Expand Down Expand Up @@ -982,7 +958,7 @@ function completeAbstractValue(
returnType: GraphQLAbstractType,
fieldNodes: $ReadOnlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: ResponsePath,
path: Path,
result: mixed,
): PromiseOrValue<ObjMap<mixed>> {
const resolveTypeFn = returnType.resolveType || exeContext.typeResolver;
Expand Down Expand Up @@ -1066,7 +1042,7 @@ function completeObjectValue(
returnType: GraphQLObjectType,
fieldNodes: $ReadOnlyArray<FieldNode>,
info: GraphQLResolveInfo,
path: ResponsePath,
path: Path,
result: mixed,
): PromiseOrValue<ObjMap<mixed>> {
// If there is an isTypeOf predicate function, call it with the
Expand Down Expand Up @@ -1119,7 +1095,7 @@ function collectAndExecuteSubfields(
exeContext: ExecutionContext,
returnType: GraphQLObjectType,
fieldNodes: $ReadOnlyArray<FieldNode>,
path: ResponsePath,
path: Path,
result: mixed,
): PromiseOrValue<ObjMap<mixed>> {
// Collect sub-fields to execute to complete this value.
Expand Down
9 changes: 3 additions & 6 deletions src/execution/index.js
@@ -1,11 +1,8 @@
// @flow strict

export {
execute,
defaultFieldResolver,
defaultTypeResolver,
responsePathAsArray,
} from './execute';
export { pathToArray as responsePathAsArray } from '../jsutils/Path';

export { execute, defaultFieldResolver, defaultTypeResolver } from './execute';
export type { ExecutionArgs, ExecutionResult } from './execute';

export { getDirectiveValues } from './values';
26 changes: 26 additions & 0 deletions src/jsutils/Path.js
@@ -0,0 +1,26 @@
// @flow strict

export type Path = {|
+prev: Path | void,
+key: string | number,
|};

/**
* Given a Path and a key, return a new Path containing the new key.
*/
export function addPath(prev: $ReadOnly<Path> | void, key: string | number) {
return { prev, key };
}

/**
* Given a Path, return an Array of the path keys.
*/
export function pathToArray(path: $ReadOnly<Path>): Array<string | number> {
const flattened = [];
let curr = path;
while (curr) {
flattened.push(curr.key);
curr = curr.prev;
}
return flattened.reverse();
}
7 changes: 2 additions & 5 deletions src/subscription/subscribe.js
Expand Up @@ -2,19 +2,18 @@

import { isAsyncIterable } from 'iterall';
import inspect from '../jsutils/inspect';
import { addPath, pathToArray } from '../jsutils/Path';
import { GraphQLError } from '../error/GraphQLError';
import { locatedError } from '../error/locatedError';
import {
type ExecutionResult,
addPath,
assertValidExecutionArguments,
buildExecutionContext,
buildResolveInfo,
collectFields,
execute,
getFieldDef,
resolveFieldValueOrError,
responsePathAsArray,
} from '../execution/execute';
import { type GraphQLSchema } from '../type/schema';
import mapAsyncIterator from './mapAsyncIterator';
Expand Down Expand Up @@ -269,9 +268,7 @@ export function createSourceEventStream(
// If eventStream is an Error, rethrow a located error.
if (eventStream instanceof Error) {
return {
errors: [
locatedError(eventStream, fieldNodes, responsePathAsArray(path)),
],
errors: [locatedError(eventStream, fieldNodes, pathToArray(path))],
};
}

Expand Down
8 changes: 2 additions & 6 deletions src/type/definition.js
Expand Up @@ -15,6 +15,7 @@ import keyValMap from '../jsutils/keyValMap';
import mapValue from '../jsutils/mapValue';
import isObjectLike from '../jsutils/isObjectLike';
import { type ObjMap } from '../jsutils/ObjMap';
import { type Path } from '../jsutils/Path';
import { Kind } from '../language/kinds';
import { valueFromASTUntyped } from '../utilities/valueFromASTUntyped';
import {
Expand Down Expand Up @@ -859,19 +860,14 @@ export type GraphQLResolveInfo = {|
+fieldNodes: $ReadOnlyArray<FieldNode>,
+returnType: GraphQLOutputType,
+parentType: GraphQLObjectType,
+path: ResponsePath,
+path: Path,
+schema: GraphQLSchema,
+fragments: ObjMap<FragmentDefinitionNode>,
+rootValue: mixed,
+operation: OperationDefinitionNode,
+variableValues: { [variable: string]: mixed, ... },
|};

export type ResponsePath = {|
+prev: ResponsePath | void,
+key: string | number,
|};

export type GraphQLFieldConfig<
TSource,
TContext,
Expand Down
3 changes: 2 additions & 1 deletion src/type/index.js
@@ -1,5 +1,7 @@
// @flow strict

export type { Path as ResponsePath } from '../jsutils/Path';

export {
// Predicate
isSchema,
Expand Down Expand Up @@ -150,7 +152,6 @@ export type {
GraphQLIsTypeOfFn,
GraphQLObjectTypeConfig,
GraphQLResolveInfo,
ResponsePath,
GraphQLScalarTypeConfig,
GraphQLTypeResolver,
GraphQLUnionTypeConfig,
Expand Down
19 changes: 7 additions & 12 deletions src/utilities/coerceValue.js
Expand Up @@ -6,6 +6,7 @@ import inspect from '../jsutils/inspect';
import didYouMean from '../jsutils/didYouMean';
import isObjectLike from '../jsutils/isObjectLike';
import suggestionList from '../jsutils/suggestionList';
import { type Path, addPath, pathToArray } from '../jsutils/Path';
import { GraphQLError } from '../error/GraphQLError';
import { type ASTNode } from '../language/ast';
import {
Expand All @@ -22,8 +23,6 @@ type CoercedValue = {|
+value: mixed,
|};

type Path = {| +prev: Path | void, +key: string | number |};

/**
* Coerces a JavaScript value given a GraphQL Type.
*
Expand Down Expand Up @@ -108,12 +107,11 @@ export function coerceValue(
let errors;
const coercedValue = [];
forEach((value: any), (itemValue, index) => {
const itemPath = { prev: path, key: index };
const coercedItem = coerceValue(
itemValue,
itemType,
blameNode,
itemPath,
addPath(path, index),
);
if (coercedItem.errors) {
errors = add(errors, coercedItem.errors);
Expand Down Expand Up @@ -144,7 +142,7 @@ export function coerceValue(

// Ensure every defined field is valid.
for (const field of objectValues(fields)) {
const fieldPath = { prev: path, key: field.name };
const fieldPath = addPath(path, field.name);
const fieldValue = value[field.name];
if (fieldValue === undefined) {
if (field.defaultValue !== undefined) {
Expand Down Expand Up @@ -215,14 +213,11 @@ function coercionError(message, blameNode, path, subMessage, originalError) {

// Build a string describing the path into the value where the error was found
if (path) {
const segmentStrings = [];
for (let currentPath = path; currentPath; currentPath = currentPath.prev) {
const { key } = currentPath;
segmentStrings.unshift(
typeof key === 'string' ? '.' + key : '[' + key.toString() + ']',
);
fullMessage += ' at value';
for (const key of pathToArray(path)) {
fullMessage +=
typeof key === 'string' ? '.' + key : '[' + key.toString() + ']';
}
fullMessage += ' at value' + segmentStrings.join('');
}

fullMessage += subMessage ? '.' + subMessage : '.';
Expand Down