mirror of
https://github.com/retailcrm/graphql-php.git
synced 2024-11-22 04:46:04 +03:00
Updated executor and it's tests for april2016 specs
This commit is contained in:
parent
00f12b3197
commit
c3d7a49a08
@ -1,773 +0,0 @@
|
||||
<?php
|
||||
namespace GraphQL\Executor;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Language\AST\Document;
|
||||
use GraphQL\Language\AST\Field;
|
||||
use GraphQL\Language\AST\FragmentDefinition;
|
||||
use GraphQL\Language\AST\Node;
|
||||
use GraphQL\Language\AST\OperationDefinition;
|
||||
use GraphQL\Language\AST\SelectionSet;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\AbstractType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Utils;
|
||||
|
||||
/**
|
||||
* @deprecated Use regular executor instead
|
||||
*
|
||||
* Terminology
|
||||
*
|
||||
* "Definitions" are the generic name for top-level statements in the document.
|
||||
* Examples of this include:
|
||||
* 1) Operations (such as a query)
|
||||
* 2) Fragments
|
||||
*
|
||||
* "Operations" are a generic name for requests in the document.
|
||||
* Examples of this include:
|
||||
* 1) query,
|
||||
* 2) mutation
|
||||
*
|
||||
* "Selections" are the statements that can appear legally and at
|
||||
* single level of the query. These include:
|
||||
* 1) field references e.g "a"
|
||||
* 2) fragment "spreads" e.g. "...c"
|
||||
* 3) inline fragment "spreads" e.g. "...on Type { a }"
|
||||
*/
|
||||
class MappingExecutor
|
||||
{
|
||||
private static $UNDEFINED;
|
||||
|
||||
private static $defaultResolveFn = [__CLASS__, 'defaultResolveFn'];
|
||||
|
||||
/**
|
||||
* Custom default resolve function
|
||||
*
|
||||
* @param $fn
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function setDefaultResolveFn($fn)
|
||||
{
|
||||
Utils::invariant(is_callable($fn), 'Expecting callable, but got ' . Utils::getVariableType($fn));
|
||||
self::$defaultResolveFn = $fn;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param Document $ast
|
||||
* @param $rootValue
|
||||
* @param array|\ArrayAccess $variableValues
|
||||
* @param null $operationName
|
||||
* @return ExecutionResult
|
||||
*/
|
||||
public static function execute(Schema $schema, Document $ast, $rootValue = null, $variableValues = null, $operationName = null)
|
||||
{
|
||||
if (!self::$UNDEFINED) {
|
||||
self::$UNDEFINED = new \stdClass();
|
||||
}
|
||||
|
||||
if (null !== $variableValues) {
|
||||
Utils::invariant(
|
||||
is_array($variableValues) || $variableValues instanceof \ArrayAccess,
|
||||
"Variable values are expected to be array or instance of ArrayAccess, got " . Utils::getVariableType($variableValues)
|
||||
);
|
||||
}
|
||||
if (null !== $operationName) {
|
||||
Utils::invariant(
|
||||
is_string($operationName),
|
||||
"Operation name is supposed to be string, got " . Utils::getVariableType($operationName)
|
||||
);
|
||||
}
|
||||
|
||||
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $variableValues, $operationName);
|
||||
|
||||
try {
|
||||
$data = self::executeOperation($exeContext, $exeContext->operation, $rootValue);
|
||||
} catch (Error $e) {
|
||||
$exeContext->addError($e);
|
||||
$data = null;
|
||||
}
|
||||
|
||||
return new ExecutionResult($data, $exeContext->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a ExecutionContext object from the arguments passed to
|
||||
* execute, which we will pass throughout the other execution methods.
|
||||
*/
|
||||
private static function buildExecutionContext(Schema $schema, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null)
|
||||
{
|
||||
$errors = [];
|
||||
$operations = [];
|
||||
$fragments = [];
|
||||
|
||||
foreach ($documentAst->definitions as $statement) {
|
||||
switch ($statement->kind) {
|
||||
case Node::OPERATION_DEFINITION:
|
||||
$operations[$statement->name ? $statement->name->value : ''] = $statement;
|
||||
break;
|
||||
case Node::FRAGMENT_DEFINITION:
|
||||
$fragments[$statement->name->value] = $statement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$operationName && count($operations) !== 1) {
|
||||
throw new Error(
|
||||
'Must provide operation name if query contains multiple operations.'
|
||||
);
|
||||
}
|
||||
|
||||
$opName = $operationName ?: key($operations);
|
||||
if (empty($operations[$opName])) {
|
||||
throw new Error('Unknown operation named ' . $opName);
|
||||
}
|
||||
$operation = $operations[$opName];
|
||||
|
||||
$variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []);
|
||||
$exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors);
|
||||
return $exeContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the "Evaluating operations" section of the spec.
|
||||
*/
|
||||
private static function executeOperation(ExecutionContext $exeContext, OperationDefinition $operation, $rootValue)
|
||||
{
|
||||
$type = self::getOperationRootType($exeContext->schema, $operation);
|
||||
$fields = self::collectFields($exeContext, $type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
|
||||
|
||||
if ($operation->operation === 'mutation') {
|
||||
$result = self::executeFieldsSerially($exeContext, $type, [$rootValue], $fields);
|
||||
} else {
|
||||
$result = self::executeFields($exeContext, $type, [$rootValue], $fields);
|
||||
}
|
||||
|
||||
return null === $result || $result === [] ? [] : $result[0];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Extracts the root type of the operation from the schema.
|
||||
*
|
||||
* @param Schema $schema
|
||||
* @param OperationDefinition $operation
|
||||
* @return ObjectType
|
||||
* @throws Error
|
||||
*/
|
||||
private static function getOperationRootType(Schema $schema, OperationDefinition $operation)
|
||||
{
|
||||
switch ($operation->operation) {
|
||||
case 'query':
|
||||
return $schema->getQueryType();
|
||||
case 'mutation':
|
||||
$mutationType = $schema->getMutationType();
|
||||
if (!$mutationType) {
|
||||
throw new Error(
|
||||
'Schema is not configured for mutations',
|
||||
[$operation]
|
||||
);
|
||||
}
|
||||
return $mutationType;
|
||||
default:
|
||||
throw new Error(
|
||||
'Can only execute queries and mutations',
|
||||
[$operation]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the "Evaluating selection sets" section of the spec
|
||||
* for "write" mode.
|
||||
*
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ObjectType $parentType
|
||||
* @param $sourceList
|
||||
* @param $fields
|
||||
* @return array
|
||||
* @throws Error
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function executeFieldsSerially(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
|
||||
{
|
||||
$results = [];
|
||||
foreach ($fields as $responseName => $fieldASTs) {
|
||||
self::resolveField($exeContext, $parentType, $sourceList, $fieldASTs, $responseName, $results);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the "Evaluating selection sets" section of the spec
|
||||
* for "read" mode.
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ObjectType $parentType
|
||||
* @param $sourceList
|
||||
* @param $fields
|
||||
* @return array
|
||||
*/
|
||||
private static function executeFields(ExecutionContext $exeContext, ObjectType $parentType, $sourceList, $fields)
|
||||
{
|
||||
// Native PHP doesn't support promises.
|
||||
// Custom executor should be built for platforms like ReactPHP
|
||||
return self::executeFieldsSerially($exeContext, $parentType, $sourceList, $fields);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a selectionSet, adds all of the fields in that selection to
|
||||
* the passed in map of fields, and returns it at the end.
|
||||
*
|
||||
* @return \ArrayObject
|
||||
*/
|
||||
private static function collectFields(
|
||||
ExecutionContext $exeContext,
|
||||
ObjectType $type,
|
||||
SelectionSet $selectionSet,
|
||||
$fields,
|
||||
$visitedFragmentNames
|
||||
)
|
||||
{
|
||||
for ($i = 0; $i < count($selectionSet->selections); $i++) {
|
||||
$selection = $selectionSet->selections[$i];
|
||||
switch ($selection->kind) {
|
||||
case Node::FIELD:
|
||||
if (!self::shouldIncludeNode($exeContext, $selection->directives)) {
|
||||
continue;
|
||||
}
|
||||
$name = self::getFieldEntryKey($selection);
|
||||
if (!isset($fields[$name])) {
|
||||
$fields[$name] = new \ArrayObject();
|
||||
}
|
||||
$fields[$name][] = $selection;
|
||||
break;
|
||||
case Node::INLINE_FRAGMENT:
|
||||
if (!self::shouldIncludeNode($exeContext, $selection->directives) ||
|
||||
!self::doesFragmentConditionMatch($exeContext, $selection, $type)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
self::collectFields(
|
||||
$exeContext,
|
||||
$type,
|
||||
$selection->selectionSet,
|
||||
$fields,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
break;
|
||||
case Node::FRAGMENT_SPREAD:
|
||||
$fragName = $selection->name->value;
|
||||
if (!empty($visitedFragmentNames[$fragName]) || !self::shouldIncludeNode($exeContext, $selection->directives)) {
|
||||
continue;
|
||||
}
|
||||
$visitedFragmentNames[$fragName] = true;
|
||||
|
||||
/** @var FragmentDefinition|null $fragment */
|
||||
$fragment = isset($exeContext->fragments[$fragName]) ? $exeContext->fragments[$fragName] : null;
|
||||
if (!$fragment ||
|
||||
!self::shouldIncludeNode($exeContext, $fragment->directives) ||
|
||||
!self::doesFragmentConditionMatch($exeContext, $fragment, $type)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
self::collectFields(
|
||||
$exeContext,
|
||||
$type,
|
||||
$fragment->selectionSet,
|
||||
$fields,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a field should be included based on the @include and @skip
|
||||
* directives, where @skip has higher precedence than @include.
|
||||
*/
|
||||
private static function shouldIncludeNode(ExecutionContext $exeContext, $directives)
|
||||
{
|
||||
$skipDirective = Directive::skipDirective();
|
||||
$includeDirective = Directive::includeDirective();
|
||||
|
||||
/** @var \GraphQL\Language\AST\Directive $skipAST */
|
||||
$skipAST = $directives
|
||||
? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($skipDirective) {
|
||||
return $directive->name->value === $skipDirective->name;
|
||||
})
|
||||
: null;
|
||||
|
||||
if ($skipAST) {
|
||||
$argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues);
|
||||
return empty($argValues['if']);
|
||||
}
|
||||
|
||||
/** @var \GraphQL\Language\AST\Directive $includeAST */
|
||||
$includeAST = $directives
|
||||
? Utils::find($directives, function(\GraphQL\Language\AST\Directive $directive) use ($includeDirective) {
|
||||
return $directive->name->value === $includeDirective->name;
|
||||
})
|
||||
: null;
|
||||
|
||||
if ($includeAST) {
|
||||
$argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues);
|
||||
return !empty($argValues['if']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a fragment is applicable to the given type.
|
||||
*/
|
||||
private static function doesFragmentConditionMatch(ExecutionContext $exeContext,/* FragmentDefinition | InlineFragment*/ $fragment, ObjectType $type)
|
||||
{
|
||||
$conditionalType = Utils\TypeInfo::typeFromAST($exeContext->schema, $fragment->typeCondition);
|
||||
if ($conditionalType === $type) {
|
||||
return true;
|
||||
}
|
||||
if ($conditionalType instanceof InterfaceType ||
|
||||
$conditionalType instanceof UnionType
|
||||
) {
|
||||
return $conditionalType->isPossibleType($type);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the logic to compute the key of a given fields entry
|
||||
*/
|
||||
private static function getFieldEntryKey(Field $node)
|
||||
{
|
||||
return $node->alias ? $node->alias->value : $node->name->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given list of parent type values returns corresponding list of field values
|
||||
*
|
||||
* In particular, this
|
||||
* figures out the value that the field returns by calling its `resolve` or `map` function,
|
||||
* then calls `completeValue` on each value to serialize scalars, or execute the sub-selection-set
|
||||
* for objects.
|
||||
*
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ObjectType $parentType
|
||||
* @param $sourceValueList
|
||||
* @param $fieldASTs
|
||||
* @return array
|
||||
* @throws Error
|
||||
*/
|
||||
private static function resolveField(ExecutionContext $exeContext, ObjectType $parentType, $sourceValueList, $fieldASTs, $responseName, &$resolveResult)
|
||||
{
|
||||
$fieldAST = $fieldASTs[0];
|
||||
$fieldName = $fieldAST->name->value;
|
||||
|
||||
$fieldDef = self::getFieldDef($exeContext->schema, $parentType, $fieldName);
|
||||
|
||||
if (!$fieldDef) {
|
||||
return ;
|
||||
}
|
||||
|
||||
$returnType = $fieldDef->getType();
|
||||
|
||||
// Build hash of arguments from the field.arguments AST, using the
|
||||
// variables scope to fulfill any variable references.
|
||||
// TODO: find a way to memoize, in case this field is within a List type.
|
||||
$args = Values::getArgumentValues(
|
||||
$fieldDef->args,
|
||||
$fieldAST->arguments,
|
||||
$exeContext->variableValues
|
||||
);
|
||||
|
||||
// The resolve function's optional third argument is a collection of
|
||||
// information about the current execution state.
|
||||
$info = new ResolveInfo([
|
||||
'fieldName' => $fieldName,
|
||||
'fieldASTs' => $fieldASTs,
|
||||
'returnType' => $returnType,
|
||||
'parentType' => $parentType,
|
||||
'schema' => $exeContext->schema,
|
||||
'fragments' => $exeContext->fragments,
|
||||
'rootValue' => $exeContext->rootValue,
|
||||
'operation' => $exeContext->operation,
|
||||
'variableValues' => $exeContext->variableValues,
|
||||
]);
|
||||
|
||||
$mapFn = $fieldDef->mapFn;
|
||||
|
||||
// If an error occurs while calling the field `map` or `resolve` function, ensure that
|
||||
// it is wrapped as a GraphQLError with locations. Log this error and return
|
||||
// null if allowed, otherwise throw the error so the parent field can handle
|
||||
// it.
|
||||
if ($mapFn) {
|
||||
try {
|
||||
$mapped = call_user_func($mapFn, $sourceValueList, $args, $info);
|
||||
$validType = is_array($mapped) || ($mapped instanceof \Traversable && $mapped instanceof \Countable);
|
||||
$mappedCount = count($mapped);
|
||||
$sourceCount = count($sourceValueList);
|
||||
|
||||
Utils::invariant(
|
||||
$validType && count($mapped) === count($sourceValueList),
|
||||
"Function `map` of $parentType.$fieldName is expected to return array or " .
|
||||
"countable traversable with exact same number of items as list being mapped. ".
|
||||
"Got '%s' with count '$mappedCount' against '$sourceCount' expected.",
|
||||
Utils::getVariableType($mapped)
|
||||
);
|
||||
|
||||
} catch (\Exception $error) {
|
||||
$reportedError = Error::createLocatedError($error, $fieldASTs);
|
||||
|
||||
if ($returnType instanceof NonNull) {
|
||||
throw $reportedError;
|
||||
}
|
||||
|
||||
$exeContext->addError($reportedError);
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($mapped as $index => $value) {
|
||||
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
|
||||
$exeContext,
|
||||
$returnType,
|
||||
$fieldASTs,
|
||||
$info,
|
||||
$value
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (isset($fieldDef->resolveFn)) {
|
||||
$resolveFn = $fieldDef->resolveFn;
|
||||
} else if (isset($parentType->resolveFieldFn)) {
|
||||
$resolveFn = $parentType->resolveFieldFn;
|
||||
} else {
|
||||
$resolveFn = self::$defaultResolveFn;
|
||||
}
|
||||
|
||||
foreach ($sourceValueList as $index => $value) {
|
||||
try {
|
||||
$resolved = call_user_func($resolveFn, $value, $args, $info);
|
||||
} catch (\Exception $error) {
|
||||
$reportedError = Error::createLocatedError($error, $fieldASTs);
|
||||
|
||||
if ($returnType instanceof NonNull) {
|
||||
throw $reportedError;
|
||||
}
|
||||
|
||||
$exeContext->addError($reportedError);
|
||||
$resolved = null;
|
||||
}
|
||||
|
||||
$resolveResult[$index][$responseName] = self::completeValueCatchingError(
|
||||
$exeContext,
|
||||
$returnType,
|
||||
$fieldASTs,
|
||||
$info,
|
||||
$resolved
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function completeValueCatchingError(
|
||||
ExecutionContext $exeContext,
|
||||
Type $returnType,
|
||||
$fieldASTs,
|
||||
ResolveInfo $info,
|
||||
$result
|
||||
)
|
||||
{
|
||||
// If the field type is non-nullable, then it is resolved without any
|
||||
// protection from errors.
|
||||
if ($returnType instanceof NonNull) {
|
||||
return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
|
||||
}
|
||||
|
||||
// Otherwise, error protection is applied, logging the error and resolving
|
||||
// a null value for this field if one is encountered.
|
||||
try {
|
||||
return self::completeValue($exeContext, $returnType, $fieldASTs, $info, $result);
|
||||
} catch (Error $err) {
|
||||
$exeContext->addError($err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the instructions for completeValue as defined in the
|
||||
* "Field entries" section of the spec.
|
||||
*
|
||||
* If the field type is Non-Null, then this recursively completes the value
|
||||
* for the inner type. It throws a field error if that completion returns null,
|
||||
* as per the "Nullability" section of the spec.
|
||||
*
|
||||
* If the field type is a List, then this recursively completes the value
|
||||
* for the inner type on each item in the list.
|
||||
*
|
||||
* If the field type is a Scalar or Enum, ensures the completed value is a legal
|
||||
* value of the type by calling the `serialize` method of GraphQL type
|
||||
* definition.
|
||||
*
|
||||
* Otherwise, the field type expects a sub-selection set, and will complete the
|
||||
* value by evaluating all sub-selections.
|
||||
*/
|
||||
private static function completeValue(ExecutionContext $exeContext, Type $returnType,/* Array<Field> */ $fieldASTs, ResolveInfo $info, &$result)
|
||||
{
|
||||
// If field type is NonNull, complete for inner type, and throw field error
|
||||
// if result is null.
|
||||
if ($returnType instanceof NonNull) {
|
||||
$completed = self::completeValue(
|
||||
$exeContext,
|
||||
$returnType->getWrappedType(),
|
||||
$fieldASTs,
|
||||
$info,
|
||||
$result
|
||||
);
|
||||
if ($completed === null) {
|
||||
throw new Error(
|
||||
'Cannot return null for non-nullable type.',
|
||||
$fieldASTs instanceof \ArrayObject ? $fieldASTs->getArrayCopy() : $fieldASTs
|
||||
);
|
||||
}
|
||||
return $completed;
|
||||
}
|
||||
|
||||
// If result is null-like, return null.
|
||||
if (null === $result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If field type is Scalar or Enum, serialize to a valid value, returning
|
||||
// null if serialization is not possible.
|
||||
if ($returnType instanceof ScalarType ||
|
||||
$returnType instanceof EnumType) {
|
||||
return $returnType->serialize($result);
|
||||
}
|
||||
|
||||
// If field type is List, and return type is Composite - complete by executing these fields with list value as parameter
|
||||
if ($returnType instanceof ListOfType) {
|
||||
$itemType = $returnType->getWrappedType();
|
||||
|
||||
Utils::invariant(
|
||||
is_array($result) || $result instanceof \Traversable,
|
||||
'User Error: expected iterable, but did not find one.'
|
||||
);
|
||||
|
||||
// For Object[]:
|
||||
// Allow all object fields to process list value in it's `map` callback:
|
||||
if ($itemType instanceof ObjectType) {
|
||||
// Filter out nulls (as `map` doesn't expect it):
|
||||
$list = [];
|
||||
foreach ($result as $index => $item) {
|
||||
if (null !== $item) {
|
||||
$list[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$subFieldASTs = self::collectSubFields($exeContext, $itemType, $fieldASTs);
|
||||
$mapped = self::executeFields($exeContext, $itemType, $list, $subFieldASTs);
|
||||
|
||||
$i = 0;
|
||||
$completed = [];
|
||||
foreach ($result as $index => $item) {
|
||||
if (null === $item) {
|
||||
// Complete nulls separately
|
||||
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||
} else {
|
||||
// Assuming same order of mapped values
|
||||
$completed[] = $mapped[$i++];
|
||||
}
|
||||
}
|
||||
return $completed;
|
||||
|
||||
} else if ($itemType instanceof AbstractType) {
|
||||
|
||||
// Values sharded by ObjectType
|
||||
$listPerObjectType = [];
|
||||
|
||||
// Helper structures to restore ordering after resolve calls
|
||||
$resultTypeMap = [];
|
||||
$typeNameMap = [];
|
||||
$cursors = [];
|
||||
$copied = [];
|
||||
|
||||
foreach ($result as $index => $item) {
|
||||
$copied[$index] = $item;
|
||||
|
||||
if (null !== $item) {
|
||||
$objectType = $itemType->getObjectType($item, $info);
|
||||
|
||||
if ($objectType && !$itemType->isPossibleType($objectType)) {
|
||||
$exeContext->addError(new Error(
|
||||
"Runtime Object type \"$objectType\" is not a possible type for \"$itemType\"."
|
||||
));
|
||||
$copied[$index] = null;
|
||||
} else {
|
||||
$listPerObjectType[$objectType->name][] = $item;
|
||||
$resultTypeMap[$index] = $objectType->name;
|
||||
$typeNameMap[$objectType->name] = $objectType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$mapped = [];
|
||||
foreach ($listPerObjectType as $typeName => $list) {
|
||||
$objectType = $typeNameMap[$typeName];
|
||||
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
|
||||
$mapped[$typeName] = self::executeFields($exeContext, $objectType, $list, $subFieldASTs);
|
||||
$cursors[$typeName] = 0;
|
||||
}
|
||||
|
||||
// Restore order:
|
||||
$completed = [];
|
||||
foreach ($copied as $index => $item) {
|
||||
if (null === $item) {
|
||||
// Complete nulls separately
|
||||
$completed[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||
} else {
|
||||
$typeName = $resultTypeMap[$index];
|
||||
$completed[] = $mapped[$typeName][$cursors[$typeName]++];
|
||||
}
|
||||
}
|
||||
|
||||
return $completed;
|
||||
} else {
|
||||
|
||||
// For simple lists:
|
||||
$tmp = [];
|
||||
foreach ($result as $item) {
|
||||
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||
}
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ($returnType instanceof ObjectType) {
|
||||
$objectType = $returnType;
|
||||
|
||||
} else if ($returnType instanceof AbstractType) {
|
||||
$objectType = $returnType->getObjectType($result, $info);
|
||||
|
||||
if ($objectType && !$returnType->isPossibleType($objectType)) {
|
||||
throw new Error(
|
||||
"Runtime Object type \"$objectType\" is not a possible type for \"$returnType\"."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$objectType = null;
|
||||
}
|
||||
|
||||
if (!$objectType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If there is an isTypeOf predicate function, call it with the
|
||||
// current result. If isTypeOf returns false, then raise an error rather
|
||||
// than continuing execution.
|
||||
if (false === $objectType->isTypeOf($result, $info)) {
|
||||
throw new Error(
|
||||
"Expected value of type $objectType but got: $result.",
|
||||
$fieldASTs
|
||||
);
|
||||
}
|
||||
|
||||
// Collect sub-fields to execute to complete this value.
|
||||
$subFieldASTs = self::collectSubFields($exeContext, $objectType, $fieldASTs);
|
||||
$executed = self::executeFields($exeContext, $objectType, [$result], $subFieldASTs);
|
||||
return isset($executed[0]) ? $executed[0] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ObjectType $objectType
|
||||
* @param $fieldASTs
|
||||
* @return \ArrayObject
|
||||
*/
|
||||
private static function collectSubFields(ExecutionContext $exeContext, ObjectType $objectType, $fieldASTs)
|
||||
{
|
||||
$subFieldASTs = new \ArrayObject();
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
for ($i = 0; $i < count($fieldASTs); $i++) {
|
||||
$selectionSet = $fieldASTs[$i]->selectionSet;
|
||||
if ($selectionSet) {
|
||||
$subFieldASTs = self::collectFields(
|
||||
$exeContext,
|
||||
$objectType,
|
||||
$selectionSet,
|
||||
$subFieldASTs,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
}
|
||||
}
|
||||
return $subFieldASTs;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a resolve function is not given, then a default resolve behavior is used
|
||||
* which takes the property of the source object of the same name as the field
|
||||
* and returns it as the result, or if it's a function, returns the result
|
||||
* of calling that function.
|
||||
*/
|
||||
public static function defaultResolveFn($source, $args, ResolveInfo $info)
|
||||
{
|
||||
$fieldName = $info->fieldName;
|
||||
$property = null;
|
||||
|
||||
if (is_array($source) || $source instanceof \ArrayAccess) {
|
||||
if (isset($source[$fieldName])) {
|
||||
$property = $source[$fieldName];
|
||||
}
|
||||
} else if (is_object($source)) {
|
||||
if (isset($source->{$fieldName})) {
|
||||
$property = $source->{$fieldName};
|
||||
}
|
||||
}
|
||||
|
||||
return $property instanceof \Closure ? $property($source) : $property;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method looks up the field on the given type defintion.
|
||||
* It has special casing for the two introspection fields, __schema
|
||||
* and __typename. __typename is special because it can always be
|
||||
* queried as a field, even in situations where no other fields
|
||||
* are allowed, like on a Union. __schema could get automatically
|
||||
* added to the query type, but that would require mutating type
|
||||
* definitions, which would cause issues.
|
||||
*
|
||||
* @return FieldDefinition
|
||||
*/
|
||||
private static function getFieldDef(Schema $schema, ObjectType $parentType, $fieldName)
|
||||
{
|
||||
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
|
||||
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
|
||||
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
|
||||
|
||||
if ($fieldName === $schemaMetaFieldDef->name && $schema->getQueryType() === $parentType) {
|
||||
return $schemaMetaFieldDef;
|
||||
} else if ($fieldName === $typeMetaFieldDef->name && $schema->getQueryType() === $parentType) {
|
||||
return $typeMetaFieldDef;
|
||||
} else if ($fieldName === $typeNameMetaFieldDef->name) {
|
||||
return $typeNameMetaFieldDef;
|
||||
}
|
||||
|
||||
$tmp = $parentType->getFields();
|
||||
return isset($tmp[$fieldName]) ? $tmp[$fieldName] : null;
|
||||
}
|
||||
}
|
@ -24,10 +24,15 @@ class ExecutionContext
|
||||
public $fragments;
|
||||
|
||||
/**
|
||||
* @var
|
||||
* @var mixed
|
||||
*/
|
||||
public $rootValue;
|
||||
|
||||
/**
|
||||
* @var mixed
|
||||
*/
|
||||
public $contextValue;
|
||||
|
||||
/**
|
||||
* @var OperationDefinition
|
||||
*/
|
||||
@ -48,11 +53,12 @@ class ExecutionContext
|
||||
*/
|
||||
public $memoized = [];
|
||||
|
||||
public function __construct($schema, $fragments, $root, $operation, $variables, $errors)
|
||||
public function __construct($schema, $fragments, $root, $contextValue, $operation, $variables, $errors)
|
||||
{
|
||||
$this->schema = $schema;
|
||||
$this->fragments = $fragments;
|
||||
$this->rootValue = $root;
|
||||
$this->contextValue = $contextValue;
|
||||
$this->operation = $operation;
|
||||
$this->variableValues = $variables;
|
||||
$this->errors = $errors ?: [];
|
||||
|
@ -13,14 +13,12 @@ use GraphQL\Type\Definition\AbstractType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Utils;
|
||||
|
||||
@ -65,11 +63,12 @@ class Executor
|
||||
* @param Schema $schema
|
||||
* @param Document $ast
|
||||
* @param $rootValue
|
||||
* @param $contextValue
|
||||
* @param array|\ArrayAccess $variableValues
|
||||
* @param null $operationName
|
||||
* @return ExecutionResult
|
||||
*/
|
||||
public static function execute(Schema $schema, Document $ast, $rootValue = null, $variableValues = null, $operationName = null)
|
||||
public static function execute(Schema $schema, Document $ast, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
|
||||
{
|
||||
if (!self::$UNDEFINED) {
|
||||
self::$UNDEFINED = new \stdClass();
|
||||
@ -88,7 +87,7 @@ class Executor
|
||||
);
|
||||
}
|
||||
|
||||
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $variableValues, $operationName);
|
||||
$exeContext = self::buildExecutionContext($schema, $ast, $rootValue, $contextValue, $variableValues, $operationName);
|
||||
|
||||
try {
|
||||
$data = self::executeOperation($exeContext, $exeContext->operation, $rootValue);
|
||||
@ -104,37 +103,58 @@ class Executor
|
||||
* Constructs a ExecutionContext object from the arguments passed to
|
||||
* execute, which we will pass throughout the other execution methods.
|
||||
*/
|
||||
private static function buildExecutionContext(Schema $schema, Document $documentAst, $rootValue, $rawVariableValues, $operationName = null)
|
||||
private static function buildExecutionContext(
|
||||
Schema $schema,
|
||||
Document $documentAst,
|
||||
$rootValue,
|
||||
$contextValue,
|
||||
$rawVariableValues,
|
||||
$operationName = null
|
||||
)
|
||||
{
|
||||
$errors = [];
|
||||
$operations = [];
|
||||
$fragments = [];
|
||||
$operation = null;
|
||||
|
||||
foreach ($documentAst->definitions as $statement) {
|
||||
switch ($statement->kind) {
|
||||
foreach ($documentAst->definitions as $definition) {
|
||||
switch ($definition->kind) {
|
||||
case Node::OPERATION_DEFINITION:
|
||||
$operations[$statement->name ? $statement->name->value : ''] = $statement;
|
||||
if (!$operationName && $operation) {
|
||||
throw new Error(
|
||||
'Must provide operation name if query contains multiple operations.'
|
||||
);
|
||||
}
|
||||
if (!$operationName ||
|
||||
(isset($definition->name) && $definition->name->value === $operationName)) {
|
||||
$operation = $definition;
|
||||
}
|
||||
break;
|
||||
case Node::FRAGMENT_DEFINITION:
|
||||
$fragments[$statement->name->value] = $statement;
|
||||
$fragments[$definition->name->value] = $definition;
|
||||
break;
|
||||
default:
|
||||
throw new Error(
|
||||
"GraphQL cannot execute a request containing a {$definition->kind}.",
|
||||
[$definition]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$operationName && count($operations) !== 1) {
|
||||
throw new Error(
|
||||
'Must provide operation name if query contains multiple operations.'
|
||||
);
|
||||
if (!$operation) {
|
||||
if ($operationName) {
|
||||
throw new Error("Unknown operation named \"$operationName\".");
|
||||
} else {
|
||||
throw new Error('Must provide an operation.');
|
||||
}
|
||||
}
|
||||
|
||||
$opName = $operationName ?: key($operations);
|
||||
if (empty($operations[$opName])) {
|
||||
throw new Error('Unknown operation named ' . $opName);
|
||||
}
|
||||
$operation = $operations[$opName];
|
||||
$variableValues = Values::getVariableValues(
|
||||
$schema,
|
||||
$operation->variableDefinitions ?: [],
|
||||
$rawVariableValues ?: []
|
||||
);
|
||||
|
||||
$variableValues = Values::getVariableValues($schema, $operation->variableDefinitions ?: [], $rawVariableValues ?: []);
|
||||
$exeContext = new ExecutionContext($schema, $fragments, $rootValue, $operation, $variableValues, $errors);
|
||||
$exeContext = new ExecutionContext($schema, $fragments, $rootValue, $contextValue, $operation, $variableValues, $errors);
|
||||
return $exeContext;
|
||||
}
|
||||
|
||||
@ -218,18 +238,21 @@ class Executor
|
||||
* Given a selectionSet, adds all of the fields in that selection to
|
||||
* the passed in map of fields, and returns it at the end.
|
||||
*
|
||||
* CollectFields requires the "runtime type" of an object. For a field which
|
||||
* returns and Interface or Union type, the "runtime type" will be the actual
|
||||
* Object type returned by that field.
|
||||
*
|
||||
* @return \ArrayObject
|
||||
*/
|
||||
private static function collectFields(
|
||||
ExecutionContext $exeContext,
|
||||
ObjectType $type,
|
||||
ObjectType $runtimeType,
|
||||
SelectionSet $selectionSet,
|
||||
$fields,
|
||||
$visitedFragmentNames
|
||||
)
|
||||
{
|
||||
for ($i = 0; $i < count($selectionSet->selections); $i++) {
|
||||
$selection = $selectionSet->selections[$i];
|
||||
foreach ($selectionSet->selections as $selection) {
|
||||
switch ($selection->kind) {
|
||||
case Node::FIELD:
|
||||
if (!self::shouldIncludeNode($exeContext, $selection->directives)) {
|
||||
@ -243,13 +266,13 @@ class Executor
|
||||
break;
|
||||
case Node::INLINE_FRAGMENT:
|
||||
if (!self::shouldIncludeNode($exeContext, $selection->directives) ||
|
||||
!self::doesFragmentConditionMatch($exeContext, $selection, $type)
|
||||
!self::doesFragmentConditionMatch($exeContext, $selection, $runtimeType)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
self::collectFields(
|
||||
$exeContext,
|
||||
$type,
|
||||
$runtimeType,
|
||||
$selection->selectionSet,
|
||||
$fields,
|
||||
$visitedFragmentNames
|
||||
@ -264,15 +287,12 @@ class Executor
|
||||
|
||||
/** @var FragmentDefinition|null $fragment */
|
||||
$fragment = isset($exeContext->fragments[$fragName]) ? $exeContext->fragments[$fragName] : null;
|
||||
if (!$fragment ||
|
||||
!self::shouldIncludeNode($exeContext, $fragment->directives) ||
|
||||
!self::doesFragmentConditionMatch($exeContext, $fragment, $type)
|
||||
) {
|
||||
if (!$fragment || !self::doesFragmentConditionMatch($exeContext, $fragment, $runtimeType)) {
|
||||
continue;
|
||||
}
|
||||
self::collectFields(
|
||||
$exeContext,
|
||||
$type,
|
||||
$runtimeType,
|
||||
$fragment->selectionSet,
|
||||
$fields,
|
||||
$visitedFragmentNames
|
||||
@ -301,7 +321,9 @@ class Executor
|
||||
|
||||
if ($skipAST) {
|
||||
$argValues = Values::getArgumentValues($skipDirective->args, $skipAST->arguments, $exeContext->variableValues);
|
||||
return empty($argValues['if']);
|
||||
if (isset($argValues['if']) && $argValues['if'] === true) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var \GraphQL\Language\AST\Directive $includeAST */
|
||||
@ -313,7 +335,9 @@ class Executor
|
||||
|
||||
if ($includeAST) {
|
||||
$argValues = Values::getArgumentValues($includeDirective->args, $includeAST->arguments, $exeContext->variableValues);
|
||||
return !empty($argValues['if']);
|
||||
if (isset($argValues['if']) && $argValues['if'] === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -334,10 +358,8 @@ class Executor
|
||||
if ($conditionalType === $type) {
|
||||
return true;
|
||||
}
|
||||
if ($conditionalType instanceof InterfaceType ||
|
||||
$conditionalType instanceof UnionType
|
||||
) {
|
||||
return $conditionalType->isPossibleType($type);
|
||||
if ($conditionalType instanceof AbstractType) {
|
||||
return $exeContext->schema->isPossibleType($conditionalType, $type);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -401,9 +423,14 @@ class Executor
|
||||
$resolveFn = self::$defaultResolveFn;
|
||||
}
|
||||
|
||||
// The resolve function's optional third argument is a context value that
|
||||
// is provided to every resolve function within an execution. It is commonly
|
||||
// used to represent an authenticated user, or request-specific caches.
|
||||
$context = $exeContext->contextValue;
|
||||
|
||||
// Get the resolve function, regardless of if its result is normal
|
||||
// or abrupt (error).
|
||||
$result = self::resolveOrError($resolveFn, $source, $args, $info);
|
||||
$result = self::resolveOrError($resolveFn, $source, $args, $context, $info);
|
||||
|
||||
$result = self::completeValueCatchingError(
|
||||
$exeContext,
|
||||
@ -418,15 +445,17 @@ class Executor
|
||||
|
||||
// Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField`
|
||||
// function. Returns the result of resolveFn or the abrupt-return Error object.
|
||||
private static function resolveOrError($resolveFn, $source, $args, $info)
|
||||
private static function resolveOrError($resolveFn, $source, $args, $context, $info)
|
||||
{
|
||||
try {
|
||||
return call_user_func($resolveFn, $source, $args, $info);
|
||||
return call_user_func($resolveFn, $source, $args, $context, $info);
|
||||
} catch (\Exception $error) {
|
||||
return $error;
|
||||
}
|
||||
}
|
||||
|
||||
// This is a small wrapper around completeValue which detects and logs errors
|
||||
// in the execution context.
|
||||
public static function completeValueCatchingError(
|
||||
ExecutionContext $exeContext,
|
||||
Type $returnType,
|
||||
@ -468,10 +497,22 @@ class Executor
|
||||
* value of the type by calling the `serialize` method of GraphQL type
|
||||
* definition.
|
||||
*
|
||||
* If the field is an abstract type, determine the runtime type of the value
|
||||
* and then complete based on that type
|
||||
*
|
||||
* Otherwise, the field type expects a sub-selection set, and will complete the
|
||||
* value by evaluating all sub-selections.
|
||||
*
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param Type $returnType
|
||||
* @param Field[] $fieldASTs
|
||||
* @param ResolveInfo $info
|
||||
* @param $result
|
||||
* @return array|null
|
||||
* @throws Error
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function completeValue(ExecutionContext $exeContext, Type $returnType,/* Array<Field> */ $fieldASTs, ResolveInfo $info, &$result)
|
||||
private static function completeValue(ExecutionContext $exeContext, Type $returnType, $fieldASTs, ResolveInfo $info, &$result)
|
||||
{
|
||||
if ($result instanceof \Exception) {
|
||||
throw Error::createLocatedError($result, $fieldASTs);
|
||||
@ -489,7 +530,7 @@ class Executor
|
||||
);
|
||||
if ($completed === null) {
|
||||
throw new Error(
|
||||
'Cannot return null for non-nullable type.',
|
||||
'Cannot return null for non-nullable field ' . $info->parentType . '.' . $info->fieldName . '.',
|
||||
$fieldASTs instanceof \ArrayObject ? $fieldASTs->getArrayCopy() : $fieldASTs
|
||||
);
|
||||
}
|
||||
@ -503,81 +544,26 @@ class Executor
|
||||
|
||||
// If field type is List, complete each item in the list with the inner type
|
||||
if ($returnType instanceof ListOfType) {
|
||||
$itemType = $returnType->getWrappedType();
|
||||
Utils::invariant(
|
||||
is_array($result) || $result instanceof \Traversable,
|
||||
'User Error: expected iterable, but did not find one.'
|
||||
);
|
||||
|
||||
$tmp = [];
|
||||
foreach ($result as $item) {
|
||||
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||
}
|
||||
return $tmp;
|
||||
return self::completeListValue($exeContext, $returnType, $fieldASTs, $info, $result);
|
||||
}
|
||||
|
||||
// If field type is Scalar or Enum, serialize to a valid value, returning
|
||||
// null if serialization is not possible.
|
||||
if ($returnType instanceof ScalarType ||
|
||||
$returnType instanceof EnumType) {
|
||||
Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type');
|
||||
return $returnType->serialize($result);
|
||||
return self::completeLeafValue($returnType, $result);
|
||||
}
|
||||
|
||||
if ($returnType instanceof AbstractType) {
|
||||
return self::completeAbstractValue($exeContext, $returnType, $fieldASTs, $info, $result);
|
||||
}
|
||||
|
||||
// Field type must be Object, Interface or Union and expect sub-selections.
|
||||
if ($returnType instanceof ObjectType) {
|
||||
$runtimeType = $returnType;
|
||||
} else if ($returnType instanceof AbstractType) {
|
||||
$runtimeType = $returnType->getObjectType($result, $info);
|
||||
|
||||
if ($runtimeType && !$returnType->isPossibleType($runtimeType)) {
|
||||
throw new Error(
|
||||
"Runtime Object type \"$runtimeType\" is not a possible type for \"$returnType\"."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$runtimeType = null;
|
||||
return self::completeObjectValue($exeContext, $returnType, $fieldASTs, $info, $result);
|
||||
}
|
||||
|
||||
if (!$runtimeType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If there is an isTypeOf predicate function, call it with the
|
||||
// current result. If isTypeOf returns false, then raise an error rather
|
||||
// than continuing execution.
|
||||
if (false === $runtimeType->isTypeOf($result, $info)) {
|
||||
throw new Error(
|
||||
"Expected value of type $runtimeType but got: " . Utils::getVariableType($result),
|
||||
$fieldASTs
|
||||
);
|
||||
}
|
||||
|
||||
// Collect sub-fields to execute to complete this value.
|
||||
$subFieldASTs = new \ArrayObject();
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
for ($i = 0; $i < count($fieldASTs); $i++) {
|
||||
// Get memoized value if it exists
|
||||
$uid = self::getFieldUid($fieldASTs[$i], $runtimeType);
|
||||
if (isset($exeContext->memoized['collectSubFields'][$uid])) {
|
||||
$subFieldASTs = $exeContext->memoized['collectSubFields'][$uid];
|
||||
}
|
||||
else {
|
||||
$selectionSet = $fieldASTs[$i]->selectionSet;
|
||||
if ($selectionSet) {
|
||||
$subFieldASTs = self::collectFields(
|
||||
$exeContext,
|
||||
$runtimeType,
|
||||
$selectionSet,
|
||||
$subFieldASTs,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
$exeContext->memoized['collectSubFields'][$uid] = $subFieldASTs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::executeFields($exeContext, $runtimeType, $result, $subFieldASTs);
|
||||
throw new Error("Cannot complete value of unexpected type \"{$returnType}\".");
|
||||
}
|
||||
|
||||
|
||||
@ -587,7 +573,7 @@ class Executor
|
||||
* and returns it as the result, or if it's a function, returns the result
|
||||
* of calling that function.
|
||||
*/
|
||||
public static function defaultResolveFn($source, $args, ResolveInfo $info)
|
||||
public static function defaultResolveFn($source, $args, $context, ResolveInfo $info)
|
||||
{
|
||||
$fieldName = $info->fieldName;
|
||||
$property = null;
|
||||
@ -644,4 +630,135 @@ class Executor
|
||||
{
|
||||
return $fieldAST->loc->start . '-' . $fieldAST->loc->end . '-' . $fieldType->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a value of an abstract type by determining the runtime object type
|
||||
* of that value, then complete the value for that type.
|
||||
*
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param AbstractType $returnType
|
||||
* @param $fieldASTs
|
||||
* @param ResolveInfo $info
|
||||
* @param $result
|
||||
* @return mixed
|
||||
* @throws Error
|
||||
*/
|
||||
private static function completeAbstractValue(ExecutionContext $exeContext, AbstractType $returnType, $fieldASTs, ResolveInfo $info, &$result)
|
||||
{
|
||||
$resolveType = $returnType->getResolveTypeFn();
|
||||
|
||||
$runtimeType = $resolveType ?
|
||||
call_user_func($resolveType, $result, $exeContext->contextValue, $info) :
|
||||
Type::getTypeOf($result, $exeContext->contextValue, $info, $returnType);
|
||||
|
||||
if (!($runtimeType instanceof ObjectType)) {
|
||||
throw new Error(
|
||||
"Abstract type {$returnType} must resolve to an Object type at runtime " .
|
||||
"for field {$info->parentType}.{$info->fieldName} with value \"" . print_r($result, true) . "\"," .
|
||||
"received \"$runtimeType\".",
|
||||
$fieldASTs
|
||||
);
|
||||
}
|
||||
|
||||
if (!$exeContext->schema->isPossibleType($returnType, $runtimeType)) {
|
||||
throw new Error(
|
||||
"Runtime Object type \"$runtimeType\" is not a possible type for \"$returnType\".",
|
||||
$fieldASTs
|
||||
);
|
||||
}
|
||||
return self::completeObjectValue($exeContext, $runtimeType, $fieldASTs, $info, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a list value by completing each item in the list with the
|
||||
* inner type
|
||||
*
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ListOfType $returnType
|
||||
* @param $fieldASTs
|
||||
* @param ResolveInfo $info
|
||||
* @param $result
|
||||
* @return array
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function completeListValue(ExecutionContext $exeContext, ListOfType $returnType, $fieldASTs, ResolveInfo $info, &$result)
|
||||
{
|
||||
$itemType = $returnType->getWrappedType();
|
||||
Utils::invariant(
|
||||
is_array($result) || $result instanceof \Traversable,
|
||||
'User Error: expected iterable, but did not find one for field ' . $info->parentType . '.' . $info->fieldName . '.'
|
||||
);
|
||||
|
||||
$tmp = [];
|
||||
foreach ($result as $item) {
|
||||
$tmp[] = self::completeValueCatchingError($exeContext, $itemType, $fieldASTs, $info, $item);
|
||||
}
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a Scalar or Enum by serializing to a valid value, returning
|
||||
* null if serialization is not possible.
|
||||
*
|
||||
* @param Type $returnType
|
||||
* @param $result
|
||||
* @return mixed
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function completeLeafValue(Type $returnType, &$result)
|
||||
{
|
||||
Utils::invariant(method_exists($returnType, 'serialize'), 'Missing serialize method on type');
|
||||
return $returnType->serialize($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete an Object value by executing all sub-selections.
|
||||
*
|
||||
* @param ExecutionContext $exeContext
|
||||
* @param ObjectType $returnType
|
||||
* @param $fieldASTs
|
||||
* @param ResolveInfo $info
|
||||
* @param $result
|
||||
* @return array
|
||||
* @throws Error
|
||||
*/
|
||||
private static function completeObjectValue(ExecutionContext $exeContext, ObjectType $returnType, $fieldASTs, ResolveInfo $info, &$result)
|
||||
{
|
||||
// If there is an isTypeOf predicate function, call it with the
|
||||
// current result. If isTypeOf returns false, then raise an error rather
|
||||
// than continuing execution.
|
||||
if (false === $returnType->isTypeOf($result, $exeContext->contextValue, $info)) {
|
||||
throw new Error(
|
||||
"Expected value of type $returnType but got: " . Utils::getVariableType($result),
|
||||
$fieldASTs
|
||||
);
|
||||
}
|
||||
|
||||
// Collect sub-fields to execute to complete this value.
|
||||
$subFieldASTs = new \ArrayObject();
|
||||
$visitedFragmentNames = new \ArrayObject();
|
||||
|
||||
$fieldsCount = count($fieldASTs);
|
||||
for ($i = 0; $i < $fieldsCount; $i++) {
|
||||
// Get memoized value if it exists
|
||||
$uid = self::getFieldUid($fieldASTs[$i], $returnType);
|
||||
if (isset($exeContext->memoized['collectSubFields'][$uid])) {
|
||||
$subFieldASTs = $exeContext->memoized['collectSubFields'][$uid];
|
||||
} else {
|
||||
$selectionSet = $fieldASTs[$i]->selectionSet;
|
||||
if ($selectionSet) {
|
||||
$subFieldASTs = self::collectFields(
|
||||
$exeContext,
|
||||
$returnType,
|
||||
$selectionSet,
|
||||
$subFieldASTs,
|
||||
$visitedFragmentNames
|
||||
);
|
||||
$exeContext->memoized['collectSubFields'][$uid] = $subFieldASTs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return self::executeFields($exeContext, $returnType, $result, $subFieldASTs);
|
||||
}
|
||||
}
|
||||
|
@ -158,16 +158,14 @@ class Values
|
||||
if ($type instanceof ListOfType) {
|
||||
$itemType = $type->getWrappedType();
|
||||
if (is_array($value)) {
|
||||
return array_reduce(
|
||||
$value,
|
||||
function ($acc, $item, $index) use ($itemType) {
|
||||
$errors = self::isValidPHPValue($item, $itemType);
|
||||
return array_merge($acc, Utils::map($errors, function ($error) use ($index) {
|
||||
return "In element #$index: $error";
|
||||
}));
|
||||
},
|
||||
[]
|
||||
);
|
||||
$tmp = [];
|
||||
foreach ($value as $index => $item) {
|
||||
$errors = self::isValidPHPValue($item, $itemType);
|
||||
$tmp = array_merge($tmp, Utils::map($errors, function ($error) use ($index) {
|
||||
return "In element #$index: $error";
|
||||
}));
|
||||
}
|
||||
return $tmp;
|
||||
}
|
||||
return self::isValidPHPValue($value, $itemType);
|
||||
}
|
||||
@ -190,7 +188,7 @@ class Values
|
||||
|
||||
// Ensure every defined field is valid.
|
||||
foreach ($fields as $fieldName => $tmp) {
|
||||
$newErrors = self::isValidPHPValue($value[$fieldName], $fields[$fieldName]->getType());
|
||||
$newErrors = self::isValidPHPValue(isset($value[$fieldName]) ? $value[$fieldName] : null, $fields[$fieldName]->getType());
|
||||
$errors = array_merge(
|
||||
$errors,
|
||||
Utils::map($newErrors, function ($error) use ($fieldName) {
|
||||
|
@ -3,6 +3,7 @@ namespace GraphQL;
|
||||
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\Executor;
|
||||
use GraphQL\Language\AST\Document;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\Source;
|
||||
use GraphQL\Validator\DocumentValidator;
|
||||
@ -18,9 +19,9 @@ class GraphQL
|
||||
* @param string|null $operationName
|
||||
* @return array
|
||||
*/
|
||||
public static function execute(Schema $schema, $requestString, $rootValue = null, $variableValues = null, $operationName = null)
|
||||
public static function execute(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
|
||||
{
|
||||
return self::executeAndReturnResult($schema, $requestString, $rootValue, $variableValues, $operationName)->toArray();
|
||||
return self::executeAndReturnResult($schema, $requestString, $rootValue, $contextValue, $variableValues, $operationName)->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,11 +32,15 @@ class GraphQL
|
||||
* @param null $operationName
|
||||
* @return array|ExecutionResult
|
||||
*/
|
||||
public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $variableValues = null, $operationName = null)
|
||||
public static function executeAndReturnResult(Schema $schema, $requestString, $rootValue = null, $contextValue = null, $variableValues = null, $operationName = null)
|
||||
{
|
||||
try {
|
||||
$source = new Source($requestString ?: '', 'GraphQL request');
|
||||
$documentAST = Parser::parse($source);
|
||||
if ($requestString instanceof Document) {
|
||||
$documentAST = $requestString;
|
||||
} else {
|
||||
$source = new Source($requestString ?: '', 'GraphQL request');
|
||||
$documentAST = Parser::parse($source);
|
||||
}
|
||||
|
||||
/** @var QueryComplexity $queryComplexity */
|
||||
$queryComplexity = DocumentValidator::getRule('QueryComplexity');
|
||||
@ -46,7 +51,7 @@ class GraphQL
|
||||
if (!empty($validationErrors)) {
|
||||
return new ExecutionResult(null, $validationErrors);
|
||||
} else {
|
||||
return Executor::execute($schema, $documentAST, $rootValue, $variableValues, $operationName);
|
||||
return Executor::execute($schema, $documentAST, $rootValue, $contextValue, $variableValues, $operationName);
|
||||
}
|
||||
} catch (Error $e) {
|
||||
return new ExecutionResult(null, [$e]);
|
||||
|
138
src/Schema.php
138
src/Schema.php
@ -3,18 +3,13 @@ namespace GraphQL;
|
||||
|
||||
use GraphQL\Type\Definition\AbstractType;
|
||||
use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Definition\WrappingType;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
|
||||
class Schema
|
||||
{
|
||||
@ -79,20 +74,44 @@ class Schema
|
||||
|
||||
protected function _init(array $config)
|
||||
{
|
||||
Utils::invariant(isset($config['query']) || isset($config['mutation']), "Either query or mutation type must be set");
|
||||
|
||||
$config += [
|
||||
'query' => null,
|
||||
'mutation' => null,
|
||||
'subscription' => null,
|
||||
'types' => [],
|
||||
'directives' => [],
|
||||
'validate' => true
|
||||
];
|
||||
|
||||
Utils::invariant(
|
||||
$config['query'] instanceof ObjectType,
|
||||
"Schema query must be Object Type but got: " . Utils::getVariableType($config['query'])
|
||||
);
|
||||
|
||||
$this->_queryType = $config['query'];
|
||||
|
||||
Utils::invariant(
|
||||
!$config['mutation'] || $config['mutation'] instanceof ObjectType,
|
||||
"Schema mutation must be Object Type if provided but got: " . Utils::getVariableType($config['mutation'])
|
||||
);
|
||||
$this->_mutationType = $config['mutation'];
|
||||
|
||||
Utils::invariant(
|
||||
!$config['subscription'] || $config['subscription'] instanceof ObjectType,
|
||||
"Schema subscription must be Object Type if provided but got: " . Utils::getVariableType($config['subscription'])
|
||||
);
|
||||
$this->_subscriptionType = $config['subscription'];
|
||||
|
||||
Utils::invariant(
|
||||
!$config['types'] || is_array($config['types']),
|
||||
"Schema types must be Array if provided but got: " . Utils::getVariableType($config['types'])
|
||||
);
|
||||
|
||||
Utils::invariant(
|
||||
!$config['directives'] || (is_array($config['directives']) && Utils::every($config['directives'], function($d) {return $d instanceof Directive;})),
|
||||
"Schema directives must be Directive[] if provided but got " . Utils::getVariableType($config['directives'])
|
||||
);
|
||||
|
||||
$this->_directives = array_merge($config['directives'], [
|
||||
Directive::includeDirective(),
|
||||
Directive::skipDirective()
|
||||
@ -109,11 +128,10 @@ class Schema
|
||||
$initialTypes = array_merge($initialTypes, $config['types']);
|
||||
}
|
||||
|
||||
$map = [];
|
||||
foreach ($initialTypes as $type) {
|
||||
$this->_extractTypes($type, $map);
|
||||
$this->_extractTypes($type);
|
||||
}
|
||||
$this->_typeMap = $map + Type::getInternalTypes();
|
||||
$this->_typeMap += Type::getInternalTypes();
|
||||
|
||||
// Keep track of all implementations by interface name.
|
||||
$this->_implementations = [];
|
||||
@ -124,86 +142,6 @@ class Schema
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($config['validate']) {
|
||||
$this->validate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additionaly validate schema for integrity
|
||||
*/
|
||||
public function validate()
|
||||
{
|
||||
// Enforce correct interface implementations
|
||||
foreach ($this->_typeMap as $typeName => $type) {
|
||||
if ($type instanceof ObjectType) {
|
||||
foreach ($type->getInterfaces() as $iface) {
|
||||
$this->_assertObjectImplementsInterface($type, $iface);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $object
|
||||
* @param InterfaceType $iface
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function _assertObjectImplementsInterface(ObjectType $object, InterfaceType $iface)
|
||||
{
|
||||
$objectFieldMap = $object->getFields();
|
||||
$ifaceFieldMap = $iface->getFields();
|
||||
|
||||
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
|
||||
Utils::invariant(
|
||||
isset($objectFieldMap[$fieldName]),
|
||||
"\"$iface\" expects field \"$fieldName\" but \"$object\" does not provide it"
|
||||
);
|
||||
|
||||
/** @var $ifaceField FieldDefinition */
|
||||
/** @var $objectField FieldDefinition */
|
||||
$objectField = $objectFieldMap[$fieldName];
|
||||
|
||||
Utils::invariant(
|
||||
TypeInfo::isTypeSubTypeOf($this, $objectField->getType(), $ifaceField->getType()),
|
||||
"$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " .
|
||||
"$object.$fieldName provides type \"{$objectField->getType()}\"."
|
||||
);
|
||||
|
||||
foreach ($ifaceField->args as $ifaceArg) {
|
||||
/** @var $ifaceArg FieldArgument */
|
||||
/** @var $objectArg FieldArgument */
|
||||
$argName = $ifaceArg->name;
|
||||
$objectArg = $objectField->getArg($argName);
|
||||
|
||||
// Assert interface field arg exists on object field.
|
||||
Utils::invariant(
|
||||
$objectArg,
|
||||
"$iface.$fieldName expects argument \"$argName\" but $object.$fieldName does not provide it."
|
||||
);
|
||||
|
||||
// Assert interface field arg type matches object field arg type.
|
||||
// (invariant)
|
||||
Utils::invariant(
|
||||
TypeInfo::isEqualType($ifaceArg->getType(), $objectArg->getType()),
|
||||
"$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " .
|
||||
"but $object.$fieldName($argName:) provides " .
|
||||
"type \"{$objectArg->getType()}\""
|
||||
);
|
||||
|
||||
// Assert argument set invariance.
|
||||
foreach ($objectField->args as $objectArg) {
|
||||
$argName = $objectArg->name;
|
||||
$ifaceArg = $ifaceField->getArg($argName);
|
||||
Utils::invariant(
|
||||
$ifaceArg,
|
||||
"$iface.$fieldName does not define argument \"$argName\" but " .
|
||||
"$object.$fieldName provides it."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -258,7 +196,7 @@ class Schema
|
||||
return $abstractType->getTypes();
|
||||
}
|
||||
Utils::invariant($abstractType instanceof InterfaceType);
|
||||
return $this->_implementations[$abstractType->name];
|
||||
return isset($this->_implementations[$abstractType->name]) ? $this->_implementations[$abstractType->name] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -305,24 +243,24 @@ class Schema
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function _extractTypes($type, &$map)
|
||||
protected function _extractTypes($type)
|
||||
{
|
||||
if (!$type) {
|
||||
return $map;
|
||||
return $this->_typeMap;
|
||||
}
|
||||
|
||||
if ($type instanceof WrappingType) {
|
||||
return $this->_extractTypes($type->getWrappedType(), $map);
|
||||
return $this->_extractTypes($type->getWrappedType(true));
|
||||
}
|
||||
|
||||
if (!empty($map[$type->name])) {
|
||||
if (!empty($this->_typeMap[$type->name])) {
|
||||
Utils::invariant(
|
||||
$map[$type->name] === $type,
|
||||
$this->_typeMap[$type->name] === $type,
|
||||
"Schema must contain unique named types but contains multiple types named \"$type\"."
|
||||
);
|
||||
return $map;
|
||||
return $this->_typeMap;
|
||||
}
|
||||
$map[$type->name] = $type;
|
||||
$this->_typeMap[$type->name] = $type;
|
||||
|
||||
$nestedTypes = [];
|
||||
|
||||
@ -342,8 +280,8 @@ class Schema
|
||||
}
|
||||
}
|
||||
foreach ($nestedTypes as $type) {
|
||||
$this->_extractTypes($type, $map);
|
||||
$this->_extractTypes($type);
|
||||
}
|
||||
return $map;
|
||||
return $this->_typeMap;
|
||||
}
|
||||
}
|
||||
|
@ -10,18 +10,7 @@ GraphQLInterfaceType |
|
||||
GraphQLUnionType;
|
||||
*/
|
||||
/**
|
||||
* @return array<ObjectType>
|
||||
* @return callable|null
|
||||
*/
|
||||
// public function getPossibleTypes();
|
||||
|
||||
/**
|
||||
* @return ObjectType
|
||||
*/
|
||||
// public function getObjectType($value, ResolveInfo $info);
|
||||
|
||||
/**
|
||||
* @param Type $type
|
||||
* @return bool
|
||||
*/
|
||||
// public function isPossibleType(Type $type);
|
||||
public function getResolveTypeFn();
|
||||
}
|
||||
|
@ -17,6 +17,9 @@ class FieldDefinition
|
||||
*/
|
||||
private $type;
|
||||
|
||||
/**
|
||||
* @var OutputType
|
||||
*/
|
||||
private $resolvedType;
|
||||
|
||||
/**
|
||||
|
@ -13,21 +13,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @var array<GraphQLObjectType>
|
||||
*/
|
||||
private $_implementations = [];
|
||||
|
||||
/**
|
||||
* @var \Closure[]
|
||||
*/
|
||||
private static $_lazyLoadImplementations = [];
|
||||
|
||||
/**
|
||||
* @var {[typeName: string]: boolean}
|
||||
*/
|
||||
private $_possibleTypeNames;
|
||||
|
||||
/**
|
||||
* @var callback
|
||||
*/
|
||||
@ -38,45 +23,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
*/
|
||||
public $config;
|
||||
|
||||
/**
|
||||
* Queue the update of the interfaces to know about this implementation.
|
||||
* This is an rare and unfortunate use of mutation in the type definition
|
||||
* implementations, but avoids an expensive "getPossibleTypes"
|
||||
* implementation for Interface types.
|
||||
*
|
||||
* @param ObjectType $impl
|
||||
*/
|
||||
public static function addImplementationToInterfaces(ObjectType $impl)
|
||||
{
|
||||
self::$_lazyLoadImplementations[] = function() use ($impl) {
|
||||
/** @var self $interface */
|
||||
foreach ($impl->getInterfaces() as $interface) {
|
||||
$interface->addImplementation($impl);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Process ImplementationToInterfaces Queue
|
||||
*/
|
||||
public static function loadImplementationToInterfaces()
|
||||
{
|
||||
foreach (self::$_lazyLoadImplementations as $lazyLoadImplementation) {
|
||||
$lazyLoadImplementation();
|
||||
}
|
||||
self::$_lazyLoadImplementations = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a implemented object type to interface
|
||||
*
|
||||
* @param ObjectType $impl
|
||||
*/
|
||||
protected function addImplementation(ObjectType $impl)
|
||||
{
|
||||
$this->_implementations[] = $impl;
|
||||
}
|
||||
|
||||
/**
|
||||
* InterfaceType constructor.
|
||||
* @param array $config
|
||||
@ -89,7 +35,7 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
FieldDefinition::getDefinition(),
|
||||
Config::KEY_AS_NAME | Config::MAYBE_THUNK
|
||||
),
|
||||
'resolveType' => Config::CALLBACK, // function($value, ResolveInfo $info) => ObjectType
|
||||
'resolveType' => Config::CALLBACK, // function($value, $context, ResolveInfo $info) => ObjectType
|
||||
'description' => Config::STRING
|
||||
]);
|
||||
|
||||
@ -128,38 +74,10 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<GraphQLObjectType>
|
||||
* @return callable|null
|
||||
*/
|
||||
public function getPossibleTypes()
|
||||
public function getResolveTypeFn()
|
||||
{
|
||||
return $this->_implementations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $type
|
||||
* @return bool
|
||||
*/
|
||||
public function isPossibleType(Type $type)
|
||||
{
|
||||
$possibleTypeNames = $this->_possibleTypeNames;
|
||||
if (null === $possibleTypeNames) {
|
||||
$this->_possibleTypeNames = $possibleTypeNames = array_reduce($this->getPossibleTypes(), function(&$map, Type $possibleType) {
|
||||
$map[$possibleType->name] = true;
|
||||
return $map;
|
||||
}, []);
|
||||
}
|
||||
return !empty($possibleTypeNames[$type->name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param ResolveInfo $info
|
||||
* @return Type|null
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function getObjectType($value, ResolveInfo $info)
|
||||
{
|
||||
$resolver = $this->_resolveTypeFn;
|
||||
return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
|
||||
return $this->_resolveTypeFn;
|
||||
}
|
||||
}
|
||||
|
@ -95,10 +95,6 @@ class ObjectType extends Type implements OutputType, CompositeType
|
||||
$this->resolveFieldFn = isset($config['resolveField']) ? $config['resolveField'] : null;
|
||||
$this->_isTypeOf = isset($config['isTypeOf']) ? $config['isTypeOf'] : null;
|
||||
$this->config = $config;
|
||||
|
||||
if (isset($config['interfaces'])) {
|
||||
InterfaceType::addImplementationToInterfaces($this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -152,10 +148,11 @@ class ObjectType extends Type implements OutputType, CompositeType
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param $context
|
||||
* @return bool|null
|
||||
*/
|
||||
public function isTypeOf($value, ResolveInfo $info)
|
||||
public function isTypeOf($value, $context, ResolveInfo $info)
|
||||
{
|
||||
return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value, $info) : null;
|
||||
return isset($this->_isTypeOf) ? call_user_func($this->_isTypeOf, $value, $context, $info) : null;
|
||||
}
|
||||
}
|
||||
|
@ -185,30 +185,18 @@ GraphQLNonNull;
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param mixed $context
|
||||
* @param AbstractType $abstractType
|
||||
* @return Type
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function getTypeOf($value, ResolveInfo $info, AbstractType $abstractType)
|
||||
public static function getTypeOf($value, $context, ResolveInfo $info, AbstractType $abstractType)
|
||||
{
|
||||
$possibleTypes = $abstractType->getPossibleTypes();
|
||||
$possibleTypes = $info->schema->getPossibleTypes($abstractType);
|
||||
|
||||
for ($i = 0; $i < count($possibleTypes); $i++) {
|
||||
foreach ($possibleTypes as $type) {
|
||||
/** @var ObjectType $type */
|
||||
$type = $possibleTypes[$i];
|
||||
$isTypeOf = $type->isTypeOf($value, $info);
|
||||
|
||||
if ($isTypeOf === null) {
|
||||
// TODO: move this to a JS impl specific type system validation step
|
||||
// so the error can be found before execution.
|
||||
throw new \Exception(
|
||||
'Non-Object Type ' . $abstractType->name . ' does not implement ' .
|
||||
'getObjectType and Object Type ' . $type->name . ' does not implement ' .
|
||||
'isTypeOf. There is no way to determine if a value is of this type.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($isTypeOf) {
|
||||
if ($type->isTypeOf($value, $context, $info)) {
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
/**
|
||||
* @var callback
|
||||
*/
|
||||
private $_resolveType;
|
||||
private $_resolveTypeFn;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
@ -44,7 +44,7 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
$this->name = $config['name'];
|
||||
$this->description = isset($config['description']) ? $config['description'] : null;
|
||||
$this->_types = $config['types'];
|
||||
$this->_resolveType = isset($config['resolveType']) ? $config['resolveType'] : null;
|
||||
$this->_resolveTypeFn = isset($config['resolveType']) ? $config['resolveType'] : null;
|
||||
$this->_config = $config;
|
||||
}
|
||||
|
||||
@ -85,15 +85,10 @@ class UnionType extends Type implements AbstractType, OutputType, CompositeType
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $value
|
||||
* @param ResolveInfo $info
|
||||
*
|
||||
* @return Type
|
||||
* @throws \Exception
|
||||
* @return callable|null
|
||||
*/
|
||||
public function getObjectType($value, ResolveInfo $info)
|
||||
public function getResolveTypeFn()
|
||||
{
|
||||
$resolver = $this->_resolveType;
|
||||
return $resolver ? call_user_func($resolver, $value, $info) : Type::getTypeOf($value, $info, $this);
|
||||
return $this->_resolveTypeFn;
|
||||
}
|
||||
}
|
||||
|
@ -426,7 +426,7 @@ EOD;
|
||||
],
|
||||
'possibleTypes' => [
|
||||
'type' => Type::listOf(Type::nonNull([__CLASS__, '_type'])),
|
||||
'resolve' => function ($type, $args, ResolveInfo $info) {
|
||||
'resolve' => function ($type, $args, $context, ResolveInfo $info) {
|
||||
if ($type instanceof InterfaceType || $type instanceof UnionType) {
|
||||
return $info->schema->getPossibleTypes($type);
|
||||
}
|
||||
@ -635,6 +635,7 @@ EOD;
|
||||
'resolve' => function (
|
||||
$source,
|
||||
$args,
|
||||
$context,
|
||||
ResolveInfo $info
|
||||
) {
|
||||
return $info->schema;
|
||||
@ -654,7 +655,7 @@ EOD;
|
||||
'args' => [
|
||||
['name' => 'name', 'type' => Type::nonNull(Type::string())]
|
||||
],
|
||||
'resolve' => function ($source, $args, ResolveInfo $info) {
|
||||
'resolve' => function ($source, $args, $context, ResolveInfo $info) {
|
||||
return $info->schema->getType($args['name']);
|
||||
}
|
||||
]);
|
||||
@ -673,6 +674,7 @@ EOD;
|
||||
'resolve' => function (
|
||||
$source,
|
||||
$args,
|
||||
$context,
|
||||
ResolveInfo $info
|
||||
) {
|
||||
return $info->parentType->name;
|
||||
|
@ -3,10 +3,13 @@ namespace GraphQL\Type;
|
||||
|
||||
use GraphQL\Error;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Utils;
|
||||
|
||||
class SchemaValidator
|
||||
{
|
||||
@ -19,7 +22,8 @@ class SchemaValidator
|
||||
self::noInputTypesAsOutputFieldsRule(),
|
||||
self::noOutputTypesAsInputArgsRule(),
|
||||
self::typesInterfacesMustShowThemAsPossibleRule(),
|
||||
self::interfacePossibleTypesMustImplementTheInterfaceRule()
|
||||
self::interfacePossibleTypesMustImplementTheInterfaceRule(),
|
||||
self::interfacesAreCorrectlyImplemented()
|
||||
];
|
||||
}
|
||||
return self::$rules;
|
||||
@ -30,7 +34,7 @@ class SchemaValidator
|
||||
return function ($context) {
|
||||
$operationMayNotBeInputType = function (Type $type, $operation) {
|
||||
if (!Type::isOutputType($type)) {
|
||||
return new Error("Schema $operation type $type must be an object type!");
|
||||
return new Error("Schema $operation must be Object Type but got: $type.");
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@ -56,6 +60,14 @@ class SchemaValidator
|
||||
}
|
||||
}
|
||||
|
||||
$subscriptionType = $schema->getSubscriptionType();
|
||||
if ($subscriptionType) {
|
||||
$subscriptionError = $operationMayNotBeInputType($subscriptionType, 'subscription');
|
||||
if ($subscriptionError !== null) {
|
||||
$errors[] = $subscriptionError;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($typeMap as $typeName => $type) {
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||
$fields = $type->getFields();
|
||||
@ -152,6 +164,91 @@ class SchemaValidator
|
||||
};
|
||||
}
|
||||
|
||||
// Enforce correct interface implementations
|
||||
public static function interfacesAreCorrectlyImplemented()
|
||||
{
|
||||
return function($context) {
|
||||
/** @var Schema $schema */
|
||||
$schema = $context['schema'];
|
||||
|
||||
$errors = [];
|
||||
foreach ($schema->getTypeMap() as $typeName => $type) {
|
||||
if ($type instanceof ObjectType) {
|
||||
foreach ($type->getInterfaces() as $iface) {
|
||||
try {
|
||||
// FIXME: rework to return errors instead
|
||||
self::assertObjectImplementsInterface($schema, $type, $iface);
|
||||
} catch (\Exception $e) {
|
||||
$errors[] = $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $errors;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectType $object
|
||||
* @param InterfaceType $iface
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected static function assertObjectImplementsInterface(Schema $schema, ObjectType $object, InterfaceType $iface)
|
||||
{
|
||||
$objectFieldMap = $object->getFields();
|
||||
$ifaceFieldMap = $iface->getFields();
|
||||
|
||||
foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
|
||||
Utils::invariant(
|
||||
isset($objectFieldMap[$fieldName]),
|
||||
"\"$iface\" expects field \"$fieldName\" but \"$object\" does not provide it"
|
||||
);
|
||||
|
||||
/** @var $ifaceField FieldDefinition */
|
||||
/** @var $objectField FieldDefinition */
|
||||
$objectField = $objectFieldMap[$fieldName];
|
||||
|
||||
Utils::invariant(
|
||||
Utils\TypeInfo::isTypeSubTypeOf($schema, $objectField->getType(), $ifaceField->getType()),
|
||||
"$iface.$fieldName expects type \"{$ifaceField->getType()}\" but " .
|
||||
"$object.$fieldName provides type \"{$objectField->getType()}\"."
|
||||
);
|
||||
|
||||
foreach ($ifaceField->args as $ifaceArg) {
|
||||
/** @var $ifaceArg FieldArgument */
|
||||
/** @var $objectArg FieldArgument */
|
||||
$argName = $ifaceArg->name;
|
||||
$objectArg = $objectField->getArg($argName);
|
||||
|
||||
// Assert interface field arg exists on object field.
|
||||
Utils::invariant(
|
||||
$objectArg,
|
||||
"$iface.$fieldName expects argument \"$argName\" but $object.$fieldName does not provide it."
|
||||
);
|
||||
|
||||
// Assert interface field arg type matches object field arg type.
|
||||
// (invariant)
|
||||
Utils::invariant(
|
||||
Utils\TypeInfo::isEqualType($ifaceArg->getType(), $objectArg->getType()),
|
||||
"$iface.$fieldName($argName:) expects type \"{$ifaceArg->getType()}\" " .
|
||||
"but $object.$fieldName($argName:) provides " .
|
||||
"type \"{$objectArg->getType()}\""
|
||||
);
|
||||
|
||||
// Assert argument set invariance.
|
||||
foreach ($objectField->args as $objectArg) {
|
||||
$argName = $objectArg->name;
|
||||
$ifaceArg = $ifaceField->getArg($argName);
|
||||
Utils::invariant(
|
||||
$ifaceArg,
|
||||
"$iface.$fieldName does not define argument \"$argName\" but " .
|
||||
"$object.$fieldName provides it."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Schema $schema
|
||||
* @param array <callable>|null $argRules
|
||||
@ -171,4 +268,4 @@ class SchemaValidator
|
||||
}
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
<?php
|
||||
namespace GraphQL;
|
||||
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\WrappingType;
|
||||
use GraphQL\Utils\SchemaUtils;
|
||||
use \Traversable, \InvalidArgumentException;
|
||||
|
||||
class Utils
|
||||
@ -168,6 +171,21 @@ class Utils
|
||||
return $grouped;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $traversable
|
||||
* @param callable $predicate
|
||||
* @return bool
|
||||
*/
|
||||
public static function every($traversable, callable $predicate)
|
||||
{
|
||||
foreach ($traversable as $key => $value) {
|
||||
if (!$predicate($value, $key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $test
|
||||
* @param string $message
|
||||
@ -193,6 +211,13 @@ class Utils
|
||||
*/
|
||||
public static function getVariableType($var)
|
||||
{
|
||||
if ($var instanceof Type) {
|
||||
// FIXME: Replace with schema printer call
|
||||
if ($var instanceof WrappingType) {
|
||||
$var = $var->getWrappedType(true);
|
||||
}
|
||||
return $var->name;
|
||||
}
|
||||
return is_object($var) ? get_class($var) : gettype($var);
|
||||
}
|
||||
|
||||
|
@ -3,17 +3,25 @@ namespace GraphQL\Tests\Executor;
|
||||
|
||||
use GraphQL\Executor\ExecutionResult;
|
||||
use GraphQL\Executor\Executor;
|
||||
use GraphQL\FormattedError;
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
|
||||
spl_autoload_call('GraphQL\Tests\Executor\TestClasses');
|
||||
|
||||
class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
|
||||
// Execute: Handles execution of abstract types
|
||||
|
||||
/**
|
||||
* @it isTypeOf used to resolve runtime type for Interface
|
||||
*/
|
||||
public function testIsTypeOfUsedToResolveRuntimeTypeForInterface()
|
||||
{
|
||||
// isTypeOf used to resolve runtime type for Interface
|
||||
@ -58,7 +66,8 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
]
|
||||
]
|
||||
])
|
||||
]),
|
||||
'types' => [$catType, $dogType]
|
||||
]);
|
||||
|
||||
$query = '{
|
||||
@ -83,10 +92,11 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($schema, Parser::parse($query)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @it isTypeOf used to resolve runtime type for Union
|
||||
*/
|
||||
public function testIsTypeOfUsedToResolveRuntimeTypeForUnion()
|
||||
{
|
||||
// isTypeOf used to resolve runtime type for Union
|
||||
|
||||
$dogType = new ObjectType([
|
||||
'name' => 'Dog',
|
||||
'isTypeOf' => function($obj) { return $obj instanceof Dog; },
|
||||
@ -148,6 +158,9 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($schema, Parser::parse($query)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @it resolveType on Interface yields useful error
|
||||
*/
|
||||
function testResolveTypeOnInterfaceYieldsUsefulError()
|
||||
{
|
||||
$DogType = null;
|
||||
@ -198,21 +211,24 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema(new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'pets' => [
|
||||
'type' => Type::listOf($PetType),
|
||||
'resolve' => function () {
|
||||
return [
|
||||
new Dog('Odie', true),
|
||||
new Cat('Garfield', false),
|
||||
new Human('Jon')
|
||||
];
|
||||
}
|
||||
]
|
||||
]
|
||||
]));
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'pets' => [
|
||||
'type' => Type::listOf($PetType),
|
||||
'resolve' => function () {
|
||||
return [
|
||||
new Dog('Odie', true),
|
||||
new Cat('Garfield', false),
|
||||
new Human('Jon')
|
||||
];
|
||||
}
|
||||
]
|
||||
],
|
||||
]),
|
||||
'types' => [$DogType, $CatType]
|
||||
]);
|
||||
|
||||
|
||||
$query = '{
|
||||
@ -236,11 +252,110 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
],
|
||||
'errors' => [
|
||||
[ 'message' => 'Runtime Object type "Human" is not a possible type for "Pet".' ]
|
||||
FormattedError::create(
|
||||
'Runtime Object type "Human" is not a possible type for "Pet".',
|
||||
[new SourceLocation(2, 11)]
|
||||
)
|
||||
]
|
||||
];
|
||||
$actual = GraphQL::execute($schema, $query);
|
||||
|
||||
$this->assertEquals($expected, Executor::execute($schema, Parser::parse($query))->toArray());
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it resolveType on Union yields useful error
|
||||
*/
|
||||
public function testResolveTypeOnUnionYieldsUsefulError()
|
||||
{
|
||||
$HumanType = new ObjectType([
|
||||
'name' => 'Human',
|
||||
'fields' => [
|
||||
'name' => ['type' => Type::string()],
|
||||
]
|
||||
]);
|
||||
|
||||
$DogType = new ObjectType([
|
||||
'name' => 'Dog',
|
||||
'fields' => [
|
||||
'name' => ['type' => Type::string()],
|
||||
'woofs' => ['type' => Type::boolean()],
|
||||
]
|
||||
]);
|
||||
|
||||
$CatType = new ObjectType([
|
||||
'name' => 'Cat',
|
||||
'fields' => [
|
||||
'name' => ['type' => Type::string()],
|
||||
'meows' => ['type' => Type::boolean()],
|
||||
]
|
||||
]);
|
||||
|
||||
$PetType = new UnionType([
|
||||
'name' => 'Pet',
|
||||
'resolveType' => function ($obj) use ($DogType, $CatType, $HumanType) {
|
||||
if ($obj instanceof Dog) {
|
||||
return $DogType;
|
||||
}
|
||||
if ($obj instanceof Cat) {
|
||||
return $CatType;
|
||||
}
|
||||
if ($obj instanceof Human) {
|
||||
return $HumanType;
|
||||
}
|
||||
},
|
||||
'types' => [$DogType, $CatType]
|
||||
]);
|
||||
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'pets' => [
|
||||
'type' => Type::listOf($PetType),
|
||||
'resolve' => function () {
|
||||
return [
|
||||
new Dog('Odie', true),
|
||||
new Cat('Garfield', false),
|
||||
new Human('Jon')
|
||||
];
|
||||
}
|
||||
]
|
||||
]
|
||||
])
|
||||
]);
|
||||
|
||||
$query = '{
|
||||
pets {
|
||||
... on Dog {
|
||||
name
|
||||
woofs
|
||||
}
|
||||
... on Cat {
|
||||
name
|
||||
meows
|
||||
}
|
||||
}
|
||||
}';
|
||||
|
||||
$result = GraphQL::execute($schema, $query);
|
||||
$expected = [
|
||||
'data' => [
|
||||
'pets' => [
|
||||
['name' => 'Odie',
|
||||
'woofs' => true],
|
||||
['name' => 'Garfield',
|
||||
'meows' => false],
|
||||
null
|
||||
]
|
||||
],
|
||||
'errors' => [
|
||||
FormattedError::create(
|
||||
'Runtime Object type "Human" is not a possible type for "Pet".',
|
||||
[new SourceLocation(2, 11)]
|
||||
)
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
|
@ -9,14 +9,20 @@ use GraphQL\Type\Definition\Type;
|
||||
|
||||
class DirectivesTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Execute: handles directives
|
||||
// works without directives
|
||||
// Describe: Execute: handles directives
|
||||
|
||||
/**
|
||||
* @describe works without directives
|
||||
* @it basic query works
|
||||
*/
|
||||
public function testWorksWithoutDirectives()
|
||||
{
|
||||
// basic query works
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery('{ a, b }'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @describe works on scalars
|
||||
*/
|
||||
public function testWorksOnScalars()
|
||||
{
|
||||
// if true includes scalar
|
||||
@ -32,6 +38,9 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery('{ a, b @skip(if: true) }'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @describe works on fragment spreads
|
||||
*/
|
||||
public function testWorksOnFragmentSpreads()
|
||||
{
|
||||
// if false omits fragment spread
|
||||
@ -83,6 +92,9 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
}
|
||||
|
||||
/**
|
||||
* @describe works on inline fragment
|
||||
*/
|
||||
public function testWorksOnInlineFragment()
|
||||
{
|
||||
// if false omits inline fragment
|
||||
@ -142,57 +154,80 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
}
|
||||
|
||||
public function testWorksOnFragment()
|
||||
/**
|
||||
* @describe works on anonymous inline fragment
|
||||
*/
|
||||
public function testWorksOnAnonymousInlineFragment()
|
||||
{
|
||||
// if false omits fragment
|
||||
// if false omits anonymous inline fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag
|
||||
}
|
||||
fragment Frag on TestType @include(if: false) {
|
||||
b
|
||||
... @include(if: false) {
|
||||
b
|
||||
}
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
|
||||
// if true includes fragment
|
||||
// if true includes anonymous inline fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag
|
||||
}
|
||||
fragment Frag on TestType @include(if: true) {
|
||||
b
|
||||
... @include(if: true) {
|
||||
b
|
||||
}
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
|
||||
|
||||
// unless false includes fragment
|
||||
// unless false includes anonymous inline fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag
|
||||
}
|
||||
fragment Frag on TestType @skip(if: false) {
|
||||
b
|
||||
... @skip(if: false) {
|
||||
b
|
||||
}
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a', 'b' => 'b']], $this->executeTestQuery($q));
|
||||
|
||||
// unless true omits fragment
|
||||
// unless true includes anonymous inline fragment
|
||||
$q = '
|
||||
query Q {
|
||||
a
|
||||
...Frag
|
||||
}
|
||||
fragment Frag on TestType @skip(if: true) {
|
||||
b
|
||||
... @skip(if: true) {
|
||||
b
|
||||
}
|
||||
}
|
||||
';
|
||||
$this->assertEquals(['data' => ['a' => 'a']], $this->executeTestQuery($q));
|
||||
}
|
||||
|
||||
/**
|
||||
* @describe works with skip and include directives
|
||||
*/
|
||||
public function testWorksWithSkipAndIncludeDirectives()
|
||||
{
|
||||
// include and no skip
|
||||
$this->assertEquals(
|
||||
['data' => ['a' => 'a', 'b' => 'b']],
|
||||
$this->executeTestQuery('{ a, b @include(if: true) @skip(if: false) }')
|
||||
);
|
||||
|
||||
// include and skip
|
||||
$this->assertEquals(
|
||||
['data' => ['a' => 'a']],
|
||||
$this->executeTestQuery('{ a, b @include(if: true) @skip(if: true) }')
|
||||
);
|
||||
|
||||
// no include or skip
|
||||
$this->assertEquals(
|
||||
['data' => ['a' => 'a']],
|
||||
$this->executeTestQuery('{ a, b @include(if: false) @skip(if: false) }')
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@ -202,13 +237,18 @@ class DirectivesTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
private static function getSchema()
|
||||
{
|
||||
return self::$schema ?: (self::$schema = new Schema(new ObjectType([
|
||||
'name' => 'TestType',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
'b' => ['type' => Type::string()]
|
||||
]
|
||||
])));
|
||||
if (!self::$schema) {
|
||||
self::$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'TestType',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
'b' => ['type' => Type::string()]
|
||||
]
|
||||
])
|
||||
]);
|
||||
}
|
||||
return self::$schema;
|
||||
}
|
||||
|
||||
private static function getData()
|
||||
|
@ -11,6 +11,10 @@ use GraphQL\Type\Definition\Type;
|
||||
class ExecutorSchemaTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Execute: Handles execution with a complex schema
|
||||
|
||||
/**
|
||||
* @it executes using a schema
|
||||
*/
|
||||
public function testExecutesUsingASchema()
|
||||
{
|
||||
$BlogArticle = null;
|
||||
@ -83,7 +87,7 @@ class ExecutorSchemaTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]);
|
||||
|
||||
$BlogSchema = new Schema($BlogQuery);
|
||||
$BlogSchema = new Schema(['query' => $BlogQuery]);
|
||||
|
||||
|
||||
$request = '
|
||||
|
@ -137,9 +137,9 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
'deeper' => [ 'type' => Type::listOf($dataType) ]
|
||||
]
|
||||
]);
|
||||
$schema = new Schema($dataType);
|
||||
$schema = new Schema(['query' => $dataType]);
|
||||
|
||||
$this->assertEquals($expected, Executor::execute($schema, $ast, $data, ['size' => 100], 'Example')->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($schema, $ast, $data, null, ['size' => 100], 'Example')->toArray());
|
||||
}
|
||||
|
||||
public function testMergesParallelFragments()
|
||||
@ -180,7 +180,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]
|
||||
]);
|
||||
$schema = new Schema($Type);
|
||||
$schema = new Schema(['query' => $Type]);
|
||||
$expected = [
|
||||
'data' => [
|
||||
'a' => 'Apple',
|
||||
@ -212,20 +212,22 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
];
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
$schema = new Schema(new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => [
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($context) use ($doc, &$gotHere) {
|
||||
$this->assertEquals('thing', $context['contextThing']);
|
||||
$gotHere = true;
|
||||
}
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => [
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($context) use ($doc, &$gotHere) {
|
||||
$this->assertEquals('thing', $context['contextThing']);
|
||||
$gotHere = true;
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
]));
|
||||
])
|
||||
]);
|
||||
|
||||
Executor::execute($schema, $ast, $data, [], 'Example');
|
||||
Executor::execute($schema, $ast, $data, null, [], 'Example');
|
||||
$this->assertEquals(true, $gotHere);
|
||||
}
|
||||
|
||||
@ -240,24 +242,26 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
$gotHere = false;
|
||||
|
||||
$docAst = Parser::parse($doc);
|
||||
$schema = new Schema(new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'b' => [
|
||||
'args' => [
|
||||
'numArg' => ['type' => Type::int()],
|
||||
'stringArg' => ['type' => Type::string()]
|
||||
],
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($_, $args) use (&$gotHere) {
|
||||
$this->assertEquals(123, $args['numArg']);
|
||||
$this->assertEquals('foo', $args['stringArg']);
|
||||
$gotHere = true;
|
||||
}
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'b' => [
|
||||
'args' => [
|
||||
'numArg' => ['type' => Type::int()],
|
||||
'stringArg' => ['type' => Type::string()]
|
||||
],
|
||||
'type' => Type::string(),
|
||||
'resolve' => function ($_, $args) use (&$gotHere) {
|
||||
$this->assertEquals(123, $args['numArg']);
|
||||
$this->assertEquals('foo', $args['stringArg']);
|
||||
$gotHere = true;
|
||||
}
|
||||
]
|
||||
]
|
||||
]
|
||||
]));
|
||||
Executor::execute($schema, $docAst, null, [], 'Example');
|
||||
])
|
||||
]);
|
||||
Executor::execute($schema, $docAst, null, null, [], 'Example');
|
||||
$this->assertSame($gotHere, true);
|
||||
}
|
||||
|
||||
@ -296,17 +300,19 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
];
|
||||
|
||||
$docAst = Parser::parse($doc);
|
||||
$schema = new Schema(new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'sync' => ['type' => Type::string()],
|
||||
'syncError' => ['type' => Type::string()],
|
||||
'syncRawError' => [ 'type' => Type::string() ],
|
||||
'async' => ['type' => Type::string()],
|
||||
'asyncReject' => ['type' => Type::string() ],
|
||||
'asyncError' => ['type' => Type::string()],
|
||||
]
|
||||
]));
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'sync' => ['type' => Type::string()],
|
||||
'syncError' => ['type' => Type::string()],
|
||||
'syncRawError' => [ 'type' => Type::string() ],
|
||||
'async' => ['type' => Type::string()],
|
||||
'asyncReject' => ['type' => Type::string() ],
|
||||
'asyncError' => ['type' => Type::string()],
|
||||
]
|
||||
])
|
||||
]);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
@ -336,12 +342,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
$doc = '{ a }';
|
||||
$data = ['a' => 'b'];
|
||||
$ast = Parser::parse($doc);
|
||||
$schema = new Schema(new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
]
|
||||
]));
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
]
|
||||
])
|
||||
]);
|
||||
|
||||
$ex = Executor::execute($schema, $ast, $data);
|
||||
|
||||
@ -353,12 +361,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
$doc = 'query Example { a }';
|
||||
$data = [ 'a' => 'b' ];
|
||||
$ast = Parser::parse($doc);
|
||||
$schema = new Schema(new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => [ 'type' => Type::string() ],
|
||||
]
|
||||
]));
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => [ 'type' => Type::string() ],
|
||||
]
|
||||
])
|
||||
]);
|
||||
|
||||
$ex = Executor::execute($schema, $ast, $data);
|
||||
$this->assertEquals(['data' => ['a' => 'b']], $ex->toArray());
|
||||
@ -369,12 +379,14 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
$doc = 'query Example { a } query OtherExample { a }';
|
||||
$data = [ 'a' => 'b' ];
|
||||
$ast = Parser::parse($doc);
|
||||
$schema = new Schema(new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => [ 'type' => Type::string() ],
|
||||
]
|
||||
]));
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => [ 'type' => Type::string() ],
|
||||
]
|
||||
])
|
||||
]);
|
||||
|
||||
try {
|
||||
Executor::execute($schema, $ast, $data);
|
||||
@ -389,22 +401,22 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
$doc = 'query Q { a } mutation M { c }';
|
||||
$data = ['a' => 'b', 'c' => 'd'];
|
||||
$ast = Parser::parse($doc);
|
||||
$schema = new Schema(
|
||||
new ObjectType([
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Q',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
]
|
||||
]),
|
||||
new ObjectType([
|
||||
'mutation' => new ObjectType([
|
||||
'name' => 'M',
|
||||
'fields' => [
|
||||
'c' => ['type' => Type::string()],
|
||||
]
|
||||
])
|
||||
);
|
||||
]);
|
||||
|
||||
$queryResult = Executor::execute($schema, $ast, $data, [], 'Q');
|
||||
$queryResult = Executor::execute($schema, $ast, $data, null, [], 'Q');
|
||||
$this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray());
|
||||
}
|
||||
|
||||
@ -413,21 +425,21 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
$doc = 'query Q { a } mutation M { c }';
|
||||
$data = [ 'a' => 'b', 'c' => 'd' ];
|
||||
$ast = Parser::parse($doc);
|
||||
$schema = new Schema(
|
||||
new ObjectType([
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Q',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
]
|
||||
]),
|
||||
new ObjectType([
|
||||
'mutation' => new ObjectType([
|
||||
'name' => 'M',
|
||||
'fields' => [
|
||||
'c' => [ 'type' => Type::string() ],
|
||||
]
|
||||
])
|
||||
);
|
||||
$mutationResult = Executor::execute($schema, $ast, $data, [], 'M');
|
||||
]);
|
||||
$mutationResult = Executor::execute($schema, $ast, $data, null, [], 'M');
|
||||
$this->assertEquals(['data' => ['c' => 'd']], $mutationResult->toArray());
|
||||
}
|
||||
|
||||
@ -447,14 +459,16 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$data = ['a' => 'b'];
|
||||
$ast = Parser::parse($doc);
|
||||
$schema = new Schema(new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
]
|
||||
]));
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
]
|
||||
])
|
||||
]);
|
||||
|
||||
$queryResult = Executor::execute($schema, $ast, $data, [], 'Q');
|
||||
$queryResult = Executor::execute($schema, $ast, $data, null, [], 'Q');
|
||||
$this->assertEquals(['data' => ['a' => 'b']], $queryResult->toArray());
|
||||
}
|
||||
|
||||
@ -464,28 +478,28 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
thisIsIllegalDontIncludeMe
|
||||
}';
|
||||
$ast = Parser::parse($doc);
|
||||
$schema = new Schema(
|
||||
new ObjectType([
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Q',
|
||||
'fields' => [
|
||||
'a' => ['type' => Type::string()],
|
||||
]
|
||||
]),
|
||||
new ObjectType([
|
||||
'mutation' => new ObjectType([
|
||||
'name' => 'M',
|
||||
'fields' => [
|
||||
'c' => ['type' => Type::string()],
|
||||
]
|
||||
])
|
||||
);
|
||||
]);
|
||||
$mutationResult = Executor::execute($schema, $ast);
|
||||
$this->assertEquals(['data' => []], $mutationResult->toArray());
|
||||
}
|
||||
|
||||
public function testDoesNotIncludeArgumentsThatWereNotSet()
|
||||
{
|
||||
$schema = new Schema(
|
||||
new ObjectType([
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'field' => [
|
||||
@ -501,7 +515,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
]);
|
||||
|
||||
$query = Parser::parse('{ field(a: true, c: false, e: 0) }');
|
||||
$result = Executor::execute($schema, $query);
|
||||
@ -516,8 +530,8 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
public function testSubstitutesArgumentWithDefaultValue()
|
||||
{
|
||||
$schema = new Schema(
|
||||
new ObjectType([
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Type',
|
||||
'fields' => [
|
||||
'field' => [
|
||||
@ -535,7 +549,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]
|
||||
])
|
||||
);
|
||||
]);
|
||||
|
||||
$query = Parser::parse('{ field }');
|
||||
$result = Executor::execute($schema, $query);
|
||||
|
@ -12,376 +12,186 @@ use GraphQL\Type\Definition\Type;
|
||||
|
||||
class ListsTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Execute: Handles list nullability
|
||||
|
||||
public function testHandlesListsWhenTheyReturnNonNullValues()
|
||||
private function check($testType, $testData, $expected)
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
list,
|
||||
}
|
||||
}
|
||||
';
|
||||
$data = ['test' => $testData];
|
||||
$dataType = null;
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = ['data' => ['nest' => ['list' => [1,2]]]];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesListsOfNonNullsWhenTheyReturnNonNullValues()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
listOfNonNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => [
|
||||
'listOfNonNull' => [1, 2],
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesNonNullListsOfWhenTheyReturnNonNullValues()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
nonNullList,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => [
|
||||
'nonNullList' => [1, 2],
|
||||
]
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNonNullValues()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
nonNullListOfNonNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => [
|
||||
'nonNullListOfNonNull' => [1, 2],
|
||||
]
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesListsWhenTheyReturnNullAsAValue()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
listContainsNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => [
|
||||
'listContainsNull' => [1, null, 2],
|
||||
]
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesListsOfNonNullsWhenTheyReturnNullAsAValue()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
listOfNonNullContainsNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => [
|
||||
'listOfNonNullContainsNull' => null
|
||||
]
|
||||
],
|
||||
'errors' => [
|
||||
FormattedError::create(
|
||||
'Cannot return null for non-nullable type.',
|
||||
[new SourceLocation(4, 11)]
|
||||
)
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesNonNullListsOfWhenTheyReturnNullAsAValue()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
nonNullListContainsNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => ['nonNullListContainsNull' => [1, null, 2]]
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNullAsAValue()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
nonNullListOfNonNullContainsNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => null
|
||||
],
|
||||
'errors' => [
|
||||
FormattedError::create(
|
||||
'Cannot return null for non-nullable type.',
|
||||
[new SourceLocation(4, 11)]
|
||||
)
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesListsWhenTheyReturnNull()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
listReturnsNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => [
|
||||
'listReturnsNull' => null
|
||||
]
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesListsOfNonNullsWhenTheyReturnNull()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
listOfNonNullReturnsNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => [
|
||||
'listOfNonNullReturnsNull' => null
|
||||
]
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesNonNullListsOfWhenTheyReturnNull()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
nonNullListReturnsNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => null,
|
||||
],
|
||||
'errors' => [
|
||||
FormattedError::create(
|
||||
'Cannot return null for non-nullable type.',
|
||||
[new SourceLocation(4, 11)]
|
||||
)
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testHandlesNonNullListsOfNonNullsWhenTheyReturnNull()
|
||||
{
|
||||
$doc = '
|
||||
query Q {
|
||||
nest {
|
||||
nonNullListOfNonNullReturnsNull,
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$ast = Parser::parse($doc);
|
||||
|
||||
$expected = [
|
||||
'data' => [
|
||||
'nest' => null
|
||||
],
|
||||
'errors' => [
|
||||
FormattedError::create(
|
||||
'Cannot return null for non-nullable type.',
|
||||
[new SourceLocation(4, 11)]
|
||||
)
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, $this->data(), [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
|
||||
|
||||
private function schema()
|
||||
{
|
||||
$dataType = new ObjectType([
|
||||
'name' => 'DataType',
|
||||
'fields' => [
|
||||
'list' => [
|
||||
'type' => Type::listOf(Type::int())
|
||||
],
|
||||
'listOfNonNull' => [
|
||||
'type' => Type::listOf(Type::nonNull(Type::int()))
|
||||
],
|
||||
'nonNullList' => [
|
||||
'type' => Type::nonNull(Type::listOf(Type::int()))
|
||||
],
|
||||
'nonNullListOfNonNull' => [
|
||||
'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::int())))
|
||||
],
|
||||
'listContainsNull' => [
|
||||
'type' => Type::listOf(Type::int())
|
||||
],
|
||||
'listOfNonNullContainsNull' => [
|
||||
'type' => Type::listOf(Type::nonNull(Type::int())),
|
||||
],
|
||||
'nonNullListContainsNull' => [
|
||||
'type' => Type::nonNull(Type::listOf(Type::int()))
|
||||
],
|
||||
'nonNullListOfNonNullContainsNull' => [
|
||||
'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::int())))
|
||||
],
|
||||
'listReturnsNull' => [
|
||||
'type' => Type::listOf(Type::int())
|
||||
],
|
||||
'listOfNonNullReturnsNull' => [
|
||||
'type' => Type::listOf(Type::nonNull(Type::int()))
|
||||
],
|
||||
'nonNullListReturnsNull' => [
|
||||
'type' => Type::nonNull(Type::listOf(Type::int()))
|
||||
],
|
||||
'nonNullListOfNonNullReturnsNull' => [
|
||||
'type' => Type::nonNull(Type::listOf(Type::nonNull(Type::int())))
|
||||
],
|
||||
'nest' => ['type' => function () use (&$dataType) {
|
||||
return $dataType;
|
||||
}]
|
||||
]
|
||||
'fields' => function () use (&$testType, &$dataType, $data) {
|
||||
return [
|
||||
'test' => [
|
||||
'type' => $testType
|
||||
],
|
||||
'nest' => [
|
||||
'type' => $dataType,
|
||||
'resolve' => function () use ($data) {
|
||||
return $data;
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
]);
|
||||
|
||||
$schema = new Schema($dataType);
|
||||
return $schema;
|
||||
$schema = new Schema([
|
||||
'query' => $dataType
|
||||
]);
|
||||
|
||||
$ast = Parser::parse('{ nest { test } }');
|
||||
|
||||
$result = Executor::execute($schema, $ast, $data);
|
||||
$this->assertEquals($expected, $result->toArray());
|
||||
}
|
||||
|
||||
private function data()
|
||||
// Describe: Execute: Handles list nullability
|
||||
|
||||
/**
|
||||
* @describe [T]
|
||||
*/
|
||||
public function testHandlesNullableLists()
|
||||
{
|
||||
return [
|
||||
'list' => function () {
|
||||
return [1, 2];
|
||||
},
|
||||
'listOfNonNull' => function () {
|
||||
return [1, 2];
|
||||
},
|
||||
'nonNullList' => function () {
|
||||
return [1, 2];
|
||||
},
|
||||
'nonNullListOfNonNull' => function () {
|
||||
return [1, 2];
|
||||
},
|
||||
'listContainsNull' => function () {
|
||||
return [1, null, 2];
|
||||
},
|
||||
'listOfNonNullContainsNull' => function () {
|
||||
return [1, null, 2];
|
||||
},
|
||||
'nonNullListContainsNull' => function () {
|
||||
return [1, null, 2];
|
||||
},
|
||||
'nonNullListOfNonNullContainsNull' => function () {
|
||||
return [1, null, 2];
|
||||
},
|
||||
'listReturnsNull' => function () {
|
||||
return null;
|
||||
},
|
||||
'listOfNonNullReturnsNull' => function () {
|
||||
return null;
|
||||
},
|
||||
'nonNullListReturnsNull' => function () {
|
||||
return null;
|
||||
},
|
||||
'nonNullListOfNonNullReturnsNull' => function () {
|
||||
return null;
|
||||
},
|
||||
'nest' => function () {
|
||||
return self::data();
|
||||
}
|
||||
];
|
||||
$type = Type::listOf(Type::int());
|
||||
|
||||
// Contains values
|
||||
$this->check(
|
||||
$type,
|
||||
[ 1, 2 ],
|
||||
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
|
||||
);
|
||||
|
||||
// Contains null
|
||||
$this->check(
|
||||
$type,
|
||||
[ 1, null, 2 ],
|
||||
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
|
||||
);
|
||||
|
||||
// Returns null
|
||||
$this->check(
|
||||
$type,
|
||||
null,
|
||||
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @describe [T]!
|
||||
*/
|
||||
public function testHandlesNonNullableLists()
|
||||
{
|
||||
$type = Type::nonNull(Type::listOf(Type::int()));
|
||||
|
||||
// Contains values
|
||||
$this->check(
|
||||
$type,
|
||||
[ 1, 2 ],
|
||||
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
|
||||
);
|
||||
|
||||
// Contains null
|
||||
$this->check(
|
||||
$type,
|
||||
[ 1, null, 2 ],
|
||||
[ 'data' => [ 'nest' => [ 'test' => [ 1, null, 2 ] ] ] ]
|
||||
);
|
||||
|
||||
// Returns null
|
||||
$this->check(
|
||||
$type,
|
||||
null,
|
||||
[
|
||||
'data' => [ 'nest' => null ],
|
||||
'errors' => [
|
||||
FormattedError::create(
|
||||
'Cannot return null for non-nullable field DataType.test.',
|
||||
[ new SourceLocation(1, 10) ]
|
||||
)
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @describe [T!]
|
||||
*/
|
||||
public function testHandlesListOfNonNulls()
|
||||
{
|
||||
$type = Type::listOf(Type::nonNull(Type::int()));
|
||||
|
||||
// Contains values
|
||||
$this->check(
|
||||
$type,
|
||||
[ 1, 2 ],
|
||||
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
|
||||
);
|
||||
|
||||
// Contains null
|
||||
$this->check(
|
||||
$type,
|
||||
[ 1, null, 2 ],
|
||||
[
|
||||
'data' => [ 'nest' => [ 'test' => null ] ],
|
||||
'errors' => [
|
||||
FormattedError::create(
|
||||
'Cannot return null for non-nullable field DataType.test.',
|
||||
[ new SourceLocation(1, 10) ]
|
||||
)
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
// Returns null
|
||||
$this->check(
|
||||
$type,
|
||||
null,
|
||||
[ 'data' => [ 'nest' => [ 'test' => null ] ] ]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @describe [T!]!
|
||||
*/
|
||||
public function testHandlesNonNullListOfNonNulls()
|
||||
{
|
||||
$type = Type::nonNull(Type::listOf(Type::nonNull(Type::int())));
|
||||
|
||||
// Contains values
|
||||
$this->check(
|
||||
$type,
|
||||
[ 1, 2 ],
|
||||
[ 'data' => [ 'nest' => [ 'test' => [ 1, 2 ] ] ] ]
|
||||
);
|
||||
|
||||
|
||||
// Contains null
|
||||
$this->check(
|
||||
$type,
|
||||
[ 1, null, 2 ],
|
||||
[
|
||||
'data' => [ 'nest' => null ],
|
||||
'errors' => [
|
||||
FormattedError::create(
|
||||
'Cannot return null for non-nullable field DataType.test.',
|
||||
[ new SourceLocation(1, 10) ]
|
||||
)
|
||||
]
|
||||
]
|
||||
);
|
||||
|
||||
// Returns null
|
||||
$this->check(
|
||||
$type,
|
||||
null,
|
||||
[
|
||||
'data' => [ 'nest' => null ],
|
||||
'errors' => [
|
||||
FormattedError::create(
|
||||
'Cannot return null for non-nullable field DataType.test.',
|
||||
[ new SourceLocation(1, 10) ]
|
||||
)
|
||||
]
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,10 @@ use GraphQL\Type\Definition\Type;
|
||||
class MutationsTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Execute: Handles mutation execution ordering
|
||||
|
||||
/**
|
||||
* @it evaluates mutations serially
|
||||
*/
|
||||
public function testEvaluatesMutationsSerially()
|
||||
{
|
||||
$doc = 'mutation M {
|
||||
@ -32,7 +36,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
}';
|
||||
$ast = Parser::parse($doc);
|
||||
$mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M');
|
||||
$mutationResult = Executor::execute($this->schema(), $ast, new Root(6));
|
||||
$expected = [
|
||||
'data' => [
|
||||
'first' => [
|
||||
@ -55,6 +59,9 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, $mutationResult->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it evaluates mutations correctly in the presense of a failed mutation
|
||||
*/
|
||||
public function testEvaluatesMutationsCorrectlyInThePresenseOfAFailedMutation()
|
||||
{
|
||||
$doc = 'mutation M {
|
||||
@ -78,7 +85,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
}';
|
||||
$ast = Parser::parse($doc);
|
||||
$mutationResult = Executor::execute($this->schema(), $ast, new Root(6), null, 'M');
|
||||
$mutationResult = Executor::execute($this->schema(), $ast, new Root(6));
|
||||
$expected = [
|
||||
'data' => [
|
||||
'first' => [
|
||||
@ -118,14 +125,14 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
|
||||
],
|
||||
'name' => 'NumberHolder',
|
||||
]);
|
||||
$schema = new Schema(
|
||||
new ObjectType([
|
||||
$schema = new Schema([
|
||||
'query' => new ObjectType([
|
||||
'fields' => [
|
||||
'numberHolder' => ['type' => $numberHolderType],
|
||||
],
|
||||
'name' => 'Query',
|
||||
]),
|
||||
new ObjectType([
|
||||
'mutation' => new ObjectType([
|
||||
'fields' => [
|
||||
'immediatelyChangeTheNumber' => [
|
||||
'type' => $numberHolderType,
|
||||
@ -158,7 +165,7 @@ class MutationsTest extends \PHPUnit_Framework_TestCase
|
||||
],
|
||||
'name' => 'Mutation',
|
||||
])
|
||||
);
|
||||
]);
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
|
@ -68,10 +68,14 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]);
|
||||
|
||||
$this->schema = new Schema($dataType);
|
||||
$this->schema = new Schema(['query' => $dataType]);
|
||||
}
|
||||
|
||||
// Execute: handles non-nullable types
|
||||
|
||||
/**
|
||||
* @it nulls a nullable field that throws synchronously
|
||||
*/
|
||||
public function testNullsANullableFieldThatThrowsSynchronously()
|
||||
{
|
||||
$doc = '
|
||||
@ -93,7 +97,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
|
||||
)
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testNullsASynchronouslyReturnedObjectThatContainsANonNullableFieldThatThrowsSynchronously()
|
||||
@ -117,7 +121,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
|
||||
FormattedError::create($this->nonNullSyncError->message, [new SourceLocation(4, 11)])
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testNullsAComplexTreeOfNullableFieldsThatThrow()
|
||||
@ -149,7 +153,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
|
||||
FormattedError::create($this->syncError->message, [new SourceLocation(6, 13)]),
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, [], 'Q')->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->throwingData, null, [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testNullsANullableFieldThatSynchronouslyReturnsNull()
|
||||
@ -167,7 +171,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
|
||||
'sync' => null,
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function test4()
|
||||
@ -188,10 +192,10 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
|
||||
'nest' => null
|
||||
],
|
||||
'errors' => [
|
||||
FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(4, 11)])
|
||||
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(4, 11)])
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function test5()
|
||||
@ -227,7 +231,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
|
||||
],
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, [], 'Q')->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->nullingData, null, [], 'Q')->toArray());
|
||||
}
|
||||
|
||||
public function testNullsTheTopLevelIfSyncNonNullableFieldThrows()
|
||||
@ -255,7 +259,7 @@ class NonNullTest extends \PHPUnit_Framework_TestCase
|
||||
$expected = [
|
||||
'data' => null,
|
||||
'errors' => [
|
||||
FormattedError::create('Cannot return null for non-nullable type.', [new SourceLocation(2, 17)]),
|
||||
FormattedError::create('Cannot return null for non-nullable field DataType.nonNullSync.', [new SourceLocation(2, 17)]),
|
||||
]
|
||||
];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, Parser::parse($doc), $this->nullingData)->toArray());
|
||||
|
97
tests/Executor/ResolveTest.php
Normal file
97
tests/Executor/ResolveTest.php
Normal file
@ -0,0 +1,97 @@
|
||||
<?php
|
||||
namespace GraphQL\Tests\Executor;
|
||||
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
class ResolveTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Execute: resolve function
|
||||
|
||||
private function buildSchema($testField)
|
||||
{
|
||||
return new Schema([
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'test' => $testField
|
||||
]
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it default function accesses properties
|
||||
*/
|
||||
public function testDefaultFunctionAccessesProperties()
|
||||
{
|
||||
$schema = $this->buildSchema(['type' => Type::string()]);
|
||||
|
||||
$source = [
|
||||
'test' => 'testValue'
|
||||
];
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['test' => 'testValue']],
|
||||
GraphQL::execute($schema, '{ test }', $source)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it default function calls methods
|
||||
*/
|
||||
public function testDefaultFunctionCallsMethods()
|
||||
{
|
||||
$schema = $this->buildSchema(['type' => Type::string()]);
|
||||
$_secret = 'secretValue' . uniqid();
|
||||
|
||||
$source = [
|
||||
'test' => function () use ($_secret) {
|
||||
return $_secret;
|
||||
}
|
||||
];
|
||||
$this->assertEquals(
|
||||
['data' => ['test' => $_secret]],
|
||||
GraphQL::execute($schema, '{ test }', $source)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it uses provided resolve function
|
||||
*/
|
||||
public function testUsesProvidedResolveFunction()
|
||||
{
|
||||
$schema = $this->buildSchema([
|
||||
'type' => Type::string(),
|
||||
'args' => [
|
||||
'aStr' => ['type' => Type::string()],
|
||||
'aInt' => ['type' => Type::int()],
|
||||
],
|
||||
'resolve' => function ($source, $args) {
|
||||
return json_encode([$source, $args]);
|
||||
}
|
||||
]);
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['test' => '[null,[]]']],
|
||||
GraphQL::execute($schema, '{ test }')
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['test' => '["Source!",[]]']],
|
||||
GraphQL::execute($schema, '{ test }', 'Source!')
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['test' => '["Source!",{"aStr":"String!"}]']],
|
||||
GraphQL::execute($schema, '{ test(aStr: "String!") }', 'Source!')
|
||||
);
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['test' => '["Source!",{"aStr":"String!","aInt":-123}]']],
|
||||
GraphQL::execute($schema, '{ test(aInt: -123, aStr: "String!") }', 'Source!')
|
||||
);
|
||||
}
|
||||
}
|
@ -4,11 +4,13 @@ namespace GraphQL\Tests\Executor;
|
||||
require_once __DIR__ . '/TestClasses.php';
|
||||
|
||||
use GraphQL\Executor\Executor;
|
||||
use GraphQL\GraphQL;
|
||||
use GraphQL\Language\Parser;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\Config;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ResolveInfo;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
|
||||
@ -79,7 +81,10 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
]);
|
||||
|
||||
$this->schema = new Schema($PersonType);
|
||||
$this->schema = new Schema([
|
||||
'query' => $PersonType,
|
||||
'types' => [ $PetType ]
|
||||
]);
|
||||
|
||||
$this->garfield = new Cat('Garfield', false);
|
||||
$this->odie = new Dog('Odie', true);
|
||||
@ -89,6 +94,10 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
|
||||
// Execute: Union and intersection types
|
||||
|
||||
/**
|
||||
* @it can introspect on union and intersection types
|
||||
*/
|
||||
public function testCanIntrospectOnUnionAndIntersectionTypes()
|
||||
{
|
||||
|
||||
@ -125,9 +134,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
|
||||
],
|
||||
'interfaces' => null,
|
||||
'possibleTypes' => [
|
||||
['name' => 'Person'],
|
||||
['name' => 'Dog'],
|
||||
['name' => 'Cat'],
|
||||
['name' => 'Person']
|
||||
['name' => 'Cat']
|
||||
],
|
||||
'enumValues' => null,
|
||||
'inputFields' => null
|
||||
@ -149,6 +158,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it executes using union types
|
||||
*/
|
||||
public function testExecutesUsingUnionTypes()
|
||||
{
|
||||
// NOTE: This is an *invalid* query, but it should be an *executable* query.
|
||||
@ -178,6 +190,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it executes union types with inline fragments
|
||||
*/
|
||||
public function testExecutesUnionTypesWithInlineFragments()
|
||||
{
|
||||
// This is the valid version of the query in the above test.
|
||||
@ -212,6 +227,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it executes using interface types
|
||||
*/
|
||||
public function testExecutesUsingInterfaceTypes()
|
||||
{
|
||||
// NOTE: This is an *invalid* query, but it should be an *executable* query.
|
||||
@ -241,6 +259,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it executes interface types with inline fragments
|
||||
*/
|
||||
public function testExecutesInterfaceTypesWithInlineFragments()
|
||||
{
|
||||
// This is the valid version of the query in the above test.
|
||||
@ -274,6 +295,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows fragment conditions to be abstract types
|
||||
*/
|
||||
public function testAllowsFragmentConditionsToBeAbstractTypes()
|
||||
{
|
||||
$ast = Parser::parse('
|
||||
@ -325,4 +349,55 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it gets execution info in resolver
|
||||
*/
|
||||
public function testGetsExecutionInfoInResolver()
|
||||
{
|
||||
$encounteredContext = null;
|
||||
$encounteredSchema = null;
|
||||
$encounteredRootValue = null;
|
||||
$PersonType2 = null;
|
||||
|
||||
$NamedType2 = new InterfaceType([
|
||||
'name' => 'Named',
|
||||
'fields' => [
|
||||
'name' => ['type' => Type::string()]
|
||||
],
|
||||
'resolveType' => function ($obj, $context, ResolveInfo $info) use (&$encounteredContext, &$encounteredSchema, &$encounteredRootValue, &$PersonType2) {
|
||||
$encounteredContext = $context;
|
||||
$encounteredSchema = $info->schema;
|
||||
$encounteredRootValue = $info->rootValue;
|
||||
return $PersonType2;
|
||||
}
|
||||
]);
|
||||
|
||||
$PersonType2 = new ObjectType([
|
||||
'name' => 'Person',
|
||||
'interfaces' => [$NamedType2],
|
||||
'fields' => [
|
||||
'name' => ['type' => Type::string()],
|
||||
'friends' => ['type' => Type::listOf($NamedType2)],
|
||||
],
|
||||
]);
|
||||
|
||||
$schema2 = new Schema([
|
||||
'query' => $PersonType2
|
||||
]);
|
||||
|
||||
$john2 = new Person('John', [], [$this->liz]);
|
||||
|
||||
$context = ['authToken' => '123abc'];
|
||||
|
||||
$ast = Parser::parse('{ name, friends { name } }');
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['name' => 'John', 'friends' => [['name' => 'Liz']]]],
|
||||
GraphQL::execute($schema2, $ast, $john2, $context)
|
||||
);
|
||||
$this->assertSame($context, $encounteredContext);
|
||||
$this->assertSame($schema2, $encounteredSchema);
|
||||
$this->assertSame($john2, $encounteredRootValue);
|
||||
}
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\ScalarType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
@ -19,6 +18,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
// Execute: Handles inputs
|
||||
// Handles objects and nullability
|
||||
|
||||
/**
|
||||
* @describe using inline structs
|
||||
*/
|
||||
public function testUsingInlineStructs()
|
||||
{
|
||||
// executes with complex input:
|
||||
@ -46,10 +48,8 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$expected = ['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']];
|
||||
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
|
||||
}
|
||||
|
||||
public function testDoesNotUseIncorrectValue()
|
||||
{
|
||||
// does not use incorrect value
|
||||
$doc = '
|
||||
{
|
||||
fieldWithObjectInput(input: ["foo", "bar", "baz"])
|
||||
@ -62,8 +62,23 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
'data' => ['fieldWithObjectInput' => null]
|
||||
];
|
||||
$this->assertEquals($expected, $result);
|
||||
|
||||
// properly runs parseLiteral on complex scalar types
|
||||
$doc = '
|
||||
{
|
||||
fieldWithObjectInput(input: {a: "foo", d: "SerializedValue"})
|
||||
}
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$this->assertEquals(
|
||||
['data' => ['fieldWithObjectInput' => '{"a":"foo","d":"DeserializedValue"}']],
|
||||
Executor::execute($this->schema(), $ast)->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @describe using variables
|
||||
*/
|
||||
public function testUsingVariables()
|
||||
{
|
||||
// executes with complex input:
|
||||
@ -78,7 +93,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']],
|
||||
Executor::execute($schema, $ast, null, $params)->toArray()
|
||||
Executor::execute($schema, $ast, null, null, $params)->toArray()
|
||||
);
|
||||
|
||||
// uses default value when not provided:
|
||||
@ -94,17 +109,16 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
];
|
||||
$this->assertEquals($expected, $result);
|
||||
|
||||
|
||||
// properly parses single value to array:
|
||||
$params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => 'baz']];
|
||||
$this->assertEquals(
|
||||
['data' => ['fieldWithObjectInput' => '{"a":"foo","b":["bar"],"c":"baz"}']],
|
||||
Executor::execute($schema, $ast, null, $params)->toArray()
|
||||
Executor::execute($schema, $ast, null, null, $params)->toArray()
|
||||
);
|
||||
|
||||
// executes with complex scalar input:
|
||||
$params = [ 'input' => [ 'c' => 'foo', 'd' => 'SerializedValue' ] ];
|
||||
$result = Executor::execute($schema, $ast, null, $params)->toArray();
|
||||
$result = Executor::execute($schema, $ast, null, null, $params)->toArray();
|
||||
$expected = [
|
||||
'data' => [
|
||||
'fieldWithObjectInput' => '{"c":"foo","d":"DeserializedValue"}'
|
||||
@ -115,14 +129,13 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
// errors on null for nested non-null:
|
||||
$params = ['input' => ['a' => 'foo', 'b' => 'bar', 'c' => null]];
|
||||
$expected = FormattedError::create(
|
||||
'Variable $input expected value of type ' .
|
||||
'TestInputObject but got: ' .
|
||||
'{"a":"foo","b":"bar","c":null}.',
|
||||
'Variable "$input" got invalid value {"a":"foo","b":"bar","c":null}.'. "\n".
|
||||
'In field "c": Expected "String!", found null.',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
|
||||
try {
|
||||
Executor::execute($schema, $ast, null, $params);
|
||||
Executor::execute($schema, $ast, null, null, $params);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $err) {
|
||||
$this->assertEquals($expected, Error::formatError($err));
|
||||
@ -132,11 +145,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$params = [ 'input' => 'foo bar' ];
|
||||
|
||||
try {
|
||||
Executor::execute($schema, $ast, null, $params);
|
||||
Executor::execute($schema, $ast, null, null, $params);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $error) {
|
||||
$expected = FormattedError::create(
|
||||
'Variable $input expected value of type TestInputObject but got: "foo bar".',
|
||||
'Variable "$input" got invalid value "foo bar".'."\n".
|
||||
'Expected "TestInputObject", found not an object.',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
$this->assertEquals($expected, Error::formatError($error));
|
||||
@ -146,36 +160,61 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$params = ['input' => ['a' => 'foo', 'b' => 'bar']];
|
||||
|
||||
try {
|
||||
Executor::execute($schema, $ast, null, $params);
|
||||
Executor::execute($schema, $ast, null, null, $params);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $e) {
|
||||
$expected = FormattedError::create(
|
||||
'Variable $input expected value of type ' .
|
||||
'TestInputObject but got: {"a":"foo","b":"bar"}.',
|
||||
'Variable "$input" got invalid value {"a":"foo","b":"bar"}.'. "\n".
|
||||
'In field "c": Expected "String!", found null.',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
|
||||
$this->assertEquals($expected, Error::formatError($e));
|
||||
}
|
||||
|
||||
// errors on deep nested errors and with many errors
|
||||
$nestedDoc = '
|
||||
query q($input: TestNestedInputObject) {
|
||||
fieldWithNestedObjectInput(input: $input)
|
||||
}
|
||||
';
|
||||
$nestedAst = Parser::parse($nestedDoc);
|
||||
$params = [ 'input' => [ 'na' => [ 'a' => 'foo' ] ] ];
|
||||
|
||||
try {
|
||||
Executor::execute($schema, $nestedAst, null, null, $params);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $error) {
|
||||
$expected = FormattedError::create(
|
||||
'Variable "$input" got invalid value {"na":{"a":"foo"}}.' . "\n" .
|
||||
'In field "na": In field "c": Expected "String!", found null.' . "\n" .
|
||||
'In field "nb": Expected "String!", found null.',
|
||||
[new SourceLocation(2, 19)]
|
||||
);
|
||||
$this->assertEquals($expected, Error::formatError($error));
|
||||
}
|
||||
|
||||
// errors on addition of unknown input field
|
||||
$params = ['input' => [ 'a' => 'foo', 'b' => 'bar', 'c' => 'baz', 'd' => 'dog' ]];
|
||||
|
||||
try {
|
||||
Executor::execute($schema, $ast, null, $params);
|
||||
Executor::execute($schema, $ast, null, null, $params);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $e) {
|
||||
$expected = FormattedError::create(
|
||||
'Variable $input expected value of type TestInputObject but ' .
|
||||
'got: {"a":"foo","b":"bar","c":"baz","d":"dog"}.',
|
||||
'Variable "$input" got invalid value {"a":"foo","b":"bar","c":"baz","d":"dog"}.'."\n".
|
||||
'In field "d": Expected type "ComplexScalar", found "dog".',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
$this->assertEquals($expected, Error::formatError($e));
|
||||
}
|
||||
}
|
||||
|
||||
// Describe: Handles nullable scalars
|
||||
|
||||
// Handles nullable scalars
|
||||
/**
|
||||
* @it allows nullable inputs to be omitted
|
||||
*/
|
||||
public function testAllowsNullableInputsToBeOmitted()
|
||||
{
|
||||
$doc = '
|
||||
@ -191,6 +230,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows nullable inputs to be omitted in a variable
|
||||
*/
|
||||
public function testAllowsNullableInputsToBeOmittedInAVariable()
|
||||
{
|
||||
$doc = '
|
||||
@ -204,6 +246,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows nullable inputs to be omitted in an unlisted variable
|
||||
*/
|
||||
public function testAllowsNullableInputsToBeOmittedInAnUnlistedVariable()
|
||||
{
|
||||
$doc = '
|
||||
@ -216,6 +261,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows nullable inputs to be set to null in a variable
|
||||
*/
|
||||
public function testAllowsNullableInputsToBeSetToNullInAVariable()
|
||||
{
|
||||
$doc = '
|
||||
@ -229,6 +277,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => null])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows nullable inputs to be set to a value in a variable
|
||||
*/
|
||||
public function testAllowsNullableInputsToBeSetToAValueInAVariable()
|
||||
{
|
||||
$doc = '
|
||||
@ -238,9 +289,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = ['data' => ['fieldWithNullableStringInput' => '"a"']];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['value' => 'a'])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows nullable inputs to be set to a value directly
|
||||
*/
|
||||
public function testAllowsNullableInputsToBeSetToAValueDirectly()
|
||||
{
|
||||
$doc = '
|
||||
@ -254,10 +308,13 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
|
||||
|
||||
// Handles non-nullable scalars
|
||||
// Describe: Handles non-nullable scalars
|
||||
|
||||
/**
|
||||
* @it does not allow non-nullable inputs to be omitted in a variable
|
||||
*/
|
||||
public function testDoesntAllowNonNullableInputsToBeOmittedInAVariable()
|
||||
{
|
||||
// does not allow non-nullable inputs to be omitted in a variable
|
||||
$doc = '
|
||||
query SetsNonNullable($value: String!) {
|
||||
fieldWithNonNullableStringInput(input: $value)
|
||||
@ -269,16 +326,18 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $e) {
|
||||
$expected = FormattedError::create(
|
||||
'Variable $value expected value of type String! but got: null.',
|
||||
'Variable "$value" of required type "String!" was not provided.',
|
||||
[new SourceLocation(2, 31)]
|
||||
);
|
||||
$this->assertEquals($expected, Error::formatError($e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @it does not allow non-nullable inputs to be set to null in a variable
|
||||
*/
|
||||
public function testDoesNotAllowNonNullableInputsToBeSetToNullInAVariable()
|
||||
{
|
||||
// does not allow non-nullable inputs to be set to null in a variable
|
||||
$doc = '
|
||||
query SetsNonNullable($value: String!) {
|
||||
fieldWithNonNullableStringInput(input: $value)
|
||||
@ -291,13 +350,16 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $e) {
|
||||
$expected = FormattedError::create(
|
||||
'Variable $value expected value of type String! but got: null.',
|
||||
'Variable "$value" of required type "String!" was not provided.',
|
||||
[new SourceLocation(2, 31)]
|
||||
);
|
||||
$this->assertEquals($expected, Error::formatError($e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows non-nullable inputs to be set to a value in a variable
|
||||
*/
|
||||
public function testAllowsNonNullableInputsToBeSetToAValueInAVariable()
|
||||
{
|
||||
$doc = '
|
||||
@ -307,9 +369,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = ['data' => ['fieldWithNonNullableStringInput' => '"a"']];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['value' => 'a'])->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['value' => 'a'])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows non-nullable inputs to be set to a value directly
|
||||
*/
|
||||
public function testAllowsNonNullableInputsToBeSetToAValueDirectly()
|
||||
{
|
||||
$doc = '
|
||||
@ -323,6 +388,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it passes along null for non-nullable inputs if explcitly set in the query
|
||||
*/
|
||||
public function testPassesAlongNullForNonNullableInputsIfExplcitlySetInTheQuery()
|
||||
{
|
||||
$doc = '
|
||||
@ -335,7 +403,11 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast)->toArray());
|
||||
}
|
||||
|
||||
// Handles lists and nullability
|
||||
// Describe: Handles lists and nullability
|
||||
|
||||
/**
|
||||
* @it allows lists to be null
|
||||
*/
|
||||
public function testAllowsListsToBeNull()
|
||||
{
|
||||
$doc = '
|
||||
@ -349,6 +421,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows lists to contain values
|
||||
*/
|
||||
public function testAllowsListsToContainValues()
|
||||
{
|
||||
$doc = '
|
||||
@ -358,9 +433,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = ['data' => ['list' => '["A"]']];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A']])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows lists to contain null
|
||||
*/
|
||||
public function testAllowsListsToContainNull()
|
||||
{
|
||||
$doc = '
|
||||
@ -370,9 +448,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = ['data' => ['list' => '["A",null,"B"]']];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A',null,'B']])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it does not allow non-null lists to be null
|
||||
*/
|
||||
public function testDoesNotAllowNonNullListsToBeNull()
|
||||
{
|
||||
$doc = '
|
||||
@ -382,18 +463,21 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = FormattedError::create(
|
||||
'Variable $input expected value of type [String]! but got: null.',
|
||||
'Variable "$input" of required type "[String]!" was not provided.',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
|
||||
try {
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => null])->toArray());
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $e) {
|
||||
$this->assertEquals($expected, Error::formatError($e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows non-null lists to contain values
|
||||
*/
|
||||
public function testAllowsNonNullListsToContainValues()
|
||||
{
|
||||
$doc = '
|
||||
@ -403,9 +487,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = ['data' => ['nnList' => '["A"]']];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => 'A'])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows non-null lists to contain null
|
||||
*/
|
||||
public function testAllowsNonNullListsToContainNull()
|
||||
{
|
||||
$doc = '
|
||||
@ -416,9 +503,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = ['data' => ['nnList' => '["A",null,"B"]']];
|
||||
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A',null,'B']])->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A',null,'B']])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows lists of non-nulls to be null
|
||||
*/
|
||||
public function testAllowsListsOfNonNullsToBeNull()
|
||||
{
|
||||
$doc = '
|
||||
@ -431,6 +521,9 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => null])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows lists of non-nulls to contain values
|
||||
*/
|
||||
public function testAllowsListsOfNonNullsToContainValues()
|
||||
{
|
||||
$doc = '
|
||||
@ -441,9 +534,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = ['data' => ['listNN' => '["A"]']];
|
||||
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => 'A'])->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => 'A'])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it does not allow lists of non-nulls to contain null
|
||||
*/
|
||||
public function testDoesNotAllowListsOfNonNullsToContainNull()
|
||||
{
|
||||
$doc = '
|
||||
@ -453,18 +549,22 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = FormattedError::create(
|
||||
'Variable $input expected value of type [String!] but got: ["A",null,"B"].',
|
||||
'Variable "$input" got invalid value ["A",null,"B"].' . "\n" .
|
||||
'In element #1: Expected "String!", found null.',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
|
||||
try {
|
||||
Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]);
|
||||
Executor::execute($this->schema(), $ast, null, null, ['input' => ['A', null, 'B']]);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $e) {
|
||||
$this->assertEquals($expected, Error::formatError($e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @it does not allow non-null lists of non-nulls to be null
|
||||
*/
|
||||
public function testDoesNotAllowNonNullListsOfNonNullsToBeNull()
|
||||
{
|
||||
$doc = '
|
||||
@ -474,16 +574,20 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = FormattedError::create(
|
||||
'Variable $input expected value of type [String!]! but got: null.',
|
||||
'Variable "$input" of required type "[String!]!" was not provided.',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
try {
|
||||
Executor::execute($this->schema(), $ast, null, ['input' => null]);
|
||||
Executor::execute($this->schema(), $ast, null, null, ['input' => null]);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $e) {
|
||||
$this->assertEquals($expected, Error::formatError($e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @it allows non-null lists of non-nulls to contain values
|
||||
*/
|
||||
public function testAllowsNonNullListsOfNonNullsToContainValues()
|
||||
{
|
||||
$doc = '
|
||||
@ -493,9 +597,12 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = ['data' => ['nnListNN' => '["A"]']];
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, ['input' => ['A']])->toArray());
|
||||
$this->assertEquals($expected, Executor::execute($this->schema(), $ast, null, null, ['input' => ['A']])->toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* @it does not allow non-null lists of non-nulls to contain null
|
||||
*/
|
||||
public function testDoesNotAllowNonNullListsOfNonNullsToContainNull()
|
||||
{
|
||||
$doc = '
|
||||
@ -505,16 +612,116 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$expected = FormattedError::create(
|
||||
'Variable $input expected value of type [String!]! but got: ["A",null,"B"].',
|
||||
'Variable "$input" got invalid value ["A",null,"B"].'."\n".
|
||||
'In element #1: Expected "String!", found null.',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
try {
|
||||
Executor::execute($this->schema(), $ast, null, ['input' => ['A', null, 'B']]);
|
||||
Executor::execute($this->schema(), $ast, null, null, ['input' => ['A', null, 'B']]);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $e) {
|
||||
$this->assertEquals($expected, Error::formatError($e));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @it does not allow invalid types to be used as values
|
||||
*/
|
||||
public function testDoesNotAllowInvalidTypesToBeUsedAsValues()
|
||||
{
|
||||
$doc = '
|
||||
query q($input: TestType!) {
|
||||
fieldWithObjectInput(input: $input)
|
||||
}
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$vars = [ 'input' => [ 'list' => [ 'A', 'B' ] ] ];
|
||||
|
||||
try {
|
||||
Executor::execute($this->schema(), $ast, null, null, $vars);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $error) {
|
||||
$expected = FormattedError::create(
|
||||
'Variable "$input" expected value of type "TestType!" which cannot ' .
|
||||
'be used as an input type.',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
$this->assertEquals($expected, Error::formatError($error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @it does not allow unknown types to be used as values
|
||||
*/
|
||||
public function testDoesNotAllowUnknownTypesToBeUsedAsValues()
|
||||
{
|
||||
$doc = '
|
||||
query q($input: UnknownType!) {
|
||||
fieldWithObjectInput(input: $input)
|
||||
}
|
||||
';
|
||||
$ast = Parser::parse($doc);
|
||||
$vars = ['input' => 'whoknows'];
|
||||
|
||||
try {
|
||||
Executor::execute($this->schema(), $ast, null, null, $vars);
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (Error $error) {
|
||||
$expected = FormattedError::create(
|
||||
'Variable "$input" expected value of type "UnknownType!" which ' .
|
||||
'cannot be used as an input type.',
|
||||
[new SourceLocation(2, 17)]
|
||||
);
|
||||
$this->assertEquals($expected, Error::formatError($error));
|
||||
}
|
||||
}
|
||||
|
||||
// Describe: Execute: Uses argument default values
|
||||
/**
|
||||
* @it when no argument provided
|
||||
*/
|
||||
public function testWhenNoArgumentProvided()
|
||||
{
|
||||
$ast = Parser::parse('{
|
||||
fieldWithDefaultArgumentValue
|
||||
}');
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']],
|
||||
Executor::execute($this->schema(), $ast)->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it when nullable variable provided
|
||||
*/
|
||||
public function testWhenNullableVariableProvided()
|
||||
{
|
||||
$ast = Parser::parse('query optionalVariable($optional: String) {
|
||||
fieldWithDefaultArgumentValue(input: $optional)
|
||||
}');
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']],
|
||||
Executor::execute($this->schema(), $ast)->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it when argument provided cannot be parsed
|
||||
*/
|
||||
public function testWhenArgumentProvidedCannotBeParsed()
|
||||
{
|
||||
$ast = Parser::parse('{
|
||||
fieldWithDefaultArgumentValue(input: WRONG_TYPE)
|
||||
}');
|
||||
|
||||
$this->assertEquals(
|
||||
['data' => ['fieldWithDefaultArgumentValue' => '"Hello World"']],
|
||||
Executor::execute($this->schema(), $ast)->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function schema()
|
||||
{
|
||||
@ -530,6 +737,14 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]);
|
||||
|
||||
$TestNestedInputObject = new InputObjectType([
|
||||
'name' => 'TestNestedInputObject',
|
||||
'fields' => [
|
||||
'na' => [ 'type' => Type::nonNull($TestInputObject) ],
|
||||
'nb' => [ 'type' => Type::nonNull(Type::string()) ],
|
||||
],
|
||||
]);
|
||||
|
||||
$TestType = new ObjectType([
|
||||
'name' => 'TestType',
|
||||
'fields' => [
|
||||
@ -561,6 +776,18 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
return isset($args['input']) ? json_encode($args['input']) : null;
|
||||
}
|
||||
],
|
||||
'fieldWithNestedInputObject' => [
|
||||
'type' => Type::string(),
|
||||
'args' => [
|
||||
'input' => [
|
||||
'type' => $TestNestedInputObject,
|
||||
'defaultValue' => 'Hello World'
|
||||
]
|
||||
],
|
||||
'resolve' => function($_, $args) {
|
||||
return isset($args['input']) ? json_encode($args['input']) : null;
|
||||
}
|
||||
],
|
||||
'list' => [
|
||||
'type' => Type::string(),
|
||||
'args' => ['input' => ['type' => Type::listOf(Type::string())]],
|
||||
@ -592,7 +819,7 @@ class VariablesTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema($TestType);
|
||||
$schema = new Schema(['query' => $TestType]);
|
||||
return $schema;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,10 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Star Wars Introspection Tests
|
||||
// Basic Introspection
|
||||
// it('Allows querying the schema for types')
|
||||
|
||||
/**
|
||||
* @it Allows querying the schema for types
|
||||
*/
|
||||
public function testAllowsQueryingTheSchemaForTypes()
|
||||
{
|
||||
$query = '
|
||||
@ -26,8 +29,8 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
['name' => 'Query'],
|
||||
['name' => 'Episode'],
|
||||
['name' => 'Character'],
|
||||
['name' => 'Human'],
|
||||
['name' => 'String'],
|
||||
['name' => 'Human'],
|
||||
['name' => 'Droid'],
|
||||
['name' => '__Schema'],
|
||||
['name' => '__Type'],
|
||||
@ -37,6 +40,7 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
['name' => '__InputValue'],
|
||||
['name' => '__EnumValue'],
|
||||
['name' => '__Directive'],
|
||||
['name' => '__DirectiveLocation'],
|
||||
['name' => 'ID'],
|
||||
['name' => 'Float'],
|
||||
['name' => 'Int']
|
||||
@ -46,7 +50,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// it('Allows querying the schema for query type')
|
||||
/**
|
||||
* @it Allows querying the schema for query type
|
||||
*/
|
||||
public function testAllowsQueryingTheSchemaForQueryType()
|
||||
{
|
||||
$query = '
|
||||
@ -68,7 +74,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// it('Allows querying the schema for a specific type')
|
||||
/**
|
||||
* @it Allows querying the schema for a specific type
|
||||
*/
|
||||
public function testAllowsQueryingTheSchemaForASpecificType()
|
||||
{
|
||||
$query = '
|
||||
@ -86,7 +94,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// it('Allows querying the schema for an object kind')
|
||||
/**
|
||||
* @it Allows querying the schema for an object kind
|
||||
*/
|
||||
public function testAllowsQueryingForAnObjectKind()
|
||||
{
|
||||
$query = '
|
||||
@ -106,7 +116,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// it('Allows querying the schema for an interface kind')
|
||||
/**
|
||||
* @it Allows querying the schema for an interface kind
|
||||
*/
|
||||
public function testAllowsQueryingForInterfaceKind()
|
||||
{
|
||||
$query = '
|
||||
@ -126,7 +138,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// it('Allows querying the schema for object fields')
|
||||
/**
|
||||
* @it Allows querying the schema for object fields
|
||||
*/
|
||||
public function testAllowsQueryingForObjectFields()
|
||||
{
|
||||
$query = '
|
||||
@ -188,7 +202,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// it('Allows querying the schema for nested object fields')
|
||||
/**
|
||||
* @it Allows querying the schema for nested object fields
|
||||
*/
|
||||
public function testAllowsQueryingTheSchemaForNestedObjectFields()
|
||||
{
|
||||
$query = '
|
||||
@ -268,6 +284,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows querying the schema for field args
|
||||
*/
|
||||
public function testAllowsQueryingTheSchemaForFieldArgs()
|
||||
{
|
||||
$query = '
|
||||
@ -359,7 +378,9 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// it('Allows querying the schema for documentation')
|
||||
/**
|
||||
* @it Allows querying the schema for documentation
|
||||
*/
|
||||
public function testAllowsQueryingTheSchemaForDocumentation()
|
||||
{
|
||||
$query = '
|
||||
|
@ -9,9 +9,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
// Star Wars Query Tests
|
||||
// Basic Queries
|
||||
|
||||
/**
|
||||
* @it Correctly identifies R2-D2 as the hero of the Star Wars Saga
|
||||
*/
|
||||
public function testCorrectlyIdentifiesR2D2AsTheHeroOfTheStarWarsSaga()
|
||||
{
|
||||
// Correctly identifies R2-D2 as the hero of the Star Wars Saga
|
||||
$query = '
|
||||
query HeroNameQuery {
|
||||
hero {
|
||||
@ -27,6 +29,9 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows us to query for the ID and friends of R2-D2
|
||||
*/
|
||||
public function testAllowsUsToQueryForTheIDAndFriendsOfR2D2()
|
||||
{
|
||||
$query = '
|
||||
@ -60,7 +65,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// Nested Queries
|
||||
// Describe: Nested Queries
|
||||
|
||||
/**
|
||||
* @it Allows us to query for the friends of friends of R2-D2
|
||||
*/
|
||||
public function testAllowsUsToQueryForTheFriendsOfFriendsOfR2D2()
|
||||
{
|
||||
$query = '
|
||||
@ -117,7 +126,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// Using IDs and query parameters to refetch objects
|
||||
// Describe: Using IDs and query parameters to refetch objects
|
||||
|
||||
/**
|
||||
* @it Using IDs and query parameters to refetch objects
|
||||
*/
|
||||
public function testAllowsUsToQueryForLukeSkywalkerDirectlyUsingHisID()
|
||||
{
|
||||
$query = '
|
||||
@ -136,9 +149,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID
|
||||
*/
|
||||
public function testGenericQueryToGetLukeSkywalkerById()
|
||||
{
|
||||
// Allows us to create a generic query, then use it to fetch Luke Skywalker using his ID
|
||||
$query = '
|
||||
query FetchSomeIDQuery($someId: String!) {
|
||||
human(id: $someId) {
|
||||
@ -158,9 +173,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQueryWithParams($query, $params, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows us to create a generic query, then use it to fetch Han Solo using his ID
|
||||
*/
|
||||
public function testGenericQueryToGetHanSoloById()
|
||||
{
|
||||
// Allows us to create a generic query, then use it to fetch Han Solo using his ID
|
||||
$query = '
|
||||
query FetchSomeIDQuery($someId: String!) {
|
||||
human(id: $someId) {
|
||||
@ -179,9 +196,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQueryWithParams($query, $params, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows us to create a generic query, then pass an invalid ID to get null back
|
||||
*/
|
||||
public function testGenericQueryWithInvalidId()
|
||||
{
|
||||
// Allows us to create a generic query, then pass an invalid ID to get null back
|
||||
$query = '
|
||||
query humanQuery($id: String!) {
|
||||
human(id: $id) {
|
||||
@ -199,9 +218,12 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
|
||||
// Using aliases to change the key in the response
|
||||
|
||||
/**
|
||||
* @it Allows us to query for Luke, changing his key with an alias
|
||||
*/
|
||||
function testLukeKeyAlias()
|
||||
{
|
||||
// Allows us to query for Luke, changing his key with an alias
|
||||
$query = '
|
||||
query FetchLukeAliased {
|
||||
luke: human(id: "1000") {
|
||||
@ -217,9 +239,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows us to query for both Luke and Leia, using two root fields and an alias
|
||||
*/
|
||||
function testTwoRootKeysAsAnAlias()
|
||||
{
|
||||
// Allows us to query for both Luke and Leia, using two root fields and an alias
|
||||
$query = '
|
||||
query FetchLukeAndLeiaAliased {
|
||||
luke: human(id: "1000") {
|
||||
@ -242,9 +266,12 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
|
||||
// Uses fragments to express more complex queries
|
||||
|
||||
/**
|
||||
* @it Allows us to query using duplicated content
|
||||
*/
|
||||
function testQueryUsingDuplicatedContent()
|
||||
{
|
||||
// Allows us to query using duplicated content
|
||||
$query = '
|
||||
query DuplicateFields {
|
||||
luke: human(id: "1000") {
|
||||
@ -270,9 +297,11 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows us to use a fragment to avoid duplicating content
|
||||
*/
|
||||
function testUsingFragment()
|
||||
{
|
||||
// Allows us to use a fragment to avoid duplicating content
|
||||
$query = '
|
||||
query UseFragment {
|
||||
luke: human(id: "1000") {
|
||||
@ -302,58 +331,9 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
function testFragmentWithProjection()
|
||||
{
|
||||
$query = '
|
||||
query UseFragment {
|
||||
human(id: "1003") {
|
||||
name
|
||||
...fa
|
||||
...fb
|
||||
}
|
||||
}
|
||||
|
||||
fragment fa on Character {
|
||||
friends {
|
||||
id
|
||||
}
|
||||
}
|
||||
fragment fb on Character {
|
||||
friends {
|
||||
name
|
||||
}
|
||||
}
|
||||
';
|
||||
|
||||
$expected = [
|
||||
'human' => [
|
||||
'name' => 'Leia Organa',
|
||||
'friends' => [
|
||||
[
|
||||
'name' => 'Luke Skywalker',
|
||||
'id' => '1000'
|
||||
|
||||
],
|
||||
[
|
||||
'name' => 'Han Solo',
|
||||
'id' => '1002'
|
||||
],
|
||||
[
|
||||
'name' => 'C-3PO',
|
||||
'id' => '2000'
|
||||
],
|
||||
[
|
||||
'name' => 'R2-D2',
|
||||
'id' => '2001'
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
// Using __typename to find the type of an object
|
||||
/**
|
||||
* @it Using __typename to find the type of an object
|
||||
*/
|
||||
public function testVerifyThatR2D2IsADroid()
|
||||
{
|
||||
$query = '
|
||||
@ -373,6 +353,9 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertValidQuery($query, $expected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows us to verify that Luke is a human
|
||||
*/
|
||||
public function testVerifyThatLukeIsHuman()
|
||||
{
|
||||
$query = '
|
||||
@ -407,6 +390,6 @@ class StarWarsQueryTest extends \PHPUnit_Framework_TestCase
|
||||
*/
|
||||
private function assertValidQueryWithParams($query, $params, $expected)
|
||||
{
|
||||
$this->assertEquals(['data' => $expected], GraphQL::execute(StarWarsSchema::build(), $query, null, $params));
|
||||
$this->assertEquals(['data' => $expected], GraphQL::execute(StarWarsSchema::build(), $query, null, null, $params));
|
||||
}
|
||||
}
|
||||
|
@ -291,6 +291,6 @@ class StarWarsSchema
|
||||
]
|
||||
]);
|
||||
|
||||
return new Schema($queryType);
|
||||
return new Schema(['query' => $queryType]);
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,10 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// Star Wars Validation Tests
|
||||
// Basic Queries
|
||||
|
||||
/**
|
||||
* @it Validates a complex but valid query
|
||||
*/
|
||||
public function testValidatesAComplexButValidQuery()
|
||||
{
|
||||
$query = '
|
||||
@ -32,9 +36,11 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(true, empty($errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Notes that non-existent fields are invalid
|
||||
*/
|
||||
public function testThatNonExistentFieldsAreInvalid()
|
||||
{
|
||||
// Notes that non-existent fields are invalid
|
||||
$query = '
|
||||
query HeroSpaceshipQuery {
|
||||
hero {
|
||||
@ -46,6 +52,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(false, empty($errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Requires fields on objects
|
||||
*/
|
||||
public function testRequiresFieldsOnObjects()
|
||||
{
|
||||
$query = '
|
||||
@ -58,9 +67,11 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(false, empty($errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Disallows fields on scalars
|
||||
*/
|
||||
public function testDisallowsFieldsOnScalars()
|
||||
{
|
||||
|
||||
$query = '
|
||||
query HeroFieldsOnScalarQuery {
|
||||
hero {
|
||||
@ -74,6 +85,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(false, empty($errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Disallows object fields on interfaces
|
||||
*/
|
||||
public function testDisallowsObjectFieldsOnInterfaces()
|
||||
{
|
||||
$query = '
|
||||
@ -88,6 +102,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(false, empty($errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows object fields in fragments
|
||||
*/
|
||||
public function testAllowsObjectFieldsInFragments()
|
||||
{
|
||||
$query = '
|
||||
@ -106,6 +123,9 @@ class StartWarsValidationTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertEquals(true, empty($errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* @it Allows object fields in inline fragments
|
||||
*/
|
||||
public function testAllowsObjectFieldsInInlineFragments()
|
||||
{
|
||||
$query = '
|
||||
|
@ -11,6 +11,7 @@ use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Utils;
|
||||
|
||||
class DefinitionTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
@ -415,7 +416,7 @@ class DefinitionTest extends \PHPUnit_Framework_TestCase
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (\Exception $e) {
|
||||
$this->assertSame(
|
||||
'Error in "BadUnion" type definition: expecting callable or instance of GraphQL\Type\Definition\ObjectType at "types:0", but got "' . get_class($type) . '"',
|
||||
'Error in "BadUnion" type definition: expecting callable or instance of GraphQL\Type\Definition\ObjectType at "types:0", but got "' . Utils::getVariableType($type) . '"',
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
|
@ -189,6 +189,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
||||
$this->schema,
|
||||
'query test($color: Color!) { colorEnum(fromEnum: $color) }',
|
||||
null,
|
||||
null,
|
||||
['color' => 'BLUE']
|
||||
)
|
||||
);
|
||||
@ -205,6 +206,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
||||
$this->schema,
|
||||
'mutation x($color: Color!) { favoriteEnum(color: $color) }',
|
||||
null,
|
||||
null,
|
||||
['color' => 'GREEN']
|
||||
)
|
||||
);
|
||||
@ -224,6 +226,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
||||
$this->schema,
|
||||
'subscription x($color: Color!) { subscribeToEnum(color: $color) }',
|
||||
null,
|
||||
null,
|
||||
['color' => 'GREEN']
|
||||
)
|
||||
);
|
||||
@ -295,7 +298,7 @@ class EnumTypeTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
private function expectFailure($query, $vars, $err)
|
||||
{
|
||||
$result = GraphQL::executeAndReturnResult($this->schema, $query, null, $vars);
|
||||
$result = GraphQL::executeAndReturnResult($this->schema, $query, null, null, $vars);
|
||||
$this->assertEquals(1, count($result->errors));
|
||||
|
||||
$this->assertEquals(
|
||||
|
@ -1099,9 +1099,6 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase
|
||||
|
||||
$actual = GraphQL::execute($emptySchema, $request);
|
||||
|
||||
// print_r($actual);
|
||||
// exit;
|
||||
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
|
@ -135,7 +135,7 @@ class ResolveInfoTest extends \PHPUnit_Framework_TestCase
|
||||
'fields' => [
|
||||
'article' => [
|
||||
'type' => $article,
|
||||
'resolve' => function($value, $args, ResolveInfo $info) use (&$hasCalled, &$actualDefaultSelection, &$actualDeepSelection) {
|
||||
'resolve' => function($value, $args, $context, ResolveInfo $info) use (&$hasCalled, &$actualDefaultSelection, &$actualDeepSelection) {
|
||||
$hasCalled = true;
|
||||
$actualDefaultSelection = $info->getFieldSelection();
|
||||
$actualDeepSelection = $info->getFieldSelection(5);
|
||||
@ -145,7 +145,7 @@ class ResolveInfoTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema($blogQuery);
|
||||
$schema = new Schema(['query' => $blogQuery]);
|
||||
$result = GraphQL::execute($schema, $doc);
|
||||
|
||||
$this->assertTrue($hasCalled);
|
||||
|
@ -2,6 +2,8 @@
|
||||
namespace GraphQL\Tests\Type;
|
||||
|
||||
use GraphQL\Schema;
|
||||
use GraphQL\Type\Definition\CustomScalarType;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InterfaceType;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
@ -11,63 +13,402 @@ use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\SchemaValidator;
|
||||
use GraphQL\Utils;
|
||||
|
||||
class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
public $someInputType;
|
||||
private $someInputObjectType;
|
||||
|
||||
private $someScalarType;
|
||||
|
||||
private $someObjectType;
|
||||
|
||||
private $objectWithIsTypeOf;
|
||||
|
||||
private $someEnumType;
|
||||
|
||||
private $someUnionType;
|
||||
|
||||
private $someInterfaceType;
|
||||
|
||||
private $outputTypes;
|
||||
|
||||
private $noOutputTypes;
|
||||
|
||||
private $inputTypes;
|
||||
|
||||
private $noInputTypes;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->someInputType = new InputObjectType([
|
||||
'name' => 'SomeInputType',
|
||||
$this->someScalarType = new CustomScalarType([
|
||||
'name' => 'SomeScalar',
|
||||
'serialize' => function() {},
|
||||
'parseValue' => function() {},
|
||||
'parseLiteral' => function() {}
|
||||
]);
|
||||
|
||||
$this->someObjectType = new ObjectType([
|
||||
'name' => 'SomeObject',
|
||||
'fields' => ['f' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$this->objectWithIsTypeOf = new ObjectType([
|
||||
'name' => 'ObjectWithIsTypeOf',
|
||||
'isTypeOf' => function() {return true;},
|
||||
'fields' => ['f' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$this->someUnionType = new UnionType([
|
||||
'name' => 'SomeUnion',
|
||||
'resolveType' => function() {return null;},
|
||||
'types' => [$this->someObjectType]
|
||||
]);
|
||||
|
||||
$this->someInterfaceType = new InterfaceType([
|
||||
'name' => 'SomeInterface',
|
||||
'resolveType' => function() {return null;},
|
||||
'fields' => ['f' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$this->someEnumType = new EnumType([
|
||||
'name' => 'SomeEnum',
|
||||
'resolveType' => function() {return null;},
|
||||
'fields' => ['f' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$this->someInputObjectType = new InputObjectType([
|
||||
'name' => 'SomeInputObject',
|
||||
'fields' => [
|
||||
'val' => [ 'type' => Type::float(), 'defaultValue' => 42 ]
|
||||
'val' => ['type' => Type::float(), 'defaultValue' => 42]
|
||||
]
|
||||
]);
|
||||
|
||||
$this->outputTypes = $this->withModifiers([
|
||||
Type::string(),
|
||||
$this->someScalarType,
|
||||
$this->someEnumType,
|
||||
$this->someObjectType,
|
||||
$this->someUnionType,
|
||||
$this->someInterfaceType
|
||||
]);
|
||||
|
||||
$this->noOutputTypes = $this->withModifiers([
|
||||
$this->someInputObjectType
|
||||
]);
|
||||
$this->noOutputTypes[] = 'SomeString';
|
||||
|
||||
$this->inputTypes = $this->withModifiers([
|
||||
Type::string(),
|
||||
$this->someScalarType,
|
||||
$this->someEnumType,
|
||||
$this->someInputObjectType
|
||||
]);
|
||||
|
||||
$this->noInputTypes = $this->withModifiers([
|
||||
$this->someObjectType,
|
||||
$this->someUnionType,
|
||||
$this->someInterfaceType
|
||||
]);
|
||||
$this->noInputTypes[] = 'SomeString';
|
||||
}
|
||||
|
||||
private function withModifiers($types)
|
||||
{
|
||||
return array_merge(
|
||||
Utils::map($types, function($type) {return Type::listOf($type);}),
|
||||
Utils::map($types, function($type) {return Type::nonNull($type);}),
|
||||
Utils::map($types, function($type) {return Type::nonNull(Type::listOf($type));})
|
||||
);
|
||||
}
|
||||
|
||||
private function schemaWithFieldType($type)
|
||||
{
|
||||
return [
|
||||
'query' => new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => ['f' => ['type' => $type]]
|
||||
]),
|
||||
'types' => [$type],
|
||||
];
|
||||
}
|
||||
|
||||
private function expectPasses($schemaConfig)
|
||||
{
|
||||
$schema = new Schema(['validate' => true] + $schemaConfig);
|
||||
$errors = SchemaValidator::validate($schema);
|
||||
$this->assertEquals([], $errors);
|
||||
}
|
||||
|
||||
private function expectFails($schemaConfig, $error)
|
||||
{
|
||||
try {
|
||||
$schema = new Schema($schemaConfig);
|
||||
$errors = SchemaValidator::validate($schema);
|
||||
if ($errors) {
|
||||
throw $errors[0];
|
||||
}
|
||||
$this->fail('Expected exception not thrown');
|
||||
} catch (\Exception $e) {
|
||||
$this->assertEquals($e->getMessage(), $error);
|
||||
}
|
||||
}
|
||||
|
||||
// Type System: A Schema must have Object root types
|
||||
|
||||
/**
|
||||
* @it accepts a Schema whose query type is an object type
|
||||
*/
|
||||
public function testAcceptsSchemaWithQueryTypeOfObjectType()
|
||||
{
|
||||
$this->expectPasses([
|
||||
'query' => $this->someObjectType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it accepts a Schema whose query and mutation types are object types
|
||||
*/
|
||||
public function testAcceptsSchemaWithQueryAndMutationTypesOfObjectType()
|
||||
{
|
||||
$MutationType = new ObjectType([
|
||||
'name' => 'Mutation',
|
||||
'fields' => ['edit' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$this->expectPasses([
|
||||
'query' => $this->someObjectType,
|
||||
'mutation' => $MutationType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it accepts a Schema whose query and subscription types are object types
|
||||
*/
|
||||
public function testAcceptsSchemaWhoseQueryAndSubscriptionTypesAreObjectTypes()
|
||||
{
|
||||
$SubscriptionType = new ObjectType([
|
||||
'name' => 'Subscription',
|
||||
'fields' => ['subscribe' => ['type' => Type::string()]]
|
||||
]);
|
||||
|
||||
$this->expectPasses([
|
||||
'query' => $this->someObjectType,
|
||||
'subscription' => $SubscriptionType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it rejects a Schema without a query type
|
||||
*/
|
||||
public function testRejectsSchemaWithoutQueryType()
|
||||
{
|
||||
$this->expectFails([], 'Schema query must be Object Type but got: NULL');
|
||||
}
|
||||
|
||||
/**
|
||||
* @it rejects a Schema whose query type is an input type
|
||||
*/
|
||||
public function testRejectsSchemaWhoseQueryTypeIsAnInputType()
|
||||
{
|
||||
$this->expectFails(
|
||||
['query' => $this->someInputObjectType],
|
||||
'Schema query must be Object Type but got: SomeInputObject'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it rejects a Schema whose mutation type is an input type
|
||||
*/
|
||||
public function testRejectsSchemaWhoseMutationTypeIsInputType()
|
||||
{
|
||||
$this->expectFails(
|
||||
['query' => $this->someObjectType, 'mutation' => $this->someInputObjectType],
|
||||
'Schema mutation must be Object Type if provided but got: SomeInputObject'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it rejects a Schema whose subscription type is an input type
|
||||
*/
|
||||
public function testRejectsSchemaWhoseSubscriptionTypeIsInputType()
|
||||
{
|
||||
$this->expectFails(
|
||||
[
|
||||
'query' => $this->someObjectType,
|
||||
'subscription' => $this->someInputObjectType
|
||||
],
|
||||
'Schema subscription must be Object Type if provided but got: SomeInputObject'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it rejects a Schema whose directives are incorrectly typed
|
||||
*/
|
||||
public function testRejectsSchemaWhoseDirectivesAreIncorrectlyTyped()
|
||||
{
|
||||
$this->expectFails(
|
||||
[
|
||||
'query' => $this->someObjectType,
|
||||
'directives' => [ 'somedirective' ]
|
||||
],
|
||||
'Schema directives must be Directive[] if provided but got array'
|
||||
);
|
||||
}
|
||||
|
||||
// Type System: A Schema must contain uniquely named types
|
||||
|
||||
/**
|
||||
* @it rejects a Schema which redefines a built-in type
|
||||
*/
|
||||
public function testRejectsSchemaWhichRedefinesBuiltInType()
|
||||
{
|
||||
$FakeString = new CustomScalarType([
|
||||
'name' => 'String',
|
||||
'serialize' => function() {return null;},
|
||||
]);
|
||||
|
||||
$QueryType = new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'normal' => [ 'type' => Type::string() ],
|
||||
'fake' => [ 'type' => $FakeString ],
|
||||
]
|
||||
]);
|
||||
|
||||
$this->expectFails(
|
||||
[ 'query' => $QueryType ],
|
||||
'Schema must contain unique named types but contains multiple types named "String".'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it rejects a Schema which defines an object type twice
|
||||
*/
|
||||
public function testRejectsSchemaWhichDefinesObjectTypeTwice()
|
||||
{
|
||||
$A = new ObjectType([
|
||||
'name' => 'SameName',
|
||||
'fields' => ['f' => ['type' => Type::string()]],
|
||||
]);
|
||||
|
||||
$B = new ObjectType([
|
||||
'name' => 'SameName',
|
||||
'fields' => ['f' => ['type' => Type::string()]],
|
||||
]);
|
||||
|
||||
$QueryType = new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'a' => ['type' => $A],
|
||||
'b' => ['type' => $B]
|
||||
]
|
||||
]);
|
||||
|
||||
$this->expectFails(
|
||||
['query' => $QueryType],
|
||||
'Schema must contain unique named types but contains multiple types named "SameName".'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it rejects a Schema which have same named objects implementing an interface
|
||||
*/
|
||||
public function testRejectsSchemaWhichHaveSameNamedObjectsImplementingInterface()
|
||||
{
|
||||
$AnotherInterface = new InterfaceType([
|
||||
'name' => 'AnotherInterface',
|
||||
'resolveType' => function () {
|
||||
return null;
|
||||
},
|
||||
'fields' => ['f' => ['type' => Type::string()]],
|
||||
]);
|
||||
|
||||
$FirstBadObject = new ObjectType([
|
||||
'name' => 'BadObject',
|
||||
'interfaces' => [$AnotherInterface],
|
||||
'fields' => ['f' => ['type' => Type::string()]],
|
||||
]);
|
||||
|
||||
$SecondBadObject = new ObjectType([
|
||||
'name' => 'BadObject',
|
||||
'interfaces' => [$AnotherInterface],
|
||||
'fields' => ['f' => ['type' => Type::string()]],
|
||||
]);
|
||||
|
||||
$QueryType = new ObjectType([
|
||||
'name' => 'Query',
|
||||
'fields' => [
|
||||
'iface' => ['type' => $AnotherInterface],
|
||||
]
|
||||
]);
|
||||
|
||||
$this->expectFails(
|
||||
[
|
||||
'query' => $QueryType,
|
||||
'types' => [$FirstBadObject, $SecondBadObject]
|
||||
],
|
||||
'Schema must contain unique named types but contains multiple types named "BadObject".'
|
||||
);
|
||||
}
|
||||
|
||||
// Type System: Objects must have fields
|
||||
|
||||
/**
|
||||
* @it accepts an Object type with fields object
|
||||
*/
|
||||
public function testAcceptsAnObjectTypeWithFieldsObject()
|
||||
{
|
||||
$schemaConfig = $this->schemaWithFieldType(new ObjectType([
|
||||
'name' => 'SomeObject',
|
||||
'fields' => [
|
||||
'f' => [ 'type' => Type::string() ]
|
||||
]
|
||||
]));
|
||||
$this->expectPasses($schemaConfig);
|
||||
}
|
||||
|
||||
/**
|
||||
* @it accepts an Object type with a field function
|
||||
*/
|
||||
public function testAcceptsAnObjectTypeWithFieldFunction()
|
||||
{
|
||||
$schemaConfig = $this->schemaWithFieldType(new ObjectType([
|
||||
'name' => 'SomeObject',
|
||||
'fields' => function() {
|
||||
return [
|
||||
'f' => ['type' => Type::string()]
|
||||
];
|
||||
}
|
||||
]));
|
||||
$this->expectPasses($schemaConfig);
|
||||
}
|
||||
|
||||
// Type System Config
|
||||
public function testPassesOnTheIntrospectionSchema()
|
||||
{
|
||||
$schema = new Schema(Introspection::_schema());
|
||||
$errors = SchemaValidator::validate($schema);
|
||||
$this->assertEmpty($errors);
|
||||
}
|
||||
|
||||
|
||||
// Rule: NoInputTypesAsOutputFields
|
||||
public function testRejectsSchemaWhoseQueryOrMutationTypeIsAnInputType()
|
||||
{
|
||||
$schema = new Schema($this->someInputType);
|
||||
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]);
|
||||
$this->checkValidationResult($validationResult, 'query');
|
||||
|
||||
$schema = new Schema(null, $this->someInputType);
|
||||
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]);
|
||||
$this->checkValidationResult($validationResult, 'mutation');
|
||||
$this->expectPasses(['query' => Introspection::_schema()]);
|
||||
}
|
||||
|
||||
public function testRejectsASchemaThatUsesAnInputTypeAsAField()
|
||||
{
|
||||
$kinds = [
|
||||
'GraphQL\Type\Definition\ObjectType',
|
||||
'GraphQL\Type\Definition\InterfaceType',
|
||||
];
|
||||
foreach ($kinds as $kind) {
|
||||
$someOutputType = new $kind([
|
||||
'name' => 'SomeOutputType',
|
||||
'fields' => [
|
||||
'sneaky' => ['type' => function() {return $this->someInputType;}]
|
||||
'sneaky' => ['type' => function() {return $this->someInputObjectType;}]
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema($someOutputType);
|
||||
$schema = new Schema(['query' => $someOutputType]);
|
||||
$validationResult = SchemaValidator::validate($schema, [SchemaValidator::noInputTypesAsOutputFieldsRule()]);
|
||||
|
||||
$this->assertSame(1, count($validationResult));
|
||||
$this->assertSame(
|
||||
'Field SomeOutputType.sneaky is of type SomeInputType, which is an ' .
|
||||
'Field SomeOutputType.sneaky is of type SomeInputObject, which is an ' .
|
||||
'input type, but field types must be output types!',
|
||||
$validationResult[0]->message
|
||||
);
|
||||
@ -85,13 +426,13 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
|
||||
'name' => 'SomeOutputType',
|
||||
'fields' => [
|
||||
'fieldWithArg' => [
|
||||
'args' => ['someArg' => ['type' => $this->someInputType]],
|
||||
'args' => ['someArg' => ['type' => $this->someInputObjectType]],
|
||||
'type' => Type::float()
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$schema = new Schema($someOutputType);
|
||||
$schema = new Schema(['query' => $someOutputType]);
|
||||
$errors = SchemaValidator::validate($schema, [$rule]);
|
||||
$this->assertEmpty($errors);
|
||||
}
|
||||
@ -101,7 +442,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
|
||||
$this->assertNotEmpty($validationErrors, "Should not validate");
|
||||
$this->assertEquals(1, count($validationErrors));
|
||||
$this->assertEquals(
|
||||
"Schema $operationType type SomeInputType must be an object type!",
|
||||
"Schema $operationType must be Object Type but got: SomeInputObject.",
|
||||
$validationErrors[0]->message
|
||||
);
|
||||
}
|
||||
@ -153,7 +494,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// rejects a schema with a list of objects as an input field arg
|
||||
$listObjects = new ListOfType(new ObjectType([
|
||||
'name' => 'SomeInputType',
|
||||
'name' => 'SomeInputObject',
|
||||
'fields' => ['f' => ['type' => Type::float()]]
|
||||
]));
|
||||
$this->assertRejectingFieldArgOfType($listObjects);
|
||||
@ -174,7 +515,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// accepts a schema with a list of input type as an input field arg
|
||||
$this->assertAcceptingFieldArgOfType(new ListOfType(new InputObjectType([
|
||||
'name' => 'SomeInputType'
|
||||
'name' => 'SomeInputObject'
|
||||
])));
|
||||
}
|
||||
|
||||
@ -182,7 +523,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
|
||||
{
|
||||
// accepts a schema with a nonnull input type as an input field arg
|
||||
$this->assertAcceptingFieldArgOfType(new NonNull(new InputObjectType([
|
||||
'name' => 'SomeInputType'
|
||||
'name' => 'SomeInputObject'
|
||||
])));
|
||||
}
|
||||
|
||||
@ -219,7 +560,7 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
|
||||
]
|
||||
]);
|
||||
|
||||
return new Schema($queryType);
|
||||
return new Schema(['query' => $queryType]);
|
||||
}
|
||||
|
||||
private function expectRejectionBecauseFieldIsNotInputType($errors, $fieldTypeName)
|
||||
@ -233,83 +574,8 @@ class SchemaValidatorTest extends \PHPUnit_Framework_TestCase
|
||||
}
|
||||
|
||||
|
||||
// Rule: InterfacePossibleTypesMustImplementTheInterface
|
||||
|
||||
public function testAcceptsInterfaceWithSubtypeDeclaredUsingOurInfra()
|
||||
{
|
||||
// accepts an interface with a subtype declared using our infra
|
||||
$this->assertAcceptingAnInterfaceWithANormalSubtype(SchemaValidator::interfacePossibleTypesMustImplementTheInterfaceRule());
|
||||
}
|
||||
|
||||
public function testRejectsWhenAPossibleTypeDoesNotImplementTheInterface()
|
||||
{
|
||||
// TODO: Validation for interfaces / implementors
|
||||
}
|
||||
|
||||
private function assertAcceptingAnInterfaceWithANormalSubtype($rule)
|
||||
{
|
||||
$interfaceType = new InterfaceType([
|
||||
'name' => 'InterfaceType',
|
||||
'fields' => []
|
||||
]);
|
||||
|
||||
$subType = new ObjectType([
|
||||
'name' => 'SubType',
|
||||
'fields' => [],
|
||||
'interfaces' => [$interfaceType]
|
||||
]);
|
||||
|
||||
$schema = new Schema($interfaceType, $subType);
|
||||
|
||||
$errors = SchemaValidator::validate($schema, [$rule]);
|
||||
$this->assertEmpty($errors);
|
||||
}
|
||||
|
||||
|
||||
// Rule: TypesInterfacesMustShowThemAsPossible
|
||||
|
||||
public function testAcceptsInterfaceWithASubtypeDeclaredUsingOurInfra()
|
||||
{
|
||||
// accepts an interface with a subtype declared using our infra
|
||||
$this->assertAcceptingAnInterfaceWithANormalSubtype(SchemaValidator::typesInterfacesMustShowThemAsPossibleRule());
|
||||
}
|
||||
|
||||
public function testRejectsWhenAnImplementationIsNotAPossibleType()
|
||||
{
|
||||
// rejects when an implementation is not a possible type
|
||||
$interfaceType = new InterfaceType([
|
||||
'name' => 'InterfaceType',
|
||||
'fields' => []
|
||||
]);
|
||||
|
||||
$subType = new ObjectType([
|
||||
'name' => 'SubType',
|
||||
'fields' => [],
|
||||
'interfaces' => []
|
||||
]);
|
||||
|
||||
$tmp = new \ReflectionObject($subType);
|
||||
$prop = $tmp->getProperty('_interfaces');
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue($subType, [$interfaceType]);
|
||||
|
||||
// Sanity check the test.
|
||||
$this->assertEquals([$interfaceType], $subType->getInterfaces());
|
||||
$this->assertSame(false, $interfaceType->isPossibleType($subType));
|
||||
|
||||
// Need to make sure SubType is in the schema! We rely on
|
||||
// possibleTypes to be able to see it unless it's explicitly used.
|
||||
$schema = new Schema($interfaceType, $subType);
|
||||
|
||||
// Another sanity check.
|
||||
$this->assertSame($subType, $schema->getType('SubType'));
|
||||
|
||||
$errors = SchemaValidator::validate($schema, [SchemaValidator::typesInterfacesMustShowThemAsPossibleRule()]);
|
||||
$this->assertSame(1, count($errors));
|
||||
$this->assertSame(
|
||||
'SubType implements interface InterfaceType, but InterfaceType does ' .
|
||||
'not list it as possible!',
|
||||
$errors[0]->message
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user