From fc71c301dbcf4f6f49e0c2e929e128679316721b Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Mon, 1 Mar 2021 14:08:22 -0700 Subject: [PATCH 01/17] - ignore nested wp-graphql directory used for CI --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 71060d584..12f78e30c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ plugin-build !src/Admin/GraphiQL/app/build coverage/* schema.graphql +wp-graphql/ phpunit.xml docker-output node_modules From aefe23f6ca848998e57804e74027de094e7d4bd3 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Mon, 1 Mar 2021 15:54:30 -0700 Subject: [PATCH 02/17] - Update graphql-php to v14.5.1 --- composer.json | 2 +- composer.lock | 75 +++++++++++++++++++++++++-------------------------- 2 files changed, 37 insertions(+), 40 deletions(-) diff --git a/composer.json b/composer.json index 30486f84e..9cbb86d85 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 06a57bfed..04f513d19 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": "b38e53437b5b232a0ac8b07cc5f8f054", + "content-hash": "4a36d4d8570ac15157a4b23005f5bae8", "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": [ @@ -3144,16 +3144,16 @@ }, { "name": "php-webdriver/webdriver", - "version": "1.9.0", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/php-webdriver/php-webdriver.git", - "reference": "e3633154554605274cc9d59837f55a7427d72003" + "reference": "cd9290b95b7651d495bd69253d6e3ef469a7f211" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/e3633154554605274cc9d59837f55a7427d72003", - "reference": "e3633154554605274cc9d59837f55a7427d72003", + "url": "https://api.github.com/repos/php-webdriver/php-webdriver/zipball/cd9290b95b7651d495bd69253d6e3ef469a7f211", + "reference": "cd9290b95b7651d495bd69253d6e3ef469a7f211", "shasum": "" }, "require": { @@ -3169,7 +3169,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "^2.0", - "ondram/ci-detector": "^2.1 || ^3.5", + "ondram/ci-detector": "^2.1 || ^3.5 || ^4.0", "php-coveralls/php-coveralls": "^2.4", "php-mock/php-mock-phpunit": "^1.1 || ^2.0", "php-parallel-lint/php-parallel-lint": "^1.2", @@ -3209,9 +3209,9 @@ ], "support": { "issues": "https://github.com/php-webdriver/php-webdriver/issues", - "source": "https://github.com/php-webdriver/php-webdriver/tree/1.9.0" + "source": "https://github.com/php-webdriver/php-webdriver/tree/1.10.0" }, - "time": "2020-11-19T15:21:05+00:00" + "time": "2021-02-25T13:38:09+00:00" }, { "name": "phpcompatibility/php-compatibility", @@ -3657,16 +3657,16 @@ }, { "name": "phpstan/phpstan", - "version": "0.12.78", + "version": "0.12.80", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "eecce8d2ee3cac6769f37b4cb1998b2715f82984" + "reference": "c6a1b17f22ecf708d434d6bee05092647ec7e686" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/eecce8d2ee3cac6769f37b4cb1998b2715f82984", - "reference": "eecce8d2ee3cac6769f37b4cb1998b2715f82984", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6a1b17f22ecf708d434d6bee05092647ec7e686", + "reference": "c6a1b17f22ecf708d434d6bee05092647ec7e686", "shasum": "" }, "require": { @@ -3697,7 +3697,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.78" + "source": "https://github.com/phpstan/phpstan/tree/0.12.80" }, "funding": [ { @@ -3713,7 +3713,7 @@ "type": "tidelift" } ], - "time": "2021-02-20T13:24:36+00:00" + "time": "2021-02-28T20:22:43+00:00" }, { "name": "phpunit/php-code-coverage", @@ -7695,16 +7695,16 @@ }, { "name": "szepeviktor/phpstan-wordpress", - "version": "v0.7.3", + "version": "v0.7.4", "source": { "type": "git", "url": "https://github.com/szepeviktor/phpstan-wordpress.git", - "reference": "07349418f182cbe72a92b2e34ca98f0c0aace566" + "reference": "c816f18af8644382a3aea7e3b653be5e90d824a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/07349418f182cbe72a92b2e34ca98f0c0aace566", - "reference": "07349418f182cbe72a92b2e34ca98f0c0aace566", + "url": "https://api.github.com/repos/szepeviktor/phpstan-wordpress/zipball/c816f18af8644382a3aea7e3b653be5e90d824a7", + "reference": "c816f18af8644382a3aea7e3b653be5e90d824a7", "shasum": "" }, "require": { @@ -7747,7 +7747,7 @@ ], "support": { "issues": "https://github.com/szepeviktor/phpstan-wordpress/issues", - "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v0.7.3" + "source": "https://github.com/szepeviktor/phpstan-wordpress/tree/v0.7.4" }, "funding": [ { @@ -7755,7 +7755,7 @@ "type": "custom" } ], - "time": "2021-02-17T21:38:11+00:00" + "time": "2021-03-01T18:31:08+00:00" }, { "name": "theseer/tokenizer", @@ -10126,33 +10126,30 @@ }, { "name": "wp-graphql/wp-graphql-testcase", - "version": "v1.0.0", + "version": "v1.0.1", "source": { "type": "git", "url": "https://github.com/wp-graphql/wp-graphql-testcase.git", - "reference": "85d7fcfd93263e41c719272be965d235857dcc7e" + "reference": "15b3b4d2cf865c4b3e1aeae781306ed1e48a9f99" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-graphql/wp-graphql-testcase/zipball/85d7fcfd93263e41c719272be965d235857dcc7e", - "reference": "85d7fcfd93263e41c719272be965d235857dcc7e", + "url": "https://api.github.com/repos/wp-graphql/wp-graphql-testcase/zipball/15b3b4d2cf865c4b3e1aeae781306ed1e48a9f99", + "reference": "15b3b4d2cf865c4b3e1aeae781306ed1e48a9f99", "shasum": "" }, "require": { "codeception/module-asserts": "^1.0", - "codeception/module-cli": "^1.0", - "codeception/module-db": "^1.0", - "codeception/module-filesystem": "^1.0", - "codeception/module-phpbrowser": "^1.0", - "codeception/module-webdriver": "^1.0", + "codeception/module-rest": "^1.2", "codeception/util-universalframework": "^1.0", "composer/installers": "~1.0", - "lucatume/wp-browser": "^2.6" + "lucatume/wp-browser": "^2.6", + "phpunit/phpunit": "<=9.4.4" }, "require-dev": { "johnpbloch/wordpress": "^5.4", "simpod/php-coveralls-mirror": "^3.0", - "wp-graphql/wp-graphql": "^0.12.1" + "wp-graphql/wp-graphql": "^1.1.8" }, "type": "library", "extra": { @@ -10184,9 +10181,9 @@ "description": "Codeception module for WPGraphQL API testing", "support": { "issues": "https://github.com/wp-graphql/wp-graphql-testcase/issues", - "source": "https://github.com/wp-graphql/wp-graphql-testcase/tree/develop" + "source": "https://github.com/wp-graphql/wp-graphql-testcase/tree/v1.0.1" }, - "time": "2020-08-26T17:14:57+00:00" + "time": "2021-02-26T23:52:26+00:00" }, { "name": "zordius/lightncandy", From 424edb90e72998c766957c2c5141702e7126322c Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 11 Mar 2021 10:10:35 -0700 Subject: [PATCH 03/17] - update priorities of access function hooks so that Types are available before connections are registered --- access-functions.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/access-functions.php b/access-functions.php index 0404905ca..b55beac5d 100644 --- a/access-functions.php +++ b/access-functions.php @@ -310,7 +310,7 @@ function register_graphql_connection( array $config ) { function( \WPGraphQL\Registry\TypeRegistry $type_registry ) use ( $config ) { $type_registry->register_connection( $config ); }, - 10 + 50 ); } @@ -347,7 +347,7 @@ function deregister_graphql_field( string $type_name, string $field_name ) { function( \WPGraphQL\Registry\TypeRegistry $type_registry ) use ( $type_name, $field_name ) { $type_registry->deregister_field( $type_name, $field_name ); }, - 10 + 5 ); } From 3f0527dc3f8df854e5a1aef220c3327a701f5fa8 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 11 Mar 2021 10:12:22 -0700 Subject: [PATCH 04/17] - Update composer.lock to reflect updates to dependencies --- composer.lock | 313 +++++++++++++++++++++++++------------------------- 1 file changed, 157 insertions(+), 156 deletions(-) diff --git a/composer.lock b/composer.lock index 04f513d19..f9321722c 100644 --- a/composer.lock +++ b/composer.lock @@ -382,16 +382,16 @@ }, { "name": "codeception/lib-innerbrowser", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/Codeception/lib-innerbrowser.git", - "reference": "b7406c710684c255d9b067d7795269a5585a0406" + "reference": "693e116f81ef98eae98c43ef785a726faf87394e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/b7406c710684c255d9b067d7795269a5585a0406", - "reference": "b7406c710684c255d9b067d7795269a5585a0406", + "url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/693e116f81ef98eae98c43ef785a726faf87394e", + "reference": "693e116f81ef98eae98c43ef785a726faf87394e", "shasum": "" }, "require": { @@ -436,9 +436,9 @@ ], "support": { "issues": "https://github.com/Codeception/lib-innerbrowser/issues", - "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.4.0" + "source": "https://github.com/Codeception/lib-innerbrowser/tree/1.4.1" }, - "time": "2021-01-29T18:17:25+00:00" + "time": "2021-03-02T08:01:54+00:00" }, { "name": "codeception/module-asserts", @@ -710,16 +710,16 @@ }, { "name": "codeception/module-rest", - "version": "1.2.7", + "version": "1.2.8", "source": { "type": "git", "url": "https://github.com/Codeception/module-rest.git", - "reference": "beeb5a91a97d042273bf10f00063e9b8f541879a" + "reference": "4c8c6bd75f67a25ff91bdce0d6c135c5e7aa5e10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/module-rest/zipball/beeb5a91a97d042273bf10f00063e9b8f541879a", - "reference": "beeb5a91a97d042273bf10f00063e9b8f541879a", + "url": "https://api.github.com/repos/Codeception/module-rest/zipball/4c8c6bd75f67a25ff91bdce0d6c135c5e7aa5e10", + "reference": "4c8c6bd75f67a25ff91bdce0d6c135c5e7aa5e10", "shasum": "" }, "require": { @@ -758,9 +758,9 @@ ], "support": { "issues": "https://github.com/Codeception/module-rest/issues", - "source": "https://github.com/Codeception/module-rest/tree/1.2.7" + "source": "https://github.com/Codeception/module-rest/tree/1.2.8" }, - "time": "2020-11-04T16:58:11+00:00" + "time": "2021-03-02T06:47:59+00:00" }, { "name": "codeception/module-webdriver", @@ -2013,16 +2013,16 @@ }, { "name": "guzzlehttp/promises", - "version": "1.4.0", + "version": "1.4.1", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "60d379c243457e073cff02bc323a2a86cb355631" + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631", - "reference": "60d379c243457e073cff02bc323a2a86cb355631", + "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", + "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", "shasum": "" }, "require": { @@ -2062,9 +2062,9 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/1.4.0" + "source": "https://github.com/guzzle/promises/tree/1.4.1" }, - "time": "2020-09-30T07:37:28+00:00" + "time": "2021-03-07T09:25:29+00:00" }, { "name": "guzzlehttp/psr7", @@ -2191,16 +2191,16 @@ }, { "name": "illuminate/collections", - "version": "v8.29.0", + "version": "v8.32.1", "source": { "type": "git", "url": "https://github.com/illuminate/collections.git", - "reference": "3313bb9066ad60d91a3407b3ef02c114e2ecf8eb" + "reference": "d7cc717a00064b40fa63a8ad522042005e1de1ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/collections/zipball/3313bb9066ad60d91a3407b3ef02c114e2ecf8eb", - "reference": "3313bb9066ad60d91a3407b3ef02c114e2ecf8eb", + "url": "https://api.github.com/repos/illuminate/collections/zipball/d7cc717a00064b40fa63a8ad522042005e1de1ed", + "reference": "d7cc717a00064b40fa63a8ad522042005e1de1ed", "shasum": "" }, "require": { @@ -2241,20 +2241,20 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2021-02-21T16:09:39+00:00" + "time": "2021-03-08T17:22:22+00:00" }, { "name": "illuminate/contracts", - "version": "v8.29.0", + "version": "v8.32.1", "source": { "type": "git", "url": "https://github.com/illuminate/contracts.git", - "reference": "b91459a9a0bd0de204c3cae6859ebd02dbcee6c6" + "reference": "9c7a9868d7485a82663d67109429094c8e4ed56d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/contracts/zipball/b91459a9a0bd0de204c3cae6859ebd02dbcee6c6", - "reference": "b91459a9a0bd0de204c3cae6859ebd02dbcee6c6", + "url": "https://api.github.com/repos/illuminate/contracts/zipball/9c7a9868d7485a82663d67109429094c8e4ed56d", + "reference": "9c7a9868d7485a82663d67109429094c8e4ed56d", "shasum": "" }, "require": { @@ -2289,11 +2289,11 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2021-01-20T14:18:13+00:00" + "time": "2021-02-26T13:17:03+00:00" }, { "name": "illuminate/macroable", - "version": "v8.29.0", + "version": "v8.32.1", "source": { "type": "git", "url": "https://github.com/illuminate/macroable.git", @@ -2339,16 +2339,16 @@ }, { "name": "illuminate/support", - "version": "v8.29.0", + "version": "v8.32.1", "source": { "type": "git", "url": "https://github.com/illuminate/support.git", - "reference": "b745df9ef3120d173368fdf56eecc1fc40a9f90c" + "reference": "2ef7ff288366a1ebe32f633196a1b90bd443acc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/b745df9ef3120d173368fdf56eecc1fc40a9f90c", - "reference": "b745df9ef3120d173368fdf56eecc1fc40a9f90c", + "url": "https://api.github.com/repos/illuminate/support/zipball/2ef7ff288366a1ebe32f633196a1b90bd443acc3", + "reference": "2ef7ff288366a1ebe32f633196a1b90bd443acc3", "shasum": "" }, "require": { @@ -2403,7 +2403,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2021-02-21T15:57:40+00:00" + "time": "2021-03-05T15:22:14+00:00" }, { "name": "justinrainbow/json-schema", @@ -2840,16 +2840,16 @@ }, { "name": "nesbot/carbon", - "version": "2.45.1", + "version": "2.46.0", "source": { "type": "git", "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "528783b188bdb853eb21239b1722831e0f000a8d" + "reference": "2fd2c4a77d58a4e95234c8a61c5df1f157a91bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/528783b188bdb853eb21239b1722831e0f000a8d", - "reference": "528783b188bdb853eb21239b1722831e0f000a8d", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/2fd2c4a77d58a4e95234c8a61c5df1f157a91bf4", + "reference": "2fd2c4a77d58a4e95234c8a61c5df1f157a91bf4", "shasum": "" }, "require": { @@ -2929,7 +2929,7 @@ "type": "tidelift" } ], - "time": "2021-02-11T18:30:17+00:00" + "time": "2021-02-24T17:30:44+00:00" }, { "name": "nikic/php-parser", @@ -3657,16 +3657,16 @@ }, { "name": "phpstan/phpstan", - "version": "0.12.80", + "version": "0.12.81", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "c6a1b17f22ecf708d434d6bee05092647ec7e686" + "reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/c6a1b17f22ecf708d434d6bee05092647ec7e686", - "reference": "c6a1b17f22ecf708d434d6bee05092647ec7e686", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/0dd5b0ebeff568f7000022ea5f04aa86ad3124b8", + "reference": "0dd5b0ebeff568f7000022ea5f04aa86ad3124b8", "shasum": "" }, "require": { @@ -3697,7 +3697,7 @@ "description": "PHPStan - PHP Static Analysis Tool", "support": { "issues": "https://github.com/phpstan/phpstan/issues", - "source": "https://github.com/phpstan/phpstan/tree/0.12.80" + "source": "https://github.com/phpstan/phpstan/tree/0.12.81" }, "funding": [ { @@ -3713,7 +3713,7 @@ "type": "tidelift" } ], - "time": "2021-02-28T20:22:43+00:00" + "time": "2021-03-08T22:03:02+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4138,27 +4138,22 @@ }, { "name": "psr/container", - "version": "1.0.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.2.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -4171,7 +4166,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common Container Interface (PHP FIG PSR-11)", @@ -4185,9 +4180,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" + "source": "https://github.com/php-fig/container/tree/1.1.1" }, - "time": "2017-02-14T16:28:37+00:00" + "time": "2021-03-05T17:36:06+00:00" }, { "name": "psr/event-dispatcher", @@ -5653,16 +5648,16 @@ }, { "name": "softcreatr/jsonpath", - "version": "0.7.2", + "version": "0.7.3", "source": { "type": "git", "url": "https://github.com/SoftCreatR/JSONPath.git", - "reference": "46689608586a8081be399342755c36e179f3b5fc" + "reference": "e3ae75112a5247e3b27a7e0e72155eae261d8c72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/46689608586a8081be399342755c36e179f3b5fc", - "reference": "46689608586a8081be399342755c36e179f3b5fc", + "url": "https://api.github.com/repos/SoftCreatR/JSONPath/zipball/e3ae75112a5247e3b27a7e0e72155eae261d8c72", + "reference": "e3ae75112a5247e3b27a7e0e72155eae261d8c72", "shasum": "" }, "require": { @@ -5707,6 +5702,7 @@ "description": "JSONPath implementation for parsing, searching and flattening arrays", "support": { "email": "hello@1-2.dev", + "forum": "https://github.com/SoftCreatR/JSONPath/discussions", "issues": "https://github.com/SoftCreatR/JSONPath/issues", "source": "https://github.com/SoftCreatR/JSONPath" }, @@ -5716,7 +5712,7 @@ "type": "github" } ], - "time": "2020-10-27T11:37:08+00:00" + "time": "2021-03-08T14:18:21+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -5776,16 +5772,16 @@ }, { "name": "symfony/browser-kit", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "b03b2057ed53ee4eab2e8f372084d7722b7b8ffd" + "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/b03b2057ed53ee4eab2e8f372084d7722b7b8ffd", - "reference": "b03b2057ed53ee4eab2e8f372084d7722b7b8ffd", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/3ca3a57ce9860318b20a924fec5daf5c6db44d93", + "reference": "3ca3a57ce9860318b20a924fec5daf5c6db44d93", "shasum": "" }, "require": { @@ -5827,7 +5823,7 @@ "description": "Simulates the behavior of a web browser, allowing you to make requests, click on links and submit forms programmatically", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/browser-kit/tree/v5.2.3" + "source": "https://github.com/symfony/browser-kit/tree/v5.2.4" }, "funding": [ { @@ -5843,20 +5839,20 @@ "type": "tidelift" } ], - "time": "2021-01-27T12:56:27+00:00" + "time": "2021-02-22T06:48:33+00:00" }, { "name": "symfony/config", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "50e0e1314a3b2609d32b6a5a0d0fb5342494c4ab" + "reference": "212d54675bf203ff8aef7d8cee8eecfb72f4a263" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/50e0e1314a3b2609d32b6a5a0d0fb5342494c4ab", - "reference": "50e0e1314a3b2609d32b6a5a0d0fb5342494c4ab", + "url": "https://api.github.com/repos/symfony/config/zipball/212d54675bf203ff8aef7d8cee8eecfb72f4a263", + "reference": "212d54675bf203ff8aef7d8cee8eecfb72f4a263", "shasum": "" }, "require": { @@ -5905,7 +5901,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v5.2.3" + "source": "https://github.com/symfony/config/tree/v5.2.4" }, "funding": [ { @@ -5921,20 +5917,20 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2021-02-23T23:58:19+00:00" }, { "name": "symfony/console", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a" + "reference": "d6d0cc30d8c0fda4e7b213c20509b0159a8f4556" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/89d4b176d12a2946a1ae4e34906a025b7b6b135a", - "reference": "89d4b176d12a2946a1ae4e34906a025b7b6b135a", + "url": "https://api.github.com/repos/symfony/console/zipball/d6d0cc30d8c0fda4e7b213c20509b0159a8f4556", + "reference": "d6d0cc30d8c0fda4e7b213c20509b0159a8f4556", "shasum": "" }, "require": { @@ -6002,7 +5998,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.2.3" + "source": "https://github.com/symfony/console/tree/v5.2.4" }, "funding": [ { @@ -6018,11 +6014,11 @@ "type": "tidelift" } ], - "time": "2021-01-28T22:06:19+00:00" + "time": "2021-02-23T10:08:49+00:00" }, { "name": "symfony/css-selector", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -6067,7 +6063,7 @@ "description": "Converts CSS selectors to XPath expressions", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/css-selector/tree/v5.2.3" + "source": "https://github.com/symfony/css-selector/tree/v5.2.4" }, "funding": [ { @@ -6154,16 +6150,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "5d89ceb53ec65e1973a555072fac8ed5ecad3384" + "reference": "400e265163f65aceee7e904ef532e15228de674b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/5d89ceb53ec65e1973a555072fac8ed5ecad3384", - "reference": "5d89ceb53ec65e1973a555072fac8ed5ecad3384", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/400e265163f65aceee7e904ef532e15228de674b", + "reference": "400e265163f65aceee7e904ef532e15228de674b", "shasum": "" }, "require": { @@ -6208,7 +6204,7 @@ "description": "Eases DOM navigation for HTML and XML documents", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dom-crawler/tree/v5.2.3" + "source": "https://github.com/symfony/dom-crawler/tree/v5.2.4" }, "funding": [ { @@ -6224,20 +6220,20 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:01:46+00:00" + "time": "2021-02-15T18:55:04+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367" + "reference": "d08d6ec121a425897951900ab692b612a61d6240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4f9760f8074978ad82e2ce854dff79a71fe45367", - "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d08d6ec121a425897951900ab692b612a61d6240", + "reference": "d08d6ec121a425897951900ab692b612a61d6240", "shasum": "" }, "require": { @@ -6293,7 +6289,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.3" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.4" }, "funding": [ { @@ -6309,7 +6305,7 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:36:42+00:00" + "time": "2021-02-18T17:12:37+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -6392,16 +6388,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "262d033b57c73e8b59cd6e68a45c528318b15038" + "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/262d033b57c73e8b59cd6e68a45c528318b15038", - "reference": "262d033b57c73e8b59cd6e68a45c528318b15038", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/710d364200997a5afde34d9fe57bd52f3cc1e108", + "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108", "shasum": "" }, "require": { @@ -6434,7 +6430,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.2.3" + "source": "https://github.com/symfony/filesystem/tree/v5.2.4" }, "funding": [ { @@ -6450,20 +6446,20 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:01:46+00:00" + "time": "2021-02-12T10:38:38+00:00" }, { "name": "symfony/finder", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "4adc8d172d602008c204c2e16956f99257248e03" + "reference": "0d639a0943822626290d169965804f79400e6a04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/4adc8d172d602008c204c2e16956f99257248e03", - "reference": "4adc8d172d602008c204c2e16956f99257248e03", + "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", + "reference": "0d639a0943822626290d169965804f79400e6a04", "shasum": "" }, "require": { @@ -6495,7 +6491,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.3" + "source": "https://github.com/symfony/finder/tree/v5.2.4" }, "funding": [ { @@ -6511,7 +6507,7 @@ "type": "tidelift" } ], - "time": "2021-01-28T22:06:19+00:00" + "time": "2021-02-15T18:55:04+00:00" }, { "name": "symfony/polyfill-ctype", @@ -7164,7 +7160,7 @@ }, { "name": "symfony/process", - "version": "v4.4.19", + "version": "v4.4.20", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -7205,7 +7201,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v4.4.19" + "source": "https://github.com/symfony/process/tree/v4.4.20" }, "funding": [ { @@ -7304,7 +7300,7 @@ }, { "name": "symfony/stopwatch", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -7346,7 +7342,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.2.3" + "source": "https://github.com/symfony/stopwatch/tree/v5.2.4" }, "funding": [ { @@ -7366,16 +7362,16 @@ }, { "name": "symfony/string", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "c95468897f408dd0aca2ff582074423dd0455122" + "reference": "4e78d7d47061fa183639927ec40d607973699609" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/c95468897f408dd0aca2ff582074423dd0455122", - "reference": "c95468897f408dd0aca2ff582074423dd0455122", + "url": "https://api.github.com/repos/symfony/string/zipball/4e78d7d47061fa183639927ec40d607973699609", + "reference": "4e78d7d47061fa183639927ec40d607973699609", "shasum": "" }, "require": { @@ -7429,7 +7425,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.3" + "source": "https://github.com/symfony/string/tree/v5.2.4" }, "funding": [ { @@ -7445,20 +7441,20 @@ "type": "tidelift" } ], - "time": "2021-01-25T15:14:59+00:00" + "time": "2021-02-16T10:20:28+00:00" }, { "name": "symfony/translation", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "c021864d4354ee55160ddcfd31dc477a1bc77949" + "reference": "74b0353ab34ff4cca827a2cf909e325d96815e60" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/c021864d4354ee55160ddcfd31dc477a1bc77949", - "reference": "c021864d4354ee55160ddcfd31dc477a1bc77949", + "url": "https://api.github.com/repos/symfony/translation/zipball/74b0353ab34ff4cca827a2cf909e325d96815e60", + "reference": "74b0353ab34ff4cca827a2cf909e325d96815e60", "shasum": "" }, "require": { @@ -7475,7 +7471,7 @@ "symfony/yaml": "<4.4" }, "provide": { - "symfony/translation-implementation": "2.0" + "symfony/translation-implementation": "2.3" }, "require-dev": { "psr/log": "~1.0", @@ -7522,7 +7518,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v5.2.3" + "source": "https://github.com/symfony/translation/tree/v5.2.4" }, "funding": [ { @@ -7538,7 +7534,7 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2021-03-04T15:41:09+00:00" }, { "name": "symfony/translation-contracts", @@ -7620,16 +7616,16 @@ }, { "name": "symfony/yaml", - "version": "v5.2.3", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0" + "reference": "7d6ae0cce3c33965af681a4355f1c4de326ed277" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/338cddc6d74929f6adf19ca5682ac4b8e109cdb0", - "reference": "338cddc6d74929f6adf19ca5682ac4b8e109cdb0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/7d6ae0cce3c33965af681a4355f1c4de326ed277", + "reference": "7d6ae0cce3c33965af681a4355f1c4de326ed277", "shasum": "" }, "require": { @@ -7675,7 +7671,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v5.2.3" + "source": "https://github.com/symfony/yaml/tree/v5.2.4" }, "funding": [ { @@ -7691,7 +7687,7 @@ "type": "tidelift" } ], - "time": "2021-02-03T04:42:09+00:00" + "time": "2021-02-22T15:48:39+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -7936,30 +7932,35 @@ }, { "name": "webmozart/assert", - "version": "1.9.1", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0 || ^8.0", + "php": "^7.2 || ^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<3.9.1" + "vimeo/psalm": "<4.6.1 || 4.6.2" }, "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" + "phpunit/phpunit": "^8.5.13" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -7983,9 +7984,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.9.1" + "source": "https://github.com/webmozarts/assert/tree/1.10.0" }, - "time": "2020-07-08T17:02:28+00:00" + "time": "2021-03-09T10:59:23+00:00" }, { "name": "wp-cli/cache-command", @@ -8741,16 +8742,16 @@ }, { "name": "wp-cli/export-command", - "version": "v2.0.5", + "version": "v2.0.6", "source": { "type": "git", "url": "https://github.com/wp-cli/export-command.git", - "reference": "015725833e7e0a89b188df4fb66b88415d4414ec" + "reference": "df2e1ff4fb7e969c54c57febccdc9d2de1e5f499" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/export-command/zipball/015725833e7e0a89b188df4fb66b88415d4414ec", - "reference": "015725833e7e0a89b188df4fb66b88415d4414ec", + "url": "https://api.github.com/repos/wp-cli/export-command/zipball/df2e1ff4fb7e969c54c57febccdc9d2de1e5f499", + "reference": "df2e1ff4fb7e969c54c57febccdc9d2de1e5f499", "shasum": "" }, "require": { @@ -8797,9 +8798,9 @@ "homepage": "https://github.com/wp-cli/export-command", "support": { "issues": "https://github.com/wp-cli/export-command/issues", - "source": "https://github.com/wp-cli/export-command/tree/v2.0.5" + "source": "https://github.com/wp-cli/export-command/tree/v2.0.6" }, - "time": "2020-12-07T19:30:08+00:00" + "time": "2021-01-14T12:16:33+00:00" }, { "name": "wp-cli/extension-command", @@ -9338,16 +9339,16 @@ }, { "name": "wp-cli/php-cli-tools", - "version": "v0.11.11", + "version": "v0.11.12", "source": { "type": "git", "url": "https://github.com/wp-cli/php-cli-tools.git", - "reference": "fe9c7c44a9e1bf2196ec51dc38da0593dbf2993f" + "reference": "e472e08489f7504d9e8c5c5a057e1419cd1b2b3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/fe9c7c44a9e1bf2196ec51dc38da0593dbf2993f", - "reference": "fe9c7c44a9e1bf2196ec51dc38da0593dbf2993f", + "url": "https://api.github.com/repos/wp-cli/php-cli-tools/zipball/e472e08489f7504d9e8c5c5a057e1419cd1b2b3e", + "reference": "e472e08489f7504d9e8c5c5a057e1419cd1b2b3e", "shasum": "" }, "require": { @@ -9367,15 +9368,15 @@ "MIT" ], "authors": [ - { - "name": "James Logsdon", - "email": "jlogsdon@php.net", - "role": "Developer" - }, { "name": "Daniel Bachhuber", "email": "daniel@handbuilt.co", "role": "Maintainer" + }, + { + "name": "James Logsdon", + "email": "jlogsdon@php.net", + "role": "Developer" } ], "description": "Console utilities for PHP", @@ -9386,9 +9387,9 @@ ], "support": { "issues": "https://github.com/wp-cli/php-cli-tools/issues", - "source": "https://github.com/wp-cli/php-cli-tools/tree/master" + "source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.12" }, - "time": "2018-09-04T13:28:00+00:00" + "time": "2021-03-03T12:43:49+00:00" }, { "name": "wp-cli/rewrite-command", From 47c216c08d21da8168f5d1a8b2d45fdb2339e4da Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 11 Mar 2021 10:12:59 -0700 Subject: [PATCH 05/17] - update CLI script to allow output dir to be defined for generating static schema --- cli/wp-cli.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cli/wp-cli.php b/cli/wp-cli.php index 9445063fa..6d262af0e 100644 --- a/cli/wp-cli.php +++ b/cli/wp-cli.php @@ -29,6 +29,10 @@ public function generate_static_schema( $args, $assoc_args ) { */ $file_path = get_temp_dir() . 'schema.graphql'; + if ( isset( $assoc_args['output_dir'] ) ) { + $file_path = trailingslashit( $assoc_args['output_dir'] ) . 'schema.graphql'; + } + if ( ! defined( 'GRAPHQL_REQUEST') ) { define( 'GRAPHQL_REQUEST', true ); } From 563661370ff1e9621cc9848448eea6418a534267 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 11 Mar 2021 10:13:49 -0700 Subject: [PATCH 06/17] - Update WPObjectType to support passing connections in the Type registry when registering a new Type --- src/Type/WPObjectType.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Type/WPObjectType.php b/src/Type/WPObjectType.php index 78c4fcc2e..db084d0af 100644 --- a/src/Type/WPObjectType.php +++ b/src/Type/WPObjectType.php @@ -42,6 +42,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 ) { @@ -99,6 +100,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 * From 46f621e4a6892b54df5be60664ac71a991a10580 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 11 Mar 2021 10:15:06 -0700 Subject: [PATCH 07/17] - Update WPInterfaceType to support passing interfaces that can be implemented by other interfaces - Update WPInterfaceType to support passing connections that the Interface can have when registering an InterfaceType --- src/Type/WPInterfaceType.php | 100 ++++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/src/Type/WPInterfaceType.php b/src/Type/WPInterfaceType.php index a435489a4..20f0f9dcf 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,13 +20,110 @@ 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 ) ) { + $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 ); + + } + } + $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; From 1782893ee2760548200ab1b073055ab91111d1ee Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 11 Mar 2021 10:18:11 -0700 Subject: [PATCH 08/17] - Update Term model to have a databaseId field so that it can properly implement the databaseIdentifier Interface --- src/Model/Term.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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; }, From 5caf2291e5da24f3738eb18bfc8c2fd1e7c8d045 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 11 Mar 2021 10:20:27 -0700 Subject: [PATCH 09/17] - Update the User Model to include a databaseId field so it can work with the User type that implements the databaseIdentifier interface --- src/Model/User.php | 5 +++++ 1 file changed, 5 insertions(+) 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 ) ) { From 66532cd09a6b6d035058d17d954a2c3298d3cc09 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Thu, 11 Mar 2021 10:26:45 -0700 Subject: [PATCH 10/17] - Deleted several Connection/$name.php files and moved the connection registrars to the Types being registered. So now, for many connections, instead of directly using `register_graphql_connection` the Type and/or Interface can define connections when being registered. - Updated several interfaces to declare other interfaces they implement - Introduced a new ConnectionInterface and Edge Interface - Introduced a new HierarchicalNode interface - Introduced a new Previewable Interface - Removed the ContentRevisionUnion and MenuItemObjectUnion Types - Updated tests to include back compat checks for users that need the MenuItemObjectUnion in their queries. They can drop in a snippet to add it back to their API if they need it. --- src/Connection/Commenter.php | 49 -- src/Connection/Comments.php | 2 +- src/Connection/ContentTypes.php | 76 -- src/Connection/EnqueuedScripts.php | 69 -- src/Connection/EnqueuedStylesheets.php | 69 -- src/Connection/MediaItems.php | 42 - src/Connection/MenuItemLinkableConnection.php | 55 -- src/Connection/Menus.php | 69 -- src/Connection/Plugins.php | 33 - src/Connection/PostObjects.php | 352 ++++---- src/Connection/Revisions.php | 49 -- src/Connection/TermObjects.php | 3 +- src/Connection/Themes.php | 41 - src/Connection/UserRoles.php | 83 -- src/Registry/TypeRegistry.php | 94 +- src/Type/InterfaceType/CommenterInterface.php | 72 +- .../InterfaceType/ConnectionInterface.php | 47 + src/Type/InterfaceType/ContentNode.php | 93 +- src/Type/InterfaceType/ContentTemplate.php | 2 + src/Type/InterfaceType/DatabaseIdentifier.php | 21 +- src/Type/InterfaceType/EnqueuedAsset.php | 104 +-- .../InterfaceType/HierarchicalContentNode.php | 9 + src/Type/InterfaceType/HierarchicalNode.php | 48 ++ .../InterfaceType/HierarchicalTermNode.php | 35 +- src/Type/InterfaceType/MenuItemLinkable.php | 20 +- src/Type/InterfaceType/NodeWithAuthor.php | 8 +- src/Type/InterfaceType/NodeWithComments.php | 4 + .../InterfaceType/NodeWithContentEditor.php | 3 + src/Type/InterfaceType/NodeWithExcerpt.php | 3 + .../InterfaceType/NodeWithFeaturedImage.php | 26 + .../InterfaceType/NodeWithPageAttributes.php | 3 + src/Type/InterfaceType/NodeWithRevisions.php | 3 + src/Type/InterfaceType/NodeWithTemplate.php | 3 + src/Type/InterfaceType/NodeWithTitle.php | 8 +- src/Type/InterfaceType/NodeWithTrackbacks.php | 4 + src/Type/InterfaceType/Previewable.php | 53 ++ src/Type/InterfaceType/TermNode.php | 36 +- .../UniformResourceIdentifiable.php | 6 +- src/Type/ObjectType/Comment.php | 28 + src/Type/ObjectType/CommentAuthor.php | 5 +- src/Type/ObjectType/MenuItem.php | 97 +-- src/Type/ObjectType/PostObject.php | 38 +- src/Type/ObjectType/RootQuery.php | 235 +++-- src/Type/ObjectType/Taxonomy.php | 20 +- src/Type/ObjectType/TermObject.php | 1 + src/Type/ObjectType/User.php | 59 +- src/Type/Union/ContentRevisionUnion.php | 57 -- src/Type/Union/MenuItemObjectUnion.php | 90 -- tests/wpunit/AccessFunctionsTest.php | 54 ++ tests/wpunit/EnqueuedScriptsTest.php | 3 +- tests/wpunit/MediaItemQueriesTest.php | 84 ++ .../wpunit/MenuItemConnectionQueriesTest.php | 815 ++++++++++-------- tests/wpunit/MenuItemQueriesTest.php | 230 +++-- 53 files changed, 1792 insertions(+), 1721 deletions(-) delete mode 100644 src/Connection/Commenter.php delete mode 100644 src/Connection/ContentTypes.php delete mode 100644 src/Connection/EnqueuedScripts.php delete mode 100644 src/Connection/EnqueuedStylesheets.php delete mode 100644 src/Connection/MediaItems.php delete mode 100644 src/Connection/MenuItemLinkableConnection.php delete mode 100644 src/Connection/Menus.php delete mode 100644 src/Connection/Plugins.php delete mode 100644 src/Connection/Revisions.php delete mode 100644 src/Connection/Themes.php delete mode 100644 src/Connection/UserRoles.php create mode 100644 src/Type/InterfaceType/ConnectionInterface.php create mode 100644 src/Type/InterfaceType/HierarchicalNode.php create mode 100644 src/Type/InterfaceType/Previewable.php delete mode 100644 src/Type/Union/ContentRevisionUnion.php delete mode 100644 src/Type/Union/MenuItemObjectUnion.php 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..c1ec2b25f 100644 --- a/src/Connection/Comments.php +++ b/src/Connection/Comments.php @@ -121,7 +121,7 @@ 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', 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/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 602c518da..6bdc19167 100644 --- a/src/Connection/PostObjects.php +++ b/src/Connection/PostObjects.php @@ -32,58 +32,64 @@ 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', + 'fromFieldName' => 'contentNodes', + 'connectionArgs' => self::get_connection_args(), + 'queryClass' => 'WP_Query', + 'resolve' => function( PostType $post_type, $args, AppContext $context, ResolveInfo $info ) { - $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); - $resolver->set_query_arg( 'p', $post->parentDatabaseId ); + $resolver = new PostObjectConnectionResolver( $post_type, $args, $context, $info ); + $resolver->set_query_arg( 'post_type', $post_type->name ); - return $resolver->one_to_one()->get_connection(); + 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; + } + + $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); + $resolver->set_query_arg( 'p', $post->parentDatabaseId ); + + return $resolver->one_to_one()->get_connection(); + + }, + ] + ); register_graphql_connection( [ @@ -108,69 +114,98 @@ 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', + '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' => 'MediaItem', + 'toType' => 'ContentNode', + '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; + } - 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 ) { + $resolver = new PostObjectConnectionResolver( $post, $args, $context, $info ); + $resolver->set_query_arg( 'p', $post->parentDatabaseId ); - if ( $post->isRevision ) { - $id = $post->parentDatabaseId; - } else { - $id = $post->ID; - } + return $resolver->one_to_one()->get_connection(); - $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 ); + }, + ] + ); + + 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 ) { + + if ( $post->isRevision ) { + $id = $post->parentDatabaseId; + } else { + $id = $post->ID; + } - return $resolver->get_connection(); - }, - ] ); + $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->get_connection(); + }, + ] + ); /** * Registers connections for each post_type that has a connection @@ -199,28 +234,30 @@ 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, + '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(); + }, + ] + ); } /** @@ -261,14 +298,17 @@ public static function register_connections() { 'fromType' => $tax_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(); }, @@ -316,26 +356,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(); - }, - ] ) ); + }, + ] + ) + ); } } 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/TermObjects.php b/src/Connection/TermObjects.php index b1ec79c4f..edc213295 100644 --- a/src/Connection/TermObjects.php +++ b/src/Connection/TermObjects.php @@ -230,7 +230,8 @@ public static function get_connection_config( $tax_object, $args = [] ) { '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 ); + $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/Registry/TypeRegistry.php b/src/Registry/TypeRegistry.php index dbb4ae20f..c9bc49974 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; @@ -55,11 +45,13 @@ use WPGraphQL\Type\Enum\UsersConnectionOrderbyEnum; use WPGraphQL\Type\Input\UsersConnectionOrderbyInput; use WPGraphQL\Type\InterfaceType\CommenterInterface; +use WPGraphQL\Type\InterfaceType\ConnectionInterface; use WPGraphQL\Type\InterfaceType\ContentNode; use WPGraphQL\Type\InterfaceType\ContentTemplate; 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\MenuItemLinkable; use WPGraphQL\Type\InterfaceType\NodeWithAuthor; @@ -73,13 +65,12 @@ use WPGraphQL\Type\InterfaceType\NodeWithTitle; use WPGraphQL\Type\InterfaceType\Node; use WPGraphQL\Type\InterfaceType\NodeWithTrackbacks; +use WPGraphQL\Type\InterfaceType\Previewable; use WPGraphQL\Type\InterfaceType\TermNode; use WPGraphQL\Type\InterfaceType\UniformResourceIdentifiable; 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; @@ -218,12 +209,14 @@ public function init_type_registry( TypeRegistry $type_registry ) { // Register Interfaces. Node::register_type(); CommenterInterface::register_type( $type_registry ); + ConnectionInterface::register_type( $type_registry ); ContentNode::register_type( $type_registry ); ContentTemplate::register_type( $type_registry ); DatabaseIdentifier::register_type( $type_registry ); 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 ); MenuItemLinkable::register_type( $type_registry ); NodeWithAuthor::register_type( $type_registry ); NodeWithComments::register_type( $type_registry ); @@ -235,6 +228,7 @@ 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 ); TermNode::register_type( $type_registry ); UniformResourceIdentifiable::register_type( $type_registry ); @@ -296,8 +290,6 @@ 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 ); @@ -305,22 +297,11 @@ public function init_type_registry( TypeRegistry $type_registry ) { * 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 @@ -542,13 +523,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 ) { @@ -854,11 +839,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; } @@ -1047,21 +1035,21 @@ 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 ) { + public function register_connection( array $config ) { if ( ! array_key_exists( 'fromType', $config ) ) { - throw new \InvalidArgumentException( __( 'Connection config needs to have at least a fromType defined', 'wp-graphql' ) ); + 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' ) ); + 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' ) ); + throw new InvalidArgumentException( __( 'Connection config needs to have at least a fromFieldName defined', 'wp-graphql' ) ); } $from_type = $config['fromType']; @@ -1077,7 +1065,7 @@ public function register_connection( $config ) { }; $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; + $one_to_one = isset( $config['oneToOne'] ) && true === $config['oneToOne']; /** * If there are any $connectionArgs, @@ -1110,11 +1098,12 @@ public function register_connection( $config ) { $this->register_object_type( $connection_name . 'Edge', [ + // 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' ), $from_type, $to_type ), 'fields' => array_merge( [ 'node' => [ - 'type' => $to_type, + 'type' => [ 'non_null' => $to_type ], 'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ), ], ], @@ -1129,6 +1118,7 @@ public function register_connection( $config ) { $connection_name . 'Edge', [ 'description' => __( 'An edge in a connection', 'wp-graphql' ), + 'interfaces' => [ 'Edge' ], 'fields' => array_merge( [ 'cursor' => [ @@ -1137,7 +1127,7 @@ public function register_connection( $config ) { 'resolve' => $resolve_cursor, ], 'node' => [ - 'type' => $to_type, + 'type' => [ 'non_null' => $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 ) ) { @@ -1158,22 +1148,24 @@ public function register_connection( $config ) { [ // 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 ), + 'interfaces' => [ 'Connection' ], 'fields' => array_merge( [ 'pageInfo' => [ // @todo: change to PageInfo when/if the Relay lib is deprecated - 'type' => 'WPPageInfo', + 'type' => [ 'non_null' => 'WPPageInfo' ], 'description' => __( 'Information about pagination in a connection.', 'wp-graphql' ), ], 'edges' => [ 'type' => [ - 'list_of' => $connection_name . 'Edge', + 'list_of' => [ 'non_null' => $connection_name . 'Edge' ], ], + // Translators: Placeholder is the name of the connection 'description' => sprintf( __( 'Edges for the %s connection', 'wp-graphql' ), $connection_name ), ], 'nodes' => [ 'type' => [ - 'list_of' => $to_type, + 'list_of' => [ 'non_null' => $to_type ], ], 'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ), 'resolve' => function( $source, $args, $context, $info ) use ( $resolve_node ) { @@ -1249,7 +1241,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/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..507f025bd --- /dev/null +++ b/src/Type/InterfaceType/ConnectionInterface.php @@ -0,0 +1,47 @@ + __( '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' => __( 'Connection' ), + 'fields' => [ + 'edges' => [ + 'type' => [ 'list_of' => [ 'non_null' => 'Edge' ] ], + 'description' => __( 'A list of edges between connected nodes', 'wp-graphql' ), + ], + 'nodes' => [ + 'type' => [ 'list_of' => [ 'non_null' => 'Node' ] ], + 'description' => __( 'A list of connected nodes', 'wp-graphql' ), + ], + ], + ] + ); + + } +} diff --git a/src/Type/InterfaceType/ContentNode.php b/src/Type/InterfaceType/ContentNode.php index b7c774c2c..0c85e2738 100644 --- a/src/Type/InterfaceType/ContentNode.php +++ b/src/Type/InterfaceType/ContentNode.php @@ -1,10 +1,14 @@ [ 'Node', 'DatabaseIdentifier' ], 'description' => __( 'Nodes used to manage content', 'wp-graphql' ), + 'connections' => [ + 'contentType' => [ + 'toType' => '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, + ], + '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 ) { /** @@ -60,82 +103,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/ContentTemplate.php b/src/Type/InterfaceType/ContentTemplate.php index 6b74f3706..27a491c5b 100644 --- a/src/Type/InterfaceType/ContentTemplate.php +++ b/src/Type/InterfaceType/ContentTemplate.php @@ -2,6 +2,7 @@ namespace WPGraphQL\Type\InterfaceType; +use Exception; use WPGraphQL\Registry\TypeRegistry; class ContentTemplate { @@ -12,6 +13,7 @@ class ContentTemplate { * @param TypeRegistry $type_registry * * @return void + * @throws Exception */ public static function register_type( TypeRegistry $type_registry ) { register_graphql_interface_type( diff --git a/src/Type/InterfaceType/DatabaseIdentifier.php b/src/Type/InterfaceType/DatabaseIdentifier.php index dd9e1b121..c268c71e9 100644 --- a/src/Type/InterfaceType/DatabaseIdentifier.php +++ b/src/Type/InterfaceType/DatabaseIdentifier.php @@ -1,6 +1,7 @@ __( '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/MenuItemLinkable.php b/src/Type/InterfaceType/MenuItemLinkable.php index 43da5f704..774099264 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/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/TermNode.php b/src/Type/InterfaceType/TermNode.php index 768907ac3..f6b787b04 100644 --- a/src/Type/InterfaceType/TermNode.php +++ b/src/Type/InterfaceType/TermNode.php @@ -2,6 +2,8 @@ namespace WPGraphQL\Type\InterfaceType; +use WPGraphQL\Data\Connection\EnqueuedScriptsConnectionResolver; +use WPGraphQL\Data\Connection\EnqueuedStylesheetConnectionResolver; use WPGraphQL\Data\DataSource; use WPGraphQL\Model\Term; use WPGraphQL\Registry\TypeRegistry; @@ -14,6 +16,7 @@ class TermNode { * @param TypeRegistry $type_registry * * @return void + * @throws \Exception */ public static function register_type( TypeRegistry $type_registry ) { @@ -21,6 +24,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 +65,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 +97,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/UniformResourceIdentifiable.php b/src/Type/InterfaceType/UniformResourceIdentifiable.php index b4e7dc685..b33e6548c 100644 --- a/src/Type/InterfaceType/UniformResourceIdentifiable.php +++ b/src/Type/InterfaceType/UniformResourceIdentifiable.php @@ -16,21 +16,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/ObjectType/Comment.php b/src/Type/ObjectType/Comment.php index 1ef87fe3f..3f3434ecb 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,31 @@ public static function register_type() { [ 'description' => __( 'A Comment object', 'wp-graphql' ), 'interfaces' => [ 'Node', 'DatabaseIdentifier' ], + 'connections' => [ + 'author' => [ + 'toType' => 'Commenter', + '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 a32782a89..538df78b4 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,50 @@ 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', + '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 ) { + + $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 +130,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/PostObject.php b/src/Type/ObjectType/PostObject.php index 87d9b07e6..c3eb03399 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 ), ] ); @@ -161,14 +171,24 @@ public static function register_post_object_types( WP_Post_Type $post_type_objec $size = $args['size']; } - $url = wp_get_attachment_image_src( $source->ID, $size ); - if ( ! is_array( $url ) || ! isset( $url[0] ) ) { - return null; + $image = wp_get_attachment_image_src( $source->ID, $size ); + if ( $image ) { + list( $src, $width, $height ) = $image; + $sizes = wp_calculate_image_sizes( + [ + absint( $width ), + absint( $height ), + ], + $src, + null, + $source->ID + ); + + return ! empty( $sizes ) ? $sizes : null; } - $sizes = wp_calculate_image_sizes( $size, $url[0], null, $source->ID ); + return null; - return ! empty( $sizes ) ? $sizes : null; }, ], 'description' => [ diff --git a/src/Type/ObjectType/RootQuery.php b/src/Type/ObjectType/RootQuery.php index 371786bc5..bb89e6d73 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,101 @@ public static function register_type() { 'RootQuery', [ 'description' => __( 'The root entry point into the Graph', 'wp-graphql' ), + 'connections' => [ + 'contentTypes' => [ + 'toType' => 'ContentType', + 'resolve' => function( $source, $args, $context, $info ) { + $resolver = new ContentTypeConnectionResolver( $source, $args, $context, $info ); + + return $resolver->get_connection(); + }, + ], + 'menus' => [ + 'toType' => 'Menu', + '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', @@ -93,11 +195,14 @@ public static function register_type() { } if ( isset( $args['asPreview'] ) && true === $args['asPreview'] ) { - $revisions = wp_get_post_revisions( $post_id, [ - 'posts_per_page' => 1, - 'fields' => 'ids', - 'check_enabled' => false, - ] ); + $revisions = wp_get_post_revisions( + $post_id, + [ + 'posts_per_page' => 1, + 'fields' => 'ids', + 'check_enabled' => false, + ] + ); $post_id = ! empty( $revisions ) ? array_values( $revisions )[0] : null; } @@ -220,13 +325,15 @@ public static function register_type() { $id = absint( $args['id'] ); break; case 'name': - $menu = new \WP_Term_Query([ - 'taxonomy' => 'nav_menu', - 'fields' => 'ids', - 'name' => $args['id'], - 'include_children' => false, - 'count' => false, - ]); + $menu = new \WP_Term_Query( + [ + 'taxonomy' => 'nav_menu', + 'fields' => 'ids', + 'name' => $args['id'], + 'include_children' => false, + 'count' => false, + ] + ); $id = ! empty( $menu->terms ) ? (int) $menu->terms[0] : null; break; default: @@ -288,6 +395,7 @@ public static function register_type() { ], 'resolve' => function( $source, array $args, AppContext $context, $info ) { $id_components = Relay::fromGlobalId( $args['id'] ); + return ! empty( $id_components['id'] ) ? $context->get_loader( 'plugin' )->load_deferred( $id_components['id'] ) : null; }, ], @@ -321,13 +429,13 @@ public static function register_type() { case 'database_id': $taxonomy = isset( $args['taxonomy'] ) ? $args['taxonomy'] : null; if ( empty( $taxonomy ) && in_array( - $idType, - [ - 'name', - 'slug', - ], - true - ) ) { + $idType, + [ + 'name', + 'slug', + ], + true + ) ) { throw new UserError( __( 'When fetching a Term Node by "slug" or "name", the "taxonomy" also needs to be set as an input.', 'wp-graphql' ) ); } if ( 'database_id' === $idType ) { @@ -502,16 +610,22 @@ public static function register_post_object_fields() { $post_id = null; switch ( $idType ) { case 'slug': - return $context->node_resolver->resolve_uri( $args['id'], [ - 'name' => $args['id'], - 'post_type' => $post_type_object->name, - ] ); + return $context->node_resolver->resolve_uri( + $args['id'], + [ + 'name' => $args['id'], + 'post_type' => $post_type_object->name, + ] + ); case 'uri': - return $context->node_resolver->resolve_uri( $args['id'], [ - 'post_type' => $post_type_object->name, - 'archive' => false, - 'nodeType' => 'Page', - ] ); + return $context->node_resolver->resolve_uri( + $args['id'], + [ + 'post_type' => $post_type_object->name, + 'archive' => false, + 'nodeType' => 'Page', + ] + ); case 'database_id': $post_id = absint( $args['id'] ); break; @@ -530,25 +644,38 @@ public static function register_post_object_fields() { } if ( isset( $args['asPreview'] ) && true === $args['asPreview'] ) { - $revisions = wp_get_post_revisions( $post_id, [ - 'posts_per_page' => 1, - 'fields' => 'ids', - 'check_enabled' => false, - ] ); + $revisions = wp_get_post_revisions( + $post_id, + [ + 'posts_per_page' => 1, + 'fields' => 'ids', + 'check_enabled' => false, + ] + ); $post_id = ! empty( $revisions ) ? array_values( $revisions )[0] : null; } - 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 ) ) { - return null; + 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 + ) ) { + return null; + } + + return $post; } - return $post; - }); + ); }, ] ); $post_by_args = [ - 'id' => [ + 'id' => [ 'type' => 'ID', 'description' => sprintf( __( 'Get the object by its global ID', 'wp-graphql' ), $post_type_object->graphql_single_name ), ], @@ -556,7 +683,7 @@ public static function register_post_object_fields() { 'type' => 'Int', 'description' => sprintf( __( 'Get the %s by its database ID', 'wp-graphql' ), $post_type_object->graphql_single_name ), ], - 'uri' => [ + 'uri' => [ 'type' => 'String', 'description' => sprintf( __( 'Get the %s by its uri', 'wp-graphql' ), $post_type_object->graphql_single_name ), ], @@ -593,23 +720,35 @@ public static function register_post_object_fields() { $post_id = absint( $id ); } elseif ( ! empty( $args['uri'] ) ) { $uri = esc_html( $args['uri'] ); + return $context->node_resolver->resolve_uri( $uri ); } elseif ( ! empty( $args['slug'] ) ) { $slug = esc_html( $args['slug'] ); + return $context->node_resolver->resolve_uri( $slug ); } - return $context->get_loader( 'post' )->load_deferred( $post_id )->then( function( $post ) use ( $post_type_object ) { + return $context->get_loader( 'post' )->load_deferred( $post_id )->then( + function( $post ) use ( $post_type_object ) { - if ( ! $post_type_object instanceof \WP_Post_Type ) { - return null; - } + if ( ! $post_type_object instanceof \WP_Post_Type ) { + return null; + } + + if ( ! isset( $post->post_type ) || ! in_array( + $post->post_type, + [ + 'revision', + $post_type_object->name, + ], + true + ) ) { + return null; + } - if ( ! isset( $post->post_type ) || ! in_array( $post->post_type, [ 'revision', $post_type_object->name ], true ) ) { - return null; + return $post; } - return $post; - }); + ); }, ] diff --git a/src/Type/ObjectType/Taxonomy.php b/src/Type/ObjectType/Taxonomy.php index a584bce0d..dcab014a9 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,25 @@ 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', + '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 7601ffd2b..06e3042b3 100644 --- a/src/Type/ObjectType/User.php +++ b/src/Type/ObjectType/User.php @@ -3,6 +3,10 @@ namespace WPGraphQL\Type\ObjectType; +use WPGraphQL\Connection\PostObjects; +use WPGraphQL\Data\Connection\EnqueuedScriptsConnectionResolver; +use WPGraphQL\Data\Connection\EnqueuedStylesheetConnectionResolver; +use WPGraphQL\Data\Connection\UserRoleConnectionResolver; use WPGraphQL\Data\DataSource; /** @@ -22,18 +26,57 @@ public static function register_type() { 'User', [ 'description' => __( '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', + '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 236f20766..000000000 --- a/src/Type/Union/ContentRevisionUnion.php +++ /dev/null @@ -1,57 +0,0 @@ -graphql_single_name; - }, - $post_types_with_revision_support - ); - - register_graphql_union_type( - 'ContentRevisionUnion', - [ - 'typeNames' => $type_names, - '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 3ab7df01c..000000000 --- a/src/Type/Union/MenuItemObjectUnion.php +++ /dev/null @@ -1,90 +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 ) ) { - $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 ) ) { - $tax_object = get_taxonomy( $object->taxonomyName ); - - return $type_registry->get_type( $tax_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/tests/wpunit/AccessFunctionsTest.php b/tests/wpunit/AccessFunctionsTest.php index bbfabba3d..2d3613a02 100644 --- a/tests/wpunit/AccessFunctionsTest.php +++ b/tests/wpunit/AccessFunctionsTest.php @@ -316,4 +316,58 @@ 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 ); + + + } + } 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 3379fa5aa..f9a8bcff3 100644 --- a/tests/wpunit/MediaItemQueriesTest.php +++ b/tests/wpunit/MediaItemQueriesTest.php @@ -488,4 +488,88 @@ public function testQueryMediaItemBySourceUrl() { } + /** + * testPostQuery + * + * This tests creating a small size media item and retrieving bigger size image via a GraphQL query + * + * @since 1.2.5 + */ + public function testMediaItemNotExistingSizeQuery() { + + $filename = ( WPGRAPHQL_PLUGIN_DIR . '/tests/_data/images/test.png' ); + $attachment_id = $this->factory()->attachment->create_upload_object( $filename ); + + /** + * Create an attachment with a post set as it's parent + */ +// $attachment_id = $this->createPostObject( [ +// 'post_type' => 'attachment', +// 'post_content' => 'some description', +// 'post_mime_type' => 'image/jpeg', +// ] ); +// +// $meta_data = [ +// 'width' => 300, +// 'height' => 300, +// 'file' => 'example.jpg', +// 'sizes' => [ +// 'thumbnail' => [ +// 'file' => 'example-thumbnail.jpg', +// 'width' => 150, +// 'height' => 150, +// 'mime-type' => 'image/jpeg', +// ], +// ], +// 'image_meta' => [ +// 'aperture' => 0, +// 'credit' => '', +// 'camera' => '', +// 'caption' => '', +// 'created_timestamp' => 0, +// 'copyright' => '', +// 'focal_length' => 0, +// 'iso' => 0, +// 'shutter_speed' => 0, +// 'title' => '', +// 'orientation' => '1', +// 'keywords' => [], +// ], +// ]; +// +// update_post_meta( $attachment_id, '_wp_attachment_metadata', $meta_data ); +// update_post_meta( $attachment_id, '_wp_attached_file', 'example.jpg' ); + + /** + * Create the global ID based on the post_type and the created $id + */ + $attachment_global_id = \GraphQLRelay\Relay::toGlobalId( 'post', $attachment_id ); + + /** + * Create the query string to pass to the $query + */ + $query = " + query { + mediaItem(id: \"{$attachment_global_id}\") { + srcSet(size: MEDIUM) + sizes(size: MEDIUM) + } + }"; + + /** + * Run the GraphQL query + */ + $actual = graphql( [ 'query' => $query ] ); + + codecept_debug( $actual ); + + $mediaItem = $actual['data']['mediaItem']; + + $this->assertNotEmpty( $mediaItem ); + + $this->assertNotNull( $mediaItem['sizes'] ); + $this->assertEquals( '(max-width: 300px) 100vw, 300px', $mediaItem['sizes'] ); + } + + } 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']); + } } From 8c43f5cfcf8c39ba41b9661331a9314619806075 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Tue, 1 Jun 2021 16:54:40 -0600 Subject: [PATCH 11/17] - Update GraphQL-PHP in composer.json (it got reverted when merging) - Update composer.lock - Update ContentNode.php to remove unused use statements - Update ContentTemplate to add `use Exception` statement - Update DatabaseIdentifier to add `use Exception` statement, remove extra bracket - Update TermNode.php to remove unused use statements - Update code style in RootQuery.php --- composer.json | 2 +- composer.lock | 14 ++--- src/Type/InterfaceType/ContentNode.php | 11 ++-- src/Type/InterfaceType/ContentTemplate.php | 4 +- src/Type/InterfaceType/DatabaseIdentifier.php | 6 ++- src/Type/InterfaceType/TermNode.php | 2 - src/Type/ObjectType/RootQuery.php | 54 +++++++++---------- 7 files changed, 45 insertions(+), 48 deletions(-) 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 1fa39ebda..021104254 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": [ diff --git a/src/Type/InterfaceType/ContentNode.php b/src/Type/InterfaceType/ContentNode.php index 0c85e2738..85c921830 100644 --- a/src/Type/InterfaceType/ContentNode.php +++ b/src/Type/InterfaceType/ContentNode.php @@ -3,15 +3,10 @@ namespace WPGraphQL\Type\InterfaceType; use Exception; -use GraphQL\Deferred; -use GraphQL\Type\Definition\ResolveInfo; -use WPGraphQL\AppContext; use WPGraphQL\Data\Connection\ContentTypeConnectionResolver; use WPGraphQL\Data\Connection\EnqueuedScriptsConnectionResolver; use WPGraphQL\Data\Connection\EnqueuedStylesheetConnectionResolver; -use WPGraphQL\Data\DataSource; use WPGraphQL\Model\Post; -use WPGraphQL\Model\Term; use WPGraphQL\Registry\TypeRegistry; class ContentNode { @@ -42,9 +37,9 @@ public static function register_type( TypeRegistry $type_registry ) { if ( $source->isRevision ) { $parent = get_post( $source->parentDatabaseId ); - $post_type = isset( $parent->post_type ) ? $parent->post_type : null; + $post_type = $parent->post_type ?? null; } else { - $post_type = isset( $source->post_type ) ? $source->post_type : null; + $post_type = $source->post_type ?? null; } if ( empty( $post_type ) ) { @@ -84,7 +79,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 ); diff --git a/src/Type/InterfaceType/ContentTemplate.php b/src/Type/InterfaceType/ContentTemplate.php index 4d28ed097..e59552257 100644 --- a/src/Type/InterfaceType/ContentTemplate.php +++ b/src/Type/InterfaceType/ContentTemplate.php @@ -2,6 +2,8 @@ namespace WPGraphQL\Type\InterfaceType; +use Exception; + class ContentTemplate { /** @@ -22,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/DatabaseIdentifier.php b/src/Type/InterfaceType/DatabaseIdentifier.php index 2fd4039e5..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 * @@ -27,7 +29,7 @@ public static function register_type() { 'description' => __( 'The unique identifier stored in the database', 'wp-graphql' ), ], ], - ], - ]); + ] + ); } } diff --git a/src/Type/InterfaceType/TermNode.php b/src/Type/InterfaceType/TermNode.php index f6b787b04..3c5a73cf2 100644 --- a/src/Type/InterfaceType/TermNode.php +++ b/src/Type/InterfaceType/TermNode.php @@ -4,8 +4,6 @@ use WPGraphQL\Data\Connection\EnqueuedScriptsConnectionResolver; use WPGraphQL\Data\Connection\EnqueuedStylesheetConnectionResolver; -use WPGraphQL\Data\DataSource; -use WPGraphQL\Model\Term; use WPGraphQL\Registry\TypeRegistry; class TermNode { diff --git a/src/Type/ObjectType/RootQuery.php b/src/Type/ObjectType/RootQuery.php index 9e40f2a27..727d388b1 100644 --- a/src/Type/ObjectType/RootQuery.php +++ b/src/Type/ObjectType/RootQuery.php @@ -331,11 +331,11 @@ public static function register_type() { case 'name': $menu = new \WP_Term_Query( [ - 'taxonomy' => 'nav_menu', - 'fields' => 'ids', - 'name' => $args['id'], + 'taxonomy' => 'nav_menu', + 'fields' => 'ids', + 'name' => $args['id'], 'include_children' => false, - 'count' => false, + 'count' => false, ] ); $id = ! empty( $menu->terms ) ? (int) $menu->terms[0] : null; @@ -435,13 +435,13 @@ public static function register_type() { case 'database_id': $taxonomy = isset( $args['taxonomy'] ) ? $args['taxonomy'] : null; if ( empty( $taxonomy ) && in_array( - $idType, - [ - 'name', - 'slug', - ], - true - ) ) { + $idType, + [ + 'name', + 'slug', + ], + true + ) ) { throw new UserError( __( 'When fetching a Term Node by "slug" or "name", the "taxonomy" also needs to be set as an input.', 'wp-graphql' ) ); } if ( 'database_id' === $idType ) { @@ -674,13 +674,13 @@ 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 - ) ) { + $post->post_type, + [ + 'revision', + $post_type_object->name, + ], + true + ) ) { return null; } @@ -691,7 +691,7 @@ function( $post ) use ( $post_type_object ) { ] ); $post_by_args = [ - 'id' => [ + 'id' => [ 'type' => 'ID', 'description' => sprintf( __( 'Get the object by its global ID', 'wp-graphql' ), $post_type_object->graphql_single_name ), ], @@ -699,7 +699,7 @@ function( $post ) use ( $post_type_object ) { 'type' => 'Int', 'description' => sprintf( __( 'Get the %s by its database ID', 'wp-graphql' ), $post_type_object->graphql_single_name ), ], - 'uri' => [ + 'uri' => [ 'type' => 'String', 'description' => sprintf( __( 'Get the %s by its uri', 'wp-graphql' ), $post_type_object->graphql_single_name ), ], @@ -752,13 +752,13 @@ function( $post ) use ( $post_type_object ) { } if ( ! isset( $post->post_type ) || ! in_array( - $post->post_type, - [ - 'revision', - $post_type_object->name, - ], - true - ) ) { + $post->post_type, + [ + 'revision', + $post_type_object->name, + ], + true + ) ) { return null; } From 900c7531fdb4453ff5c9db2b3dfbca573487bbfa Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Wed, 2 Jun 2021 13:00:18 -0600 Subject: [PATCH 12/17] - Add `SingelNodeConnectionEdge` Interface which is implemented by all oneToOne connections - Back out change to CLI script - Back out change to register_graphql_connection hook priority --- access-functions.php | 2 +- cli/wp-cli.php | 4 ---- src/Registry/TypeRegistry.php | 3 ++- src/Type/InterfaceType/ConnectionInterface.php | 14 ++++++++++++-- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/access-functions.php b/access-functions.php index af6eba555..27686b7c9 100644 --- a/access-functions.php +++ b/access-functions.php @@ -369,7 +369,7 @@ function register_graphql_connection( array $config ) { function( TypeRegistry $type_registry ) use ( $config ) { $type_registry->register_connection( $config ); }, - 50 + 10 ); } diff --git a/cli/wp-cli.php b/cli/wp-cli.php index 6d262af0e..9445063fa 100644 --- a/cli/wp-cli.php +++ b/cli/wp-cli.php @@ -29,10 +29,6 @@ public function generate_static_schema( $args, $assoc_args ) { */ $file_path = get_temp_dir() . 'schema.graphql'; - if ( isset( $assoc_args['output_dir'] ) ) { - $file_path = trailingslashit( $assoc_args['output_dir'] ) . 'schema.graphql'; - } - if ( ! defined( 'GRAPHQL_REQUEST') ) { define( 'GRAPHQL_REQUEST', true ); } diff --git a/src/Registry/TypeRegistry.php b/src/Registry/TypeRegistry.php index 11d9757a1..e934c5499 100644 --- a/src/Registry/TypeRegistry.php +++ b/src/Registry/TypeRegistry.php @@ -1103,13 +1103,14 @@ public function register_connection( array $config ) { $this->register_object_type( $connection_name . 'Edge', [ + 'interfaces' => [ 'SingleNodeConnectionEdge' ], // 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' ), $from_type, $to_type ), 'fields' => array_merge( [ 'node' => [ 'type' => [ 'non_null' => $to_type ], - 'description' => __( 'The nodes of the connection, without the edges', 'wp-graphql' ), + 'description' => __( 'The node of the connection, without the edges', 'wp-graphql' ), ], ], $edge_fields diff --git a/src/Type/InterfaceType/ConnectionInterface.php b/src/Type/InterfaceType/ConnectionInterface.php index 507f025bd..6696f2e9a 100644 --- a/src/Type/InterfaceType/ConnectionInterface.php +++ b/src/Type/InterfaceType/ConnectionInterface.php @@ -29,11 +29,11 @@ public static function register_type( TypeRegistry $type_registry ) { register_graphql_interface_type( 'Connection', [ - 'description' => __( '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' => [ 'list_of' => [ 'non_null' => 'Edge' ] ], - 'description' => __( 'A list of edges between connected nodes', 'wp-graphql' ), + 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), ], 'nodes' => [ 'type' => [ 'list_of' => [ 'non_null' => 'Node' ] ], @@ -43,5 +43,15 @@ public static function register_type( TypeRegistry $type_registry ) { ] ); + register_graphql_interface_type( 'SingleNodeConnectionEdge', [ + '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' ), + ], + ] + ] ); + } } From 85cd960fd2ba01a6b2449e71c39332c20b50f281 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Mon, 7 Jun 2021 08:47:30 -0600 Subject: [PATCH 13/17] - Remove MenuItemObjectUnion file - Update code styling --- src/Registry/TypeRegistry.php | 2 +- .../InterfaceType/ConnectionInterface.php | 2 +- src/Type/Union/MenuItemObjectUnion.php | 92 ------------------- 3 files changed, 2 insertions(+), 94 deletions(-) delete mode 100644 src/Type/Union/MenuItemObjectUnion.php diff --git a/src/Registry/TypeRegistry.php b/src/Registry/TypeRegistry.php index e934c5499..41cc50bf5 100644 --- a/src/Registry/TypeRegistry.php +++ b/src/Registry/TypeRegistry.php @@ -1103,7 +1103,7 @@ public function register_connection( array $config ) { $this->register_object_type( $connection_name . 'Edge', [ - 'interfaces' => [ 'SingleNodeConnectionEdge' ], + 'interfaces' => [ 'SingleNodeConnectionEdge' ], // 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' ), $from_type, $to_type ), 'fields' => array_merge( diff --git a/src/Type/InterfaceType/ConnectionInterface.php b/src/Type/InterfaceType/ConnectionInterface.php index 6696f2e9a..230050e10 100644 --- a/src/Type/InterfaceType/ConnectionInterface.php +++ b/src/Type/InterfaceType/ConnectionInterface.php @@ -50,7 +50,7 @@ public static function register_type( TypeRegistry $type_registry ) { 'type' => [ 'non_null' => 'Node' ], 'description' => __( 'The connected node', 'wp-graphql' ), ], - ] + ], ] ); } 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; - } -} - From 7e9482b54543ce34e89abd290a49ed9553740a84 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Wed, 9 Jun 2021 10:16:24 -0600 Subject: [PATCH 14/17] - Add the following Interfaces: CommentConnection, CommenterConnection, ContentNodeConnection, ContentTypeConnection, MenuConnection, MenuItemConnection, MenuItemLinkableConnection, TaxonomyConnection, TermNodeConnection, UserConnection - Adds tests to ensure the interfaces exist and implement the Connection Interface and their edges implement the ConnectionEdge interface --- src/Connection/Comments.php | 11 +- src/Connection/MenuItems.php | 11 +- src/Connection/PostObjects.php | 153 ++++--- src/Connection/TermObjects.php | 46 +- src/Connection/Users.php | 56 +-- src/Registry/TypeRegistry.php | 213 +-------- .../CommentConnectionInterface.php | 46 ++ .../CommenterConnectionInterface.php | 46 ++ .../InterfaceType/ConnectionInterface.php | 5 +- src/Type/InterfaceType/ContentNode.php | 7 +- .../ContentNodeConnectionInterface.php | 46 ++ .../ContentTypeConnectionInterface.php | 46 ++ .../InterfaceType/MenuConnectionInterface.php | 46 ++ .../MenuItemConnectionInterface.php | 46 ++ .../MenuItemLinkableConnectionInterface.php | 46 ++ .../TaxonomyConnectionInterface.php | 46 ++ .../TermNodeConnectionInterface.php | 46 ++ .../InterfaceType/UserConnectionInterface.php | 46 ++ src/Type/ObjectType/Comment.php | 9 +- src/Type/ObjectType/MenuItem.php | 13 +- src/Type/ObjectType/RootQuery.php | 12 +- src/Type/ObjectType/Taxonomy.php | 7 +- src/Type/ObjectType/User.php | 12 +- src/Type/WPConnectionType.php | 410 ++++++++++++++++++ src/Type/WPInterfaceType.php | 4 +- src/Type/WPObjectType.php | 5 +- tests/wpunit/ConnectionInterfaceTest.php | 335 ++++++++++++++ 27 files changed, 1423 insertions(+), 346 deletions(-) create mode 100644 src/Type/InterfaceType/CommentConnectionInterface.php create mode 100644 src/Type/InterfaceType/CommenterConnectionInterface.php create mode 100644 src/Type/InterfaceType/ContentNodeConnectionInterface.php create mode 100644 src/Type/InterfaceType/ContentTypeConnectionInterface.php create mode 100644 src/Type/InterfaceType/MenuConnectionInterface.php create mode 100644 src/Type/InterfaceType/MenuItemConnectionInterface.php create mode 100644 src/Type/InterfaceType/MenuItemLinkableConnectionInterface.php create mode 100644 src/Type/InterfaceType/TaxonomyConnectionInterface.php create mode 100644 src/Type/InterfaceType/TermNodeConnectionInterface.php create mode 100644 src/Type/InterfaceType/UserConnectionInterface.php create mode 100644 src/Type/WPConnectionType.php create mode 100644 tests/wpunit/ConnectionInterfaceTest.php diff --git a/src/Connection/Comments.php b/src/Connection/Comments.php index c1ec2b25f..6b97a6675 100644 --- a/src/Connection/Comments.php +++ b/src/Connection/Comments.php @@ -123,11 +123,12 @@ public static function register_connections() { */ 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/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/PostObjects.php b/src/Connection/PostObjects.php index 9062f14b0..df63cd6cf 100644 --- a/src/Connection/PostObjects.php +++ b/src/Connection/PostObjects.php @@ -34,12 +34,13 @@ 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 ) { + '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 ); @@ -52,12 +53,13 @@ public static function register_connections() { register_graphql_connection( [ - 'fromType' => 'Comment', - 'toType' => 'ContentNode', - 'queryClass' => 'WP_Query', - 'oneToOne' => true, - 'fromFieldName' => 'commentedOn', - 'resolve' => function( Comment $comment, $args, AppContext $context, ResolveInfo $info ) { + '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; } @@ -71,12 +73,13 @@ public static function register_connections() { 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 ) { + '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; @@ -93,11 +96,12 @@ public static function register_connections() { 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' ], @@ -106,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 ); @@ -116,13 +120,14 @@ 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 ) { + '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; @@ -139,13 +144,14 @@ public static function register_connections() { register_graphql_connection( [ - 'fromType' => 'MediaItem', - 'toType' => 'ContentNode', - '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 ) { + '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; @@ -162,13 +168,14 @@ public static function register_connections() { 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 ) { + '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; @@ -187,14 +194,15 @@ public static function register_connections() { 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 ) { + '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; @@ -236,12 +244,13 @@ 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 ) { + '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; @@ -290,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( @@ -330,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 ); @@ -412,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/TermObjects.php b/src/Connection/TermObjects.php index 3f3652022..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,12 +229,13 @@ 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 ) { + '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/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/Registry/TypeRegistry.php b/src/Registry/TypeRegistry.php index 41cc50bf5..e693ca860 100644 --- a/src/Registry/TypeRegistry.php +++ b/src/Registry/TypeRegistry.php @@ -44,16 +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; @@ -66,8 +73,11 @@ 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\PostObjectUnion; @@ -116,6 +126,7 @@ 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; @@ -209,15 +220,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 ); 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 ); @@ -230,8 +248,11 @@ public function init_type_registry( TypeRegistry $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(); @@ -1045,196 +1066,8 @@ protected function get_connection_name( $from_type, $to_type, $from_field_name ) */ public function register_connection( array $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']; - - /** - * 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', - ], - ]; - - } - - if ( true === $one_to_one ) { - - $this->register_object_type( - $connection_name . 'Edge', - [ - 'interfaces' => [ 'SingleNodeConnectionEdge' ], - // 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' ), $from_type, $to_type ), - 'fields' => array_merge( - [ - 'node' => [ - 'type' => [ 'non_null' => $to_type ], - 'description' => __( 'The node 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' ), - 'interfaces' => [ 'Edge' ], - 'fields' => array_merge( - [ - 'cursor' => [ - 'type' => 'String', - 'description' => __( 'A cursor for use in pagination', 'wp-graphql' ), - 'resolve' => $resolve_cursor, - ], - 'node' => [ - 'type' => [ 'non_null' => $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 ), - 'interfaces' => [ 'Connection' ], - 'fields' => array_merge( - [ - 'pageInfo' => [ - // @todo: change to PageInfo when/if the Relay lib is deprecated - 'type' => [ 'non_null' => 'WPPageInfo' ], - 'description' => __( 'Information about pagination in a connection.', 'wp-graphql' ), - ], - 'edges' => [ - 'type' => [ - 'list_of' => [ 'non_null' => $connection_name . 'Edge' ], - ], - // Translators: Placeholder is the name of the connection - 'description' => sprintf( __( 'Edges for the %s connection', 'wp-graphql' ), $connection_name ), - ], - 'nodes' => [ - 'type' => [ - 'list_of' => [ 'non_null' => $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 ), - '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 ) { - /** - * Return the results - */ - return call_user_func( $resolve_connection, $root, $args, $context, $info ); - }, - ] - ); + $connection = new WPConnectionType( $config, $this ); + $connection->register_connection(); } 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/ConnectionInterface.php b/src/Type/InterfaceType/ConnectionInterface.php index 230050e10..2111214f1 100644 --- a/src/Type/InterfaceType/ConnectionInterface.php +++ b/src/Type/InterfaceType/ConnectionInterface.php @@ -32,11 +32,11 @@ public static function register_type( TypeRegistry $type_registry ) { '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' => [ 'list_of' => [ 'non_null' => 'Edge' ] ], + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'Edge' ] ] ], 'description' => __( 'A list of edges (relational context) between connected nodes', 'wp-graphql' ), ], 'nodes' => [ - 'type' => [ 'list_of' => [ 'non_null' => 'Node' ] ], + 'type' => [ 'non_null' => [ 'list_of' => [ 'non_null' => 'Node' ] ] ], 'description' => __( 'A list of connected nodes', 'wp-graphql' ), ], ], @@ -44,6 +44,7 @@ public static function register_type( TypeRegistry $type_registry ) { ); 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' => [ diff --git a/src/Type/InterfaceType/ContentNode.php b/src/Type/InterfaceType/ContentNode.php index 85c921830..cd1c9c27e 100644 --- a/src/Type/InterfaceType/ContentNode.php +++ b/src/Type/InterfaceType/ContentNode.php @@ -32,8 +32,9 @@ public static function register_type( TypeRegistry $type_registry ) { 'description' => __( 'Nodes used to manage content', 'wp-graphql' ), 'connections' => [ 'contentType' => [ - 'toType' => 'ContentType', - 'resolve' => function( Post $source, $args, $context, $info ) { + 'toType' => 'ContentType', + 'connectionInterfaces' => [ 'ContentTypeConnection' ], + 'resolve' => function( Post $source, $args, $context, $info ) { if ( $source->isRevision ) { $parent = get_post( $source->parentDatabaseId ); @@ -50,7 +51,7 @@ public static function register_type( TypeRegistry $type_registry ) { return $resolver->one_to_one()->set_query_arg( 'name', $post_type )->get_connection(); }, - 'oneToOne' => true, + 'oneToOne' => true, ], 'enqueuedScripts' => [ 'toType' => 'EnqueuedScript', 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/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/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/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/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/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/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 3f3434ecb..b26aca5d2 100644 --- a/src/Type/ObjectType/Comment.php +++ b/src/Type/ObjectType/Comment.php @@ -25,10 +25,11 @@ public static function register_type() { 'interfaces' => [ 'Node', 'DatabaseIdentifier' ], 'connections' => [ 'author' => [ - 'toType' => 'Commenter', - 'description' => __( 'The author of the comment', 'wp-graphql' ), - 'oneToOne' => true, - 'resolve' => function( $comment, $args, AppContext $context, ResolveInfo $info ) { + '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 diff --git a/src/Type/ObjectType/MenuItem.php b/src/Type/ObjectType/MenuItem.php index 3dc86d35d..0803be523 100644 --- a/src/Type/ObjectType/MenuItem.php +++ b/src/Type/ObjectType/MenuItem.php @@ -23,10 +23,15 @@ public static function register_type() { 'interfaces' => [ 'Node', 'DatabaseIdentifier' ], 'connections' => [ 'connectedNode' => [ - 'toType' => 'MenuItemLinkable', - '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 ) { + '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 ); diff --git a/src/Type/ObjectType/RootQuery.php b/src/Type/ObjectType/RootQuery.php index 727d388b1..d310a7455 100644 --- a/src/Type/ObjectType/RootQuery.php +++ b/src/Type/ObjectType/RootQuery.php @@ -34,16 +34,18 @@ public static function register_type() { 'description' => __( 'The root entry point into the Graph', 'wp-graphql' ), 'connections' => [ 'contentTypes' => [ - 'toType' => 'ContentType', - 'resolve' => function( $source, $args, $context, $info ) { + 'toType' => 'ContentType', + 'connectionInterfaces' => [ 'ContentTypeConnection' ], + 'resolve' => function( $source, $args, $context, $info ) { $resolver = new ContentTypeConnectionResolver( $source, $args, $context, $info ); return $resolver->get_connection(); }, ], 'menus' => [ - 'toType' => 'Menu', - 'connectionArgs' => [ + 'toType' => 'Menu', + 'connectionInterfaces' => [ 'MenuConnection' ], + 'connectionArgs' => [ 'id' => [ 'type' => 'Int', 'description' => __( 'The ID of the object', 'wp-graphql' ), @@ -57,7 +59,7 @@ public static function register_type() { 'description' => __( 'The slug of the menu to query items for', 'wp-graphql' ), ], ], - 'resolve' => function( $source, $args, $context, $info ) { + 'resolve' => function( $source, $args, $context, $info ) { $resolver = new MenuConnectionResolver( $source, $args, $context, $info, 'nav_menu' ); return $resolver->get_connection(); diff --git a/src/Type/ObjectType/Taxonomy.php b/src/Type/ObjectType/Taxonomy.php index dcab014a9..51fbdfe4b 100644 --- a/src/Type/ObjectType/Taxonomy.php +++ b/src/Type/ObjectType/Taxonomy.php @@ -22,9 +22,10 @@ public static function register_type() { 'interfaces' => [ 'Node' ], 'connections' => [ 'connectedContentTypes' => [ - 'toType' => 'ContentType', - 'description' => __( 'List of Content Types associated with the Taxonomy', 'wp-graphql' ), - 'resolve' => function( \WPGraphQL\Model\Taxonomy $taxonomy, $args, AppContext $context, ResolveInfo $info ) { + '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 ); diff --git a/src/Type/ObjectType/User.php b/src/Type/ObjectType/User.php index ee50b68c0..65bfab31a 100644 --- a/src/Type/ObjectType/User.php +++ b/src/Type/ObjectType/User.php @@ -1,6 +1,5 @@ [ - 'toType' => 'ContentNode', - '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 ) { + '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' ); }, ], diff --git a/src/Type/WPConnectionType.php b/src/Type/WPConnectionType.php new file mode 100644 index 000000000..eed08f1d3 --- /dev/null +++ b/src/Type/WPConnectionType.php @@ -0,0 +1,410 @@ +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->connection_fields = ! empty( $config['connectionFields'] ) && is_array( $config['connectionFields'] ) ? $config['connectionFields'] : []; + $this->connection_args = ! empty( $config['connectionArgs'] ) && is_array( $config['connectionArgs'] ) ? $config['connectionArgs'] : []; + $this->edge_fields = ! empty( $config['edgeFields'] ) && 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'] : []; + + } + + /** + * Validates that essential key/value pairs are passed to the connection config. + * + * @param array $config + */ + 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 at least a toType defined', 'wp-graphql' ) ); + } + + if ( ! array_key_exists( 'fromFieldName', $config ) ) { + throw new InvalidArgument( __( 'Connection config needs to have at least a fromFieldName defined', '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( $from_type, $to_type, $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' => ! empty( $this->config['queryClass'] ) ? $this->config['queryClass'] : null, + ] + ); + + $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 + * + * @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 + * + * @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, + 'fields' => array_merge( + [ + 'pageInfo' => [ + // @todo: change to PageInfo when/if the Relay lib is deprecated + 'type' => [ 'non_null' => 'WPPageInfo' ], + '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 + */ + 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 ), + 'description' => ! empty( $config['description'] ) ? $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; + } + + $resolve_connection = $this->resolve_connection; + + /** + * Return the results + */ + return $resolve_connection( $root, $args, $context, $info ); + }, + ] + ); + + } + + /** + * Registers the connection Types and field to the Schema + * + * @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/WPInterfaceType.php b/src/Type/WPInterfaceType.php index bbcceef68..34fa7038c 100644 --- a/src/Type/WPInterfaceType.php +++ b/src/Type/WPInterfaceType.php @@ -51,7 +51,9 @@ public function __construct( array $config, TypeRegistry $type_registry ) { 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 ) { diff --git a/src/Type/WPObjectType.php b/src/Type/WPObjectType.php index 348e71280..4b1f0be81 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; @@ -90,7 +91,9 @@ public function __construct( $config, TypeRegistry $type_registry ) { 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 ) { 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' + ] + ] ), + ] ); + + } + +} From 5e572ae57163e81098f8345eb1a30c00ff8389ef Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Wed, 9 Jun 2021 12:24:46 -0600 Subject: [PATCH 15/17] - Update docblocks --- src/Registry/TypeRegistry.php | 1 + src/Type/WPConnectionType.php | 77 +++++++++++++++++++++++++++++++---- src/Type/WPEnumType.php | 2 +- 3 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/Registry/TypeRegistry.php b/src/Registry/TypeRegistry.php index e693ca860..ce3880a75 100644 --- a/src/Registry/TypeRegistry.php +++ b/src/Registry/TypeRegistry.php @@ -207,6 +207,7 @@ public function init() { * @param TypeRegistry $type_registry * * @return void + * @throws Exception */ public function init_type_registry( TypeRegistry $type_registry ) { diff --git a/src/Type/WPConnectionType.php b/src/Type/WPConnectionType.php index eed08f1d3..afe5e6fe4 100644 --- a/src/Type/WPConnectionType.php +++ b/src/Type/WPConnectionType.php @@ -1,9 +1,11 @@ from_type = $config['fromType']; $this->to_type = $config['toType']; $this->from_field_name = $config['fromFieldName']; - $this->connection_fields = ! empty( $config['connectionFields'] ) && is_array( $config['connectionFields'] ) ? $config['connectionFields'] : []; - $this->connection_args = ! empty( $config['connectionArgs'] ) && is_array( $config['connectionArgs'] ) ? $config['connectionArgs'] : []; - $this->edge_fields = ! empty( $config['edgeFields'] ) && is_array( $config['edgeFields'] ) ? $config['edgeFields'] : []; + $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() { @@ -100,6 +153,8 @@ public function __construct( array $config = [], TypeRegistry $type_registry ) { * Validates that essential key/value pairs are passed to the connection config. * * @param array $config + * + * @return void */ protected function validate_config( array $config ) { @@ -108,11 +163,11 @@ protected function validate_config( array $config ) { } if ( ! array_key_exists( 'toType', $config ) ) { - throw new InvalidArgument( __( 'Connection config needs to have at least a toType defined', 'wp-graphql' ) ); + throw new InvalidArgument( __( 'Connection config needs to have a "toType" defined', 'wp-graphql' ) ); } - if ( ! array_key_exists( 'fromFieldName', $config ) ) { - throw new InvalidArgument( __( 'Connection config needs to have at least a fromFieldName 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' ) ); } } @@ -182,6 +237,8 @@ protected function register_connection_input() { /** * Registers the Connection Edge type to the Schema * + * @return void + * * @throws Exception */ protected function register_connection_edge_type() { @@ -261,6 +318,8 @@ protected function register_connection_edge_type() { /** * Registers the Connection Type to the Schema * + * @return void + * * @throws Exception */ protected function register_connection_type() { @@ -365,6 +424,8 @@ protected function get_pagination_args() { /** * Registers the connection in the Graph + * + * @return void */ public function register_connection_field() { @@ -374,7 +435,7 @@ public function register_connection_field() { [ 'type' => true === $this->one_to_one ? $this->connection_name . 'Edge' : $this->connection_name, 'args' => array_merge( $this->get_pagination_args(), $this->where_args ), - 'description' => ! empty( $config['description'] ) ? $config['description'] : sprintf( __( 'Connection between the %1$s type and the %2$s type', 'wp-graphql' ), $this->from_type, $this->to_type ), + '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 ) ) { @@ -396,6 +457,8 @@ public function register_connection_field() { /** * Registers the connection Types and field to the Schema * + * @return void + * * @throws Exception */ public function register_connection() { 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'] ); From 644e5c6c426ef2059e44ceab0cf842048c298012 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Wed, 9 Jun 2021 13:17:53 -0600 Subject: [PATCH 16/17] - Add `TaxonomyConnection` to connection registration for taxonomy connections --- src/Connection/Taxonomies.php | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/Connection/Taxonomies.php b/src/Connection/Taxonomies.php index e5f96466a..3ffb7f86e 100644 --- a/src/Connection/Taxonomies.php +++ b/src/Connection/Taxonomies.php @@ -17,10 +17,11 @@ 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 +34,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 +54,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(); }, ] From 188a0c132247b1ab99df02fe7218fae20320d0b4 Mon Sep 17 00:00:00 2001 From: Jason Bahl Date: Wed, 9 Jun 2021 14:23:12 -0600 Subject: [PATCH 17/17] - Update docblocks - Change `WpPageInfo` to `PageInfo` - Remove ContentRevisionUnion, TermObjectUnion, PostObjectUnion in favor of interfaces (ContentNode, TermNode) --- .github/workflows/schema-linter.yml | 2 +- src/Connection/Taxonomies.php | 2 + src/Registry/TypeRegistry.php | 5 --- src/Type/ObjectType/PageInfo.php | 2 +- src/Type/Union/ContentRevisionUnion.php | 59 ------------------------ src/Type/Union/PostObjectUnion.php | 60 ------------------------- src/Type/Union/TermObjectUnion.php | 60 ------------------------- src/Type/WPConnectionType.php | 9 ++-- src/Type/WPObjectType.php | 6 +-- tests/wpunit/AccessFunctionsTest.php | 7 +++ 10 files changed, 19 insertions(+), 193 deletions(-) delete mode 100644 src/Type/Union/ContentRevisionUnion.php delete mode 100644 src/Type/Union/PostObjectUnion.php delete mode 100644 src/Type/Union/TermObjectUnion.php 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/src/Connection/Taxonomies.php b/src/Connection/Taxonomies.php index 3ffb7f86e..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,6 +13,7 @@ class Taxonomies { * Registers connections to the Taxonomy type * * @return void + * @throws Exception */ public static function register_connections() { diff --git a/src/Registry/TypeRegistry.php b/src/Registry/TypeRegistry.php index ce3880a75..c8082a914 100644 --- a/src/Registry/TypeRegistry.php +++ b/src/Registry/TypeRegistry.php @@ -80,7 +80,6 @@ use WPGraphQL\Type\InterfaceType\UserConnectionInterface; use WPGraphQL\Type\ObjectType\EnqueuedScript; use WPGraphQL\Type\ObjectType\EnqueuedStylesheet; -use WPGraphQL\Type\Union\PostObjectUnion; use WPGraphQL\Type\Enum\AvatarRatingEnum; use WPGraphQL\Type\Enum\CommentsConnectionOrderbyEnum; use WPGraphQL\Type\Enum\MediaItemSizeEnum; @@ -125,7 +124,6 @@ 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; @@ -313,9 +311,6 @@ public function init_type_registry( TypeRegistry $type_registry ) { PostObjectsConnectionOrderbyInput::register_type(); UsersConnectionOrderbyInput::register_type(); - PostObjectUnion::register_type( $this ); - TermObjectUnion::register_type( $this ); - /** * Register core connections */ 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/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/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 index 3f70cf9fa..8f0894f90 100644 --- a/src/Type/WPConnectionType.php +++ b/src/Type/WPConnectionType.php @@ -352,13 +352,14 @@ protected function register_connection_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, - 'fields' => array_merge( + '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' => 'WPPageInfo' ], + 'type' => [ 'non_null' => 'PageInfo' ], 'description' => __( 'Information about pagination in a connection.', 'wp-graphql' ), ], 'edges' => [ diff --git a/src/Type/WPObjectType.php b/src/Type/WPObjectType.php index 4b1f0be81..9eb899000 100644 --- a/src/Type/WPObjectType.php +++ b/src/Type/WPObjectType.php @@ -82,11 +82,11 @@ 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 ) { diff --git a/tests/wpunit/AccessFunctionsTest.php b/tests/wpunit/AccessFunctionsTest.php index 9d8e2876b..bff0d5ec2 100644 --- a/tests/wpunit/AccessFunctionsTest.php +++ b/tests/wpunit/AccessFunctionsTest.php @@ -361,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' );