diff --git a/.github/workflows/schema-linter.yml b/.github/workflows/schema-linter.yml index 3c5604992..33ee04a76 100644 --- a/.github/workflows/schema-linter.yml +++ b/.github/workflows/schema-linter.yml @@ -54,7 +54,7 @@ jobs: - name: Lint the Static Schema run: | - graphql-schema-linter --except=relay-connection-types-spec,relay-page-info-spec --ignore '{"defined-types-are-used":["MenuItemsWhereArgs","PostObjectUnion","TermObjectUnion","TimezoneEnum"]}' /tmp/schema.graphql + graphql-schema-linter --except=relay-connection-types-spec --ignore '{"defined-types-are-used":["MenuItemsWhereArgs","PostObjectUnion","TermObjectUnion","TimezoneEnum"]}' /tmp/schema.graphql - name: Display ignored linting errors run: | diff --git a/.gitignore b/.gitignore index 3c2e08739..b525248b1 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ plugin-build !src/Admin/GraphiQL/app/build coverage/* schema.graphql +wp-graphql/ phpcs.xml phpunit.xml docker-output diff --git a/access-functions.php b/access-functions.php index 165df5059..27686b7c9 100644 --- a/access-functions.php +++ b/access-functions.php @@ -406,7 +406,7 @@ function deregister_graphql_field( string $type_name, string $field_name ) { function( TypeRegistry $type_registry ) use ( $type_name, $field_name ) { $type_registry->deregister_field( $type_name, $field_name ); }, - 10 + 5 ); } diff --git a/composer.json b/composer.json index 2a2f1904c..ef936129d 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ ], "require": { "php": ">=7.1.0", - "webonyx/graphql-php": "14.4.0", + "webonyx/graphql-php": "14.5.1", "ivome/graphql-relay-php": "0.5.0" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 1663d7502..3885dceb5 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e110e1060dba00e11e541b5dd84764d4", + "content-hash": "a65a14addafb8d3a0b523c16470b0ea8", "packages": [ { "name": "ivome/graphql-relay-php", @@ -53,16 +53,16 @@ }, { "name": "webonyx/graphql-php", - "version": "v14.4.0", + "version": "v14.5.1", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "aab3d49181467db064b41429cde117a7589625fc" + "reference": "3af8b92d07e0d7a9085c0b83daf86beaeb76f092" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/aab3d49181467db064b41429cde117a7589625fc", - "reference": "aab3d49181467db064b41429cde117a7589625fc", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/3af8b92d07e0d7a9085c0b83daf86beaeb76f092", + "reference": "3af8b92d07e0d7a9085c0b83daf86beaeb76f092", "shasum": "" }, "require": { @@ -107,7 +107,7 @@ ], "support": { "issues": "https://github.com/webonyx/graphql-php/issues", - "source": "https://github.com/webonyx/graphql-php/tree/v14.4.0" + "source": "https://github.com/webonyx/graphql-php/tree/v14.5.1" }, "funding": [ { @@ -115,7 +115,7 @@ "type": "open_collective" } ], - "time": "2020-12-03T16:05:21+00:00" + "time": "2021-02-05T10:51:56+00:00" } ], "packages-dev": [ @@ -991,16 +991,16 @@ }, { "name": "composer/ca-bundle", - "version": "1.2.9", + "version": "1.2.10", "source": { "type": "git", "url": "https://github.com/composer/ca-bundle.git", - "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5" + "reference": "9fdb22c2e97a614657716178093cd1da90a64aa8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/ca-bundle/zipball/78a0e288fdcebf92aa2318a8d3656168da6ac1a5", - "reference": "78a0e288fdcebf92aa2318a8d3656168da6ac1a5", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/9fdb22c2e97a614657716178093cd1da90a64aa8", + "reference": "9fdb22c2e97a614657716178093cd1da90a64aa8", "shasum": "" }, "require": { @@ -1047,7 +1047,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/ca-bundle/issues", - "source": "https://github.com/composer/ca-bundle/tree/1.2.9" + "source": "https://github.com/composer/ca-bundle/tree/1.2.10" }, "funding": [ { @@ -1063,20 +1063,20 @@ "type": "tidelift" } ], - "time": "2021-01-12T12:10:35+00:00" + "time": "2021-06-07T13:58:28+00:00" }, { "name": "composer/composer", - "version": "2.0.14", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "92b2ccbef65292ba9f2004271ef47c7231e2eed5" + "reference": "fc5c4573aafce3a018eb7f1f8f91cea423970f2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/92b2ccbef65292ba9f2004271ef47c7231e2eed5", - "reference": "92b2ccbef65292ba9f2004271ef47c7231e2eed5", + "url": "https://api.github.com/repos/composer/composer/zipball/fc5c4573aafce3a018eb7f1f8f91cea423970f2e", + "reference": "fc5c4573aafce3a018eb7f1f8f91cea423970f2e", "shasum": "" }, "require": { @@ -1111,7 +1111,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -1145,7 +1145,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/composer/issues", - "source": "https://github.com/composer/composer/tree/2.0.14" + "source": "https://github.com/composer/composer/tree/2.1.3" }, "funding": [ { @@ -1161,7 +1161,7 @@ "type": "tidelift" } ], - "time": "2021-05-21T15:03:37+00:00" + "time": "2021-06-09T14:31:20+00:00" }, { "name": "composer/metadata-minifier", @@ -2130,7 +2130,7 @@ }, { "name": "illuminate/collections", - "version": "v8.44.0", + "version": "v8.46.0", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", @@ -2184,16 +2184,16 @@ }, { "name": "illuminate/contracts", - "version": "v8.44.0", + "version": "v8.46.0", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "b9a7cf6acf1d05d863b6ef67c00cdb0e4ccea097" + "reference": "199fcedc161ba4a0b83feaddc4629f395dbf1641" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/b9a7cf6acf1d05d863b6ef67c00cdb0e4ccea097", - "reference": "b9a7cf6acf1d05d863b6ef67c00cdb0e4ccea097", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/199fcedc161ba4a0b83feaddc4629f395dbf1641", + "reference": "199fcedc161ba4a0b83feaddc4629f395dbf1641", "shasum": "" }, "require": { @@ -2228,11 +2228,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2021-05-21T19:16:29+00:00" + "time": "2021-06-01T14:53:38+00:00" }, { "name": "illuminate/macroable", - "version": "v8.44.0", + "version": "v8.46.0", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -2278,16 +2278,16 @@ }, { "name": "illuminate/support", - "version": "v8.44.0", + "version": "v8.46.0", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "8a2db3cb7e76fdbd28c6172492a79ab540a9b8e5" + "reference": "79e1ec26d58426e4f06e5b548712a8a6dc1ffdca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/8a2db3cb7e76fdbd28c6172492a79ab540a9b8e5", - "reference": "8a2db3cb7e76fdbd28c6172492a79ab540a9b8e5", + "url": "https://api.github.com/repos/illuminate/support/zipball/79e1ec26d58426e4f06e5b548712a8a6dc1ffdca", + "reference": "79e1ec26d58426e4f06e5b548712a8a6dc1ffdca", "shasum": "" }, "require": { @@ -2342,7 +2342,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2021-05-20T18:46:23+00:00" + "time": "2021-06-08T13:14:36+00:00" }, { "name": "justinrainbow/json-schema", @@ -2779,16 +2779,16 @@ }, { "name": "nesbot/carbon", - "version": "2.48.1", + "version": "2.49.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "8d1f50f1436fb4b05e7127360483dd9c6e73da16" + "reference": "93d9db91c0235c486875d22f1e08b50bdf3e6eee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/8d1f50f1436fb4b05e7127360483dd9c6e73da16", - "reference": "8d1f50f1436fb4b05e7127360483dd9c6e73da16", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/93d9db91c0235c486875d22f1e08b50bdf3e6eee", + "reference": "93d9db91c0235c486875d22f1e08b50bdf3e6eee", "shasum": "" }, "require": { @@ -2868,7 +2868,7 @@ "type": "tidelift" } ], - "time": "2021-05-26T22:08:38+00:00" + "time": "2021-06-02T07:31:40+00:00" }, { "name": "phar-io/manifest", @@ -4319,16 +4319,16 @@ }, { "name": "rmccue/requests", - "version": "v1.8.0", + "version": "v1.8.1", "source": { "type": "git", "url": "https://github.com/WordPress/Requests.git", - "reference": "afbe4790e4def03581c4a0963a1e8aa01f6030f1" + "reference": "82e6936366eac3af4d836c18b9d8c31028fe4cd5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/Requests/zipball/afbe4790e4def03581c4a0963a1e8aa01f6030f1", - "reference": "afbe4790e4def03581c4a0963a1e8aa01f6030f1", + "url": "https://api.github.com/repos/WordPress/Requests/zipball/82e6936366eac3af4d836c18b9d8c31028fe4cd5", + "reference": "82e6936366eac3af4d836c18b9d8c31028fe4cd5", "shasum": "" }, "require": { @@ -4373,9 +4373,9 @@ ], "support": { "issues": "https://github.com/WordPress/Requests/issues", - "source": "https://github.com/WordPress/Requests/tree/v1.8.0" + "source": "https://github.com/WordPress/Requests/tree/v1.8.1" }, - "time": "2021-04-27T11:05:25+00:00" + "time": "2021-06-04T09:56:25+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", diff --git a/src/Connection/Commenter.php b/src/Connection/Commenter.php deleted file mode 100644 index abb1710f7..000000000 --- a/src/Connection/Commenter.php +++ /dev/null @@ -1,49 +0,0 @@ - 'Comment', - 'toType' => 'Commenter', - 'description' => __( 'The author of the comment', 'wp-graphql' ), - 'fromFieldName' => 'author', - 'oneToOne' => true, - 'resolve' => function( $comment, $args, AppContext $context, ResolveInfo $info ) { - - /** - * If the comment has a user associated, use it to populate the author, otherwise return - * the $comment and the Union will use that to hydrate the CommentAuthor Type - */ - if ( ! empty( $comment->userId ) ) { - $node = $context->get_loader( 'user' )->load( absint( $comment->userId ) ); - } else { - $node = ! empty( $comment->commentId ) ? $context->get_loader( 'comment_author' )->load( $comment->commentId ) : null; - } - - return [ - 'node' => $node, - 'source' => $comment, - ]; - - }, - ]); - - } -} diff --git a/src/Connection/Comments.php b/src/Connection/Comments.php index 52dd7fa6a..6b97a6675 100644 --- a/src/Connection/Comments.php +++ b/src/Connection/Comments.php @@ -121,13 +121,14 @@ public static function register_connections() { * * @return array */ - public static function get_connection_config( $args = [] ) { + public static function get_connection_config( array $args = [] ) { $defaults = [ - 'fromType' => 'RootQuery', - 'toType' => 'Comment', - 'fromFieldName' => 'comments', - 'connectionArgs' => self::get_connection_args(), - 'resolve' => function( $root, $args, $context, $info ) { + 'fromType' => 'RootQuery', + 'toType' => 'Comment', + 'connectionInterfaces' => [ 'CommentConnection' ], + 'fromFieldName' => 'comments', + 'connectionArgs' => self::get_connection_args(), + 'resolve' => function( $root, $args, $context, $info ) { return DataSource::resolve_comments_connection( $root, $args, $context, $info ); }, ]; diff --git a/src/Connection/ContentTypes.php b/src/Connection/ContentTypes.php deleted file mode 100644 index bf8f45aa3..000000000 --- a/src/Connection/ContentTypes.php +++ /dev/null @@ -1,76 +0,0 @@ - 'RootQuery', - 'toType' => 'ContentType', - 'fromFieldName' => 'contentTypes', - 'resolve' => function( $source, $args, $context, $info ) { - $resolver = new ContentTypeConnectionResolver( $source, $args, $context, $info ); - return $resolver->get_connection(); - }, - ] - ); - - register_graphql_connection( - [ - 'fromType' => 'ContentNode', - 'toType' => 'ContentType', - 'fromFieldName' => 'contentType', - 'resolve' => function( Post $source, $args, $context, $info ) { - - if ( $source->isRevision ) { - $parent = get_post( $source->parentDatabaseId ); - $post_type = isset( $parent->post_type ) ? $parent->post_type : null; - } else { - $post_type = isset( $source->post_type ) ? $source->post_type : null; - } - - if ( empty( $post_type ) ) { - return null; - } - - $resolver = new ContentTypeConnectionResolver( $source, $args, $context, $info ); - return $resolver->one_to_one()->set_query_arg( 'name', $post_type )->get_connection(); - }, - 'oneToOne' => true, - ] - ); - - register_graphql_connection([ - 'fromType' => 'Taxonomy', - 'toType' => 'ContentType', - 'description' => __( 'List of Content Types associated with the Taxonomy', 'wp-graphql' ), - 'fromFieldName' => 'connectedContentTypes', - 'resolve' => function( Taxonomy $taxonomy, $args, AppContext $context, ResolveInfo $info ) { - - $connected_post_types = ! empty( $taxonomy->object_type ) ? $taxonomy->object_type : []; - $resolver = new ContentTypeConnectionResolver( $taxonomy, $args, $context, $info ); - $resolver->set_query_arg( 'contentTypeNames', $connected_post_types ); - return $resolver->get_connection(); - - }, - ]); - - } - -} diff --git a/src/Connection/EnqueuedScripts.php b/src/Connection/EnqueuedScripts.php deleted file mode 100644 index e860e3105..000000000 --- a/src/Connection/EnqueuedScripts.php +++ /dev/null @@ -1,69 +0,0 @@ - 'ContentNode', - 'toType' => 'EnqueuedScript', - 'fromFieldName' => 'enqueuedScripts', - 'resolve' => function( $source, $args, $context, $info ) { - $resolver = new EnqueuedScriptsConnectionResolver( $source, $args, $context, $info ); - return $resolver->get_connection(); - }, - ]); - - register_graphql_connection([ - 'fromType' => 'TermNode', - 'toType' => 'EnqueuedScript', - 'fromFieldName' => 'enqueuedScripts', - 'resolve' => function( $source, $args, $context, $info ) { - $resolver = new EnqueuedScriptsConnectionResolver( $source, $args, $context, $info ); - return $resolver->get_connection(); - }, - ]); - - register_graphql_connection([ - 'fromType' => 'RootQuery', - 'toType' => 'EnqueuedScript', - 'fromFieldName' => 'registeredScripts', - 'resolve' => function( $source, $args, $context, $info ) { - - // The connection resolver expects the source to include - // enqueuedScriptsQueue - $source = new \stdClass(); - $source->enqueuedScriptsQueue = []; - global $wp_scripts; - do_action( 'wp_enqueue_scripts' ); - $source->enqueuedScriptsQueue = array_keys( $wp_scripts->registered ); - $resolver = new EnqueuedScriptsConnectionResolver( $source, $args, $context, $info ); - return $resolver->get_connection(); - }, - ]); - - register_graphql_connection([ - 'fromType' => 'User', - 'toType' => 'EnqueuedScript', - 'fromFieldName' => 'enqueuedScripts', - 'resolve' => function( $source, $args, $context, $info ) { - $resolver = new EnqueuedScriptsConnectionResolver( $source, $args, $context, $info ); - return $resolver->get_connection(); - }, - ]); - - } -} diff --git a/src/Connection/EnqueuedStylesheets.php b/src/Connection/EnqueuedStylesheets.php deleted file mode 100644 index bc1fe4bbf..000000000 --- a/src/Connection/EnqueuedStylesheets.php +++ /dev/null @@ -1,69 +0,0 @@ - 'ContentNode', - 'toType' => 'EnqueuedStylesheet', - 'fromFieldName' => 'enqueuedStylesheets', - 'resolve' => function( $source, $args, $context, $info ) { - $resolver = new EnqueuedStylesheetConnectionResolver( $source, $args, $context, $info ); - return $resolver->get_connection(); - }, - ]); - - register_graphql_connection([ - 'fromType' => 'TermNode', - 'toType' => 'EnqueuedStylesheet', - 'fromFieldName' => 'enqueuedStylesheets', - 'resolve' => function( $source, $args, $context, $info ) { - $resolver = new EnqueuedStylesheetConnectionResolver( $source, $args, $context, $info ); - return $resolver->get_connection(); - }, - ]); - - register_graphql_connection([ - 'fromType' => 'RootQuery', - 'toType' => 'EnqueuedStylesheet', - 'fromFieldName' => 'registeredStylesheets', - 'resolve' => function( $source, $args, $context, $info ) { - - // The connection resolver expects the source to include - // enqueuedStylesheetsQueue - $source = new \stdClass(); - $source->enqueuedStylesheetsQueue = []; - global $wp_styles; - do_action( 'wp_enqueue_scripts' ); - $source->enqueuedStylesheetsQueue = array_keys( $wp_styles->registered ); - $resolver = new EnqueuedStylesheetConnectionResolver( $source, $args, $context, $info ); - return $resolver->get_connection(); - }, - ]); - - register_graphql_connection([ - 'fromType' => 'User', - 'toType' => 'EnqueuedStylesheet', - 'fromFieldName' => 'enqueuedStylesheets', - 'resolve' => function( $source, $args, $context, $info ) { - $resolver = new EnqueuedStylesheetConnectionResolver( $source, $args, $context, $info ); - return $resolver->get_connection(); - }, - ]); - - } -} diff --git a/src/Connection/MediaItems.php b/src/Connection/MediaItems.php deleted file mode 100644 index cc6ab309d..000000000 --- a/src/Connection/MediaItems.php +++ /dev/null @@ -1,42 +0,0 @@ - 'NodeWithFeaturedImage', - 'toType' => 'MediaItem', - 'fromFieldName' => 'featuredImage', - 'oneToOne' => true, - 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { - - if ( empty( $post->featuredImageDatabaseId ) ) { - return null; - } - - $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info, 'attachment' ); - $resolver->set_query_arg( 'p', absint( $post->featuredImageDatabaseId ) ); - return $resolver->one_to_one()->get_connection(); - - }, - ]); - - } -} diff --git a/src/Connection/MenuItemLinkableConnection.php b/src/Connection/MenuItemLinkableConnection.php deleted file mode 100644 index 83a09b417..000000000 --- a/src/Connection/MenuItemLinkableConnection.php +++ /dev/null @@ -1,55 +0,0 @@ - 'MenuItem', - 'toType' => 'MenuItemLinkable', - 'description' => __( 'Connection from MenuItem to it\'s connected node', 'wp-graphql' ), - 'fromFieldName' => 'connectedNode', - 'oneToOne' => true, - 'resolve' => function( MenuItem $menu_item, $args, AppContext $context, ResolveInfo $info ) { - - $object_id = intval( get_post_meta( $menu_item->databaseId, '_menu_item_object_id', true ) ); - $object_type = get_post_meta( $menu_item->databaseId, '_menu_item_type', true ); - - $resolver = null; - switch ( $object_type ) { - // Post object - case 'post_type': - $resolver = new PostObjectConnectionResolver( $menu_item, $args, $context, $info ); - $resolver->set_query_arg( 'p', $object_id ); - break; - - // Taxonomy term - case 'taxonomy': - $resolver = new TermObjectConnectionResolver( $menu_item, $args, $context, $info ); - $resolver->set_query_arg( 'include', $object_id ); - break; - default: - $resolved_object = null; - break; - } - - return ! empty( $resolver ) ? $resolver->one_to_one()->get_connection() : null; - - }, - ]); - - } - -} diff --git a/src/Connection/MenuItems.php b/src/Connection/MenuItems.php index eddcd94b2..3918915d7 100644 --- a/src/Connection/MenuItems.php +++ b/src/Connection/MenuItems.php @@ -96,10 +96,11 @@ public static function register_connections() { public static function get_connection_config( $args = [] ) { return array_merge( [ - 'fromType' => 'RootQuery', - 'fromFieldName' => 'menuItems', - 'toType' => 'MenuItem', - 'connectionArgs' => [ + 'fromType' => 'RootQuery', + 'fromFieldName' => 'menuItems', + 'toType' => 'MenuItem', + 'connectionInterfaces' => [ 'MenuItemConnection' ], + 'connectionArgs' => [ 'id' => [ 'type' => 'Int', 'description' => __( 'The ID of the object', 'wp-graphql' ), @@ -117,7 +118,7 @@ public static function get_connection_config( $args = [] ) { 'description' => __( 'The menu location for the menu being queried', 'wp-graphql' ), ], ], - 'resolve' => function( $source, $args, $context, $info ) { + 'resolve' => function( $source, $args, $context, $info ) { $resolver = new MenuItemConnectionResolver( $source, $args, $context, $info ); $connection = $resolver->get_connection(); diff --git a/src/Connection/Menus.php b/src/Connection/Menus.php deleted file mode 100644 index f6932e5f8..000000000 --- a/src/Connection/Menus.php +++ /dev/null @@ -1,69 +0,0 @@ - 'RootQuery', - 'toType' => 'Menu', - 'fromFieldName' => 'menus', - 'connectionArgs' => [ - 'id' => [ - 'type' => 'Int', - 'description' => __( 'The ID of the object', 'wp-graphql' ), - ], - 'location' => [ - 'type' => 'MenuLocationEnum', - 'description' => __( 'The menu location for the menu being queried', 'wp-graphql' ), - ], - 'slug' => [ - 'type' => 'String', - 'description' => __( 'The slug of the menu to query items for', 'wp-graphql' ), - ], - ], - 'resolve' => function ( $source, $args, $context, $info ) { - $resolver = new MenuConnectionResolver( $source, $args, $context, $info, 'nav_menu' ); - $connection = $resolver->get_connection(); - return $connection; - }, - ] - ); - - register_graphql_connection([ - 'fromType' => 'MenuItem', - 'toType' => 'Menu', - 'description' => __( 'The Menu a MenuItem is part of', 'wp-graphql' ), - 'fromFieldName' => 'menu', - 'oneToOne' => true, - 'resolve' => function( MenuItem $menu_item, $args, $context, $info ) { - $resolver = new MenuConnectionResolver( $menu_item, $args, $context, $info ); - $resolver->set_query_arg( 'include', $menu_item->menuDatabaseId ); - return $resolver->one_to_one()->get_connection(); - }, - ]); - } -} diff --git a/src/Connection/Plugins.php b/src/Connection/Plugins.php deleted file mode 100644 index f1ad9fa43..000000000 --- a/src/Connection/Plugins.php +++ /dev/null @@ -1,33 +0,0 @@ - 'RootQuery', - 'toType' => 'Plugin', - 'fromFieldName' => 'plugins', - 'resolve' => function ( $root, $args, $context, $info ) { - return DataSource::resolve_plugins_connection( $root, $args, $context, $info ); - }, - ] - ); - } -} diff --git a/src/Connection/PostObjects.php b/src/Connection/PostObjects.php index 939edf077..df63cd6cf 100644 --- a/src/Connection/PostObjects.php +++ b/src/Connection/PostObjects.php @@ -32,66 +32,76 @@ class PostObjects { */ public static function register_connections() { - register_graphql_connection( [ - 'fromType' => 'ContentType', - 'toType' => 'ContentNode', - 'fromFieldName' => 'contentNodes', - 'connectionArgs' => self::get_connection_args(), - 'queryClass' => 'WP_Query', - 'resolve' => function( PostType $post_type, $args, AppContext $context, ResolveInfo $info ) { - - $resolver = new PostObjectConnectionResolver( $post_type, $args, $context, $info ); - $resolver->set_query_arg( 'post_type', $post_type->name ); - - return $resolver->get_connection(); - - }, - ] ); - - register_graphql_connection( [ - 'fromType' => 'Comment', - 'toType' => 'ContentNode', - 'queryClass' => 'WP_Query', - 'oneToOne' => true, - 'fromFieldName' => 'commentedOn', - 'resolve' => function( Comment $comment, $args, AppContext $context, ResolveInfo $info ) { - if ( empty( $comment->comment_post_ID ) || ! absint( $comment->comment_post_ID ) ) { - return null; - } - $id = absint( $comment->comment_post_ID ); - $resolver = new PostObjectConnectionResolver( $comment, $args, $context, $info, 'any' ); - - return $resolver->one_to_one()->set_query_arg( 'p', $id )->set_query_arg( 'post_parent', null )->get_connection(); - }, - ] ); - - register_graphql_connection( [ - 'fromType' => 'NodeWithRevisions', - 'toType' => 'ContentNode', - 'fromFieldName' => 'revisionOf', - 'description' => __( 'If the current node is a revision, this field exposes the node this is a revision of. Returns null if the node is not a revision of another node.', 'wp-graphql' ), - 'oneToOne' => true, - 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { - - if ( ! $post->isRevision || ! isset( $post->parentDatabaseId ) || ! absint( $post->parentDatabaseId ) ) { - return null; - } + register_graphql_connection( + [ + 'fromType' => 'ContentType', + 'toType' => 'ContentNode', + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'fromFieldName' => 'contentNodes', + 'connectionArgs' => self::get_connection_args(), + 'queryClass' => 'WP_Query', + 'resolve' => function( PostType $post_type, $args, AppContext $context, ResolveInfo $info ) { + + $resolver = new PostObjectConnectionResolver( $post_type, $args, $context, $info ); + $resolver->set_query_arg( 'post_type', $post_type->name ); + + return $resolver->get_connection(); + + }, + ] + ); + + register_graphql_connection( + [ + 'fromType' => 'Comment', + 'toType' => 'ContentNode', + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'queryClass' => 'WP_Query', + 'oneToOne' => true, + 'fromFieldName' => 'commentedOn', + 'resolve' => function( Comment $comment, $args, AppContext $context, ResolveInfo $info ) { + if ( empty( $comment->comment_post_ID ) || ! absint( $comment->comment_post_ID ) ) { + return null; + } + $id = absint( $comment->comment_post_ID ); + $resolver = new PostObjectConnectionResolver( $comment, $args, $context, $info, 'any' ); + + return $resolver->one_to_one()->set_query_arg( 'p', $id )->set_query_arg( 'post_parent', null )->get_connection(); + }, + ] + ); + + register_graphql_connection( + [ + 'fromType' => 'NodeWithRevisions', + 'toType' => 'ContentNode', + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'fromFieldName' => 'revisionOf', + 'description' => __( 'If the current node is a revision, this field exposes the node this is a revision of. Returns null if the node is not a revision of another node.', 'wp-graphql' ), + 'oneToOne' => true, + 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { + + if ( ! $post->isRevision || ! isset( $post->parentDatabaseId ) || ! absint( $post->parentDatabaseId ) ) { + return null; + } - $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); - $resolver->set_query_arg( 'p', $post->parentDatabaseId ); + $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); + $resolver->set_query_arg( 'p', $post->parentDatabaseId ); - return $resolver->one_to_one()->get_connection(); + return $resolver->one_to_one()->get_connection(); - }, - ] ); + }, + ] + ); register_graphql_connection( [ - 'fromType' => 'RootQuery', - 'toType' => 'ContentNode', - 'queryClass' => 'WP_Query', - 'fromFieldName' => 'contentNodes', - 'connectionArgs' => self::get_connection_args( + 'fromType' => 'RootQuery', + 'toType' => 'ContentNode', + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'queryClass' => 'WP_Query', + 'fromFieldName' => 'contentNodes', + 'connectionArgs' => self::get_connection_args( [ 'contentTypes' => [ 'type' => [ 'list_of' => 'ContentTypeEnum' ], @@ -100,7 +110,7 @@ public static function register_connections() { ], null ), - 'resolve' => function( $source, $args, $context, $info ) { + 'resolve' => function( $source, $args, $context, $info ) { $post_types = isset( $args['where']['contentTypes'] ) && is_array( $args['where']['contentTypes'] ) ? $args['where']['contentTypes'] : \WPGraphQL::get_allowed_post_types(); return DataSource::resolve_post_objects_connection( $source, $args, $context, $info, $post_types ); @@ -108,69 +118,102 @@ public static function register_connections() { ] ); - register_graphql_connection( [ - 'fromType' => 'HierarchicalContentNode', - 'toType' => 'ContentNode', - 'fromFieldName' => 'parent', - 'connectionTypeName' => 'HierarchicalContentNodeToParentContentNodeConnection', - 'description' => __( 'The parent of the node. The parent object can be of various types', 'wp-graphql' ), - 'oneToOne' => true, - 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { - - if ( ! isset( $post->parentDatabaseId ) || ! absint( $post->parentDatabaseId ) ) { - return null; - } + register_graphql_connection( + [ + 'fromType' => 'HierarchicalContentNode', + 'toType' => 'ContentNode', + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'fromFieldName' => 'parent', + 'connectionTypeName' => 'HierarchicalContentNodeToParentContentNodeConnection', + 'description' => __( 'The parent of the node. The parent object can be of various types', 'wp-graphql' ), + 'oneToOne' => true, + 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { + + if ( ! isset( $post->parentDatabaseId ) || ! absint( $post->parentDatabaseId ) ) { + return null; + } - $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); - $resolver->set_query_arg( 'p', $post->parentDatabaseId ); + $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); + $resolver->set_query_arg( 'p', $post->parentDatabaseId ); - return $resolver->one_to_one()->get_connection(); + return $resolver->one_to_one()->get_connection(); - }, - ] ); + }, + ] + ); - register_graphql_connection( [ - 'fromType' => 'HierarchicalContentNode', - 'fromFieldName' => 'children', - 'toType' => 'ContentNode', - 'connectionTypeName' => 'HierarchicalContentNodeToContentNodeChildrenConnection', - 'connectionArgs' => self::get_connection_args(), - 'queryClass' => 'WP_Query', - 'resolve' => function( Post $post, $args, $context, $info ) { + register_graphql_connection( + [ + 'fromType' => 'MediaItem', + 'toType' => 'ContentNode', + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'fromFieldName' => 'parent', + 'connectionTypeName' => 'MediaItemToParentContentNodeConnection', + 'description' => __( 'The parent of the node. The parent object can be of various types', 'wp-graphql' ), + 'oneToOne' => true, + 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { + + if ( ! isset( $post->parentDatabaseId ) || ! absint( $post->parentDatabaseId ) ) { + return null; + } - if ( $post->isRevision ) { - $id = $post->parentDatabaseId; - } else { - $id = $post->ID; - } + $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); + $resolver->set_query_arg( 'p', $post->parentDatabaseId ); - $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info, 'any' ); - $resolver->set_query_arg( 'post_parent', $id ); - - return $resolver->get_connection(); - - }, - ] ); - - register_graphql_connection( [ - 'fromType' => 'HierarchicalContentNode', - 'toType' => 'ContentNode', - 'fromFieldName' => 'ancestors', - 'connectionArgs' => self::get_connection_args(), - 'connectionTypeName' => 'HierarchicalContentNodeToContentNodeAncestorsConnection', - 'queryClass' => 'WP_Query', - 'description' => __( 'Returns ancestors of the node. Default ordered as lowest (closest to the child) to highest (closest to the root).', 'wp-graphql' ), - 'resolve' => function( Post $post, $args, $context, $info ) { - $ancestors = get_ancestors( $post->ID, '', 'post_type' ); - if ( empty( $ancestors ) || ! is_array( $ancestors ) ) { - return null; - } - $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); - $resolver->set_query_arg( 'post__in', $ancestors ); + return $resolver->one_to_one()->get_connection(); + + }, + ] + ); + + register_graphql_connection( + [ + 'fromType' => 'HierarchicalContentNode', + 'fromFieldName' => 'children', + 'toType' => 'ContentNode', + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'connectionTypeName' => 'HierarchicalContentNodeToContentNodeChildrenConnection', + 'connectionArgs' => self::get_connection_args(), + 'queryClass' => 'WP_Query', + 'resolve' => function( Post $post, $args, $context, $info ) { + + if ( $post->isRevision ) { + $id = $post->parentDatabaseId; + } else { + $id = $post->ID; + } + + $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info, 'any' ); + $resolver->set_query_arg( 'post_parent', $id ); - return $resolver->get_connection(); - }, - ] ); + return $resolver->get_connection(); + + }, + ] + ); + + register_graphql_connection( + [ + 'fromType' => 'HierarchicalContentNode', + 'toType' => 'ContentNode', + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'fromFieldName' => 'ancestors', + 'connectionArgs' => self::get_connection_args(), + 'connectionTypeName' => 'HierarchicalContentNodeToContentNodeAncestorsConnection', + 'queryClass' => 'WP_Query', + 'description' => __( 'Returns ancestors of the node. Default ordered as lowest (closest to the child) to highest (closest to the root).', 'wp-graphql' ), + 'resolve' => function( Post $post, $args, $context, $info ) { + $ancestors = get_ancestors( $post->ID, '', 'post_type' ); + if ( empty( $ancestors ) || ! is_array( $ancestors ) ) { + return null; + } + $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); + $resolver->set_query_arg( 'post__in', $ancestors ); + + return $resolver->get_connection(); + }, + ] + ); /** * Registers connections for each post_type that has a connection @@ -199,28 +242,31 @@ public static function register_connections() { } if ( ! in_array( $post_type, [ 'attachment', 'revision' ], true ) ) { - register_graphql_connection( [ - 'fromType' => $post_type_object->graphql_single_name, - 'toType' => $post_type_object->graphql_single_name, - 'fromFieldName' => 'preview', - 'connectionTypeName' => ucfirst( $post_type_object->graphql_single_name ) . 'ToPreviewConnection', - 'oneToOne' => true, - 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { - - if ( $post->isRevision ) { - return null; - } - - if ( empty( $post->previewRevisionDatabaseId ) ) { - return null; - } - - $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info, 'revision' ); - $resolver->set_query_arg( 'p', $post->previewRevisionDatabaseId ); - - return $resolver->one_to_one()->get_connection(); - }, - ] ); + register_graphql_connection( + [ + 'fromType' => $post_type_object->graphql_single_name, + 'toType' => $post_type_object->graphql_single_name, + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'fromFieldName' => 'preview', + 'connectionTypeName' => ucfirst( $post_type_object->graphql_single_name ) . 'ToPreviewConnection', + 'oneToOne' => true, + 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { + + if ( $post->isRevision ) { + return null; + } + + if ( empty( $post->previewRevisionDatabaseId ) ) { + return null; + } + + $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info, 'revision' ); + $resolver->set_query_arg( 'p', $post->previewRevisionDatabaseId ); + + return $resolver->one_to_one()->get_connection(); + }, + ] + ); } /** @@ -253,7 +299,7 @@ public static function register_connections() { foreach ( $allowed_taxonomies as $taxonomy ) { // If the taxonomy is in the array of taxonomies registered to the post_type if ( in_array( $taxonomy, get_object_taxonomies( $post_type_object->name ), true ) ) { - /** @var \WP_Taxonomy $taxonomy_object */ + /** @var WP_Taxonomy $taxonomy_object */ $taxonomy_object = get_taxonomy( $taxonomy ); register_graphql_connection( @@ -263,14 +309,17 @@ public static function register_connections() { 'fromType' => $taxonomy_object->graphql_single_name, 'resolve' => function( Term $term, $args, AppContext $context, ResolveInfo $info ) use ( $post_type_object ) { $resolver = new PostObjectConnectionResolver( $term, $args, $context, $info, $post_type_object->name ); - $resolver->set_query_arg( 'tax_query', [ + $resolver->set_query_arg( + 'tax_query', [ - 'taxonomy' => $term->taxonomyName, - 'terms' => [ $term->term_id ], - 'field' => 'term_id', - 'include_children' => false, - ], - ] ); + [ + 'taxonomy' => $term->taxonomyName, + 'terms' => [ $term->term_id ], + 'field' => 'term_id', + 'include_children' => false, + ], + ] + ); return $resolver->get_connection(); }, @@ -290,11 +339,12 @@ public static function register_connections() { self::get_connection_config( $post_type_object, [ - 'connectionTypeName' => $post_type_object->graphql_single_name . 'ToRevisionConnection', - 'fromType' => $post_type_object->graphql_single_name, - 'toType' => $post_type_object->graphql_single_name, - 'fromFieldName' => 'revisions', - 'resolve' => function( Post $post, $args, $context, $info ) { + 'connectionTypeName' => $post_type_object->graphql_single_name . 'ToRevisionConnection', + 'fromType' => $post_type_object->graphql_single_name, + 'toType' => $post_type_object->graphql_single_name, + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'fromFieldName' => 'revisions', + 'resolve' => function( Post $post, $args, $context, $info ) { $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info, 'revision' ); $resolver->set_query_arg( 'post_parent', $post->ID ); @@ -318,26 +368,34 @@ public static function register_connections() { } // Connection from the Taxonomy to Content Nodes - register_graphql_connection( self::get_connection_config( $tax_object, [ - 'fromType' => $tax_object->graphql_single_name, - 'fromFieldName' => 'contentNodes', - 'toType' => 'ContentNode', - 'resolve' => function( Term $term, $args, $context, $info ) { - - $resolver = new PostObjectConnectionResolver( $term, $args, $context, $info, 'any' ); - $resolver->set_query_arg( 'tax_query', [ - [ - 'taxonomy' => $term->taxonomyName, - 'terms' => [ $term->term_id ], - 'field' => 'term_id', - 'include_children' => false, - ], - ] ); + register_graphql_connection( + self::get_connection_config( + $tax_object, + [ + 'fromType' => $tax_object->graphql_single_name, + 'fromFieldName' => 'contentNodes', + 'toType' => 'ContentNode', + 'resolve' => function( Term $term, $args, $context, $info ) { + + $resolver = new PostObjectConnectionResolver( $term, $args, $context, $info, 'any' ); + $resolver->set_query_arg( + 'tax_query', + [ + [ + 'taxonomy' => $term->taxonomyName, + 'terms' => [ $term->term_id ], + 'field' => 'term_id', + 'include_children' => false, + ], + ] + ); - return $resolver->get_connection(); + return $resolver->get_connection(); - }, - ] ) ); + }, + ] + ) + ); } } @@ -364,12 +422,13 @@ public static function get_connection_config( $graphql_object, $args = [] ) { return array_merge( [ - 'fromType' => 'RootQuery', - 'toType' => $graphql_object->graphql_single_name, - 'queryClass' => 'WP_Query', - 'fromFieldName' => lcfirst( $graphql_object->graphql_plural_name ), - 'connectionArgs' => $connection_args, - 'resolve' => function( $root, $args, $context, $info ) use ( $graphql_object ) { + 'fromType' => 'RootQuery', + 'toType' => $graphql_object->graphql_single_name, + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'queryClass' => 'WP_Query', + 'fromFieldName' => lcfirst( $graphql_object->graphql_plural_name ), + 'connectionArgs' => $connection_args, + 'resolve' => function( $root, $args, $context, $info ) use ( $graphql_object ) { return DataSource::resolve_post_objects_connection( $root, $args, $context, $info, $graphql_object->name ); }, ], diff --git a/src/Connection/Revisions.php b/src/Connection/Revisions.php deleted file mode 100644 index 030da0f8a..000000000 --- a/src/Connection/Revisions.php +++ /dev/null @@ -1,49 +0,0 @@ - 'RootQuery', - 'toType' => 'ContentRevisionUnion', - 'queryClass' => 'WP_Query', - 'fromFieldName' => 'revisions', - 'connectionArgs' => PostObjects::get_connection_args(), - 'resolve' => function( $root, $args, $context, $info ) { - return DataSource::resolve_post_objects_connection( $root, $args, $context, $info, 'revision' ); - }, - ] - ); - - register_graphql_connection( - [ - 'fromType' => 'User', - 'toType' => 'ContentRevisionUnion', - 'queryClass' => 'WP_Query', - 'fromFieldName' => 'revisions', - 'description' => __( 'Connection between the User and Revisions authored by the user', 'wp-graphql' ), - 'connectionArgs' => PostObjects::get_connection_args(), - 'resolve' => function( $root, $args, $context, $info ) { - return DataSource::resolve_post_objects_connection( $root, $args, $context, $info, 'revision' ); - }, - ] - ); - - } -} diff --git a/src/Connection/Taxonomies.php b/src/Connection/Taxonomies.php index e5f96466a..bd0cbad65 100644 --- a/src/Connection/Taxonomies.php +++ b/src/Connection/Taxonomies.php @@ -2,6 +2,7 @@ namespace WPGraphQL\Connection; +use Exception; use WPGraphQL\Data\Connection\TaxonomyConnectionResolver; use WPGraphQL\Model\PostType; use WPGraphQL\Model\Term; @@ -12,15 +13,17 @@ class Taxonomies { * Registers connections to the Taxonomy type * * @return void + * @throws Exception */ public static function register_connections() { register_graphql_connection( [ - 'fromType' => 'RootQuery', - 'toType' => 'Taxonomy', - 'fromFieldName' => 'taxonomies', - 'resolve' => function( $source, $args, $context, $info ) { + 'fromType' => 'RootQuery', + 'toType' => 'Taxonomy', + 'fromFieldName' => 'taxonomies', + 'connectionInterfaces' => [ 'TaxonomyConnection' ], + 'resolve' => function( $source, $args, $context, $info ) { $resolver = new TaxonomyConnectionResolver( $source, $args, $context, $info ); return $resolver->get_connection(); }, @@ -33,11 +36,12 @@ public static function register_connections() { foreach ( $taxonomies as $taxonomy ) { register_graphql_connection( [ - 'fromType' => $taxonomy->graphql_single_name, - 'toType' => 'Taxonomy', - 'fromFieldName' => 'taxonomy', - 'oneToOne' => true, - 'resolve' => function( Term $source, $args, $context, $info ) { + 'fromType' => $taxonomy->graphql_single_name, + 'toType' => 'Taxonomy', + 'connectionInterfaces' => [ 'TaxonomyConnection' ], + 'fromFieldName' => 'taxonomy', + 'oneToOne' => true, + 'resolve' => function( Term $source, $args, $context, $info ) { if ( empty( $source->taxonomyName ) ) { return null; } @@ -52,15 +56,16 @@ public static function register_connections() { register_graphql_connection( [ - 'fromType' => 'ContentType', - 'toType' => 'Taxonomy', - 'fromFieldName' => 'connectedTaxonomies', - 'resolve' => function( PostType $source, $args, $context, $info ) { + 'fromType' => 'ContentType', + 'toType' => 'Taxonomy', + 'connectionInterfaces' => [ 'TaxonomyConnection' ], + 'fromFieldName' => 'connectedTaxonomies', + 'resolve' => function( PostType $source, $args, $context, $info ) { if ( empty( $source->taxonomies ) ) { return null; } $resolver = new TaxonomyConnectionResolver( $source, $args, $context, $info ); - $resolver->setQueryArg( 'in', $source->taxonomies ); + $resolver->set_query_arg( 'in', $source->taxonomies ); return $resolver->get_connection(); }, ] diff --git a/src/Connection/TermObjects.php b/src/Connection/TermObjects.php index aa0a934fb..b8d2cacd0 100644 --- a/src/Connection/TermObjects.php +++ b/src/Connection/TermObjects.php @@ -2,6 +2,7 @@ namespace WPGraphQL\Connection; +use Exception; use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; use WPGraphQL\Data\Connection\TermObjectConnectionResolver; @@ -22,6 +23,7 @@ class TermObjects { * Register connections to TermObjects * * @return void + * @throws Exception */ public static function register_connections() { @@ -30,11 +32,12 @@ public static function register_connections() { register_graphql_connection( [ - 'fromType' => 'RootQuery', - 'toType' => 'TermNode', - 'queryClass' => 'WP_Term_Query', - 'fromFieldName' => 'terms', - 'connectionArgs' => self::get_connection_args( + 'fromType' => 'RootQuery', + 'toType' => 'TermNode', + 'connectionInterfaces' => [ 'TermNodeConnection' ], + 'queryClass' => 'WP_Term_Query', + 'fromFieldName' => 'terms', + 'connectionArgs' => self::get_connection_args( [ 'taxonomies' => [ 'type' => [ 'list_of' => 'TaxonomyEnum' ], @@ -42,12 +45,10 @@ public static function register_connections() { ], ] ), - 'resolve' => function( $source, $args, $context, $info ) { + 'resolve' => function( $source, $args, $context, $info ) { $taxonomies = isset( $args['where']['taxonomies'] ) && is_array( $args['where']['taxonomies'] ) ? $args['where']['taxonomies'] : \WPGraphQL::get_allowed_taxonomies(); $resolver = new TermObjectConnectionResolver( $source, $args, $context, $info, array_values( $taxonomies ) ); - $connection = $resolver->get_connection(); - - return $connection; + return $resolver->get_connection(); }, ] ); @@ -81,6 +82,7 @@ public static function register_connections() { 'fromType' => $post_type_object->graphql_single_name, 'toType' => $tax_object->graphql_single_name, 'fromFieldName' => $tax_object->graphql_plural_name, + 'connectionInterfaces' => [ 'TermNodeConnection' ], 'resolve' => function( Post $post, $args, AppContext $context, $info ) use ( $tax_object ) { $object_id = true === $post->isPreview && ! empty( $post->parentDatabaseId ) ? $post->parentDatabaseId : $post->ID; @@ -183,11 +185,12 @@ public static function register_connections() { } register_graphql_connection( [ - 'fromType' => $post_type_object->graphql_single_name, - 'toType' => 'TermNode', - 'fromFieldName' => 'terms', - 'queryClass' => 'WP_Term_Query', - 'connectionArgs' => self::get_connection_args( + 'fromType' => $post_type_object->graphql_single_name, + 'toType' => 'TermNode', + 'connectionInterfaces' => [ 'TermNodeConnection' ], + 'fromFieldName' => 'terms', + 'queryClass' => 'WP_Term_Query', + 'connectionArgs' => self::get_connection_args( [ 'taxonomies' => [ 'type' => [ 'list_of' => 'TaxonomyEnum' ], @@ -195,7 +198,7 @@ public static function register_connections() { ], ] ), - 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { + 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { $taxonomies = get_taxonomies( [ 'show_in_graphql' => true ] ); $terms = wp_get_post_terms( $post->ID, $taxonomies, [ 'fields' => 'ids' ] ); if ( empty( $terms ) || is_wp_error( $terms ) ) { @@ -226,13 +229,15 @@ public static function register_connections() { public static function get_connection_config( $tax_object, $args = [] ) { $defaults = [ - 'fromType' => 'RootQuery', - 'queryClass' => 'WP_Term_Query', - 'toType' => $tax_object->graphql_single_name, - 'fromFieldName' => $tax_object->graphql_plural_name, - 'connectionArgs' => self::get_connection_args(), - 'resolve' => function( $root, $args, $context, $info ) use ( $tax_object ) { - return DataSource::resolve_term_objects_connection( $root, $args, $context, $info, $tax_object->name ); + 'fromType' => 'RootQuery', + 'queryClass' => 'WP_Term_Query', + 'toType' => $tax_object->graphql_single_name, + 'fromFieldName' => $tax_object->graphql_plural_name, + 'connectionArgs' => self::get_connection_args(), + 'connectionInterfaces' => [ 'TermNodeConnection' ], + 'resolve' => function( $root, $args, $context, $info ) use ( $tax_object ) { + $connection = new TermObjectConnectionResolver( $root, $args, $context, $info, $tax_object->name ); + return $connection->get_connection(); }, ]; diff --git a/src/Connection/Themes.php b/src/Connection/Themes.php deleted file mode 100644 index b2d9b8eba..000000000 --- a/src/Connection/Themes.php +++ /dev/null @@ -1,41 +0,0 @@ - 'RootQuery', - 'toType' => 'Theme', - 'fromFieldName' => 'themes', - 'resolve' => function ( $root, $args, $context, $info ) { - $resolver = new ThemeConnectionResolver( $root, $args, $context, $info ); - return $resolver->get_connection(); - }, - ] - ); - - } - -} diff --git a/src/Connection/UserRoles.php b/src/Connection/UserRoles.php deleted file mode 100644 index 9847f4369..000000000 --- a/src/Connection/UserRoles.php +++ /dev/null @@ -1,83 +0,0 @@ - 'RootQuery', - 'toType' => 'UserRole', - 'fromFieldName' => 'userRoles', - 'resolve' => function( $user, $args, $context, $info ) { - $resolver = new UserRoleConnectionResolver( $user, $args, $context, $info ); - return $resolver->get_connection(); - }, - ] - ) - ); - - /** - * Register connection from User to User Roles - */ - register_graphql_connection( - self::get_connection_config( - [ - 'fromType' => 'User', - 'toType' => 'UserRole', - 'fromFieldName' => 'roles', - 'resolve' => function( User $user, $args, $context, $info ) { - $resolver = new UserRoleConnectionResolver( $user, $args, $context, $info ); - // Only get roles matching the slugs of the roles belonging to the user - - if ( ! empty( $user->roles ) ) { - $resolver->setQueryArg( 'slugIn', $user->roles ); - } - - return $resolver->get_connection(); - }, - ] - ) - ); - } - - /** - * Given an array of config, returns a config with the custom config merged with the defaults - * - * @param array $config - * - * @return array - */ - public static function get_connection_config( array $config ) { - return array_merge( - [ - 'fromType' => 'User', - 'toType' => 'UserRole', - 'fromFieldName' => 'roles', - ], - $config - ); - } -} diff --git a/src/Connection/Users.php b/src/Connection/Users.php index aab9e601c..50b547530 100644 --- a/src/Connection/Users.php +++ b/src/Connection/Users.php @@ -1,6 +1,7 @@ 'RootQuery', - 'toType' => 'User', - 'fromFieldName' => 'users', - 'resolve' => function ( $source, $args, $context, $info ) { + 'fromType' => 'RootQuery', + 'toType' => 'User', + 'connectionInterfaces' => [ 'UserConnection' ], + 'fromFieldName' => 'users', + 'resolve' => function ( $source, $args, $context, $info ) { return DataSource::resolve_users_connection( $source, $args, $context, $info ); }, - 'connectionArgs' => self::get_connection_args(), + 'connectionArgs' => self::get_connection_args(), ] ); register_graphql_connection([ - 'fromType' => 'ContentNode', - 'toType' => 'User', - 'connectionTypeName' => 'ContentNodeToEditLockConnection', - 'edgeFields' => [ + 'fromType' => 'ContentNode', + 'toType' => 'User', + 'connectionInterfaces' => [ 'UserConnection' ], + 'connectionTypeName' => 'ContentNodeToEditLockConnection', + 'edgeFields' => [ 'lockTimestamp' => [ 'type' => 'String', 'description' => __( 'The timestamp for when the node was last edited', 'wp-graphql' ), @@ -58,10 +62,10 @@ public static function register_connections() { }, ], ], - 'fromFieldName' => 'editingLockedBy', - 'description' => __( 'If a user has edited the node within the past 15 seconds, this will return the user that last edited. Null if the edit lock doesn\'t exist or is greater than 15 seconds', 'wp-graphql' ), - 'oneToOne' => true, - 'resolve' => function( Post $source, $args, $context, $info ) { + 'fromFieldName' => 'editingLockedBy', + 'description' => __( 'If a user has edited the node within the past 15 seconds, this will return the user that last edited. Null if the edit lock doesn\'t exist or is greater than 15 seconds', 'wp-graphql' ), + 'oneToOne' => true, + 'resolve' => function( Post $source, $args, $context, $info ) { if ( ! isset( $source->editLock[1] ) || ! absint( $source->editLock[1] ) ) { return $source->editLock; @@ -76,13 +80,14 @@ public static function register_connections() { ]); register_graphql_connection([ - 'fromType' => 'ContentNode', - 'toType' => 'User', - 'fromFieldName' => 'lastEditedBy', - 'connectionTypeName' => 'ContentNodeToEditLastConnection', - 'description' => __( 'The user that most recently edited the node', 'wp-graphql' ), - 'oneToOne' => true, - 'resolve' => function( Post $source, $args, $context, $info ) { + 'fromType' => 'ContentNode', + 'toType' => 'User', + 'connectionInterfaces' => [ 'UserConnection' ], + 'fromFieldName' => 'lastEditedBy', + 'connectionTypeName' => 'ContentNodeToEditLastConnection', + 'description' => __( 'The user that most recently edited the node', 'wp-graphql' ), + 'oneToOne' => true, + 'resolve' => function( Post $source, $args, $context, $info ) { $resolver = new UserConnectionResolver( $source, $args, $context, $info ); $resolver->set_query_arg( 'include', [ $source->editLastId ] ); @@ -92,11 +97,12 @@ public static function register_connections() { ]); register_graphql_connection( [ - 'fromType' => 'NodeWithAuthor', - 'toType' => 'User', - 'fromFieldName' => 'author', - 'oneToOne' => true, - 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { + 'fromType' => 'NodeWithAuthor', + 'toType' => 'User', + 'connectionInterfaces' => [ 'UserConnection' ], + 'fromFieldName' => 'author', + 'oneToOne' => true, + 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { $resolver = new UserConnectionResolver( $post, $args, $context, $info ); $resolver->set_query_arg( 'include', [ $post->authorDatabaseId ] ); diff --git a/src/Data/Connection/PostObjectConnectionResolver.php b/src/Data/Connection/PostObjectConnectionResolver.php index a940a5255..21e94dc8d 100644 --- a/src/Data/Connection/PostObjectConnectionResolver.php +++ b/src/Data/Connection/PostObjectConnectionResolver.php @@ -87,11 +87,8 @@ public function get_loader_name() { */ public function get_query() { // Get query class. - $queryClass = ! empty( $this->context->queryClass ) - ? $this->context->queryClass - : '\WP_Query'; - - $query = new $queryClass( $this->query_args ); + $queryClass = isset( $this->context->connection_query_class ) ? $this->context->connection_query_class : '\WP_Query'; + $query = new $queryClass( $this->query_args ); if ( isset( $query->query_vars['suppress_filters'] ) && true === $query->query_vars['suppress_filters'] ) { throw new InvariantViolation( __( 'WP_Query has been modified by a plugin or theme to suppress_filters, which will cause issues with WPGraphQL Execution. If you need to suppress filters for a specific reason within GraphQL, consider registering a custom field to the WPGraphQL Schema with a custom resolver.', 'wp-graphql' ) ); diff --git a/src/Data/Connection/UserConnectionResolver.php b/src/Data/Connection/UserConnectionResolver.php index c94c74140..2575a5fbb 100644 --- a/src/Data/Connection/UserConnectionResolver.php +++ b/src/Data/Connection/UserConnectionResolver.php @@ -177,8 +177,8 @@ public function get_query_args() { */ public function get_query() { // Get query class. - $queryClass = ! empty( $this->context->queryClass ) - ? $this->context->queryClass + $queryClass = isset( $this->context->connection_query_class ) + ? $this->context->connection_query_class : '\WP_User_Query'; return new $queryClass( $this->query_args ); diff --git a/src/Model/Term.php b/src/Model/Term.php index 96c7059ed..9ff67abf5 100644 --- a/src/Model/Term.php +++ b/src/Model/Term.php @@ -13,6 +13,7 @@ * * @property string $id * @property int $term_id + * @property int $databaseId * @property int $count * @property string $description * @property string $name @@ -135,9 +136,12 @@ protected function init() { 'id' => function() { return ( ! empty( $this->data->taxonomy ) && ! empty( $this->data->term_id ) ) ? Relay::toGlobalId( 'term', (string) $this->data->term_id ) : null; }, - 'term_id' => function() { + 'databaseId' => function() { return ( ! empty( $this->data->term_id ) ) ? absint( $this->data->term_id ) : null; }, + 'term_id' => function() { + return ( ! empty( $this->databaseId ) ) ? absint( $this->databaseId ) : null; + }, 'count' => function() { return ! empty( $this->data->count ) ? absint( $this->data->count ) : null; }, diff --git a/src/Model/User.php b/src/Model/User.php index 42acaf403..9fb56e729 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -12,6 +12,7 @@ * Class User - Models the data for the User object type * * @property string $id + * @property int $databaseId * @property array $capabilities * @property string $capKey * @property array $roles @@ -76,6 +77,7 @@ public function __construct( WP_User $user ) { 'isRestricted', 'id', 'userId', + 'databaseId', 'name', 'firstName', 'lastName', @@ -173,6 +175,9 @@ protected function init() { 'id' => function() { return ( ! empty( $this->data->ID ) ) ? Relay::toGlobalId( 'user', (string) $this->data->ID ) : null; }, + 'databaseId' => function() { + return $this->userId; + }, 'capabilities' => function() { if ( ! empty( $this->data->allcaps ) ) { diff --git a/src/Registry/TypeRegistry.php b/src/Registry/TypeRegistry.php index 4e4e6aee7..c8082a914 100644 --- a/src/Registry/TypeRegistry.php +++ b/src/Registry/TypeRegistry.php @@ -7,22 +7,12 @@ use GraphQL\Type\Definition\NonNull; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; -use WPGraphQL\Connection\Commenter; +use InvalidArgumentException; use WPGraphQL\Connection\Comments; -use WPGraphQL\Connection\ContentTypes; -use WPGraphQL\Connection\EnqueuedScripts; -use WPGraphQL\Connection\EnqueuedStylesheets; -use WPGraphQL\Connection\MediaItems; -use WPGraphQL\Connection\MenuItemLinkableConnection; use WPGraphQL\Connection\MenuItems; -use WPGraphQL\Connection\Menus; -use WPGraphQL\Connection\Plugins; use WPGraphQL\Connection\PostObjects; -use WPGraphQL\Connection\Revisions; use WPGraphQL\Connection\Taxonomies; use WPGraphQL\Connection\TermObjects; -use WPGraphQL\Connection\Themes; -use WPGraphQL\Connection\UserRoles; use WPGraphQL\Connection\Users; use WPGraphQL\Data\DataSource; use WPGraphQL\Mutation\CommentCreate; @@ -54,14 +44,23 @@ use WPGraphQL\Type\Enum\UserNodeIdTypeEnum; use WPGraphQL\Type\Enum\UsersConnectionOrderbyEnum; use WPGraphQL\Type\Input\UsersConnectionOrderbyInput; +use WPGraphQL\Type\InterfaceType\CommentConnectionInterface; +use WPGraphQL\Type\InterfaceType\CommenterConnectionInterface; use WPGraphQL\Type\InterfaceType\CommenterInterface; +use WPGraphQL\Type\InterfaceType\ConnectionInterface; use WPGraphQL\Type\InterfaceType\ContentNode; +use WPGraphQL\Type\InterfaceType\ContentNodeConnectionInterface; use WPGraphQL\Type\InterfaceType\ContentTemplate; +use WPGraphQL\Type\InterfaceType\ContentTypeConnectionInterface; use WPGraphQL\Type\InterfaceType\DatabaseIdentifier; use WPGraphQL\Type\InterfaceType\EnqueuedAsset; use WPGraphQL\Type\InterfaceType\HierarchicalContentNode; +use WPGraphQL\Type\InterfaceType\HierarchicalNode; use WPGraphQL\Type\InterfaceType\HierarchicalTermNode; +use WPGraphQL\Type\InterfaceType\MenuConnectionInterface; +use WPGraphQL\Type\InterfaceType\MenuItemConnectionInterface; use WPGraphQL\Type\InterfaceType\MenuItemLinkable; +use WPGraphQL\Type\InterfaceType\MenuItemLinkableConnectionInterface; use WPGraphQL\Type\InterfaceType\NodeWithAuthor; use WPGraphQL\Type\InterfaceType\NodeWithComments; use WPGraphQL\Type\InterfaceType\NodeWithContentEditor; @@ -73,13 +72,14 @@ use WPGraphQL\Type\InterfaceType\NodeWithTitle; use WPGraphQL\Type\InterfaceType\Node; use WPGraphQL\Type\InterfaceType\NodeWithTrackbacks; +use WPGraphQL\Type\InterfaceType\Previewable; +use WPGraphQL\Type\InterfaceType\TaxonomyConnectionInterface; use WPGraphQL\Type\InterfaceType\TermNode; +use WPGraphQL\Type\InterfaceType\TermNodeConnectionInterface; use WPGraphQL\Type\InterfaceType\UniformResourceIdentifiable; +use WPGraphQL\Type\InterfaceType\UserConnectionInterface; use WPGraphQL\Type\ObjectType\EnqueuedScript; use WPGraphQL\Type\ObjectType\EnqueuedStylesheet; -use WPGraphQL\Type\Union\ContentRevisionUnion; -use WPGraphQL\Type\Union\PostObjectUnion; -use WPGraphQL\Type\Union\MenuItemObjectUnion; use WPGraphQL\Type\Enum\AvatarRatingEnum; use WPGraphQL\Type\Enum\CommentsConnectionOrderbyEnum; use WPGraphQL\Type\Enum\MediaItemSizeEnum; @@ -124,7 +124,7 @@ use WPGraphQL\Type\ObjectType\User; use WPGraphQL\Type\ObjectType\UserRole; use WPGraphQL\Type\ObjectType\Settings; -use WPGraphQL\Type\Union\TermObjectUnion; +use WPGraphQL\Type\WPConnectionType; use WPGraphQL\Type\WPEnumType; use WPGraphQL\Type\WPInputObjectType; use WPGraphQL\Type\WPInterfaceType; @@ -205,6 +205,7 @@ public function init() { * @param TypeRegistry $type_registry * * @return void + * @throws Exception */ public function init_type_registry( TypeRegistry $type_registry ) { @@ -218,13 +219,22 @@ public function init_type_registry( TypeRegistry $type_registry ) { // Register Interfaces. Node::register_type(); + CommentConnectionInterface::register_type( $type_registry ); CommenterInterface::register_type( $type_registry ); + CommenterConnectionInterface::register_type( $type_registry ); + ConnectionInterface::register_type( $type_registry ); ContentNode::register_type( $type_registry ); + ContentNodeConnectionInterface::register_type( $type_registry ); ContentTemplate::register_type(); + ContentTypeConnectionInterface::register_type( $type_registry ); DatabaseIdentifier::register_type(); EnqueuedAsset::register_type( $type_registry ); - HierarchicalTermNode::register_type( $type_registry ); HierarchicalContentNode::register_type( $type_registry ); + HierarchicalNode::register_type( $type_registry ); + HierarchicalTermNode::register_type( $type_registry ); + MenuConnectionInterface::register_type( $type_registry ); + MenuItemConnectionInterface::register_type( $type_registry ); + MenuItemLinkableConnectionInterface::register_type( $type_registry ); MenuItemLinkable::register_type( $type_registry ); NodeWithAuthor::register_type( $type_registry ); NodeWithComments::register_type( $type_registry ); @@ -236,8 +246,12 @@ public function init_type_registry( TypeRegistry $type_registry ) { NodeWithTemplate::register_type( $type_registry ); NodeWithTrackbacks::register_type( $type_registry ); NodeWithPageAttributes::register_type( $type_registry ); + Previewable::register_type( $type_registry ); + TaxonomyConnectionInterface::register_type( $type_registry ); TermNode::register_type( $type_registry ); + TermNodeConnectionInterface::register_type( $type_registry ); UniformResourceIdentifiable::register_type( $type_registry ); + UserConnectionInterface::register_type( $type_registry ); // register types RootQuery::register_type(); @@ -297,31 +311,15 @@ public function init_type_registry( TypeRegistry $type_registry ) { PostObjectsConnectionOrderbyInput::register_type(); UsersConnectionOrderbyInput::register_type(); - ContentRevisionUnion::register_type( $this ); - MenuItemObjectUnion::register_type( $this ); - PostObjectUnion::register_type( $this ); - TermObjectUnion::register_type( $this ); - /** * Register core connections */ Comments::register_connections(); - Commenter::register_connections(); - EnqueuedScripts::register_connections(); - EnqueuedStylesheets::register_connections(); - MediaItems::register_connections(); - Menus::register_connections(); - MenuItemLinkableConnection::register_connections(); MenuItems::register_connections(); - Plugins::register_connections(); PostObjects::register_connections(); - ContentTypes::register_connections(); - Revisions::register_connections( $this ); Taxonomies::register_connections(); TermObjects::register_connections(); - Themes::register_connections(); Users::register_connections(); - UserRoles::register_connections(); /** * Register core mutations @@ -544,13 +542,17 @@ public function init_type_registry( TypeRegistry $type_registry ) { * to expose the URL to the Schema for multisite sites */ if ( is_multisite() ) { - register_graphql_field( 'GeneralSettings', 'url', [ - 'type' => 'String', - 'description' => __( 'Site URL.', 'wp-graphql' ), - 'resolve' => function() { - return get_site_url(); - }, - ] ); + register_graphql_field( + 'GeneralSettings', + 'url', + [ + 'type' => 'String', + 'description' => __( 'Site URL.', 'wp-graphql' ), + 'resolve' => function() { + return get_site_url(); + }, + ] + ); } foreach ( $allowed_setting_types as $group_name => $setting_type ) { @@ -857,11 +859,14 @@ protected function prepare_field( $field_name, $field_config, $type_name ) { } if ( ! isset( $field_config['type'] ) ) { - graphql_debug( sprintf( __( 'The registered field \'%s\' does not have a Type defined. Make sure to define a type for all fields.', 'wp-graphql' ), $field_name ), [ - 'type' => 'INVALID_FIELD_TYPE', - 'type_name' => $type_name, - 'field_name' => $field_name, - ] ); + graphql_debug( + sprintf( __( 'The registered field \'%s\' does not have a Type defined. Make sure to define a type for all fields.', 'wp-graphql' ), $field_name ), + [ + 'type' => 'INVALID_FIELD_TYPE', + 'type_name' => $type_name, + 'field_name' => $field_name, + ] + ); return null; } @@ -1052,202 +1057,13 @@ protected function get_connection_name( $from_type, $to_type, $from_field_name ) * @param array $config The info about the connection being registered * * @return void - * @throws \InvalidArgumentException + * @throws InvalidArgumentException * @throws Exception */ - public function register_connection( $config ) { - - if ( ! array_key_exists( 'fromType', $config ) ) { - throw new \InvalidArgumentException( __( 'Connection config needs to have at least a fromType defined', 'wp-graphql' ) ); - } - - if ( ! array_key_exists( 'toType', $config ) ) { - throw new \InvalidArgumentException( __( 'Connection config needs to have at least a toType defined', 'wp-graphql' ) ); - } - - if ( ! array_key_exists( 'fromFieldName', $config ) ) { - throw new \InvalidArgumentException( __( 'Connection config needs to have at least a fromFieldName defined', 'wp-graphql' ) ); - } - - $from_type = $config['fromType']; - $to_type = $config['toType']; - $from_field_name = $config['fromFieldName']; - $connection_fields = ! empty( $config['connectionFields'] ) && is_array( $config['connectionFields'] ) ? $config['connectionFields'] : []; - $connection_args = ! empty( $config['connectionArgs'] ) && is_array( $config['connectionArgs'] ) ? $config['connectionArgs'] : []; - $edge_fields = ! empty( $config['edgeFields'] ) && is_array( $config['edgeFields'] ) ? $config['edgeFields'] : []; - $resolve_node = array_key_exists( 'resolveNode', $config ) && is_callable( $config['resolve'] ) ? $config['resolveNode'] : null; - $resolve_cursor = array_key_exists( 'resolveCursor', $config ) && is_callable( $config['resolve'] ) ? $config['resolveCursor'] : null; - $resolve_connection = array_key_exists( 'resolve', $config ) && is_callable( $config['resolve'] ) ? $config['resolve'] : function() { - return null; - }; - $connection_name = ! empty( $config['connectionTypeName'] ) ? $config['connectionTypeName'] : $this->get_connection_name( $from_type, $to_type, $from_field_name ); - $where_args = []; - $one_to_one = isset( $config['oneToOne'] ) && true === $config['oneToOne'] ? true : false; - $queryClass = ! empty( $config['queryClass'] ) ? $config['queryClass'] : null; - $auth = ! empty( $config['auth'] ) ? $config['auth'] : null; - - /** - * If there are any $connectionArgs, - * register their inputType and configure them as $where_args to be added to the connection - * field as arguments - */ - if ( ! empty( $connection_args ) ) { - - $this->register_input_type( - $connection_name . 'WhereArgs', - [ - // Translators: Placeholder is the name of the connection - 'description' => sprintf( __( 'Arguments for filtering the %s connection', 'wp-graphql' ), $connection_name ), - 'fields' => $connection_args, - 'queryClass' => ! empty( $config['queryClass'] ) ? $config['queryClass'] : null, - ] - ); - - $where_args = [ - 'where' => [ - 'description' => __( 'Arguments for filtering the connection', 'wp-graphql' ), - 'type' => $connection_name . 'WhereArgs', - ], - ]; - - } + public function register_connection( array $config ) { - if ( true === $one_to_one ) { - - $this->register_object_type( - $connection_name . 'Edge', - [ - 'description' => sprintf( __( 'Connection between the %1$s type and the %2$s type', 'wp-graphql' ), $from_type, $to_type ), - 'fields' => array_merge( - [ - 'node' => [ - 'type' => $to_type, - 'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ), - ], - ], - $edge_fields - ), - ] - ); - - } else { - - $this->register_object_type( - $connection_name . 'Edge', - [ - 'description' => __( 'An edge in a connection', 'wp-graphql' ), - 'fields' => array_merge( - [ - 'cursor' => [ - 'type' => 'String', - 'description' => __( 'A cursor for use in pagination', 'wp-graphql' ), - 'resolve' => $resolve_cursor, - ], - 'node' => [ - 'type' => $to_type, - 'description' => __( 'The item at the end of the edge', 'wp-graphql' ), - 'resolve' => function( $source, $args, $context, ResolveInfo $info ) use ( $resolve_node ) { - if ( ! empty( $resolve_node ) && is_callable( $resolve_node ) ) { - return ! empty( $source['node'] ) ? $resolve_node( $source['node'], $args, $context, $info ) : null; - } else { - return $source['node']; - } - }, - ], - ], - $edge_fields - ), - ] - ); - - $this->register_object_type( - $connection_name, - [ - // Translators: the placeholders are the name of the Types the connection is between. - 'description' => sprintf( __( 'Connection between the %1$s type and the %2$s type', 'wp-graphql' ), $from_type, $to_type ), - 'fields' => array_merge( - [ - 'pageInfo' => [ - // @todo: change to PageInfo when/if the Relay lib is deprecated - 'type' => 'WPPageInfo', - 'description' => __( 'Information about pagination in a connection.', 'wp-graphql' ), - ], - 'edges' => [ - 'type' => [ - 'list_of' => $connection_name . 'Edge', - ], - 'description' => sprintf( __( 'Edges for the %s connection', 'wp-graphql' ), $connection_name ), - ], - 'nodes' => [ - 'type' => [ - 'list_of' => $to_type, - ], - 'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ), - 'resolve' => function( $source, $args, $context, $info ) use ( $resolve_node ) { - $nodes = []; - if ( ! empty( $source['nodes'] ) && is_array( $source['nodes'] ) ) { - if ( is_callable( $resolve_node ) ) { - foreach ( $source['nodes'] as $node ) { - $nodes[] = $resolve_node( $node, $args, $context, $info ); - } - } else { - return $source['nodes']; - } - } - - return $nodes; - }, - ], - ], - $connection_fields - ), - ] - ); - - } - - if ( true === $one_to_one ) { - $pagination_args = []; - } else { - $pagination_args = [ - 'first' => [ - 'type' => 'Int', - 'description' => __( 'The number of items to return after the referenced "after" cursor', 'wp-graphql' ), - ], - 'last' => [ - 'type' => 'Int', - 'description' => __( 'The number of items to return before the referenced "before" cursor', 'wp-graphql' ), - ], - 'after' => [ - 'type' => 'String', - 'description' => __( 'Cursor used along with the "first" argument to reference where in the dataset to get data', 'wp-graphql' ), - ], - 'before' => [ - 'type' => 'String', - 'description' => __( 'Cursor used along with the "last" argument to reference where in the dataset to get data', 'wp-graphql' ), - ], - ]; - } - - $this->register_field( - $from_type, - $from_field_name, - [ - 'type' => true === $one_to_one ? $connection_name . 'Edge' : $connection_name, - 'args' => array_merge( $pagination_args, $where_args ), - 'auth' => $auth, - 'description' => ! empty( $config['description'] ) ? $config['description'] : sprintf( __( 'Connection between the %1$s type and the %2$s type', 'wp-graphql' ), $from_type, $to_type ), - 'resolve' => function( $root, $args, $context, $info ) use ( $resolve_connection, $queryClass ) { - // Set queryClass on AppContext for use in connection resolver. - $context->queryClass = $queryClass; - - /** - * Return the results - */ - return call_user_func( $resolve_connection, $root, $args, $context, $info ); - }, - ] - ); + $connection = new WPConnectionType( $config, $this ); + $connection->register_connection(); } @@ -1260,7 +1076,7 @@ public function register_connection( $config ) { * @return void * @throws Exception */ - public function register_mutation( $mutation_name, $config ) { + public function register_mutation( string $mutation_name, array $config ) { $output_fields = [ 'clientMutationId' => [ diff --git a/src/Type/InterfaceType/CommentConnectionInterface.php b/src/Type/InterfaceType/CommentConnectionInterface.php new file mode 100644 index 000000000..80f9d4e12 --- /dev/null +++ b/src/Type/InterfaceType/CommentConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to Comment Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'CommentConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'Comment' ] ] ], + 'description' => __( 'A list of connected Comment Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'CommentConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected Comment', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'Comment' ], + 'description' => __( 'The connected Comment Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/InterfaceType/CommenterConnectionInterface.php b/src/Type/InterfaceType/CommenterConnectionInterface.php new file mode 100644 index 000000000..2c46616eb --- /dev/null +++ b/src/Type/InterfaceType/CommenterConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to Commenter Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'CommenterConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'Commenter' ] ] ], + 'description' => __( 'A list of connected Commenter Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'CommenterConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected Commenter', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'Commenter' ], + 'description' => __( 'The connected Commenter Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/InterfaceType/CommenterInterface.php b/src/Type/InterfaceType/CommenterInterface.php index 244c0f49b..ca9381cc4 100644 --- a/src/Type/InterfaceType/CommenterInterface.php +++ b/src/Type/InterfaceType/CommenterInterface.php @@ -2,6 +2,7 @@ namespace WPGraphQL\Type\InterfaceType; +use Exception; use WPGraphQL\Model\User; use WPGraphQL\Registry\TypeRegistry; @@ -18,51 +19,44 @@ class CommenterInterface { * @param TypeRegistry $type_registry * * @return void + * @throws Exception */ public static function register_type( TypeRegistry $type_registry ) { - register_graphql_interface_type( 'Commenter', [ - 'description' => __( 'The author of a comment', 'wp-graphql' ), - 'resolveType' => function( $comment_author ) use ( $type_registry ) { - if ( $comment_author instanceof User ) { - $type = $type_registry->get_type( 'User' ); - } else { - $type = $type_registry->get_type( 'CommentAuthor' ); - } - - return $type; - }, - 'fields' => [ - 'id' => [ - 'type' => [ - 'non_null' => 'ID', + register_graphql_interface_type( + 'Commenter', + [ + 'interfaces' => [ 'Node', 'DatabaseIdentifier' ], + 'description' => __( 'The author of a comment', 'wp-graphql' ), + 'resolveType' => function( $comment_author ) use ( $type_registry ) { + if ( $comment_author instanceof User ) { + $type = $type_registry->get_type( 'User' ); + } else { + $type = $type_registry->get_type( 'CommentAuthor' ); + } + + return $type; + }, + 'fields' => [ + 'name' => [ + 'type' => 'String', + 'description' => __( 'The name of the author of a comment.', 'wp-graphql' ), ], - 'description' => __( 'The globally unique identifier for the comment author.', 'wp-graphql' ), - ], - 'databaseId' => [ - 'type' => [ - 'non_null' => 'Int', + 'email' => [ + 'type' => 'String', + 'description' => __( 'The email address of the author of a comment.', 'wp-graphql' ), + ], + 'url' => [ + 'type' => 'String', + 'description' => __( 'The url of the author of a comment.', 'wp-graphql' ), + ], + 'isRestricted' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the author information is considered restricted. (not fully public)', 'wp-graphql' ), ], - 'description' => __( 'Identifies the primary key from the database.', 'wp-graphql' ), - ], - 'name' => [ - 'type' => 'String', - 'description' => __( 'The name of the author of a comment.', 'wp-graphql' ), - ], - 'email' => [ - 'type' => 'String', - 'description' => __( 'The email address of the author of a comment.', 'wp-graphql' ), - ], - 'url' => [ - 'type' => 'String', - 'description' => __( 'The url of the author of a comment.', 'wp-graphql' ), - ], - 'isRestricted' => [ - 'type' => 'Boolean', - 'description' => __( 'Whether the author information is considered restricted. (not fully public)', 'wp-graphql' ), ], - ], - ] ); + ] + ); } diff --git a/src/Type/InterfaceType/ConnectionInterface.php b/src/Type/InterfaceType/ConnectionInterface.php new file mode 100644 index 000000000..2111214f1 --- /dev/null +++ b/src/Type/InterfaceType/ConnectionInterface.php @@ -0,0 +1,58 @@ + __( 'Relational context between connected nodes', 'wp-graphql' ), + 'fields' => [ + 'node' => [ + 'type' => [ 'non_null' => 'Node' ], + 'description' => __( 'The connected node', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( + 'Connection', + [ + 'description' => __( 'A plural connection from one Node Type in the Graph to another Node Type, with support for relational data via "edges".', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'Edge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'Node' ] ] ], + 'description' => __( 'A list of connected nodes', 'wp-graphql' ), + ], + ], + ] + ); + + register_graphql_interface_type( 'SingleNodeConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'A singular connection from one Node to another, with support for relational data on the "edge" of the connection.', 'wp-graphql' ), + 'fields' => [ + 'node' => [ + 'type' => [ 'non_null' => 'Node' ], + 'description' => __( 'The connected node', 'wp-graphql' ), + ], + ], + ] ); + + } +} diff --git a/src/Type/InterfaceType/ContentNode.php b/src/Type/InterfaceType/ContentNode.php index b7c774c2c..cd1c9c27e 100644 --- a/src/Type/InterfaceType/ContentNode.php +++ b/src/Type/InterfaceType/ContentNode.php @@ -1,13 +1,12 @@ [ 'Node', 'DatabaseIdentifier' ], 'description' => __( 'Nodes used to manage content', 'wp-graphql' ), + 'connections' => [ + 'contentType' => [ + 'toType' => 'ContentType', + 'connectionInterfaces' => [ 'ContentTypeConnection' ], + 'resolve' => function( Post $source, $args, $context, $info ) { + + if ( $source->isRevision ) { + $parent = get_post( $source->parentDatabaseId ); + $post_type = $parent->post_type ?? null; + } else { + $post_type = $source->post_type ?? null; + } + + if ( empty( $post_type ) ) { + return null; + } + + $resolver = new ContentTypeConnectionResolver( $source, $args, $context, $info ); + + return $resolver->one_to_one()->set_query_arg( 'name', $post_type )->get_connection(); + }, + 'oneToOne' => true, + ], + 'enqueuedScripts' => [ + 'toType' => 'EnqueuedScript', + 'resolve' => function( $source, $args, $context, $info ) { + $resolver = new EnqueuedScriptsConnectionResolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + 'enqueuedStylesheets' => [ + 'toType' => 'EnqueuedStylesheet', + 'resolve' => function( $source, $args, $context, $info ) { + $resolver = new EnqueuedStylesheetConnectionResolver( $source, $args, $context, $info ); + return $resolver->get_connection(); + }, + ], + ], 'resolveType' => function( Post $post ) use ( $type_registry ) { /** @@ -41,7 +80,7 @@ public static function register_type( TypeRegistry $type_registry ) { * $post->post_type attribute. */ $type = null; - $post_type = isset( $post->post_type ) ? $post->post_type : null; + $post_type = $post->post_type ?? null; if ( isset( $post->post_type ) && 'revision' === $post->post_type ) { $parent = get_post( $post->parentDatabaseId ); @@ -60,82 +99,58 @@ public static function register_type( TypeRegistry $type_registry ) { }, 'fields' => [ - 'id' => [ - 'type' => [ - 'non_null' => 'ID', - ], - 'description' => __( 'The globally unique identifier of the node.', 'wp-graphql' ), - ], - 'template' => [ + 'template' => [ 'type' => 'ContentTemplate', 'description' => __( 'The template assigned to a node of content', 'wp-graphql' ), ], - 'databaseId' => [ - 'type' => [ - 'non_null' => 'Int', - ], - 'description' => __( 'The ID of the node in the database.', 'wp-graphql' ), - ], - 'date' => [ + 'date' => [ 'type' => 'String', 'description' => __( 'Post publishing date.', 'wp-graphql' ), ], - 'dateGmt' => [ + 'dateGmt' => [ 'type' => 'String', 'description' => __( 'The publishing date set in GMT.', 'wp-graphql' ), ], - 'enclosure' => [ + 'enclosure' => [ 'type' => 'String', 'description' => __( 'The RSS enclosure for the object', 'wp-graphql' ), ], - 'status' => [ + 'status' => [ 'type' => 'String', 'description' => __( 'The current status of the object', 'wp-graphql' ), ], - 'slug' => [ + 'slug' => [ 'type' => 'String', 'description' => __( 'The uri slug for the post. This is equivalent to the WP_Post->post_name field and the post_name column in the database for the "post_objects" table.', 'wp-graphql' ), ], - 'modified' => [ + 'modified' => [ 'type' => 'String', 'description' => __( 'The local modified time for a post. If a post was recently updated the modified field will change to match the corresponding time.', 'wp-graphql' ), ], - 'modifiedGmt' => [ + 'modifiedGmt' => [ 'type' => 'String', 'description' => __( 'The GMT modified time for a post. If a post was recently updated the modified field will change to match the corresponding time in GMT.', 'wp-graphql' ), ], - 'guid' => [ + 'guid' => [ 'type' => 'String', 'description' => __( 'The global unique identifier for this post. This currently matches the value stored in WP_Post->guid and the guid column in the "post_objects" database table.', 'wp-graphql' ), ], - 'desiredSlug' => [ + 'desiredSlug' => [ 'type' => 'String', 'description' => __( 'The desired slug of the post', 'wp-graphql' ), ], - 'link' => [ + 'link' => [ 'type' => 'String', 'description' => __( 'The permalink of the post', 'wp-graphql' ), ], - 'uri' => [ + 'uri' => [ 'type' => [ 'non_null' => 'String' ], 'description' => __( 'URI path for the resource', 'wp-graphql' ), ], - 'isRestricted' => [ + 'isRestricted' => [ 'type' => 'Boolean', 'description' => __( 'Whether the object is restricted from the current viewer', 'wp-graphql' ), ], - 'isPreview' => [ - 'type' => 'Boolean', - 'description' => __( 'Whether the object is a node in the preview state', 'wp-graphql' ), - ], - 'previewRevisionDatabaseId' => [ - 'type' => 'Int', - 'description' => __( 'The database id of the preview node', 'wp-graphql' ), - ], - 'previewRevisionId' => [ - 'type' => 'ID', - 'description' => __( 'Whether the object is a node in the preview state', 'wp-graphql' ), - ], ], ] ); diff --git a/src/Type/InterfaceType/ContentNodeConnectionInterface.php b/src/Type/InterfaceType/ContentNodeConnectionInterface.php new file mode 100644 index 000000000..5c6c6f9b2 --- /dev/null +++ b/src/Type/InterfaceType/ContentNodeConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to Content Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'ContentNodeConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'ContentNode' ] ] ], + 'description' => __( 'A list of connected Content Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'ContentNodeConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected Content Node', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'ContentNode' ], + 'description' => __( 'The connected Content Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/InterfaceType/ContentTemplate.php b/src/Type/InterfaceType/ContentTemplate.php index 7409df2b2..e59552257 100644 --- a/src/Type/InterfaceType/ContentTemplate.php +++ b/src/Type/InterfaceType/ContentTemplate.php @@ -2,12 +2,15 @@ namespace WPGraphQL\Type\InterfaceType; +use Exception; + class ContentTemplate { /** * Register the ContentTemplate Interface * * @return void + * @throws Exception */ public static function register_type() { register_graphql_interface_type( @@ -21,7 +24,7 @@ public static function register_type() { ], ], 'resolveType' => function( $value ) { - return isset( $value['__typename'] ) ? $value['__typename'] : 'DefaultTemplate'; + return $value['__typename'] ?? 'DefaultTemplate'; }, ] ); diff --git a/src/Type/InterfaceType/ContentTypeConnectionInterface.php b/src/Type/InterfaceType/ContentTypeConnectionInterface.php new file mode 100644 index 000000000..e2b8138a2 --- /dev/null +++ b/src/Type/InterfaceType/ContentTypeConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to Content Type Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'ContentTypeConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'ContentType' ] ] ], + 'description' => __( 'A list of connected Content Type Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'ContentTypeConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected Content Type Node', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'ContentType' ], + 'description' => __( 'The connected Content Type Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/InterfaceType/DatabaseIdentifier.php b/src/Type/InterfaceType/DatabaseIdentifier.php index 6db5eca66..6dae085aa 100644 --- a/src/Type/InterfaceType/DatabaseIdentifier.php +++ b/src/Type/InterfaceType/DatabaseIdentifier.php @@ -2,6 +2,8 @@ namespace WPGraphQL\Type\InterfaceType; +use Exception; + /** * Class DatabaseIdentifier * @@ -13,17 +15,21 @@ class DatabaseIdentifier { * Register the DatabaseIdentifier Interface. * * @return void + * @throws Exception */ public static function register_type() { - register_graphql_interface_type( 'DatabaseIdentifier', [ - 'description' => __( 'Object that can be identified with a Database ID', 'wp-graphql' ), - 'fields' => [ - 'databaseId' => [ - 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'The unique identifier stored in the database', 'wp-graphql' ), + register_graphql_interface_type( + 'DatabaseIdentifier', + [ + 'description' => __( 'Object that can be identified with a Database ID', 'wp-graphql' ), + 'fields' => [ + 'databaseId' => [ + 'type' => [ 'non_null' => 'Int' ], + 'description' => __( 'The unique identifier stored in the database', 'wp-graphql' ), + ], ], - ], - ]); + ] + ); } } diff --git a/src/Type/InterfaceType/EnqueuedAsset.php b/src/Type/InterfaceType/EnqueuedAsset.php index 4809bb4c1..379a7fef2 100644 --- a/src/Type/InterfaceType/EnqueuedAsset.php +++ b/src/Type/InterfaceType/EnqueuedAsset.php @@ -1,5 +1,6 @@ __( 'Asset enqueued by the CMS', 'wp-graphql' ), - 'resolveType' => function( $asset ) use ( $type_registry ) { + register_graphql_interface_type( + 'EnqueuedAsset', + [ + 'description' => __( 'Asset enqueued by the CMS', 'wp-graphql' ), + 'interfaces' => [ 'Node' ], + 'resolveType' => function( $asset ) use ( $type_registry ) { - /** - * The resolveType callback is used at runtime to determine what Type an object - * implementing the ContentNode Interface should be resolved as. - * - * You can filter this centrally using the "graphql_wp_interface_type_config" filter - * to override if you need something other than a Post object to be resolved via the - * $post->post_type attribute. - */ - $type = null; + /** + * The resolveType callback is used at runtime to determine what Type an object + * implementing the ContentNode Interface should be resolved as. + * + * You can filter this centrally using the "graphql_wp_interface_type_config" filter + * to override if you need something other than a Post object to be resolved via the + * $post->post_type attribute. + */ + $type = null; - if ( isset( $asset['type'] ) ) { - $type = $type_registry->get_type( $asset['type'] ); - } + if ( isset( $asset['type'] ) ) { + $type = $type_registry->get_type( $asset['type'] ); + } - return ! empty( $type ) ? $type : null; + return ! empty( $type ) ? $type : null; - }, - 'fields' => [ - 'id' => [ - 'type' => [ - 'non_null' => 'ID', + }, + 'fields' => [ + 'handle' => [ + 'type' => 'String', + 'description' => __( 'The handle of the enqueued asset', 'wp-graphql' ), ], - 'description' => __( 'The ID of the enqueued asset', 'wp-graphql' ), - ], - 'handle' => [ - 'type' => 'String', - 'description' => __( 'The handle of the enqueued asset', 'wp-graphql' ), - ], - 'version' => [ - 'type' => 'String', - 'description' => __( 'The version of the enqueued asset', 'wp-graphql' ), - ], - 'src' => [ - 'type' => 'String', - 'description' => __( 'The source of the asset', 'wp-graphql' ), - ], - 'dependencies' => [ - 'type' => [ - 'list_of' => 'EnqueuedScript', + 'version' => [ + 'type' => 'String', + 'description' => __( 'The version of the enqueued asset', 'wp-graphql' ), + ], + 'src' => [ + 'type' => 'String', + 'description' => __( 'The source of the asset', 'wp-graphql' ), + ], + 'dependencies' => [ + 'type' => [ + 'list_of' => 'EnqueuedScript', + ], + 'description' => __( 'Dependencies needed to use this asset', 'wp-graphql' ), + ], + 'args' => [ + 'type' => 'Boolean', + 'description' => __( '@todo', 'wp-graphql' ), + ], + 'extra' => [ + 'type' => 'String', + 'description' => __( 'Extra information needed for the script', 'wp-graphql' ), + 'resolve' => function( $asset ) { + return isset( $asset->extra['data'] ) ? $asset->extra['data'] : null; + }, ], - 'description' => __( 'Dependencies needed to use this asset', 'wp-graphql' ), - ], - 'args' => [ - 'type' => 'Boolean', - 'description' => __( '@todo', 'wp-graphql' ), - ], - 'extra' => [ - 'type' => 'String', - 'description' => __( 'Extra information needed for the script', 'wp-graphql' ), - 'resolve' => function( $asset ) { - return isset( $asset->extra['data'] ) ? $asset->extra['data'] : null; - }, ], - ], - ]); + ] + ); } diff --git a/src/Type/InterfaceType/HierarchicalContentNode.php b/src/Type/InterfaceType/HierarchicalContentNode.php index 8b85b5938..330f6c516 100644 --- a/src/Type/InterfaceType/HierarchicalContentNode.php +++ b/src/Type/InterfaceType/HierarchicalContentNode.php @@ -1,6 +1,8 @@ __( 'Content node with hierarchical (parent/child) relationships', 'wp-graphql' ), + 'interfaces' => [ + 'Node', + 'ContentNode', + 'DatabaseIdentifier', + 'HierarchicalNode', + ], 'fields' => [ 'parentId' => [ 'type' => 'ID', diff --git a/src/Type/InterfaceType/HierarchicalNode.php b/src/Type/InterfaceType/HierarchicalNode.php new file mode 100644 index 000000000..a38f101f0 --- /dev/null +++ b/src/Type/InterfaceType/HierarchicalNode.php @@ -0,0 +1,48 @@ + __( 'Node with hierarchical (parent/child) relationships', 'wp-graphql' ), + 'interfaces' => [ + 'Node', + 'DatabaseIdentifier', + ], + 'fields' => [ + 'parentId' => [ + 'type' => 'ID', + 'description' => __( 'The globally unique identifier of the parent node.', 'wp-graphql' ), + ], + 'parentDatabaseId' => [ + 'type' => 'Int', + 'description' => __( 'Database id of the parent node', 'wp-graphql' ), + ], + ], + ] + ); + + } + +} diff --git a/src/Type/InterfaceType/HierarchicalTermNode.php b/src/Type/InterfaceType/HierarchicalTermNode.php index d55139544..df15b4fe7 100644 --- a/src/Type/InterfaceType/HierarchicalTermNode.php +++ b/src/Type/InterfaceType/HierarchicalTermNode.php @@ -1,6 +1,8 @@ __( 'Term node with hierarchical (parent/child) relationships', 'wp-graphql' ), - 'fields' => [ - 'parentId' => [ - 'type' => 'ID', - 'description' => __( 'The globally unique identifier of the parent node.', 'wp-graphql' ), + register_graphql_interface_type( + 'HierarchicalTermNode', + [ + 'description' => __( 'Term node with hierarchical (parent/child) relationships', 'wp-graphql' ), + 'interfaces' => [ + 'Node', + 'TermNode', + 'DatabaseIdentifier', + 'HierarchicalNode', + 'UniformResourceIdentifiable', ], - 'parentDatabaseId' => [ - 'type' => 'Int', - 'description' => __( 'Database id of the parent node', 'wp-graphql' ), + 'fields' => [ + 'parentId' => [ + 'type' => 'ID', + 'description' => __( 'The globally unique identifier of the parent node.', 'wp-graphql' ), + ], + 'parentDatabaseId' => [ + 'type' => 'Int', + 'description' => __( 'Database id of the parent node', 'wp-graphql' ), + ], ], - ], - ]); + ] + ); } diff --git a/src/Type/InterfaceType/MenuConnectionInterface.php b/src/Type/InterfaceType/MenuConnectionInterface.php new file mode 100644 index 000000000..3788402c2 --- /dev/null +++ b/src/Type/InterfaceType/MenuConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to Menu Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'MenuConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'Menu' ] ] ], + 'description' => __( 'A list of connected Menu Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'MenuConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected Menu Node', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'Menu' ], + 'description' => __( 'The connected Menu Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/InterfaceType/MenuItemConnectionInterface.php b/src/Type/InterfaceType/MenuItemConnectionInterface.php new file mode 100644 index 000000000..9d159135d --- /dev/null +++ b/src/Type/InterfaceType/MenuItemConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to Menu Item Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'MenuItemConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'MenuItem' ] ] ], + 'description' => __( 'A list of connected Menu Item Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'MenuItemConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected Menu Item Node', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'MenuItem' ], + 'description' => __( 'The connected Menu Item Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/InterfaceType/MenuItemLinkable.php b/src/Type/InterfaceType/MenuItemLinkable.php index 9b72af048..cf6e783e7 100644 --- a/src/Type/InterfaceType/MenuItemLinkable.php +++ b/src/Type/InterfaceType/MenuItemLinkable.php @@ -2,6 +2,7 @@ namespace WPGraphQL\Type\InterfaceType; +use Exception; use WPGraphQL\Model\Post; use WPGraphQL\Model\Term; use WPGraphQL\Registry\TypeRegistry; @@ -15,27 +16,14 @@ class MenuItemLinkable { * @param TypeRegistry $type_registry Instance of the WPGraphQL Type Registry * * @return void + * @throws Exception */ public static function register_type( TypeRegistry $type_registry ) { register_graphql_interface_type( 'MenuItemLinkable', [ + 'interfaces' => [ 'Node', 'UniformResourceIdentifiable', 'DatabaseIdentifier' ], 'description' => __( 'Nodes that can be linked to as Menu Items', 'wp-graphql' ), - 'fields' => [ - 'uri' => [ - 'type' => [ 'non_null' => 'String' ], - 'description' => __( 'The unique resource identifier path', 'wp-graphql' ), - ], - 'id' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'The unique resource identifier path', 'wp-graphql' ), - ], - 'databaseId' => [ - 'type' => [ - 'non_null' => 'Int', - ], - 'description' => __( 'The unique resource identifier path', 'wp-graphql' ), - ], - ], + 'fields' => [], 'resolveType' => function( $node ) use ( $type_registry ) { switch ( true ) { diff --git a/src/Type/InterfaceType/MenuItemLinkableConnectionInterface.php b/src/Type/InterfaceType/MenuItemLinkableConnectionInterface.php new file mode 100644 index 000000000..8e8a048c6 --- /dev/null +++ b/src/Type/InterfaceType/MenuItemLinkableConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to Menu Item Linkable Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'MenuItemLinkableConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'MenuItemLinkable' ] ] ], + 'description' => __( 'A list of connected Menu Item Linkable Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'MenuItemLinkableConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected Menu Item Linkable Node', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'MenuItemLinkable' ], + 'description' => __( 'The connected Menu Item Linkable Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/InterfaceType/NodeWithAuthor.php b/src/Type/InterfaceType/NodeWithAuthor.php index f2e8fafe1..ba1dd350d 100644 --- a/src/Type/InterfaceType/NodeWithAuthor.php +++ b/src/Type/InterfaceType/NodeWithAuthor.php @@ -1,10 +1,8 @@ __( 'A node that can have an author assigned to it', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'DatabaseIdentifier', 'ContentNode' ], 'fields' => [ 'authorId' => [ 'type' => 'ID', diff --git a/src/Type/InterfaceType/NodeWithComments.php b/src/Type/InterfaceType/NodeWithComments.php index a7097aa4b..4673f854e 100644 --- a/src/Type/InterfaceType/NodeWithComments.php +++ b/src/Type/InterfaceType/NodeWithComments.php @@ -1,6 +1,8 @@ __( 'A node that can have comments associated with it', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'DatabaseIdentifier', 'ContentNode' ], 'fields' => [ 'commentCount' => [ 'type' => 'Int', diff --git a/src/Type/InterfaceType/NodeWithContentEditor.php b/src/Type/InterfaceType/NodeWithContentEditor.php index 74557d2de..dbb2c6913 100644 --- a/src/Type/InterfaceType/NodeWithContentEditor.php +++ b/src/Type/InterfaceType/NodeWithContentEditor.php @@ -1,6 +1,7 @@ __( 'A node that supports the content editor', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'ContentNode', 'DatabaseIdentifier' ], 'fields' => [ 'content' => [ 'type' => 'String', diff --git a/src/Type/InterfaceType/NodeWithExcerpt.php b/src/Type/InterfaceType/NodeWithExcerpt.php index 8c493ca82..7d4a46e46 100644 --- a/src/Type/InterfaceType/NodeWithExcerpt.php +++ b/src/Type/InterfaceType/NodeWithExcerpt.php @@ -1,6 +1,7 @@ __( 'A node that can have an excerpt', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'ContentNode', 'DatabaseIdentifier' ], 'fields' => [ 'excerpt' => [ 'type' => 'String', diff --git a/src/Type/InterfaceType/NodeWithFeaturedImage.php b/src/Type/InterfaceType/NodeWithFeaturedImage.php index 62dc6017b..39c9b9a60 100644 --- a/src/Type/InterfaceType/NodeWithFeaturedImage.php +++ b/src/Type/InterfaceType/NodeWithFeaturedImage.php @@ -1,6 +1,12 @@ __( 'A node that can have a featured image set', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'ContentNode', 'DatabaseIdentifier' ], + 'connections' => [ + 'featuredImage' => [ + 'toType' => 'MediaItem', + 'oneToOne' => true, + 'resolve' => function( Post $post, $args, AppContext $context, ResolveInfo $info ) { + + if ( empty( $post->featuredImageDatabaseId ) ) { + return null; + } + + $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info, 'attachment' ); + $resolver->set_query_arg( 'p', absint( $post->featuredImageDatabaseId ) ); + + return $resolver->one_to_one()->get_connection(); + + }, + ], + ], 'fields' => [ 'featuredImageId' => [ 'type' => 'ID', diff --git a/src/Type/InterfaceType/NodeWithPageAttributes.php b/src/Type/InterfaceType/NodeWithPageAttributes.php index c09b47525..b0f821154 100644 --- a/src/Type/InterfaceType/NodeWithPageAttributes.php +++ b/src/Type/InterfaceType/NodeWithPageAttributes.php @@ -1,6 +1,7 @@ __( 'A node that can have page attributes', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'ContentNode', 'DatabaseIdentifier' ], 'fields' => [ 'menuOrder' => [ 'type' => 'Int', diff --git a/src/Type/InterfaceType/NodeWithRevisions.php b/src/Type/InterfaceType/NodeWithRevisions.php index eeb2c0669..944a30387 100644 --- a/src/Type/InterfaceType/NodeWithRevisions.php +++ b/src/Type/InterfaceType/NodeWithRevisions.php @@ -1,6 +1,7 @@ __( 'A node that can have revisions', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'ContentNode', 'DatabaseIdentifier' ], 'fields' => [ 'isRevision' => [ 'type' => 'Boolean', diff --git a/src/Type/InterfaceType/NodeWithTemplate.php b/src/Type/InterfaceType/NodeWithTemplate.php index 98c89de47..158227e9c 100644 --- a/src/Type/InterfaceType/NodeWithTemplate.php +++ b/src/Type/InterfaceType/NodeWithTemplate.php @@ -1,6 +1,7 @@ __( 'A node that can have a template associated with it', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'ContentNode', 'DatabaseIdentifier' ], 'fields' => [ 'template' => [ 'description' => __( 'The template assigned to the node', 'wp-graphql' ), diff --git a/src/Type/InterfaceType/NodeWithTitle.php b/src/Type/InterfaceType/NodeWithTitle.php index fb4dfaa84..e51f8e4d2 100644 --- a/src/Type/InterfaceType/NodeWithTitle.php +++ b/src/Type/InterfaceType/NodeWithTitle.php @@ -1,10 +1,8 @@ __( 'A node that NodeWith a title', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'ContentNode', 'DatabaseIdentifier' ], 'fields' => [ 'title' => [ 'type' => 'String', diff --git a/src/Type/InterfaceType/NodeWithTrackbacks.php b/src/Type/InterfaceType/NodeWithTrackbacks.php index 91092272d..a7a7715fb 100644 --- a/src/Type/InterfaceType/NodeWithTrackbacks.php +++ b/src/Type/InterfaceType/NodeWithTrackbacks.php @@ -1,6 +1,8 @@ __( 'A node that can have trackbacks and pingbacks', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'ContentNode', 'DatabaseIdentifier' ], 'fields' => [ 'toPing' => [ 'type' => [ 'list_of' => 'String' ], diff --git a/src/Type/InterfaceType/Previewable.php b/src/Type/InterfaceType/Previewable.php new file mode 100644 index 000000000..3631d9e60 --- /dev/null +++ b/src/Type/InterfaceType/Previewable.php @@ -0,0 +1,53 @@ + __( 'Nodes that can be seen in a preview (unpublished) state.', 'wp-graphql' ), + 'fields' => [ + 'isPreview' => [ + 'type' => 'Boolean', + 'description' => __( 'Whether the object is a node in the preview state', 'wp-graphql' ), + ], + 'previewRevisionDatabaseId' => [ + 'type' => 'Int', + 'description' => __( 'The database id of the preview node', 'wp-graphql' ), + ], + 'previewRevisionId' => [ + 'type' => 'ID', + 'description' => __( 'Whether the object is a node in the preview state', 'wp-graphql' ), + ], + ], + 'resolveType' => function( Post $post ) use ( $type_registry ) { + + $type = 'Post'; + + $post_type_object = isset( $post->post_type ) ? get_post_type_object( $post->post_type ) : null; + + if ( isset( $post_type_object->graphql_single_name ) ) { + $type = $type_registry->get_type( $post_type_object->graphql_single_name ); + } + + return $type; + }, + ] + ); + } +} diff --git a/src/Type/InterfaceType/TaxonomyConnectionInterface.php b/src/Type/InterfaceType/TaxonomyConnectionInterface.php new file mode 100644 index 000000000..056e2838b --- /dev/null +++ b/src/Type/InterfaceType/TaxonomyConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to Taxonomy Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'TaxonomyConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'Taxonomy' ] ] ], + 'description' => __( 'A list of connected Taxonomy Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'TaxonomyConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected Taxonomy Node', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'Taxonomy' ], + 'description' => __( 'The connected Taxonomy Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/InterfaceType/TermNode.php b/src/Type/InterfaceType/TermNode.php index 768907ac3..3c5a73cf2 100644 --- a/src/Type/InterfaceType/TermNode.php +++ b/src/Type/InterfaceType/TermNode.php @@ -2,8 +2,8 @@ namespace WPGraphQL\Type\InterfaceType; -use WPGraphQL\Data\DataSource; -use WPGraphQL\Model\Term; +use WPGraphQL\Data\Connection\EnqueuedScriptsConnectionResolver; +use WPGraphQL\Data\Connection\EnqueuedStylesheetConnectionResolver; use WPGraphQL\Registry\TypeRegistry; class TermNode { @@ -14,6 +14,7 @@ class TermNode { * @param TypeRegistry $type_registry * * @return void + * @throws \Exception */ public static function register_type( TypeRegistry $type_registry ) { @@ -21,6 +22,24 @@ public static function register_type( TypeRegistry $type_registry ) { 'TermNode', [ 'description' => __( 'Terms are nodes within a Taxonomy, used to group and relate other nodes.', 'wp-graphql' ), + 'interfaces' => [ 'Node', 'UniformResourceIdentifiable', 'DatabaseIdentifier' ], + 'connections' => [ + 'enqueuedScripts' => [ + 'toType' => 'EnqueuedScript', + 'resolve' => function( $source, $args, $context, $info ) { + $resolver = new EnqueuedScriptsConnectionResolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + 'enqueuedStylesheets' => [ + 'toType' => 'EnqueuedStylesheet', + 'resolve' => function( $source, $args, $context, $info ) { + $resolver = new EnqueuedStylesheetConnectionResolver( $source, $args, $context, $info ); + return $resolver->get_connection(); + }, + ], + ], 'resolveType' => function( $term ) use ( $type_registry ) { /** @@ -44,17 +63,6 @@ public static function register_type( TypeRegistry $type_registry ) { }, 'fields' => [ - 'id' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'Unique identifier for the term', 'wp-graphql' ), - ], - 'databaseId' => [ - 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'Identifies the primary key from the database.', 'wp-graphql' ), - 'resolve' => function( Term $term, $args, $context, $info ) { - return absint( $term->term_id ); - }, - ], 'count' => [ 'type' => 'Int', 'description' => __( 'The number of objects connected to the object', 'wp-graphql' ), @@ -87,10 +95,6 @@ public static function register_type( TypeRegistry $type_registry ) { 'type' => 'String', 'description' => __( 'The link to the term', 'wp-graphql' ), ], - 'uri' => [ - 'type' => [ 'non_null' => 'String' ], - 'description' => __( 'The unique resource identifier path', 'wp-graphql' ), - ], ], ] ); diff --git a/src/Type/InterfaceType/TermNodeConnectionInterface.php b/src/Type/InterfaceType/TermNodeConnectionInterface.php new file mode 100644 index 000000000..580de53e5 --- /dev/null +++ b/src/Type/InterfaceType/TermNodeConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to Term Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'TermNodeConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'TermNode' ] ] ], + 'description' => __( 'A list of connected Term Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'TermNodeConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected Term Node', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'TermNode' ], + 'description' => __( 'The connected Term Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/InterfaceType/UniformResourceIdentifiable.php b/src/Type/InterfaceType/UniformResourceIdentifiable.php index 6205885bc..c1d0ba874 100644 --- a/src/Type/InterfaceType/UniformResourceIdentifiable.php +++ b/src/Type/InterfaceType/UniformResourceIdentifiable.php @@ -17,21 +17,19 @@ class UniformResourceIdentifiable { * * @param TypeRegistry $type_registry * @return void + * @throws \Exception */ public static function register_type( TypeRegistry $type_registry ) { register_graphql_interface_type( 'UniformResourceIdentifiable', [ 'description' => __( 'Any node that has a URI', 'wp-graphql' ), + 'interfaces' => [ 'Node' ], 'fields' => [ 'uri' => [ 'type' => 'String', 'description' => __( 'The unique resource identifier path', 'wp-graphql' ), ], - 'id' => [ - 'type' => [ 'non_null' => 'ID' ], - 'description' => __( 'The unique resource identifier path', 'wp-graphql' ), - ], ], 'resolveType' => function( $node ) use ( $type_registry ) { diff --git a/src/Type/InterfaceType/UserConnectionInterface.php b/src/Type/InterfaceType/UserConnectionInterface.php new file mode 100644 index 000000000..e0e9f38d3 --- /dev/null +++ b/src/Type/InterfaceType/UserConnectionInterface.php @@ -0,0 +1,46 @@ + [ 'Connection' ], + 'description' => __( 'Connection to User Nodes', 'wp-graphql' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'UserConnectionEdge' ] ] ], + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'User' ] ] ], + 'description' => __( 'A list of connected User Nodes', 'wp-graphql' ), + ], + ], + ] ); + + register_graphql_interface_type( 'UserConnectionEdge', [ + 'interfaces' => [ 'Edge' ], + 'description' => __( 'Edge between a Node and a connected User', 'wp-graphql' ), + 'fields' => [ + 'type' => [ 'non_null' => 'User' ], + 'description' => __( 'The connected User Node', 'wp-graphql' ), + ], + ]); + + } + +} diff --git a/src/Type/ObjectType/Comment.php b/src/Type/ObjectType/Comment.php index 1ef87fe3f..b26aca5d2 100644 --- a/src/Type/ObjectType/Comment.php +++ b/src/Type/ObjectType/Comment.php @@ -2,6 +2,9 @@ namespace WPGraphQL\Type\ObjectType; +use GraphQL\Type\Definition\ResolveInfo; +use WPGraphQL\AppContext; + /** * Class Comment * @@ -20,6 +23,32 @@ public static function register_type() { [ 'description' => __( 'A Comment object', 'wp-graphql' ), 'interfaces' => [ 'Node', 'DatabaseIdentifier' ], + 'connections' => [ + 'author' => [ + 'toType' => 'Commenter', + 'connectionInterfaces' => [ 'CommenterConnection' ], + 'description' => __( 'The author of the comment', 'wp-graphql' ), + 'oneToOne' => true, + 'resolve' => function( $comment, $args, AppContext $context, ResolveInfo $info ) { + + /** + * If the comment has a user associated, use it to populate the author, otherwise return + * the $comment and the Union will use that to hydrate the CommentAuthor Type + */ + if ( ! empty( $comment->userId ) ) { + $node = $context->get_loader( 'user' )->load( absint( $comment->userId ) ); + } else { + $node = ! empty( $comment->commentId ) ? $context->get_loader( 'comment_author' )->load( $comment->commentId ) : null; + } + + return [ + 'node' => $node, + 'source' => $comment, + ]; + + }, + ], + ], 'fields' => [ 'id' => [ 'description' => __( 'The globally unique identifier for the comment object', 'wp-graphql' ), diff --git a/src/Type/ObjectType/CommentAuthor.php b/src/Type/ObjectType/CommentAuthor.php index 8ceaff85d..cba6bba65 100644 --- a/src/Type/ObjectType/CommentAuthor.php +++ b/src/Type/ObjectType/CommentAuthor.php @@ -14,11 +14,8 @@ public static function register_type() { 'CommentAuthor', [ 'description' => __( 'A Comment Author object', 'wp-graphql' ), - 'interfaces' => [ 'Node', 'Commenter' ], + 'interfaces' => [ 'Node', 'Commenter', 'DatabaseIdentifier' ], 'fields' => [ - 'id' => [ - 'description' => __( 'The globally unique identifier for the comment author object', 'wp-graphql' ), - ], 'name' => [ 'type' => 'String', 'description' => __( 'The name for the comment author.', 'wp-graphql' ), diff --git a/src/Type/ObjectType/MenuItem.php b/src/Type/ObjectType/MenuItem.php index 570d955c3..0803be523 100644 --- a/src/Type/ObjectType/MenuItem.php +++ b/src/Type/ObjectType/MenuItem.php @@ -4,6 +4,9 @@ use GraphQL\Type\Definition\ResolveInfo; use WPGraphQL\AppContext; +use WPGraphQL\Data\Connection\MenuConnectionResolver; +use WPGraphQL\Data\Connection\PostObjectConnectionResolver; +use WPGraphQL\Data\Connection\TermObjectConnectionResolver; class MenuItem { @@ -18,6 +21,55 @@ public static function register_type() { [ 'description' => __( 'Navigation menu items are the individual items assigned to a menu. These are rendered as the links in a navigation menu.', 'wp-graphql' ), 'interfaces' => [ 'Node', 'DatabaseIdentifier' ], + 'connections' => [ + 'connectedNode' => [ + 'toType' => 'MenuItemLinkable', + 'connectionInterfaces' => [ 'MenuItemLinkableConnection' ], + 'description' => __( 'Connection from MenuItem to it\'s connected node', 'wp-graphql' ), + 'oneToOne' => true, + 'resolve' => function( \WPGraphQL\Model\MenuItem $menu_item, $args, AppContext $context, ResolveInfo $info ) { + + if ( ! isset( $menu_item->databaseId ) ) { + return null; + } + + $object_id = intval( get_post_meta( $menu_item->databaseId, '_menu_item_object_id', true ) ); + $object_type = get_post_meta( $menu_item->databaseId, '_menu_item_type', true ); + + $resolver = null; + switch ( $object_type ) { + // Post object + case 'post_type': + $resolver = new PostObjectConnectionResolver( $menu_item, $args, $context, $info ); + $resolver->set_query_arg( 'p', $object_id ); + break; + + // Taxonomy term + case 'taxonomy': + $resolver = new TermObjectConnectionResolver( $menu_item, $args, $context, $info ); + $resolver->set_query_arg( 'include', $object_id ); + break; + default: + $resolved_object = null; + break; + } + + return ! empty( $resolver ) ? $resolver->one_to_one()->get_connection() : null; + + }, + ], + 'menu' => [ + 'toType' => 'Menu', + 'description' => __( 'The Menu a MenuItem is part of', 'wp-graphql' ), + 'oneToOne' => true, + 'resolve' => function( \WPGraphQL\Model\MenuItem $menu_item, $args, $context, $info ) { + $resolver = new MenuConnectionResolver( $menu_item, $args, $context, $info ); + $resolver->set_query_arg( 'include', $menu_item->menuDatabaseId ); + + return $resolver->one_to_one()->get_connection(); + }, + ], + ], 'fields' => [ 'id' => [ 'description' => __( 'The globally unique identifier of the nav menu item object.', 'wp-graphql' ), @@ -83,56 +135,6 @@ public static function register_type() { ], 'description' => __( 'The locations the menu item\'s Menu is assigned to', 'wp-graphql' ), ], - 'connectedObject' => [ - 'type' => 'MenuItemObjectUnion', - 'deprecationReason' => __( 'Deprecated in favor of the connectedNode field', 'wp-graphql' ), - 'description' => __( 'The object connected to this menu item.', 'wp-graphql' ), - 'resolve' => function( $menu_item, array $args, AppContext $context, $info ) { - - $object_id = intval( get_post_meta( $menu_item->menuItemId, '_menu_item_object_id', true ) ); - $object_type = get_post_meta( $menu_item->menuItemId, '_menu_item_type', true ); - - switch ( $object_type ) { - // Post object - case 'post_type': - $resolved_object = $context->get_loader( 'post' )->load_deferred( $object_id ); - break; - - // Taxonomy term - case 'taxonomy': - $resolved_object = $context->get_loader( 'term' )->load_deferred( $object_id ); - break; - default: - $resolved_object = null; - break; - } - - /** - * Allow users to override how nav menu items are resolved. - * This is useful since we often add taxonomy terms to menus - * but would prefer to represent the menu item in other ways, - * e.g., a linked post object (or vice-versa). - * - * @param \WP_Post|\WP_Term $resolved_object Post or term connected to MenuItem - * @param array $args Array of arguments input in the field as part of the GraphQL query - * @param AppContext $context Object containing app context that gets passed down the resolve tree - * @param ResolveInfo $info Info about fields passed down the resolve tree - * @param int $object_id Post or term ID of connected object - * @param string $object_type Type of connected object ("post_type" or "taxonomy") - * - * @since 0.0.30 - */ - return apply_filters( - 'graphql_resolve_menu_item', - $resolved_object, - $args, - $context, - $info, - $object_id, - $object_type - ); - }, - ], ], ] ); diff --git a/src/Type/ObjectType/PageInfo.php b/src/Type/ObjectType/PageInfo.php index bf50d8839..93a85614e 100644 --- a/src/Type/ObjectType/PageInfo.php +++ b/src/Type/ObjectType/PageInfo.php @@ -19,7 +19,7 @@ public static function register_type() { * back to PageInfo – which would be another breaking change at that time */ register_graphql_object_type( - 'WPPageInfo', + 'PageInfo', [ 'description' => __( 'Information about pagination in a connection.', 'wp-graphql' ), 'fields' => [ diff --git a/src/Type/ObjectType/PostObject.php b/src/Type/ObjectType/PostObject.php index e17b17868..28930db6d 100644 --- a/src/Type/ObjectType/PostObject.php +++ b/src/Type/ObjectType/PostObject.php @@ -25,10 +25,15 @@ public static function register_post_object_types( WP_Post_Type $post_type_objec $single_name = $post_type_object->graphql_single_name; - $interfaces = [ 'Node', 'ContentNode', 'DatabaseIdentifier', 'NodeWithTemplate' ]; + $interfaces = [ 'Node', 'ContentNode', 'DatabaseIdentifier' ]; if ( true === $post_type_object->public ) { $interfaces[] = 'UniformResourceIdentifiable'; + $interfaces[] = 'NodeWithTemplate'; + + if ( 'attachment' !== $post_type_object->name ) { + $interfaces[] = 'Previewable'; + } } if ( post_type_supports( $post_type_object->name, 'title' ) ) { @@ -67,7 +72,12 @@ public static function register_post_object_types( WP_Post_Type $post_type_objec $interfaces[] = 'NodeWithPageAttributes'; } - if ( $post_type_object->hierarchical || in_array( + if ( $post_type_object->hierarchical ) { + $interfaces[] = 'HierarchicalContentNode'; + $interfaces[] = 'HierarchicalNode'; + } + + if ( in_array( $post_type_object->name, [ 'attachment', @@ -75,7 +85,7 @@ public static function register_post_object_types( WP_Post_Type $post_type_objec ], true ) ) { - $interfaces[] = 'HierarchicalContentNode'; + $interfaces[] = 'HierarchicalNode'; } if ( true === $post_type_object->show_in_nav_menus ) { @@ -87,7 +97,7 @@ public static function register_post_object_types( WP_Post_Type $post_type_objec [ /* translators: post object singular name w/ description */ 'description' => sprintf( __( 'The %s type', 'wp-graphql' ), $single_name ), - 'interfaces' => $interfaces, + 'interfaces' => array_unique( $interfaces ), 'fields' => self::get_post_object_fields( $post_type_object, $type_registry ), ] ); diff --git a/src/Type/ObjectType/RootQuery.php b/src/Type/ObjectType/RootQuery.php index f3dbe968a..d310a7455 100644 --- a/src/Type/ObjectType/RootQuery.php +++ b/src/Type/ObjectType/RootQuery.php @@ -6,6 +6,13 @@ use GraphQL\Type\Definition\ResolveInfo; use GraphQLRelay\Relay; use WPGraphQL\AppContext; +use WPGraphQL\Connection\PostObjects; +use WPGraphQL\Data\Connection\ContentTypeConnectionResolver; +use WPGraphQL\Data\Connection\EnqueuedScriptsConnectionResolver; +use WPGraphQL\Data\Connection\EnqueuedStylesheetConnectionResolver; +use WPGraphQL\Data\Connection\MenuConnectionResolver; +use WPGraphQL\Data\Connection\ThemeConnectionResolver; +use WPGraphQL\Data\Connection\UserRoleConnectionResolver; use WPGraphQL\Data\DataSource; /** @@ -25,6 +32,103 @@ public static function register_type() { 'RootQuery', [ 'description' => __( 'The root entry point into the Graph', 'wp-graphql' ), + 'connections' => [ + 'contentTypes' => [ + 'toType' => 'ContentType', + 'connectionInterfaces' => [ 'ContentTypeConnection' ], + 'resolve' => function( $source, $args, $context, $info ) { + $resolver = new ContentTypeConnectionResolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + 'menus' => [ + 'toType' => 'Menu', + 'connectionInterfaces' => [ 'MenuConnection' ], + 'connectionArgs' => [ + 'id' => [ + 'type' => 'Int', + 'description' => __( 'The ID of the object', 'wp-graphql' ), + ], + 'location' => [ + 'type' => 'MenuLocationEnum', + 'description' => __( 'The menu location for the menu being queried', 'wp-graphql' ), + ], + 'slug' => [ + 'type' => 'String', + 'description' => __( 'The slug of the menu to query items for', 'wp-graphql' ), + ], + ], + 'resolve' => function( $source, $args, $context, $info ) { + $resolver = new MenuConnectionResolver( $source, $args, $context, $info, 'nav_menu' ); + + return $resolver->get_connection(); + }, + ], + 'plugins' => [ + 'toType' => 'Plugin', + 'resolve' => function( $root, $args, $context, $info ) { + return DataSource::resolve_plugins_connection( $root, $args, $context, $info ); + }, + ], + 'revisions' => [ + 'toType' => 'ContentNode', + 'queryClass' => 'WP_Query', + 'connectionArgs' => PostObjects::get_connection_args(), + 'resolve' => function( $root, $args, $context, $info ) { + return DataSource::resolve_post_objects_connection( $root, $args, $context, $info, 'revision' ); + }, + ], + 'registeredScripts' => [ + 'toType' => 'EnqueuedScript', + 'resolve' => function( $source, $args, $context, $info ) { + + // The connection resolver expects the source to include + // enqueuedScriptsQueue + $source = new \stdClass(); + $source->enqueuedScriptsQueue = []; + global $wp_scripts; + do_action( 'wp_enqueue_scripts' ); + $source->enqueuedScriptsQueue = array_keys( $wp_scripts->registered ); + $resolver = new EnqueuedScriptsConnectionResolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + 'registeredStylesheets' => [ + 'toType' => 'EnqueuedStylesheet', + 'resolve' => function( $source, $args, $context, $info ) { + + // The connection resolver expects the source to include + // enqueuedStylesheetsQueue + $source = new \stdClass(); + $source->enqueuedStylesheetsQueue = []; + global $wp_styles; + do_action( 'wp_enqueue_scripts' ); + $source->enqueuedStylesheetsQueue = array_keys( $wp_styles->registered ); + $resolver = new EnqueuedStylesheetConnectionResolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + 'themes' => [ + 'toType' => 'Theme', + 'resolve' => function( $root, $args, $context, $info ) { + $resolver = new ThemeConnectionResolver( $root, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + 'userRoles' => [ + 'toType' => 'UserRole', + 'fromFieldName' => 'userRoles', + 'resolve' => function( $user, $args, $context, $info ) { + $resolver = new UserRoleConnectionResolver( $user, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + ], 'fields' => [ 'allSettings' => [ 'type' => 'Settings', @@ -571,10 +675,14 @@ public static function register_post_object_fields() { return $context->get_loader( 'post' )->load_deferred( $post_id )->then( function( $post ) use ( $post_type_object ) { - if ( ! isset( $post->post_type ) || ! in_array( $post->post_type, [ - 'revision', - $post_type_object->name, - ], true ) ) { + if ( ! isset( $post->post_type ) || ! in_array( + $post->post_type, + [ + 'revision', + $post_type_object->name, + ], + true + ) ) { return null; } @@ -645,10 +753,14 @@ function( $post ) use ( $post_type_object ) { return null; } - if ( ! isset( $post->post_type ) || ! in_array( $post->post_type, [ - 'revision', - $post_type_object->name, - ], true ) ) { + if ( ! isset( $post->post_type ) || ! in_array( + $post->post_type, + [ + 'revision', + $post_type_object->name, + ], + true + ) ) { return null; } diff --git a/src/Type/ObjectType/Taxonomy.php b/src/Type/ObjectType/Taxonomy.php index a584bce0d..51fbdfe4b 100644 --- a/src/Type/ObjectType/Taxonomy.php +++ b/src/Type/ObjectType/Taxonomy.php @@ -2,6 +2,10 @@ namespace WPGraphQL\Type\ObjectType; +use GraphQL\Type\Definition\ResolveInfo; +use WPGraphQL\AppContext; +use WPGraphQL\Data\Connection\ContentTypeConnectionResolver; + class Taxonomy { /** @@ -11,13 +15,26 @@ class Taxonomy { */ public static function register_type() { - $allowed_post_types = \WPGraphQL::get_allowed_post_types(); - register_graphql_object_type( 'Taxonomy', [ 'description' => __( 'A taxonomy object', 'wp-graphql' ), 'interfaces' => [ 'Node' ], + 'connections' => [ + 'connectedContentTypes' => [ + 'toType' => 'ContentType', + 'connectionInterfaces' => [ 'ContentTypeConnection' ], + 'description' => __( 'List of Content Types associated with the Taxonomy', 'wp-graphql' ), + 'resolve' => function( \WPGraphQL\Model\Taxonomy $taxonomy, $args, AppContext $context, ResolveInfo $info ) { + + $connected_post_types = ! empty( $taxonomy->object_type ) ? $taxonomy->object_type : []; + $resolver = new ContentTypeConnectionResolver( $taxonomy, $args, $context, $info ); + $resolver->set_query_arg( 'contentTypeNames', $connected_post_types ); + return $resolver->get_connection(); + + }, + ], + ], 'fields' => [ 'id' => [ 'description' => __( 'The globally unique identifier of the taxonomy object.', 'wp-graphql' ), diff --git a/src/Type/ObjectType/TermObject.php b/src/Type/ObjectType/TermObject.php index 74c2d62db..bd38c642b 100644 --- a/src/Type/ObjectType/TermObject.php +++ b/src/Type/ObjectType/TermObject.php @@ -29,6 +29,7 @@ public static function register_taxonomy_object_type( WP_Taxonomy $taxonomy_obje if ( $taxonomy_object->hierarchical ) { $interfaces[] = 'HierarchicalTermNode'; + $interfaces[] = 'HierarchicalNode'; } if ( true === $taxonomy_object->show_in_nav_menus ) { diff --git a/src/Type/ObjectType/User.php b/src/Type/ObjectType/User.php index b465ef1f0..65bfab31a 100644 --- a/src/Type/ObjectType/User.php +++ b/src/Type/ObjectType/User.php @@ -1,8 +1,11 @@ __( 'A User object', 'wp-graphql' ), - 'interfaces' => [ 'Node', 'UniformResourceIdentifiable', 'Commenter', 'DatabaseIdentifier' ], + 'interfaces' => [ + 'Node', + 'UniformResourceIdentifiable', + 'Commenter', + 'DatabaseIdentifier', + ], + 'connections' => [ + 'enqueuedScripts' => [ + 'toType' => 'EnqueuedScript', + 'resolve' => function( $source, $args, $context, $info ) { + $resolver = new EnqueuedScriptsConnectionResolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + 'enqueuedStylesheets' => [ + 'toType' => 'EnqueuedStylesheet', + 'resolve' => function( $source, $args, $context, $info ) { + $resolver = new EnqueuedStylesheetConnectionResolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + 'revisions' => [ + 'toType' => 'ContentNode', + 'connectionInterfaces' => [ 'ContentNodeConnection' ], + 'queryClass' => 'WP_Query', + 'description' => __( 'Connection between the User and Revisions authored by the user', 'wp-graphql' ), + 'connectionArgs' => PostObjects::get_connection_args(), + 'resolve' => function( $root, $args, $context, $info ) { + return DataSource::resolve_post_objects_connection( $root, $args, $context, $info, 'revision' ); + }, + ], + 'roles' => [ + 'toType' => 'UserRole', + 'fromFieldName' => 'roles', + 'resolve' => function( \WPGraphQL\Model\User $user, $args, $context, $info ) { + $resolver = new UserRoleConnectionResolver( $user, $args, $context, $info ); + // Only get roles matching the slugs of the roles belonging to the user + + if ( ! empty( $user->roles ) ) { + $resolver->set_query_arg( 'slugIn', $user->roles ); + } + + return $resolver->get_connection(); + }, + ], + ], 'fields' => [ 'id' => [ 'description' => __( 'The globally unique identifier for the user object.', 'wp-graphql' ), ], - 'databaseId' => [ - 'type' => [ 'non_null' => 'Int' ], - 'description' => __( 'Identifies the primary key from the database.', 'wp-graphql' ), - 'resolve' => function( \WPGraphQL\Model\User $user ) { - return absint( $user->userId ); - }, - ], 'capabilities' => [ 'type' => [ 'list_of' => 'String', diff --git a/src/Type/Union/ContentRevisionUnion.php b/src/Type/Union/ContentRevisionUnion.php deleted file mode 100644 index 0a73d9546..000000000 --- a/src/Type/Union/ContentRevisionUnion.php +++ /dev/null @@ -1,59 +0,0 @@ -graphql_single_name; - }, - $post_types_with_revision_support - ); - - register_graphql_union_type( - 'ContentRevisionUnion', - [ - 'typeNames' => $type_names, - 'description' => __( 'A union of Content Node Types that support revisions', 'wp-graphql' ), - 'resolveType' => function( Post $object ) use ( $type_registry ) { - - $type = 'Post'; - $parent = get_post( (int) $object->parentDatabaseId ); - if ( ! empty( $parent ) && isset( $parent->post_type ) ) { - $parent_post_type_object = get_post_type_object( $parent->post_type ); - if ( isset( $parent_post_type_object->graphql_single_name ) ) { - $type = $type_registry->get_type( $parent_post_type_object->graphql_single_name ); - } - } - - return ! empty( $type ) ? $type : null; - - }, - ] - ); - } - - } -} diff --git a/src/Type/Union/MenuItemObjectUnion.php b/src/Type/Union/MenuItemObjectUnion.php deleted file mode 100644 index 197fbc4a4..000000000 --- a/src/Type/Union/MenuItemObjectUnion.php +++ /dev/null @@ -1,92 +0,0 @@ - self::get_possible_types(), - 'description' => __( 'Deprecated in favor of MenuItemLinkeable Interface', 'wp-graphql' ), - 'resolveType' => function( $object ) use ( $type_registry ) { - // Post object - if ( $object instanceof Post && isset( $object->post_type ) && ! empty( $object->post_type ) ) { - /** @var \WP_Post_Type $post_type_object */ - $post_type_object = get_post_type_object( $object->post_type ); - - return $type_registry->get_type( $post_type_object->graphql_single_name ); - } - - // Taxonomy term - if ( $object instanceof Term && ! empty( $object->taxonomyName ) ) { - /** @var \WP_Taxonomy $taxonomy_object */ - $taxonomy_object = get_taxonomy( $object->taxonomyName ); - - return $type_registry->get_type( $taxonomy_object->graphql_single_name ); - } - - return $object; - }, - ] - ); - } - - /** - * Returns a list of possible types for the union - * - * @return array - */ - public static function get_possible_types() { - - /** - * The possible types for MenuItems should be just the TermObjects and PostTypeObjects that are - * registered to "show_in_graphql" and "show_in_nav_menus" - */ - $args = [ - 'show_in_nav_menus' => true, - ]; - - $possible_types = []; - - // Add post types that are allowed in WPGraphQL. - foreach ( \WPGraphQL::get_allowed_post_types( $args ) as $type ) { - $post_type_object = get_post_type_object( $type ); - if ( isset( $post_type_object->graphql_single_name ) ) { - $possible_types[] = $post_type_object->graphql_single_name; - } - } - - // Add taxonomies that are allowed in WPGraphQL. - foreach ( get_taxonomies( $args ) as $type ) { - $tax_object = get_taxonomy( $type ); - if ( isset( $tax_object->graphql_single_name ) ) { - $possible_types[] = $tax_object->graphql_single_name; - } - } - - return $possible_types; - } -} - diff --git a/src/Type/Union/PostObjectUnion.php b/src/Type/Union/PostObjectUnion.php deleted file mode 100644 index 11bf0ea01..000000000 --- a/src/Type/Union/PostObjectUnion.php +++ /dev/null @@ -1,60 +0,0 @@ - 'PostObjectUnion', - 'typeNames' => self::get_possible_types(), - 'description' => __( 'Union between the post, page and media item types', 'wp-graphql' ), - 'resolveType' => function( $value ) use ( $type_registry ) { - - $type = null; - if ( isset( $value->post_type ) ) { - $post_type_object = get_post_type_object( $value->post_type ); - if ( isset( $post_type_object->graphql_single_name ) ) { - $type = $type_registry->get_type( $post_type_object->graphql_single_name ); - } - } - - return ! empty( $type ) ? $type : null; - }, - ] - ); - } - - /** - * Returns a list of possible types for the union - * - * @return array - */ - public static function get_possible_types() { - $possible_types = []; - $allowed_post_types = \WPGraphQL::get_allowed_post_types(); - - if ( ! empty( $allowed_post_types ) && is_array( $allowed_post_types ) ) { - foreach ( $allowed_post_types as $allowed_post_type ) { - if ( empty( $possible_types[ $allowed_post_type ] ) ) { - $post_type_object = get_post_type_object( $allowed_post_type ); - if ( isset( $post_type_object->graphql_single_name ) ) { - $possible_types[ $allowed_post_type ] = $post_type_object->graphql_single_name; - } - } - } - } - - return $possible_types; - } -} diff --git a/src/Type/Union/TermObjectUnion.php b/src/Type/Union/TermObjectUnion.php deleted file mode 100644 index 9d20618b1..000000000 --- a/src/Type/Union/TermObjectUnion.php +++ /dev/null @@ -1,60 +0,0 @@ - 'union', - 'typeNames' => self::get_possible_types(), - 'description' => __( 'Union between the Category, Tag and PostFormatPost types', 'wp-graphql' ), - 'resolveType' => function( $value ) use ( $type_registry ) { - - $type = null; - if ( isset( $value->taxonomyName ) ) { - $tax_object = get_taxonomy( $value->taxonomyName ); - if ( isset( $tax_object->graphql_single_name ) ) { - $type = $type_registry->get_type( $tax_object->graphql_single_name ); - } - } - - return ! empty( $type ) ? $type : null; - - }, - ] - ); - } - - /** - * Returns a list of possible types for the union - * - * @return array - */ - public static function get_possible_types() { - $possible_types = []; - - $allowed_taxonomies = \WPGraphQL::get_allowed_taxonomies(); - if ( ! empty( $allowed_taxonomies ) && is_array( $allowed_taxonomies ) ) { - foreach ( $allowed_taxonomies as $allowed_taxonomy ) { - if ( empty( $possible_types[ $allowed_taxonomy ] ) ) { - $tax_object = get_taxonomy( $allowed_taxonomy ); - if ( isset( $tax_object->graphql_single_name ) ) { - $possible_types[ $allowed_taxonomy ] = $tax_object->graphql_single_name; - } - } - } - } - return $possible_types; - } -} diff --git a/src/Type/WPConnectionType.php b/src/Type/WPConnectionType.php new file mode 100644 index 000000000..8f0894f90 --- /dev/null +++ b/src/Type/WPConnectionType.php @@ -0,0 +1,492 @@ +validate_config( $config ); + + $this->config = $config; + $this->type_registry = $type_registry; + $this->from_type = $config['fromType']; + $this->to_type = $config['toType']; + $this->from_field_name = $config['fromFieldName']; + $this->auth = array_key_exists( 'auth', $config ) && is_array( $config['auth'] ) ? $config['auth'] : []; + $this->connection_fields = array_key_exists( 'connectionFields', $config ) && is_array( $config['connectionFields'] ) ? $config['connectionFields'] : []; + $this->connection_args = array_key_exists( 'connectionArgs', $config ) && is_array( $config['connectionArgs'] ) ? $config['connectionArgs'] : []; + $this->edge_fields = array_key_exists( 'edgeFields', $config ) && is_array( $config['edgeFields'] ) ? $config['edgeFields'] : []; + $this->resolve_node = array_key_exists( 'resolveNode', $config ) && is_callable( $config['resolve'] ) ? $config['resolveNode'] : null; + $this->resolve_cursor = array_key_exists( 'resolveCursor', $config ) && is_callable( $config['resolve'] ) ? $config['resolveCursor'] : null; + $this->resolve_connection = array_key_exists( 'resolve', $config ) && is_callable( $config['resolve'] ) ? $config['resolve'] : function() { + return null; + }; + $this->connection_name = ! empty( $config['connectionTypeName'] ) ? $config['connectionTypeName'] : $this->get_connection_name( $this->from_type, $this->to_type, $this->from_field_name ); + $this->where_args = []; + $this->one_to_one = isset( $config['oneToOne'] ) && true === $config['oneToOne']; + $this->connection_interfaces = isset( $config['connectionInterfaces'] ) && is_array( $config['connectionInterfaces'] ) ? $config['connectionInterfaces'] : []; + $this->query_class = array_key_exists( 'queryClass', $config ) && ! empty( $config['queryClass'] ) ? $config['queryClass'] : null; + + } + + /** + * Validates that essential key/value pairs are passed to the connection config. + * + * @param array $config + * + * @return void + */ + protected function validate_config( array $config ) { + + if ( ! array_key_exists( 'fromType', $config ) ) { + throw new InvalidArgument( __( 'Connection config needs to have at least a fromType defined', 'wp-graphql' ) ); + } + + if ( ! array_key_exists( 'toType', $config ) ) { + throw new InvalidArgument( __( 'Connection config needs to have a "toType" defined', 'wp-graphql' ) ); + } + + if ( ! array_key_exists( 'fromFieldName', $config ) || ! is_string( $config['fromFieldName'] ) ) { + throw new InvalidArgument( __( 'Connection config needs to have "fromFieldName" defined as a string value', 'wp-graphql' ) ); + } + + } + + /** + * Utility method that formats the connection name given the name of the from Type and the to + * Type + * + * @param string $from_type Name of the Type the connection is coming from + * @param string $to_type Name of the Type the connection is going to + * @param string $from_field_name Acts as an alternative "toType" if connection type already defined using $to_type. + * + * @return string + */ + public function get_connection_name( string $from_type, string $to_type, string $from_field_name ) { + + // Create connection name using $from_type + To + $to_type + Connection. + $connection_name = ucfirst( $from_type ) . 'To' . ucfirst( $to_type ) . 'Connection'; + + // If connection type already exists with that connection name. Set connection name using + // $from_field_name + To + $to_type + Connection. + if ( ! empty( $this->type_registry->get_type( $connection_name ) ) ) { + $connection_name = ucfirst( $from_type ) . 'To' . ucfirst( $from_field_name ) . 'Connection'; + } + + return $connection_name; + + } + + /** + * If the connection includes connection args in the config, this registers the input args + * for the connection + * + * @return void + * + * @throws Exception + */ + protected function register_connection_input() { + + if ( empty( $this->connection_args ) ) { + return; + } + + $input_name = $this->connection_name . 'WhereArgs'; + + if ( $this->type_registry->get_type( $input_name ) ) { + return; + } + + $this->type_registry->register_input_type( + $input_name, + [ + // Translators: Placeholder is the name of the connection + 'description' => sprintf( __( 'Arguments for filtering the %s connection', 'wp-graphql' ), $this->connection_name ), + 'fields' => $this->connection_args, + 'queryClass' => $this->query_class, + ] + ); + + $this->where_args = [ + 'where' => [ + 'description' => __( 'Arguments for filtering the connection', 'wp-graphql' ), + 'type' => $this->connection_name . 'WhereArgs', + ], + ]; + + } + + /** + * Registers the Connection Edge type to the Schema + * + * @return void + * + * @throws Exception + */ + protected function register_connection_edge_type() { + + if ( true === $this->one_to_one ) { + + $interfaces = [ 'SingleNodeConnectionEdge', 'Edge' ]; + + if ( ! empty( $this->connection_interfaces ) ) { + foreach ( $this->connection_interfaces as $connection_interface ) { + $interfaces[] = $connection_interface . 'Edge'; + } + } + + $this->type_registry->register_object_type( + $this->connection_name . 'Edge', + [ + 'interfaces' => $interfaces, + // Translators: Placeholders are for the name of the Type the connection is coming from and the name of the Type the connection is going to + 'description' => sprintf( __( 'Connection between the %1$s type and the %2$s type', 'wp-graphql' ), $this->from_type, $this->to_type ), + 'fields' => array_merge( + [ + 'node' => [ + 'type' => [ 'non_null' => $this->to_type ], + 'description' => __( 'The node of the connection, without the edges', 'wp-graphql' ), + ], + ], + $this->edge_fields + ), + ] + ); + + } else { + + $interfaces = [ 'Edge' ]; + + if ( ! empty( $this->connection_interfaces ) ) { + foreach ( $this->connection_interfaces as $connection_interface ) { + $interfaces[] = $connection_interface . 'Edge'; + } + } + + $this->type_registry->register_object_type( + $this->connection_name . 'Edge', + [ + 'description' => __( 'An edge in a connection', 'wp-graphql' ), + 'interfaces' => $interfaces, + 'fields' => array_merge( + [ + 'cursor' => [ + 'type' => 'String', + 'description' => __( 'A cursor for use in pagination', 'wp-graphql' ), + 'resolve' => $this->resolve_cursor, + ], + 'node' => [ + 'type' => [ 'non_null' => $this->to_type ], + 'description' => __( 'The item at the end of the edge', 'wp-graphql' ), + 'resolve' => function( $source, $args, $context, ResolveInfo $info ) { + if ( ! empty( $this->resolve_node ) && is_callable( $this->resolve_node ) ) { + $resolve_node = $this->resolve_node; + return ! empty( $source['node'] ) ? $resolve_node( $source['node'], $args, $context, $info ) : null; + } else { + return $source['node']; + } + }, + ], + ], + $this->edge_fields + ), + ] + ); + + } + + } + + /** + * Registers the Connection Type to the Schema + * + * @return void + * + * @throws Exception + */ + protected function register_connection_type() { + + $interfaces = [ 'Connection' ]; + + if ( ! empty( $this->connection_interfaces ) ) { + foreach ( $this->connection_interfaces as $connection_interface ) { + $interfaces[] = $connection_interface; + } + } + + $this->type_registry->register_object_type( + $this->connection_name, + [ + // Translators: the placeholders are the name of the Types the connection is between. + 'description' => sprintf( __( 'Connection between the %1$s type and the %2$s type', 'wp-graphql' ), $this->from_type, $this->to_type ), + 'interfaces' => $interfaces, + 'connection_config' => $this->config, + 'fields' => array_merge( + [ + 'pageInfo' => [ + // @todo: change to PageInfo when/if the Relay lib is deprecated + 'type' => [ 'non_null' => 'PageInfo' ], + 'description' => __( 'Information about pagination in a connection.', 'wp-graphql' ), + ], + 'edges' => [ + 'type' => [ + 'non_null' => [ + 'list_of' => [ 'non_null' => $this->connection_name . 'Edge' ], + ], + ], + // Translators: Placeholder is the name of the connection + 'description' => sprintf( __( 'Edges for the %s connection', 'wp-graphql' ), $this->connection_name ), + ], + 'nodes' => [ + 'type' => [ + 'non_null' => [ + 'list_of' => [ 'non_null' => $this->to_type ], + ], + ], + 'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ), + 'resolve' => function( $source, $args, $context, $info ) { + $nodes = []; + if ( ! empty( $source['nodes'] ) && is_array( $source['nodes'] ) ) { + if ( ! empty( $this->resolve_node ) && is_callable( $this->resolve_node ) ) { + $resolve_node = $this->resolve_node; + foreach ( $source['nodes'] as $node ) { + $nodes[] = $resolve_node( $node, $args, $context, $info ); + } + } else { + return $source['nodes']; + } + } + + return $nodes; + }, + ], + ], + $this->connection_fields + ), + ] + ); + + } + + /** + * Get the args used for pagination on connections + * + * @return array|array[] + */ + protected function get_pagination_args() { + + if ( true === $this->one_to_one ) { + + $pagination_args = []; + + } else { + + $pagination_args = [ + 'first' => [ + 'type' => 'Int', + 'description' => __( 'The number of items to return after the referenced "after" cursor', 'wp-graphql' ), + ], + 'last' => [ + 'type' => 'Int', + 'description' => __( 'The number of items to return before the referenced "before" cursor', 'wp-graphql' ), + ], + 'after' => [ + 'type' => 'String', + 'description' => __( 'Cursor used along with the "first" argument to reference where in the dataset to get data', 'wp-graphql' ), + ], + 'before' => [ + 'type' => 'String', + 'description' => __( 'Cursor used along with the "last" argument to reference where in the dataset to get data', 'wp-graphql' ), + ], + ]; + + } + + return $pagination_args; + } + + /** + * Registers the connection in the Graph + * + * @return void + */ + public function register_connection_field() { + + $this->type_registry->register_field( + $this->from_type, + $this->from_field_name, + [ + 'type' => true === $this->one_to_one ? $this->connection_name . 'Edge' : $this->connection_name, + 'args' => array_merge( $this->get_pagination_args(), $this->where_args ), + 'auth' => $this->auth, + 'description' => ! empty( $this->config['description'] ) ? $this->config['description'] : sprintf( __( 'Connection between the %1$s type and the %2$s type', 'wp-graphql' ), $this->from_type, $this->to_type ), + 'resolve' => function( $root, $args, $context, $info ) { + + if ( ! isset( $this->resolve_connection ) || ! is_callable( $this->resolve_connection ) ) { + return null; + } + + $context->connection_query_class = $this->query_class; + $resolve_connection = $this->resolve_connection; + + /** + * Return the results of the connection resolver + */ + return $resolve_connection( $root, $args, $context, $info ); + }, + ] + ); + + } + + /** + * Registers the connection Types and field to the Schema + * + * @return void + * + * @throws Exception + */ + public function register_connection() { + + $this->register_connection_input(); + $this->register_connection_edge_type(); + $this->register_connection_type(); + $this->register_connection_field(); + + } + +} diff --git a/src/Type/WPEnumType.php b/src/Type/WPEnumType.php index c8674259c..fe6e59dbb 100644 --- a/src/Type/WPEnumType.php +++ b/src/Type/WPEnumType.php @@ -17,7 +17,7 @@ class WPEnumType extends EnumType { * * @param array $config */ - public function __construct( $config ) { + public function __construct( array $config ) { $name = ucfirst( $config['name'] ); $config['name'] = apply_filters( 'graphql_type_name', $name, $config, $this ); $config['values'] = self::prepare_values( $config['values'], $config['name'] ); diff --git a/src/Type/WPInterfaceType.php b/src/Type/WPInterfaceType.php index 929b178bc..34fa7038c 100644 --- a/src/Type/WPInterfaceType.php +++ b/src/Type/WPInterfaceType.php @@ -2,6 +2,7 @@ namespace WPGraphQL\Type; +use GraphQL\Error\UserError; use GraphQL\Type\Definition\InterfaceType; use WPGraphQL\Registry\TypeRegistry; @@ -19,15 +20,114 @@ class WPInterfaceType extends InterfaceType { * * @param array $config * @param TypeRegistry $type_registry + * + * @throws \Exception */ public function __construct( array $config, TypeRegistry $type_registry ) { $this->type_registry = $type_registry; + $interfaces = isset( $config['interfaces'] ) ? $config['interfaces'] : []; + + /** + * Filters the interfaces applied to an object type + * + * @param array $interfaces List of interfaces applied to the Object Type + * @param array $config The config for the Object Type + * @param WPObjectType $wp_object_type The WPObjectType instance + */ + $interfaces = apply_filters( 'graphql_interface_type_interfaces', $interfaces, $config, $this ); + $config['interfaceNames'] = $interfaces; + + /** + * Convert Interfaces from Strings to Types + */ + $config['interfaces'] = function() use ( $interfaces ) { + $new_interfaces = []; + if ( ! is_array( $interfaces ) ) { + // TODO Throw an error. + return $new_interfaces; + } + + foreach ( $interfaces as $interface ) { + if ( is_string( $interface ) ) { + if ( $interface_type = $this->type_registry->get_type( $interface ) ) { + $new_interfaces[ $interface ] = $this->type_registry->get_type( $interface ); + } + continue; + } + if ( $interface instanceof WPInterfaceType ) { + $new_interfaces[ get_class( $interface ) ] = $interface; + } + } + + return $new_interfaces; + }; + + if ( ! empty( $config['connections'] ) && is_array( $config['connections'] ) ) { + foreach ( $config['connections'] as $field_name => $connection_config ) { + + if ( ! is_array( $connection_config ) ) { + return; + } + + $connection_config['fromType'] = $config['name']; + $connection_config['fromFieldName'] = $field_name; + + register_graphql_connection( $connection_config ); + + } + } + $name = ucfirst( $config['name'] ); $config['name'] = apply_filters( 'graphql_type_name', $name, $config, $this ); $config['fields'] = function() use ( $config ) { - $fields = $this->prepare_fields( $config['fields'], $config['name'] ); + + $fields = $config['fields']; + + /** + * Get the fields of interfaces and ensure they exist as fields of this type. + * + * Types are still responsible for ensuring the fields resolve properly. + */ + if ( ! empty( $config['interfaceNames'] ) ) { + // Throw if "interfaceNames" invalid. + if ( ! is_array( $config['interfaceNames'] ) ) { + throw new UserError( + sprintf( + /* translators: %s: type name */ + __( 'Invalid value provided as "interfaceNames" on %s.', 'wp-graphql' ), + $config['name'] + ) + ); + } + + foreach ( $config['interfaceNames'] as $interface_name ) { + $interface_type = null; + if ( is_string( $interface_name ) ) { + $interface_type = $this->type_registry->get_type( $interface_name ); + } elseif ( $interface_name instanceof WPInterfaceType ) { + $interface_type = $interface_name; + } + + $interface_fields = []; + if ( ! empty( $interface_type ) && $interface_type instanceof WPInterfaceType ) { + + $interface_config_fields = $interface_type->getFields(); + + if ( ! empty( $interface_config_fields ) ) { + foreach ( $interface_config_fields as $interface_field ) { + $interface_fields[ $interface_field->name ] = $interface_field->config; + } + } + } + + $fields = array_replace_recursive( $interface_fields, $fields ); + + } + } + + $fields = $this->prepare_fields( $fields, $config['name'] ); $fields = $this->type_registry->prepare_fields( $fields, $config['name'] ); return $fields; diff --git a/src/Type/WPObjectType.php b/src/Type/WPObjectType.php index 18a6dfb14..9eb899000 100644 --- a/src/Type/WPObjectType.php +++ b/src/Type/WPObjectType.php @@ -3,6 +3,7 @@ namespace WPGraphQL\Type; use GraphQL\Error\UserError; +use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use WPGraphQL\Data\DataSource; use WPGraphQL\Registry\TypeRegistry; @@ -42,6 +43,7 @@ class WPObjectType extends ObjectType { * @param array $config * @param TypeRegistry $type_registry * + * @throws \Exception * @since 0.0.5 */ public function __construct( $config, TypeRegistry $type_registry ) { @@ -80,16 +82,18 @@ public function __construct( $config, TypeRegistry $type_registry ) { /** * Convert Interfaces from Strings to Types */ - $config['interfaces'] = function() use ( $interfaces ) { + $config['interfaces'] = function() use ( $config, $interfaces ) { $new_interfaces = []; if ( ! is_array( $interfaces ) ) { - // TODO Throw an error. - return $new_interfaces; + graphql_debug( sprintf( __( 'Interfaces registered to the %s Type were registered incorrectly', 'wp-graphql' ), $config['name'] ) ); + return []; } foreach ( $interfaces as $interface ) { if ( is_string( $interface ) ) { - $new_interfaces[ $interface ] = $this->type_registry->get_type( $interface ); + if ( $interface_type = $this->type_registry->get_type( $interface ) ) { + $new_interfaces[ $interface ] = $this->type_registry->get_type( $interface ); + } continue; } if ( $interface instanceof WPInterfaceType ) { @@ -100,6 +104,21 @@ public function __construct( $config, TypeRegistry $type_registry ) { return $new_interfaces; }; + if ( ! empty( $config['connections'] ) && is_array( $config['connections'] ) ) { + foreach ( $config['connections'] as $field_name => $connection_config ) { + + if ( ! is_array( $connection_config ) ) { + return; + } + + $connection_config['fromType'] = $config['name']; + $connection_config['fromFieldName'] = $field_name; + + register_graphql_connection( $connection_config ); + + } + } + /** * Setup the fields * diff --git a/src/Utils/InstrumentSchema.php b/src/Utils/InstrumentSchema.php index f55183601..ace4ccb3f 100644 --- a/src/Utils/InstrumentSchema.php +++ b/src/Utils/InstrumentSchema.php @@ -266,7 +266,7 @@ public static function check_field_permissions( $source, array $args, AppContext if ( ! empty( $field->config['auth']['callback'] ) && is_callable( $field->config['auth']['callback'] ) ) { $authorized = call_user_func( $field->config['auth']['callback'], $field, $field_key, $source, $args, $context, $info, $field_resolver ); - + // If callback returns explicit false throw. if ( false === $authorized ) { throw new UserError( $auth_error ); diff --git a/tests/wpunit/AccessFunctionsTest.php b/tests/wpunit/AccessFunctionsTest.php index eb60e3271..bff0d5ec2 100644 --- a/tests/wpunit/AccessFunctionsTest.php +++ b/tests/wpunit/AccessFunctionsTest.php @@ -289,6 +289,60 @@ public function testFilterInputFields() { } + public function testRegisterConnectionToNonExistentTypeReturnsDebugMessage() { + + register_graphql_connection([ + 'fromType' => 'RootQuery', + 'toType' => 'FakeType', + 'fromFieldName' => 'fakeTypeConnection', + ]); + + $actual = graphql([ + 'query' => ' + { + posts(first:1) { + nodes { + id + } + } + } + ' + ]); + + codecept_debug( $actual ); + + $this->assertArrayNotHasKey( 'errors', $actual ); + + + } + + public function testRegisterConnectionFromNonExistentTypeReturnsDebugMessage() { + + register_graphql_connection([ + 'fromType' => 'FakeType', + 'toType' => 'Post', + 'fromFieldName' => 'fakeTypeConnection', + ]); + + $actual = graphql([ + 'query' => ' + { + posts(first:1) { + nodes { + id + } + } + } + ' + ]); + + codecept_debug( $actual ); + + $this->assertArrayNotHasKey( 'errors', $actual ); + + + } + public function testRenameGraphQLFieldName() { rename_graphql_field( 'RootQuery', 'user', 'wpUser' ); @@ -307,6 +361,13 @@ public function testRenameGraphQLFieldName() { public function testRenameGraphQLType() { + // Register a Union so we can test renaming it + register_graphql_union_type( 'PostObjectUnion', [ + 'typeNames' => [ 'Post', 'Page' ], + 'description' => __( 'Union between the post, page and media item types', 'wp-graphql' ), + ]); + + rename_graphql_type( 'User', 'WPUser' ); rename_graphql_type( 'AvatarRatingEnum', 'ImageRatingEnum' ); rename_graphql_type( 'PostObjectUnion', 'CPTUnion' ); diff --git a/tests/wpunit/ConnectionInterfaceTest.php b/tests/wpunit/ConnectionInterfaceTest.php new file mode 100644 index 000000000..bf0acf69f --- /dev/null +++ b/tests/wpunit/ConnectionInterfaceTest.php @@ -0,0 +1,335 @@ +clearSchema(); + parent::setUp(); // TODO: Change the autogenerated stub + } + + public function tearDown() { + $this->clearSchema(); + parent::tearDown(); // TODO: Change the autogenerated stub + } + + public function interfaceQuery( string $name ) { + + return $this->graphql([ + 'query' => ' + query GetTypeByName($name: String!) { + __type(name: $name) { + name + interfaces { + name + } + } + } + ', + 'variables' => [ + 'name' => $name + ] + ]); + + } + + public function testCommentConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'CommentConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testCommentConnectionEdgeImplementsConnection() { + + $results = $this->interfaceQuery( 'CommentConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + + public function testCommenterConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'CommenterConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testCommenterConnectionEdgeImplementsConnection() { + + $results = $this->interfaceQuery( 'CommenterConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + + public function testContentNodeConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'ContentNodeConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testContentNodeConnectionEdgeImplementsConnection() { + + $results = $this->interfaceQuery( 'ContentNodeConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + + public function testContentTypeConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'ContentTypeConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testContentTypeConnectionEdgeImplementsConnection() { + + $results = $this->interfaceQuery( 'ContentTypeConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + + public function testMenuConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'MenuConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testMenuConnectionEdgeImplementsConnection() { + + $results = $this->interfaceQuery( 'MenuConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + + public function testMenuItemConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'MenuItemConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testMenuItemConnectionEdgeImplementsConnection() { + + $results = $this->interfaceQuery( 'MenuItemConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + + public function testMenuItemLinkableConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'MenuItemLinkableConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testMenuItemLinkableConnectionEdgeImplementsConnection() { + + $results = $this->interfaceQuery( 'MenuItemLinkableConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + + public function testTaxonomyConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'TaxonomyConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testTaxonomyEdgeConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'TaxonomyConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + + public function testTermNodeConnectionEdgeImplementsConnection() { + + $results = $this->interfaceQuery( 'TermNodeConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + + public function testTermNodeConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'TermNodeConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testUserConnectionImplementsConnection() { + + $results = $this->interfaceQuery( 'UserConnection' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Connection' + ] + ] ), + ] ); + + } + + public function testUserConnectionEdgeImplementsEdge() { + + $results = $this->interfaceQuery( 'UserConnectionEdge' ); + + $this->assertArrayNotHasKey( 'errors', $results ); + $this->assertQuerySuccessful( $results, [ + $this->expectedObject( '__type.interfaces', [ + [ + 'name' => 'Edge' + ] + ] ), + ] ); + + } + +} diff --git a/tests/wpunit/EnqueuedScriptsTest.php b/tests/wpunit/EnqueuedScriptsTest.php index 3e8bee474..4da089d06 100644 --- a/tests/wpunit/EnqueuedScriptsTest.php +++ b/tests/wpunit/EnqueuedScriptsTest.php @@ -482,8 +482,9 @@ public function testEnqueuedScriptIs_Front_Page() { // Make sure the script is NOT enqueued on Tags $actual = $this->get_tag_query( $this->tag_id ); + codecept_debug( $actual ); $this->assertArrayNotHasKey( 'errors', $actual ); - // codecept_debug( $actual ); + $scripts = $actual['data']['tag']['enqueuedScripts']['nodes']; $handles = wp_list_pluck( $scripts, 'handle' ); $sources = wp_list_pluck( $scripts, 'src' ); diff --git a/tests/wpunit/MediaItemQueriesTest.php b/tests/wpunit/MediaItemQueriesTest.php index 7415d99c1..3c6bcd12f 100644 --- a/tests/wpunit/MediaItemQueriesTest.php +++ b/tests/wpunit/MediaItemQueriesTest.php @@ -513,8 +513,8 @@ public function testMediaItemNotExistingSizeQuery() { $query = " query { mediaItem(id: \"{$attachment_global_id}\") { - srcSet(size: LARGE) - sizes(size: LARGE) + srcSet(size: MEDIUM) + sizes(size: MEDIUM) } }"; diff --git a/tests/wpunit/MenuItemConnectionQueriesTest.php b/tests/wpunit/MenuItemConnectionQueriesTest.php index 0cff37f3c..70037a4db 100644 --- a/tests/wpunit/MenuItemConnectionQueriesTest.php +++ b/tests/wpunit/MenuItemConnectionQueriesTest.php @@ -2,106 +2,208 @@ use GraphQLRelay\Relay; -class MenuItemConnectionQueriesTest extends \Codeception\TestCase\WPTestCase { - - public $admin; - - public static function setUpBeforeClass(): void { - parent::setUpBeforeClass(); - - add_theme_support( 'nav_menu_locations' ); - register_nav_menu( 'my-menu-location', 'My Menu' ); - set_theme_mod( 'nav_menu_locations', [ 'my-menu-location' => 0 ] ); - } - - public function setUp(): void { - parent::setUp(); - - $this->admin = $this->factory()->user->create( [ - 'role' => 'administrator', - 'user_email' => 'test@test.com' - ] ); - - } - - private function createMenuItem( $menu_id, $options ) { - return wp_update_nav_menu_item( $menu_id, 0, $options ); - } - - private function createMenuItems( $slug, $count ) { - $menu_id = wp_create_nav_menu( $slug ); - $menu_item_ids = []; - $post_ids = []; - - // Create some Post menu items. - for ( $x = 1; $x <= $count; $x ++ ) { - $post_id = $this->factory()->post->create( [ - 'post_status' => 'publish' - ] ); - $post_ids[] = $post_id; - - $menu_item_ids[] = $this->createMenuItem( - $menu_id, - [ - 'menu-item-title' => "Menu item {$x}", - 'menu-item-object' => 'post', - 'menu-item-object-id' => $post_id, - 'menu-item-status' => 'publish', - 'menu-item-type' => 'post_type', - ] - ); - } - - // Assign menu to location. - set_theme_mod( 'nav_menu_locations', [ 'my-menu-location' => $menu_id ] ); - - // Make sure menu items were created. - $this->assertEquals( $count, count( $menu_item_ids ) ); - $this->assertEquals( $count, count( $post_ids ) ); - - return [ - 'menu_id' => $menu_id, - 'menu_item_ids' => $menu_item_ids, - 'post_ids' => $post_ids, - ]; - } - - /** - * Some common assertions repeated for each test. - * - * @param array $created_menu_ids Created menu items. - * @param array $created_post_ids Created connected posts. - * @param array $query_results Query results. - * - * @return void - */ - private function compareResults( $created_menu_ids, $created_post_ids, $query_result ) { - $edges = $query_result['data']['menuItems']['edges']; - - // The returned menu items have the expected IDs in the expected order. - $this->assertEquals( - $created_menu_ids, - array_map( function( $menu_item ) { - return $menu_item['node']['databaseId']; - }, $edges ) - ); - - // The connected posts have the expected IDs in the expected order. - $this->assertEquals( - $created_post_ids, - array_map( function( $menu_item ) { - return $menu_item['node']['connectedObject']['postId']; - }, $edges ) - ); - } - - public function testMenuItemsQueryWithNoArgs() { - $count = 10; - $created = $this->createMenuItems( 'my-test-menu-id', $count ); - - codecept_debug( $created ); - - $query = ' +class MenuItemConnectionQueriesTest extends \Codeception\TestCase\WPTestCase +{ + + public $admin; + + public static function setUpBeforeClass(): void + { + parent::setUpBeforeClass(); + + add_theme_support('nav_menu_locations'); + register_nav_menu('my-menu-location', 'My Menu'); + set_theme_mod('nav_menu_locations', [ 'my-menu-location' => 0 ]); + } + + public function setUp(): void + { + parent::setUp(); + + $this->admin = $this->factory()->user->create([ + 'role' => 'administrator', + 'user_email' => 'test@test.com' + ]); + WPGraphQL::clear_schema(); + $this->register_types(); + + } + + public function register_types() + { + + add_action('graphql_register_types', function ($type_registry) { + + $args = [ + 'show_in_nav_menus' => true, + 'show_in_graphql' => true, + ]; + + $possible_types = []; + + // Add post types that are allowed in WPGraphQL. + foreach (get_post_types( $args ) as $type) { + $post_type_object = get_post_type_object($type); + if (isset($post_type_object->graphql_single_name)) { + $possible_types[] = $post_type_object->graphql_single_name; + } + } + + // Add taxonomies that are allowed in WPGraphQL. + foreach (get_taxonomies($args) as $type) { + $tax_object = get_taxonomy($type); + if (isset($tax_object->graphql_single_name)) { + $possible_types[] = $tax_object->graphql_single_name; + } + } + + register_graphql_union_type( + 'MenuItemObjectUnion', + [ + 'typeNames' => $possible_types, + 'description' => __('Deprecated in favor of MenuItemLinkeable Interface', 'wp-graphql'), + 'resolveType' => function ($object) use ($type_registry) { + // Post object + if ($object instanceof \WPGraphQL\Model\Post && isset($object->post_type) && ! empty($object->post_type)) { + $post_type_object = get_post_type_object($object->post_type); + + return $type_registry->get_type($post_type_object->graphql_single_name); + } + + // Taxonomy term + if ($object instanceof \WPGraphQL\Model\Term && ! empty($object->taxonomyName)) { + $tax_object = get_taxonomy($object->taxonomyName); + + return $type_registry->get_type($tax_object->graphql_single_name); + } + + return $object; + }, + ] + ); + + register_graphql_field('MenuItem', 'connectedObject', [ + 'type' => 'MenuItemObjectUnion', + 'deprecationReason' => __('Deprecated in favor of the connectedNode field', 'wp-graphql'), + 'description' => __('The object connected to this menu item.', 'wp-graphql'), + 'resolve' => function ($menu_item, array $args, $context, $info) { + + $object_id = intval(get_post_meta($menu_item->menuItemId, '_menu_item_object_id', true)); + $object_type = get_post_meta($menu_item->menuItemId, '_menu_item_type', true); + + switch ($object_type) { + // Post object + case 'post_type': + $resolved_object = $context->get_loader('post')->load_deferred($object_id); + break; + + // Taxonomy term + case 'taxonomy': + $resolved_object = $context->get_loader('term')->load_deferred($object_id); + break; + default: + $resolved_object = null; + break; + } + + + return apply_filters( + 'graphql_resolve_menu_item', + $resolved_object, + $args, + $context, + $info, + $object_id, + $object_type + ); + }, + + ]); + }); + } + + private function createMenuItem($menu_id, $options) + { + return wp_update_nav_menu_item($menu_id, 0, $options); + } + + private function createMenuItems($slug, $count) + { + $menu_id = wp_create_nav_menu($slug); + $menu_item_ids = []; + $post_ids = []; + + // Create some Post menu items. + for ($x = 1; $x <= $count; $x ++) { + $post_id = $this->factory()->post->create([ + 'post_status' => 'publish' + ]); + $post_ids[] = $post_id; + + $menu_item_ids[] = $this->createMenuItem( + $menu_id, + [ + 'menu-item-title' => "Menu item {$x}", + 'menu-item-object' => 'post', + 'menu-item-object-id' => $post_id, + 'menu-item-status' => 'publish', + 'menu-item-type' => 'post_type', + ] + ); + } + + // Assign menu to location. + set_theme_mod('nav_menu_locations', [ 'my-menu-location' => $menu_id ]); + + // Make sure menu items were created. + $this->assertEquals($count, count($menu_item_ids)); + $this->assertEquals($count, count($post_ids)); + + return [ + 'menu_id' => $menu_id, + 'menu_item_ids' => $menu_item_ids, + 'post_ids' => $post_ids, + ]; + } + + /** + * Some common assertions repeated for each test. + * + * @param array $created_menu_ids Created menu items. + * @param array $created_post_ids Created connected posts. + * @param array $query_results Query results. + * + * @return void + */ + private function compareResults($created_menu_ids, $created_post_ids, $query_result) + { + $edges = $query_result['data']['menuItems']['edges']; + + // The returned menu items have the expected IDs in the expected order. + $this->assertEquals( + $created_menu_ids, + array_map(function ($menu_item) { + return $menu_item['node']['databaseId']; + }, $edges) + ); + + // The connected posts have the expected IDs in the expected order. + $this->assertEquals( + $created_post_ids, + array_map(function ($menu_item) { + return $menu_item['node']['connectedObject']['postId']; + }, $edges) + ); + } + + public function testMenuItemsQueryWithNoArgs() + { + $count = 10; + $created = $this->createMenuItems('my-test-menu-id', $count); + + codecept_debug($created); + + $query = ' { menuItems { edges { @@ -128,28 +230,28 @@ public function testMenuItemsQueryWithNoArgs() { } '; - $actual = do_graphql_request( $query ); - - codecept_debug( $actual ); + $actual = do_graphql_request($query); - // The query should return the 10 menu items - $this->assertEquals( 10, count( $actual['data']['menuItems']['edges'] ) ); - $this->assertEquals( 10, count( $actual['data']['menuItems']['nodes'] ) ); + codecept_debug($actual); - $node_ids = wp_list_pluck( $actual['data']['menuItems']['nodes'], 'databaseId' ); - $this->assertSame( $created['menu_item_ids'], $node_ids ); + // The query should return the 10 menu items + $this->assertEquals(10, count($actual['data']['menuItems']['edges'])); + $this->assertEquals(10, count($actual['data']['menuItems']['nodes'])); - } + $node_ids = wp_list_pluck($actual['data']['menuItems']['nodes'], 'databaseId'); + $this->assertSame($created['menu_item_ids'], $node_ids); + } - public function testMenuItemsQueryNodes() { + public function testMenuItemsQueryNodes() + { - $count = 10; - $created = $this->createMenuItems( 'my-test-menu-id', $count ); + $count = 10; + $created = $this->createMenuItems('my-test-menu-id', $count); - $menu_item_id = intval( $created['menu_item_ids'][2] ); - $post_id = intval( $created['post_ids'][2] ); + $menu_item_id = intval($created['menu_item_ids'][2]); + $post_id = intval($created['post_ids'][2]); - $query = ' + $query = ' { menuItems( where: { id: ' . $menu_item_id . ' } ) { nodes { @@ -159,24 +261,24 @@ public function testMenuItemsQueryNodes() { } '; - $actual = do_graphql_request( $query ); - - foreach ( $actual['data']['menuItems']['nodes'] as $node ) { - $this->assertTrue( in_array( $node['databaseId'], [ $menu_item_id ], true ) ); - } + $actual = do_graphql_request($query); - $this->assertEquals( 1, count( $actual['data']['menuItems']['nodes'] ) ); + foreach ($actual['data']['menuItems']['nodes'] as $node) { + $this->assertTrue(in_array($node['databaseId'], [ $menu_item_id ], true)); + } - } + $this->assertEquals(1, count($actual['data']['menuItems']['nodes'])); + } - public function testMenuItemsQueryById() { - $count = 10; - $created = $this->createMenuItems( 'my-test-menu-id', $count ); + public function testMenuItemsQueryById() + { + $count = 10; + $created = $this->createMenuItems('my-test-menu-id', $count); - $menu_item_id = intval( $created['menu_item_ids'][2] ); - $post_id = intval( $created['post_ids'][2] ); + $menu_item_id = intval($created['menu_item_ids'][2]); + $post_id = intval($created['post_ids'][2]); - $query = ' + $query = ' { menuItems( where: { id: ' . $menu_item_id . ' } ) { edges { @@ -200,24 +302,25 @@ public function testMenuItemsQueryById() { } '; - $actual = do_graphql_request( $query ); + $actual = do_graphql_request($query); - // Perform some common assertions. - $this->compareResults( [ $menu_item_id ], [ $post_id ], $actual ); - } + // Perform some common assertions. + $this->compareResults([ $menu_item_id ], [ $post_id ], $actual); + } - public function testMenuItemsQueryByLocation() { - $count = 10; + public function testMenuItemsQueryByLocation() + { + $count = 10; - /** - * Create menu items that should NOT be returned because they're not assigned this location - */ - $this->createMenuItems( 'excluded-items', 10 ); - $created = $this->createMenuItems( 'my-test-menu-location', $count ); + /** + * Create menu items that should NOT be returned because they're not assigned this location + */ + $this->createMenuItems('excluded-items', 10); + $created = $this->createMenuItems('my-test-menu-location', $count); - wp_set_current_user( $this->admin ); + wp_set_current_user($this->admin); - $query = ' + $query = ' { menuItems( first: 100 where: { location: MY_MENU_LOCATION } ) { edges { @@ -242,45 +345,46 @@ public function testMenuItemsQueryByLocation() { } '; - $actual = do_graphql_request( $query ); - codecept_debug( $actual ); + $actual = do_graphql_request($query); + codecept_debug($actual); - // The returned menu items have the expected number. - $this->assertEquals( $count, count( $actual['data']['menuItems']['edges'] ) ); + // The returned menu items have the expected number. + $this->assertEquals($count, count($actual['data']['menuItems']['edges'])); - // Perform some common assertions. - $this->compareResults( $created['menu_item_ids'], $created['post_ids'], $actual ); - } + // Perform some common assertions. + $this->compareResults($created['menu_item_ids'], $created['post_ids'], $actual); + } - public function createNestedMenu( $child_count ) { - $count = 10; - $created = $this->createMenuItems( 'my-test-menu-with-child-items', $count ); + public function createNestedMenu($child_count) + { + $count = 10; + $created = $this->createMenuItems('my-test-menu-with-child-items', $count); - // Add some child items to the fourth menu item. - for ( $x = 1; $x <= $child_count; $x ++ ) { - $options = [ - 'menu-item-title' => "Child menu item {$x}", - 'menu-item-object' => 'post', - 'menu-item-object-id' => $this->factory()->post->create(), - 'menu-item-parent-id' => $created['menu_item_ids'][3], - 'menu-item-status' => 'publish', - 'menu-item-type' => 'post_type', - ]; + // Add some child items to the fourth menu item. + for ($x = 1; $x <= $child_count; $x ++) { + $options = [ + 'menu-item-title' => "Child menu item {$x}", + 'menu-item-object' => 'post', + 'menu-item-object-id' => $this->factory()->post->create(), + 'menu-item-parent-id' => $created['menu_item_ids'][3], + 'menu-item-status' => 'publish', + 'menu-item-type' => 'post_type', + ]; - $this->createMenuItem( $created['menu_id'], $options ); - } - - return $created; + $this->createMenuItem($created['menu_id'], $options); + } - } + return $created; + } - public function testMenuItemsQueryWithChildItemsAsFlat() { - $created = $this->createNestedMenu( 3 ); + public function testMenuItemsQueryWithChildItemsAsFlat() + { + $created = $this->createNestedMenu(3); - // The nesting is added to the fourth item - $parent_database_id = $created['menu_item_ids'][3]; + // The nesting is added to the fourth item + $parent_database_id = $created['menu_item_ids'][3]; - $query = ' + $query = ' { menuItems(first: 99, where: { location: MY_MENU_LOCATION } ) { edges { @@ -307,45 +411,45 @@ public function testMenuItemsQueryWithChildItemsAsFlat() { } '; - $actual = do_graphql_request( $query ); + $actual = do_graphql_request($query); - $this->assertEquals( 13, count( $actual['data']['menuItems']['edges'] ) ); + $this->assertEquals(13, count($actual['data']['menuItems']['edges'])); - $child_items_via_database_id = []; - $parent_id = null; - - foreach ( $actual['data']['menuItems']['edges'] as $edge ) { - if ( $edge['node']['databaseId'] === $parent_database_id ) { - $parent_id = $edge['node']['id']; - } + $child_items_via_database_id = []; + $parent_id = null; - // Child items have the correct parenDatabaseId - if ( $edge['node']['parentDatabaseId'] === $parent_database_id ) { - $child_items_via_database_id[] = $edge['node']; - } - } + foreach ($actual['data']['menuItems']['edges'] as $edge) { + if ($edge['node']['databaseId'] === $parent_database_id) { + $parent_id = $edge['node']['id']; + } - $this->assertNotNull( $parent_id ); - $this->assertEquals( 3, count( $child_items_via_database_id ) ); + // Child items have the correct parenDatabaseId + if ($edge['node']['parentDatabaseId'] === $parent_database_id) { + $child_items_via_database_id[] = $edge['node']; + } + } - $child_items_via_relay_id = []; + $this->assertNotNull($parent_id); + $this->assertEquals(3, count($child_items_via_database_id)); - foreach ( $actual['data']['menuItems']['edges'] as $edge ) { - // Child items have the correct relay parentId - if ( $edge['node']['parentId'] === $parent_id ) { - $child_items_via_relay_id[] = $edge['node']; - } - } + $child_items_via_relay_id = []; - $this->assertEquals( 3, count( $child_items_via_relay_id ) ); + foreach ($actual['data']['menuItems']['edges'] as $edge) { + // Child items have the correct relay parentId + if ($edge['node']['parentId'] === $parent_id) { + $child_items_via_relay_id[] = $edge['node']; + } + } - } + $this->assertEquals(3, count($child_items_via_relay_id)); + } - public function testMenuItemsQueryWithChildItemsUsingRelayParentId() { - $created = $this->createNestedMenu( 3 ); + public function testMenuItemsQueryWithChildItemsUsingRelayParentId() + { + $created = $this->createNestedMenu(3); - $query = ' + $query = ' { menuItems( where: { parentId: "0", location: MY_MENU_LOCATION } ) { edges { @@ -381,19 +485,20 @@ public function testMenuItemsQueryWithChildItemsUsingRelayParentId() { } '; - $actual = do_graphql_request( $query ); + $actual = do_graphql_request($query); - // Perform some common assertions. - $this->compareResults( $created['menu_item_ids'], $created['post_ids'], $actual ); + // Perform some common assertions. + $this->compareResults($created['menu_item_ids'], $created['post_ids'], $actual); - // The fourth menu item has the expected number of child items. - $this->assertEquals( 3, count( $actual['data']['menuItems']['edges'][3]['node']['childItems']['edges'] ) ); - } + // The fourth menu item has the expected number of child items. + $this->assertEquals(3, count($actual['data']['menuItems']['edges'][3]['node']['childItems']['edges'])); + } - public function testMenuItemsQueryWithChildItemsUsingDatabaseParentId() { - $created = $this->createNestedMenu( 3 ); + public function testMenuItemsQueryWithChildItemsUsingDatabaseParentId() + { + $created = $this->createNestedMenu(3); - $query = ' + $query = ' { menuItems( where: { parentDatabaseId: 0, location: MY_MENU_LOCATION } ) { edges { @@ -436,22 +541,23 @@ public function testMenuItemsQueryWithChildItemsUsingDatabaseParentId() { } '; - $actual = do_graphql_request( $query ); + $actual = do_graphql_request($query); - // Perform some common assertions. - $this->compareResults( $created['menu_item_ids'], $created['post_ids'], $actual ); + // Perform some common assertions. + $this->compareResults($created['menu_item_ids'], $created['post_ids'], $actual); - // The fourth menu item has the expected number of child items. - $this->assertEquals( 3, count( $actual['data']['menuItems']['edges'][3]['node']['childItems']['edges'] ) ); - } + // The fourth menu item has the expected number of child items. + $this->assertEquals(3, count($actual['data']['menuItems']['edges'][3]['node']['childItems']['edges'])); + } - public function testMenuItemsQueryWithExplicitParentDatabaseId() { - $created = $this->createNestedMenu( 3 ); + public function testMenuItemsQueryWithExplicitParentDatabaseId() + { + $created = $this->createNestedMenu(3); - // The nesting is added to the fourth item - $parent_database_id = $created['menu_item_ids'][3]; + // The nesting is added to the fourth item + $parent_database_id = $created['menu_item_ids'][3]; - $query = " + $query = " { menuItems( where: { parentDatabaseId: $parent_database_id, location: MY_MENU_LOCATION } ) { edges { @@ -476,24 +582,24 @@ public function testMenuItemsQueryWithExplicitParentDatabaseId() { } "; - $actual = do_graphql_request( $query ); - + $actual = do_graphql_request($query); - $this->assertEquals( 3, count( $actual['data']['menuItems']['edges'] ) ); - // The parentDatabaseId matches with the requested parent database id - $this->assertEquals( $parent_database_id, $actual['data']['menuItems']['edges'][0]['node']['parentDatabaseId'] ); + $this->assertEquals(3, count($actual['data']['menuItems']['edges'])); - } + // The parentDatabaseId matches with the requested parent database id + $this->assertEquals($parent_database_id, $actual['data']['menuItems']['edges'][0]['node']['parentDatabaseId']); + } - public function testMenuItemsQueryWithExplicitParentId() { - $created = $this->createNestedMenu( 3 ); + public function testMenuItemsQueryWithExplicitParentId() + { + $created = $this->createNestedMenu(3); - // The nesting is added to the fourth item. - // Convernt it to the global relay id from the database id. - $parent_id = Relay::toGlobalId( 'post', $created['menu_item_ids'][3] ); + // The nesting is added to the fourth item. + // Convernt it to the global relay id from the database id. + $parent_id = Relay::toGlobalId('post', $created['menu_item_ids'][3]); - $query = " + $query = " { menuItems( where: { parentId: \"${parent_id}\", location: MY_MENU_LOCATION } ) { edges { @@ -517,18 +623,19 @@ public function testMenuItemsQueryWithExplicitParentId() { } "; - $actual = do_graphql_request( $query ); + $actual = do_graphql_request($query); - // Perform some common assertions. - $this->assertEquals( 3, count( $actual['data']['menuItems']['edges'] ) ); - } + // Perform some common assertions. + $this->assertEquals(3, count($actual['data']['menuItems']['edges'])); + } - public function testMenuItemsQueryWithLimit() { - $count = 10; - $created = $this->createMenuItems( 'my-test-menu-location', $count ); - $limit = 5; + public function testMenuItemsQueryWithLimit() + { + $count = 10; + $created = $this->createMenuItems('my-test-menu-location', $count); + $limit = 5; - $query = ' + $query = ' { menuItems( first: ' . $limit . ' @@ -555,28 +662,29 @@ public function testMenuItemsQueryWithLimit() { } '; - $actual = do_graphql_request( $query ); + $actual = do_graphql_request($query); - // Perform some common assertions. Slice the created IDs to the limit. - $menu_item_ids = array_slice( $created['menu_item_ids'], 0, $limit ); - $post_ids = array_slice( $created['post_ids'], 0, $limit ); - $this->compareResults( $menu_item_ids, $post_ids, $actual ); - } + // Perform some common assertions. Slice the created IDs to the limit. + $menu_item_ids = array_slice($created['menu_item_ids'], 0, $limit); + $post_ids = array_slice($created['post_ids'], 0, $limit); + $this->compareResults($menu_item_ids, $post_ids, $actual); + } - public function testDraftPostsAreNotVisibleForAnonymous() { - $count = 2; - $created = $this->createMenuItems( 'my-test-menu-location', $count ); + public function testDraftPostsAreNotVisibleForAnonymous() + { + $count = 2; + $created = $this->createMenuItems('my-test-menu-location', $count); - wp_update_post( - [ - 'ID' => $created['post_ids'][0], - 'post_status' => 'draft' - ] - ); + wp_update_post( + [ + 'ID' => $created['post_ids'][0], + 'post_status' => 'draft' + ] + ); - $query = ' + $query = ' { menuItems( where: { location: MY_MENU_LOCATION } ) { nodes { @@ -597,51 +705,51 @@ public function testDraftPostsAreNotVisibleForAnonymous() { } '; - // Ensure unauthenticated request - wp_set_current_user( 0 ); - - $actual = do_graphql_request( $query ); - - $this->assertArrayNotHasKey( 'errors', $actual, print_r( $actual, true ) ); - - // Unauthenticated request still returns two _menu_ items - $this->assertEquals( $count, count( $actual['data']['menuItems']['nodes'] ) ); - - $expected = [ - 0 => [ - // But actual connected data is not available because there's no permission to do so - 'connectedObject' => null, - 'connectedNode' => null, - ], - 1 => [ - 'connectedObject' => [ - 'status' => 'publish', - ], - 'connectedNode' => [ - 'node' => [ - 'status' => 'publish', - ], - ], - ], - ]; - - $this->assertEquals( $expected, $actual['data']['menuItems']['nodes'] ); - - } - - public function testDraftPostsAreVisibleForAdmin() { - $count = 2; - $created = $this->createMenuItems( 'my-test-menu-location', $count ); - - wp_update_post( - [ - 'ID' => $created['post_ids'][0], - 'post_status' => 'draft' - ] - ); - - - $query = ' + // Ensure unauthenticated request + wp_set_current_user(0); + + $actual = do_graphql_request($query); + + $this->assertArrayNotHasKey('errors', $actual, print_r($actual, true)); + + // Unauthenticated request still returns two _menu_ items + $this->assertEquals($count, count($actual['data']['menuItems']['nodes'])); + + $expected = [ + 0 => [ + // But actual connected data is not available because there's no permission to do so + 'connectedObject' => null, + 'connectedNode' => null, + ], + 1 => [ + 'connectedObject' => [ + 'status' => 'publish', + ], + 'connectedNode' => [ + 'node' => [ + 'status' => 'publish', + ], + ], + ], + ]; + + $this->assertEquals($expected, $actual['data']['menuItems']['nodes']); + } + + public function testDraftPostsAreVisibleForAdmin() + { + $count = 2; + $created = $this->createMenuItems('my-test-menu-location', $count); + + wp_update_post( + [ + 'ID' => $created['post_ids'][0], + 'post_status' => 'draft' + ] + ); + + + $query = ' { menuItems( where: { location: MY_MENU_LOCATION } ) { nodes { @@ -662,45 +770,45 @@ public function testDraftPostsAreVisibleForAdmin() { } '; - // Authenticate as admin - wp_set_current_user( $this->admin ); - - $actual = do_graphql_request( $query ); - - $this->assertArrayNotHasKey( 'errors', $actual, print_r( $actual, true ) ); - - $this->assertEquals( $count, count( $actual['data']['menuItems']['nodes'] ) ); - - $expected = [ - 0 => [ - 'connectedObject' => [ - 'status' => 'draft', - ], - 'connectedNode' => null, - ], - 1 => [ - 'connectedObject' => [ - 'status' => 'publish', - ], - 'connectedNode' => [ - 'node' => [ - 'status' => 'publish', - ], - ], - ], - ]; - - $this->assertEquals( $expected, $actual['data']['menuItems']['nodes'] ); - - } - - public function testMenuItemsOrder() { - $created = $this->createNestedMenu( 3 ); - - // The nesting is added to the fourth item - $parent_database_id = $created['menu_item_ids'][3]; - - $query = " + // Authenticate as admin + wp_set_current_user($this->admin); + + $actual = do_graphql_request($query); + + $this->assertArrayNotHasKey('errors', $actual, print_r($actual, true)); + + $this->assertEquals($count, count($actual['data']['menuItems']['nodes'])); + + $expected = [ + 0 => [ + 'connectedObject' => [ + 'status' => 'draft', + ], + 'connectedNode' => null, + ], + 1 => [ + 'connectedObject' => [ + 'status' => 'publish', + ], + 'connectedNode' => [ + 'node' => [ + 'status' => 'publish', + ], + ], + ], + ]; + + $this->assertEquals($expected, $actual['data']['menuItems']['nodes']); + } + + public function testMenuItemsOrder() + { + $created = $this->createNestedMenu(3); + + // The nesting is added to the fourth item + $parent_database_id = $created['menu_item_ids'][3]; + + $query = " { menuItems( where: { parentDatabaseId: $parent_database_id, location: MY_MENU_LOCATION } ) { nodes { @@ -711,24 +819,21 @@ public function testMenuItemsOrder() { } "; - $actual = do_graphql_request( $query ); - - - // Assert that the `order` field actually exists and is an int - $this->assertIsInt( 3, $actual['data']['menuItems']['nodes'][0]['order'] ); - - $orders = array_map( function( $node ) { - return $node['order']; - }, $actual['data']['menuItems']['nodes'] ); + $actual = do_graphql_request($query); - // Make copy of the results that are sorted by the order - $sorted_orders = $orders; - asort( $sorted_orders ); - // Assert that the returned list was in the sorted order - $this->assertEquals( $orders, $sorted_orders ); + // Assert that the `order` field actually exists and is an int + $this->assertIsInt(3, $actual['data']['menuItems']['nodes'][0]['order']); + $orders = array_map(function ($node) { + return $node['order']; + }, $actual['data']['menuItems']['nodes']); - } + // Make copy of the results that are sorted by the order + $sorted_orders = $orders; + asort($sorted_orders); + // Assert that the returned list was in the sorted order + $this->assertEquals($orders, $sorted_orders); + } } diff --git a/tests/wpunit/MenuItemQueriesTest.php b/tests/wpunit/MenuItemQueriesTest.php index a6937d455..537484b5d 100644 --- a/tests/wpunit/MenuItemQueriesTest.php +++ b/tests/wpunit/MenuItemQueriesTest.php @@ -2,51 +2,150 @@ use GraphQLRelay\Relay; -class MenuItemQueriesTest extends \Codeception\TestCase\WPTestCase { - - public $admin; - - public function setUp(): void { - parent::setUp(); - $this->admin = $this->factory()->user->create([ - 'role' => 'administrator' - ]); - WPGraphQL::clear_schema();} - - public function tearDown(): void { - WPGraphQL::clear_schema(); - parent::tearDown(); - } - - public function testMenuItemQuery() { - - add_theme_support( 'nav_menus' ); - $location_name = 'test-location'; - register_nav_menu( $location_name, 'test menu...' ); - - $menu_slug = 'my-test-menu'; - $menu_id = wp_create_nav_menu( $menu_slug ); - $post_id = $this->factory()->post->create(); - - $menu_item_id = wp_update_nav_menu_item( - $menu_id, - 0, - [ - 'menu-item-title' => 'Menu item', - 'menu-item-object' => 'post', - 'menu-item-object-id' => $post_id, - 'menu-item-status' => 'publish', - 'menu-item-type' => 'post_type', - ] - ); - - set_theme_mod( 'nav_menu_locations', [ $location_name => $menu_id ] ); - - codecept_debug( get_theme_mod( 'nav_menu_locations' ) ); - - $menu_item_relay_id = Relay::toGlobalId( 'post', $menu_item_id ); - - $query = ' +class MenuItemQueriesTest extends \Codeception\TestCase\WPTestCase +{ + + public $admin; + + public function setUp(): void + { + parent::setUp(); + $this->admin = $this->factory()->user->create([ + 'role' => 'administrator' + ]); + WPGraphQL::clear_schema(); + $this->register_types(); + + } + + public function tearDown(): void + { + WPGraphQL::clear_schema(); + parent::tearDown(); + } + + public function register_types() + { + + add_action('graphql_register_types', function ($type_registry) { + + $args = [ + 'show_in_nav_menus' => true, + 'show_in_graphql' => true, + ]; + + $possible_types = []; + + // Add post types that are allowed in WPGraphQL. + foreach (get_post_types($args) as $type) { + $post_type_object = get_post_type_object($type); + if (isset($post_type_object->graphql_single_name)) { + $possible_types[] = $post_type_object->graphql_single_name; + } + } + + // Add taxonomies that are allowed in WPGraphQL. + foreach (get_taxonomies($args) as $type) { + $tax_object = get_taxonomy($type); + if (isset($tax_object->graphql_single_name)) { + $possible_types[] = $tax_object->graphql_single_name; + } + } + + register_graphql_union_type( + 'MenuItemObjectUnion', + [ + 'typeNames' => $possible_types, + 'description' => __('Deprecated in favor of MenuItemLinkeable Interface', 'wp-graphql'), + 'resolveType' => function ($object) use ($type_registry) { + // Post object + if ($object instanceof \WPGraphQL\Model\Post && isset($object->post_type) && ! empty($object->post_type)) { + $post_type_object = get_post_type_object($object->post_type); + + return $type_registry->get_type($post_type_object->graphql_single_name); + } + + // Taxonomy term + if ($object instanceof \WPGraphQL\Model\Term && ! empty($object->taxonomyName)) { + $tax_object = get_taxonomy($object->taxonomyName); + + return $type_registry->get_type($tax_object->graphql_single_name); + } + + return $object; + }, + ] + ); + + register_graphql_field('MenuItem', 'connectedObject', [ + 'type' => 'MenuItemObjectUnion', + 'deprecationReason' => __('Deprecated in favor of the connectedNode field', 'wp-graphql'), + 'description' => __('The object connected to this menu item.', 'wp-graphql'), + 'resolve' => function ($menu_item, array $args, $context, $info) { + + $object_id = intval(get_post_meta($menu_item->menuItemId, '_menu_item_object_id', true)); + $object_type = get_post_meta($menu_item->menuItemId, '_menu_item_type', true); + + switch ($object_type) { + // Post object + case 'post_type': + $resolved_object = $context->get_loader('post')->load_deferred($object_id); + break; + + // Taxonomy term + case 'taxonomy': + $resolved_object = $context->get_loader('term')->load_deferred($object_id); + break; + default: + $resolved_object = null; + break; + } + + return apply_filters( + 'graphql_resolve_menu_item', + $resolved_object, + $args, + $context, + $info, + $object_id, + $object_type + ); + }, + + ]); + }); + } + + public function testMenuItemQuery() + { + + add_theme_support('nav_menus'); + $location_name = 'test-location'; + register_nav_menu($location_name, 'test menu...'); + + $menu_slug = 'my-test-menu'; + $menu_id = wp_create_nav_menu($menu_slug); + $post_id = $this->factory()->post->create(); + + $menu_item_id = wp_update_nav_menu_item( + $menu_id, + 0, + [ + 'menu-item-title' => 'Menu item', + 'menu-item-object' => 'post', + 'menu-item-object-id' => $post_id, + 'menu-item-status' => 'publish', + 'menu-item-type' => 'post_type', + ] + ); + + set_theme_mod('nav_menu_locations', [ $location_name => $menu_id ]); + + codecept_debug(get_theme_mod('nav_menu_locations')); + + $menu_item_relay_id = Relay::toGlobalId('post', $menu_item_id); + + $query = ' { menuItem( id: "' . $menu_item_relay_id . '" ) { id @@ -68,21 +167,21 @@ public function testMenuItemQuery() { } '; - $actual = do_graphql_request( $query ); + $actual = do_graphql_request($query); - codecept_debug( $actual ); + codecept_debug($actual); - $this->assertEquals( $menu_item_id, $actual['data']['menuItem']['databaseId'] ); - $this->assertEquals( $menu_item_relay_id, $actual['data']['menuItem']['id'] ); - $this->assertEquals( $post_id, $actual['data']['menuItem']['connectedObject']['postId'] ); - $this->assertEquals( $menu_slug, $actual['data']['menuItem']['menu']['node']['slug'] ); - $this->assertEquals( [ \WPGraphQL\Type\WPEnumType::get_safe_name( $location_name ) ], $actual['data']['menuItem']['locations'] ); - $this->assertEquals( [ \WPGraphQL\Type\WPEnumType::get_safe_name( $location_name ) ], $actual['data']['menuItem']['menu']['node']['locations'] ); + $this->assertEquals($menu_item_id, $actual['data']['menuItem']['databaseId']); + $this->assertEquals($menu_item_relay_id, $actual['data']['menuItem']['id']); + $this->assertEquals($post_id, $actual['data']['menuItem']['connectedObject']['postId']); + $this->assertEquals($menu_slug, $actual['data']['menuItem']['menu']['node']['slug']); + $this->assertEquals([ \WPGraphQL\Type\WPEnumType::get_safe_name($location_name) ], $actual['data']['menuItem']['locations']); + $this->assertEquals([ \WPGraphQL\Type\WPEnumType::get_safe_name($location_name) ], $actual['data']['menuItem']['menu']['node']['locations']); - $old_id = Relay::toGlobalId( 'nav_menu_itemci', $menu_item_id ); + $old_id = Relay::toGlobalId('nav_menu_itemci', $menu_item_id); - $query = ' + $query = ' { menuItem( id: "' . $old_id . '" ) { id @@ -104,19 +203,16 @@ public function testMenuItemQuery() { } '; - $actual = do_graphql_request( $query ); - - - codecept_debug( $actual ); - - $this->assertEquals( $menu_item_id, $actual['data']['menuItem']['databaseId'] ); - $this->assertEquals( $menu_item_relay_id, $actual['data']['menuItem']['id'] ); - $this->assertEquals( $post_id, $actual['data']['menuItem']['connectedObject']['postId'] ); - $this->assertEquals( $menu_slug, $actual['data']['menuItem']['menu']['node']['slug'] ); - $this->assertEquals( [ \WPGraphQL\Type\WPEnumType::get_safe_name( $location_name ) ], $actual['data']['menuItem']['locations'] ); - $this->assertEquals( [ \WPGraphQL\Type\WPEnumType::get_safe_name( $location_name ) ], $actual['data']['menuItem']['menu']['node']['locations'] ); + $actual = do_graphql_request($query); - } + codecept_debug($actual); + $this->assertEquals($menu_item_id, $actual['data']['menuItem']['databaseId']); + $this->assertEquals($menu_item_relay_id, $actual['data']['menuItem']['id']); + $this->assertEquals($post_id, $actual['data']['menuItem']['connectedObject']['postId']); + $this->assertEquals($menu_slug, $actual['data']['menuItem']['menu']['node']['slug']); + $this->assertEquals([ \WPGraphQL\Type\WPEnumType::get_safe_name($location_name) ], $actual['data']['menuItem']['locations']); + $this->assertEquals([ \WPGraphQL\Type\WPEnumType::get_safe_name($location_name) ], $actual['data']['menuItem']['menu']['node']['locations']); + } } diff --git a/tests/wpunit/TypesTest.php b/tests/wpunit/TypesTest.php index 8e0bf489d..e3b0cb23b 100644 --- a/tests/wpunit/TypesTest.php +++ b/tests/wpunit/TypesTest.php @@ -351,7 +351,7 @@ public function testRegisterCustomConnectionWithAuth() { ], 'fromFieldName' => 'failingAuthConnection', 'resolve' => function() { - return [ 'nodes' => [ null, false, 0 ] ]; + return [ 'nodes' => [ [ 'test' => 'blocked' ], false, 0 ] ]; } ]); @@ -391,7 +391,7 @@ public function testRegisterCustomConnectionWithAuth() { ]; $this->assertQuerySuccessful( $response, $expected ); - + /** * Expect query to fail on both type/field-level. */ @@ -417,7 +417,7 @@ public function testRegisterCustomConnectionWithAuth() { \wp_set_current_user( 1 ); $response = $this->graphql( compact( 'query' ) ); $expected = [ - $this->expectedNode( 'failingAuthConnection.nodes', 'NULL', 0 ), + $this->expectedNode( 'failingAuthConnection.nodes', [ 'test' => null ], 0 ), $this->expectedErrorPath( 'failingAuthConnection.nodes.1.test' ), $this->expectedErrorMessage( 'Blocked on the field-level!!!', self::MESSAGE_EQUALS ), $this->expectedObject( 'failingAuthConnection.nodes.1.test', 'NULL' ),