Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

fix: output list:$type keys for Root fields that return a listOf Nodes #2861

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
116 changes: 80 additions & 36 deletions src/Utils/QueryAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

use Exception;
use GraphQL\Error\SyntaxError;
use GraphQL\Language\AST\Node;
use GraphQL\Language\AST\TypeDefinitionNode;
use GraphQL\Language\Parser;
use GraphQL\Language\Visitor;
use GraphQL\Server\OperationParams;
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\Type;
use GraphQL\Type\Schema;
Expand All @@ -17,6 +21,7 @@
use Hoa\Math\Util;
use WPGraphQL\Model\Model;
use WPGraphQL\Request;
use WPGraphQL\Type\WPConnectionType;
use WPGraphQL\WPSchema;

/**
Expand Down Expand Up @@ -296,6 +301,62 @@ public function get_query_id(): ?string {
return $this->query_id;
}


/**
* @param Type $type The Type of field
* @param FieldDefinition $field_def The field definition the type is for
* @param mixed $parent_type The Parent Type
* @param bool $is_list_type Whether the field is a list type field
*
* @return Type|String|null
*/
public function get_wrapped_field_type( Type $type, FieldDefinition $field_def, $parent_type, bool $is_list_type = false ) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Function get_wrapped_field_type has a Cognitive Complexity of 20 (exceeds 5 allowed). Consider refactoring.


if ( ! isset( $parent_type->name ) || 'RootQuery' !== $parent_type->name ) {
return null;
}

if ( $type instanceof NonNull || $type instanceof ListOfType ) {

if ( $type instanceof ListOfType && isset( $parent_type->name ) && 'RootQuery' === $parent_type->name ) {
$is_list_type = true;
}

return $this->get_wrapped_field_type( $type->getWrappedType(), $field_def, $parent_type, $is_list_type );
}

// Determine if we're dealing with a connection
if ( $type instanceof ObjectType || $type instanceof InterfaceType ) {

$interfaces = method_exists( $type, 'getInterfaces' ) ? $type->getInterfaces() : [];
$interface_names = ! empty( $interfaces ) ? array_map( static function( InterfaceType $interface ) {
return $interface->name;
}, $interfaces ) : [];

if ( array_key_exists( 'Connection', $interface_names ) ) {

if ( isset( $field_def->config['fromType'] ) && ( 'rootquery' !== strtolower( $field_def->config['fromType'] ) ) ) {
return null;
}

$to_type = $field_def->config['toType'] ?? null;
if ( empty( $to_type ) ) {
return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid too many return statements within this method.

}

return $to_type;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid too many return statements within this method.

}

if ( ! $is_list_type ) {
return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid too many return statements within this method.

}

return $type;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid too many return statements within this method.

}

return null;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid too many return statements within this method.

}

/**
* Given the Schema and a query string, return a list of GraphQL Types that are being asked for
* by the query.
Expand Down Expand Up @@ -334,7 +395,13 @@ public function set_list_types( ?Schema $schema, ?string $query ): array {
$type_info = new TypeInfo( $schema );

$visitor = [
'enter' => function ( $node, $key, $parent, $path, $ancestors ) use ( $type_info, &$type_map, $schema ) {
'enter' => function ( Node $node, $key, $parent, $path, $ancestors ) use ( $type_info, &$type_map, $schema ) {

$parent_type = $type_info->getParentType();

if ( 'Field' !== $node->kind ) {
Visitor::skipNode();
}

$type_info->enter( $node );
$field_def = $type_info->getFieldDef();
Expand All @@ -343,65 +410,42 @@ public function set_list_types( ?Schema $schema, ?string $query ): array {
return;
}

// Determine the wrapped type, which also determines if it's a listOf
$field_type = $field_def->getType();
$field_type = $this->get_wrapped_field_type( $field_type, $field_def, $parent_type );

if ( empty( $field_type->config['interfaces'] ) ) {
if ( null === $field_type ) {
return;
}

if ( ! in_array( 'Connection', $field_type->config['interfaces'], true ) ) {
return;
if ( ! empty( $field_type ) && is_string( $field_type ) ) {
$field_type = $schema->getType( Utils::format_type_name( $field_type ) );
}

if ( empty( $field_def->config['fromType'] ) || 'rootquery' !== strtolower( $field_def->config['fromType'] ) ) {
if ( ! $field_type ) {
return;
}

if ( empty( $field_def->config['toType'] ) ) {
return;
}

$to_type = $schema->getType( ucfirst( $field_def->config['toType'] ) );

if ( ! $to_type instanceof Type ) {
return;
}
$field_type = $schema->getType( $field_type );

if ( ! isset( $node->kind ) || 'Field' !== $node->kind ) {
return;
}

if ( ! $to_type instanceof ObjectType && ! $to_type instanceof InterfaceType ) {
return;
}

$interfaces = $to_type->getInterfaces();

if ( empty( $interfaces ) ) {
return;
}

// Get the interface names
$interface_names = array_keys( $interfaces );

// If the Node interface isn't applied, it's not a node type
if ( ! in_array( 'Node', $interface_names, true ) ) {
if ( ! $field_type instanceof ObjectType && ! $field_type instanceof InterfaceType ) {
return;
}

// If the type being queried is an interface (i.e. ContentNode) the publishing a new
// item of any of the possible types (post, page, etc) should invalidate
// this query, so we need to tag this query with `list:$possible_type` for each possible type
if ( $to_type instanceof InterfaceType ) {
$possible_types = $schema->getPossibleTypes( $to_type );
if ( $field_type instanceof InterfaceType ) {
$possible_types = $schema->getPossibleTypes( $field_type );
if ( ! empty( $possible_types ) ) {
foreach ( $possible_types as $possible_type ) {
$type_map[] = 'list:' . strtolower( $possible_type );
}
}
} else {
$type_map[] = 'list:' . strtolower( $to_type );
$type_map[] = 'list:' . strtolower( $field_type );
}

},
'leave' => function ( $node, $key, $parent, $path, $ancestors ) use ( $type_info ) {
$type_info->leave( $node );
Expand Down
62 changes: 62 additions & 0 deletions tests/wpunit/QueryAnalyzerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,70 @@ public function testNestedConnectionDoesNotShowInListTypes() {

$this->assertSame( [ 'list:page' ], $types );

}

public function testNonNullListOfNonNullPostMapsToListOfPosts() {

register_graphql_field( 'RootQuery', 'listOfThing', [
'type' => [
'non_null' => [
'list_of' => [
'non_null' => 'Post'
],
],
],
]);

$query = '
{
listOfThing {
__typename
}
}
';

$request = graphql([
'query' => $query,
], true );

$request->execute();

$types = $request->get_query_analyzer()->get_list_types();

$this->assertContains( 'list:post', $types );

}

public function testListOfNonNullPostMapsToListOfPosts() {

register_graphql_field( 'RootQuery', 'listOfThing', [
'type' => [
'list_of' => [
'non_null' => 'Post'
],
],
]);

$query = '
{
listOfThing {
__typename
}
}
';

$request = graphql([
'query' => $query,
], true );

$request->execute();

$types = $request->get_query_analyzer()->get_list_types();

$this->assertContains( 'list:post', $types );

}



}