Introduce an experimental GraphQL API.

- Uses the webonyx/graphql-php framework.
- Must be enabled at Settings - Advanced - GraphQL API.
- Entry point is POST at /wp-json/wc/graphql/api
- Input is a JSON object with "query" and optional "variables" fields.
- Code lives at src/Internal/GraphQL.
- Same authentication token/secrets as the REST API are used.
- Introduces a new "woocommerce_graphql_check_permissions" filter.
- Add ?verbose_errors to the url to get the exception message
  and a stack trace in case of internal error (for this to work
  either the user must be in the administrators role
  or WP_DEBUG must be set)
This commit is contained in:
Nestor Soriano 2021-03-17 08:26:49 +01:00
parent 54edb576d5
commit d6118841c8
30 changed files with 2264 additions and 23 deletions

View File

@ -33,6 +33,7 @@
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.4"
},
"default-branch": true,
"bin": [
"bin/mozart"
],
@ -53,6 +54,10 @@
}
],
"description": "Composes all dependencies as a package inside a WordPress plugin",
"support": {
"issues": "https://github.com/coenjacobs/mozart/issues",
"source": "https://github.com/coenjacobs/mozart/tree/master"
},
"funding": [
{
"url": "https://github.com/coenjacobs",
@ -144,6 +149,10 @@
"sftp",
"storage"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/1.x"
},
"funding": [
{
"url": "https://offset.earth/frankdejonge",
@ -192,6 +201,10 @@
}
],
"description": "Mime-type detection for Flysystem",
"support": {
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0"
},
"funding": [
{
"url": "https://github.com/frankdejonge",
@ -246,20 +259,24 @@
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/1.1.1"
},
"time": "2021-03-05T17:36:06+00:00"
},
{
"name": "symfony/console",
"version": "v5.2.4",
"version": "v5.2.5",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "d6d0cc30d8c0fda4e7b213c20509b0159a8f4556"
"reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/d6d0cc30d8c0fda4e7b213c20509b0159a8f4556",
"reference": "d6d0cc30d8c0fda4e7b213c20509b0159a8f4556",
"url": "https://api.github.com/repos/symfony/console/zipball/938ebbadae1b0a9c9d1ec313f87f9708609f1b79",
"reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79",
"shasum": ""
},
"require": {
@ -326,6 +343,9 @@
"console",
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.2.5"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -340,7 +360,7 @@
"type": "tidelift"
}
],
"time": "2021-02-23T10:08:49+00:00"
"time": "2021-03-06T13:42:15+00:00"
},
{
"name": "symfony/finder",
@ -384,6 +404,9 @@
],
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v5.2.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -460,6 +483,9 @@
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -538,6 +564,9 @@
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -619,6 +648,9 @@
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -696,6 +728,9 @@
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -772,6 +807,9 @@
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -852,6 +890,9 @@
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -928,6 +969,9 @@
"interoperability",
"standards"
],
"support": {
"source": "https://github.com/symfony/service-contracts/tree/master"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -1008,6 +1052,9 @@
"utf-8",
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.2.4"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -1037,5 +1084,5 @@
"platform-overrides": {
"php": "7.3"
},
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -71,6 +71,10 @@
"stylecheck",
"tests"
],
"support": {
"issues": "https://github.com/dealerdirect/phpcodesniffer-composer-installer/issues",
"source": "https://github.com/dealerdirect/phpcodesniffer-composer-installer"
},
"time": "2020-06-25T14:57:39+00:00"
},
{
@ -129,6 +133,10 @@
"phpcs",
"standards"
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues",
"source": "https://github.com/PHPCompatibility/PHPCompatibility"
},
"time": "2019-12-27T09:44:58+00:00"
},
{
@ -181,6 +189,10 @@
"polyfill",
"standards"
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues",
"source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie"
},
"time": "2021-02-15T10:24:51+00:00"
},
{
@ -231,6 +243,10 @@
"standards",
"wordpress"
],
"support": {
"issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues",
"source": "https://github.com/PHPCompatibility/PHPCompatibilityWP"
},
"time": "2019-08-28T14:22:28+00:00"
},
{
@ -282,6 +298,11 @@
"phpcs",
"standards"
],
"support": {
"issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
"source": "https://github.com/squizlabs/PHP_CodeSniffer",
"wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
},
"time": "2020-10-23T02:01:07+00:00"
},
{
@ -322,6 +343,10 @@
"woocommerce",
"wordpress"
],
"support": {
"issues": "https://github.com/woocommerce/woocommerce-sniffs/issues",
"source": "https://github.com/woocommerce/woocommerce-sniffs/tree/master"
},
"time": "2020-08-06T18:23:45+00:00"
},
{
@ -368,6 +393,11 @@
"standards",
"wordpress"
],
"support": {
"issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues",
"source": "https://github.com/WordPress/WordPress-Coding-Standards",
"wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki"
},
"time": "2020-05-13T23:57:56+00:00"
}
],
@ -381,5 +411,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -59,6 +59,10 @@
"constructor",
"instantiate"
],
"support": {
"issues": "https://github.com/doctrine/instantiator/issues",
"source": "https://github.com/doctrine/instantiator/tree/master"
},
"time": "2015-06-14T21:17:01+00:00"
},
{
@ -104,6 +108,10 @@
"object",
"object graph"
],
"support": {
"issues": "https://github.com/myclabs/DeepCopy/issues",
"source": "https://github.com/myclabs/DeepCopy/tree/1.x"
},
"time": "2017-10-19T19:58:43+00:00"
},
{
@ -159,6 +167,10 @@
}
],
"description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
"support": {
"issues": "https://github.com/phar-io/manifest/issues",
"source": "https://github.com/phar-io/manifest/tree/master"
},
"time": "2017-03-05T18:14:27+00:00"
},
{
@ -206,6 +218,10 @@
}
],
"description": "Library for handling version information and constraints",
"support": {
"issues": "https://github.com/phar-io/version/issues",
"source": "https://github.com/phar-io/version/tree/master"
},
"time": "2017-03-05T17:38:23+00:00"
},
{
@ -260,6 +276,10 @@
"reflection",
"static analysis"
],
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
"source": "https://github.com/phpDocumentor/ReflectionCommon/tree/master"
},
"time": "2017-09-11T18:02:19+00:00"
},
{
@ -312,6 +332,10 @@
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"support": {
"issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
"source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/release/4.x"
},
"time": "2019-12-28T18:55:12+00:00"
},
{
@ -357,6 +381,10 @@
"email": "me@mikevanriel.com"
}
],
"support": {
"issues": "https://github.com/phpDocumentor/TypeResolver/issues",
"source": "https://github.com/phpDocumentor/TypeResolver/tree/master"
},
"time": "2017-12-30T13:23:38+00:00"
},
{
@ -420,6 +448,10 @@
"spy",
"stub"
],
"support": {
"issues": "https://github.com/phpspec/prophecy/issues",
"source": "https://github.com/phpspec/prophecy/tree/v1.10.3"
},
"time": "2020-03-05T15:02:03+00:00"
},
{
@ -483,6 +515,10 @@
"testing",
"xunit"
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/5.3"
},
"time": "2018-04-06T15:36:58+00:00"
},
{
@ -530,6 +566,11 @@
"filesystem",
"iterator"
],
"support": {
"irc": "irc://irc.freenode.net/phpunit",
"issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
"source": "https://github.com/sebastianbergmann/php-file-iterator/tree/1.4.5"
},
"time": "2017-11-27T13:52:08+00:00"
},
{
@ -571,6 +612,10 @@
"keywords": [
"template"
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-text-template/issues",
"source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1"
},
"time": "2015-06-21T13:50:34+00:00"
},
{
@ -620,6 +665,10 @@
"keywords": [
"timer"
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-timer/issues",
"source": "https://github.com/sebastianbergmann/php-timer/tree/master"
},
"time": "2017-02-26T11:10:40+00:00"
},
{
@ -669,6 +718,10 @@
"keywords": [
"tokenizer"
],
"support": {
"issues": "https://github.com/sebastianbergmann/php-token-stream/issues",
"source": "https://github.com/sebastianbergmann/php-token-stream/tree/master"
},
"abandoned": true,
"time": "2017-11-27T05:48:46+00:00"
},
@ -754,6 +807,10 @@
"testing",
"xunit"
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/6.5.14"
},
"time": "2019-02-01T05:22:47+00:00"
},
{
@ -813,6 +870,10 @@
"mock",
"xunit"
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit-mock-objects/issues",
"source": "https://github.com/sebastianbergmann/phpunit-mock-objects/tree/5.0.10"
},
"abandoned": true,
"time": "2018-08-09T05:50:03+00:00"
},
@ -859,6 +920,10 @@
],
"description": "Looks up which function or method a line of code belongs to",
"homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/",
"support": {
"issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues",
"source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
@ -929,6 +994,10 @@
"compare",
"equality"
],
"support": {
"issues": "https://github.com/sebastianbergmann/comparator/issues",
"source": "https://github.com/sebastianbergmann/comparator/tree/master"
},
"time": "2018-02-01T13:46:46+00:00"
},
{
@ -981,6 +1050,10 @@
"keywords": [
"diff"
],
"support": {
"issues": "https://github.com/sebastianbergmann/diff/issues",
"source": "https://github.com/sebastianbergmann/diff/tree/master"
},
"time": "2017-08-03T08:09:46+00:00"
},
{
@ -1031,6 +1104,10 @@
"environment",
"hhvm"
],
"support": {
"issues": "https://github.com/sebastianbergmann/environment/issues",
"source": "https://github.com/sebastianbergmann/environment/tree/master"
},
"time": "2017-07-01T08:51:00+00:00"
},
{
@ -1098,6 +1175,10 @@
"export",
"exporter"
],
"support": {
"issues": "https://github.com/sebastianbergmann/exporter/issues",
"source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
@ -1155,6 +1236,10 @@
"keywords": [
"global state"
],
"support": {
"issues": "https://github.com/sebastianbergmann/global-state/issues",
"source": "https://github.com/sebastianbergmann/global-state/tree/2.0.0"
},
"time": "2017-04-27T15:39:26+00:00"
},
{
@ -1202,6 +1287,10 @@
],
"description": "Traverses array structures and object graphs to enumerate all referenced objects",
"homepage": "https://github.com/sebastianbergmann/object-enumerator/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
"source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
@ -1253,6 +1342,10 @@
],
"description": "Allows reflection of object attributes, including inherited and non-public ones",
"homepage": "https://github.com/sebastianbergmann/object-reflector/",
"support": {
"issues": "https://github.com/sebastianbergmann/object-reflector/issues",
"source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
@ -1312,6 +1405,10 @@
],
"description": "Provides functionality to recursively process PHP variables",
"homepage": "http://www.github.com/sebastianbergmann/recursion-context",
"support": {
"issues": "https://github.com/sebastianbergmann/recursion-context/issues",
"source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1"
},
"funding": [
{
"url": "https://github.com/sebastianbergmann",
@ -1360,6 +1457,10 @@
],
"description": "Provides a list of PHP built-in functions that operate on resources",
"homepage": "https://www.github.com/sebastianbergmann/resource-operations",
"support": {
"issues": "https://github.com/sebastianbergmann/resource-operations/issues",
"source": "https://github.com/sebastianbergmann/resource-operations/tree/master"
},
"time": "2015-07-28T20:34:47+00:00"
},
{
@ -1403,6 +1504,10 @@
],
"description": "Library that helps with managing the version number of Git-hosted PHP projects",
"homepage": "https://github.com/sebastianbergmann/version",
"support": {
"issues": "https://github.com/sebastianbergmann/version/issues",
"source": "https://github.com/sebastianbergmann/version/tree/master"
},
"time": "2016-10-03T07:35:21+00:00"
},
{
@ -1465,6 +1570,9 @@
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.19.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
@ -1519,6 +1627,10 @@
}
],
"description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
"support": {
"issues": "https://github.com/theseer/tokenizer/issues",
"source": "https://github.com/theseer/tokenizer/tree/master"
},
"time": "2019-06-13T22:48:21+00:00"
},
{
@ -1568,6 +1680,10 @@
"check",
"validate"
],
"support": {
"issues": "https://github.com/webmozarts/assert/issues",
"source": "https://github.com/webmozarts/assert/tree/1.9.1"
},
"time": "2020-07-08T17:02:28+00:00"
}
],
@ -1581,5 +1697,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -9,16 +9,16 @@
"packages-dev": [
{
"name": "gettext/gettext",
"version": "v4.8.3",
"version": "v4.8.4",
"source": {
"type": "git",
"url": "https://github.com/php-gettext/Gettext.git",
"reference": "57ff4fb16647e78e80a5909fe3c190f1c3110321"
"reference": "58bc0f7f37e78efb0f9758f93d4a0f669f0f84a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-gettext/Gettext/zipball/57ff4fb16647e78e80a5909fe3c190f1c3110321",
"reference": "57ff4fb16647e78e80a5909fe3c190f1c3110321",
"url": "https://api.github.com/repos/php-gettext/Gettext/zipball/58bc0f7f37e78efb0f9758f93d4a0f669f0f84a1",
"reference": "58bc0f7f37e78efb0f9758f93d4a0f669f0f84a1",
"shasum": ""
},
"require": {
@ -67,7 +67,26 @@
"po",
"translation"
],
"time": "2020-11-18T22:35:49+00:00"
"support": {
"email": "oom@oscarotero.com",
"issues": "https://github.com/oscarotero/Gettext/issues",
"source": "https://github.com/php-gettext/Gettext/tree/v4.8.4"
},
"funding": [
{
"url": "https://paypal.me/oscarotero",
"type": "custom"
},
{
"url": "https://github.com/oscarotero",
"type": "github"
},
{
"url": "https://www.patreon.com/misteroom",
"type": "patreon"
}
],
"time": "2021-03-10T19:35:49+00:00"
},
{
"name": "gettext/languages",
@ -128,6 +147,10 @@
"translations",
"unicode"
],
"support": {
"issues": "https://github.com/php-gettext/Languages/issues",
"source": "https://github.com/php-gettext/Languages/tree/2.6.0"
},
"time": "2019-11-13T10:30:21+00:00"
},
{
@ -173,6 +196,10 @@
}
],
"description": "Peast is PHP library that generates AST for JavaScript code",
"support": {
"issues": "https://github.com/mck89/peast/issues",
"source": "https://github.com/mck89/peast/tree/v1.12.0"
},
"time": "2021-01-08T15:16:19+00:00"
},
{
@ -219,6 +246,10 @@
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/master"
},
"time": "2019-11-23T21:40:31+00:00"
},
{
@ -268,6 +299,10 @@
"iri",
"sockets"
],
"support": {
"issues": "https://github.com/rmccue/Requests/issues",
"source": "https://github.com/rmccue/Requests/tree/master"
},
"time": "2016-10-13T00:11:37+00:00"
},
{
@ -317,6 +352,9 @@
],
"description": "Symfony Finder Component",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/3.3"
},
"time": "2017-06-01T21:01:25+00:00"
},
{
@ -374,6 +412,10 @@
],
"description": "Provides internationalization tools for WordPress projects.",
"homepage": "https://github.com/wp-cli/i18n-command",
"support": {
"issues": "https://github.com/wp-cli/i18n-command/issues",
"source": "https://github.com/wp-cli/i18n-command/tree/v2.2.6"
},
"time": "2020-12-07T19:28:27+00:00"
},
{
@ -422,6 +464,9 @@
],
"description": "A simple YAML loader/dumper class for PHP (WP-CLI fork)",
"homepage": "https://github.com/mustangostang/spyc/",
"support": {
"source": "https://github.com/wp-cli/spyc/tree/autoload"
},
"time": "2017-04-25T11:26:20+00:00"
},
{
@ -472,6 +517,10 @@
"cli",
"console"
],
"support": {
"issues": "https://github.com/wp-cli/php-cli-tools/issues",
"source": "https://github.com/wp-cli/php-cli-tools/tree/v0.11.12"
},
"time": "2021-03-03T12:43:49+00:00"
},
{
@ -534,6 +583,11 @@
"cli",
"wordpress"
],
"support": {
"docs": "https://make.wordpress.org/cli/handbook/",
"issues": "https://github.com/wp-cli/wp-cli/issues",
"source": "https://github.com/wp-cli/wp-cli"
},
"time": "2020-02-18T08:15:37+00:00"
}
],
@ -547,5 +601,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -20,6 +20,7 @@
"maxmind-db/reader": "1.6.0",
"pelago/emogrifier": "3.1.0",
"psr/container": "1.0.0",
"webonyx/graphql-php": "^0.12.6",
"woocommerce/action-scheduler": "3.1.6",
"woocommerce/woocommerce-admin": "2.0.3",
"woocommerce/woocommerce-blocks": "4.4.3"

94
composer.lock generated
View File

@ -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": "d60ef90fa87857445878bb83ba4c592c",
"content-hash": "09922bdf7cf1ab57bc0e69b8d480a93e",
"packages": [
{
"name": "automattic/jetpack-autoloader",
@ -44,6 +44,9 @@
"GPL-2.0-or-later"
],
"description": "Creates a custom autoloader for a plugin or theme.",
"support": {
"source": "https://github.com/Automattic/jetpack-autoloader/tree/v2.9.1"
},
"time": "2021-02-05T19:07:06+00:00"
},
{
@ -75,6 +78,9 @@
"GPL-2.0-or-later"
],
"description": "A wrapper for defining constants in a more testable way.",
"support": {
"source": "https://github.com/Automattic/jetpack-constants/tree/v1.5.1"
},
"time": "2020-10-28T19:00:31+00:00"
},
{
@ -205,6 +211,10 @@
"zend",
"zikula"
],
"support": {
"issues": "https://github.com/composer/installers/issues",
"source": "https://github.com/composer/installers/tree/v1.10.0"
},
"funding": [
{
"url": "https://packagist.com",
@ -279,6 +289,10 @@
"geolocation",
"maxmind"
],
"support": {
"issues": "https://github.com/maxmind/MaxMind-DB-Reader-php/issues",
"source": "https://github.com/maxmind/MaxMind-DB-Reader-php/tree/v1.6.0"
},
"time": "2019-12-19T22:59:03+00:00"
},
{
@ -353,6 +367,10 @@
"email",
"pre-processing"
],
"support": {
"issues": "https://github.com/MyIntervals/emogrifier/issues",
"source": "https://github.com/MyIntervals/emogrifier"
},
"time": "2019-12-26T19:37:31+00:00"
},
{
@ -402,6 +420,10 @@
"container-interop",
"psr"
],
"support": {
"issues": "https://github.com/php-fig/container/issues",
"source": "https://github.com/php-fig/container/tree/master"
},
"time": "2017-02-14T16:28:37+00:00"
},
{
@ -455,8 +477,60 @@
],
"description": "Symfony CssSelector Component",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/css-selector/tree/master"
},
"time": "2017-05-01T15:01:29+00:00"
},
{
"name": "webonyx/graphql-php",
"version": "v0.12.6",
"source": {
"type": "git",
"url": "https://github.com/webonyx/graphql-php.git",
"reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webonyx/graphql-php/zipball/4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95",
"reference": "4c545e5ec4fc37f6eb36c19f5a0e7feaf5979c95",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"php": ">=5.6"
},
"require-dev": {
"phpunit/phpunit": "^4.8",
"psr/http-message": "^1.0",
"react/promise": "2.*"
},
"suggest": {
"psr/http-message": "To use standard GraphQL server",
"react/promise": "To leverage async resolving on React PHP platform"
},
"type": "library",
"autoload": {
"psr-4": {
"GraphQL\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"description": "A PHP port of GraphQL reference implementation",
"homepage": "https://github.com/webonyx/graphql-php",
"keywords": [
"api",
"graphql"
],
"support": {
"issues": "https://github.com/webonyx/graphql-php/issues",
"source": "https://github.com/webonyx/graphql-php/tree/0.12.x"
},
"time": "2018-09-02T14:59:54+00:00"
},
{
"name": "woocommerce/action-scheduler",
"version": "3.1.6",
@ -490,6 +564,10 @@
],
"description": "Action Scheduler for WordPress and WooCommerce",
"homepage": "https://actionscheduler.org/",
"support": {
"issues": "https://github.com/woocommerce/action-scheduler/issues",
"source": "https://github.com/woocommerce/action-scheduler/tree/master"
},
"time": "2020-05-12T16:22:33+00:00"
},
{
@ -535,6 +613,10 @@
],
"description": "A modern, javascript-driven WooCommerce Admin experience.",
"homepage": "https://github.com/woocommerce/woocommerce-admin",
"support": {
"issues": "https://github.com/woocommerce/woocommerce-admin/issues",
"source": "https://github.com/woocommerce/woocommerce-admin/tree/v2.0.3"
},
"time": "2021-03-10T02:58:43+00:00"
},
{
@ -582,6 +664,10 @@
"gutenberg",
"woocommerce"
],
"support": {
"issues": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/issues",
"source": "https://github.com/woocommerce/woocommerce-gutenberg-products-block/tree/v4.4.3"
},
"time": "2021-02-11T18:07:48+00:00"
}
],
@ -630,6 +716,10 @@
"isolation",
"tool"
],
"support": {
"issues": "https://github.com/bamarni/composer-bin-plugin/issues",
"source": "https://github.com/bamarni/composer-bin-plugin/tree/master"
},
"time": "2020-05-03T08:27:20+00:00"
}
],
@ -645,5 +735,5 @@
"platform-overrides": {
"php": "7.0"
},
"plugin-api-version": "1.1.0"
"plugin-api-version": "2.0.0"
}

View File

@ -41,6 +41,7 @@ class WC_Settings_Advanced extends WC_Settings_Page {
'keys' => __( 'REST API', 'woocommerce' ),
'webhooks' => __( 'Webhooks', 'woocommerce' ),
'legacy_api' => __( 'Legacy API', 'woocommerce' ),
'graphql_api' => __( 'GraphQL API', 'woocommerce' ),
'woocommerce_com' => __( 'WooCommerce.com', 'woocommerce' ),
);
@ -385,11 +386,12 @@ class WC_Settings_Advanced extends WC_Settings_Page {
'id' => 'legacy_api_options',
),
array(
'title' => __( 'Legacy API', 'woocommerce' ),
'desc' => __( 'Enable the legacy REST API', 'woocommerce' ),
'id' => 'woocommerce_api_enabled',
'type' => 'checkbox',
'default' => 'no',
'title' => __( 'Legacy API', 'woocommerce' ),
'desc' => __( 'Enable the legacy REST API', 'woocommerce' ),
'desc_tip' => __( 'Enable the legacy REST API', 'woocommerce' ),
'id' => 'woocommerce_api_enabled',
'type' => 'checkbox',
'default' => 'no',
),
array(
'type' => 'sectionend',
@ -397,6 +399,34 @@ class WC_Settings_Advanced extends WC_Settings_Page {
),
)
);
} elseif ( 'graphql_api' === $current_section ) {
$graphql_entrypoint = get_rest_url( null, 'wc/graphql/api' );
// phpcs:disable WordPress.WP.I18n.InterpolatedVariableText
$settings = apply_filters(
'woocommerce_settings_graphql_api',
array(
array(
'title' => '',
'type' => 'title',
'desc' => '',
'id' => 'graphql_api_options',
),
array(
'title' => __( 'GraphQL API', 'woocommerce' ),
'desc' => __( 'Enable the GraphQL API', 'woocommerce' ),
'desc_tip' => __( "<b>The GraphQL API is experimental and not intended for production use.</b><br/>GraphQL API entry point: <code>$graphql_entrypoint</code><br/>Accepts <code>POST</code> requests with a JSON object containing a <code>query</code> field and optionally a <code>variables</code> field.", 'woocommerce' ),
'id' => 'woocommerce_graphql_api_enabled',
'type' => 'checkbox',
'default' => 'no',
),
array(
'type' => 'sectionend',
'id' => 'graphql_api_options',
),
)
);
// phpcs:enable WordPress.WP.I18n.InterpolatedVariableText
}
return apply_filters( 'woocommerce_get_settings_' . $this->id, $settings, $current_section );

View File

@ -6,6 +6,8 @@
* @since 2.6.0
*/
use Automattic\WooCommerce\Utilities\StringUtil;
defined( 'ABSPATH' ) || exit;
/**
@ -34,6 +36,16 @@ class WC_REST_Authentication {
*/
protected $auth_method = '';
private static $_instance;
public static function instance() {
if(is_null(self::$_instance)) {
self::$_instance = new self();
}
return self::$_instance;
}
/**
* Initialize authentication actions.
*/
@ -556,6 +568,10 @@ class WC_REST_Authentication {
private function check_permissions( $method ) {
$permissions = $this->user->permissions;
if($this->is_graphql_request()) {
return 'POST' === $method;
}
switch ( $method ) {
case 'HEAD':
case 'GET':
@ -635,6 +651,28 @@ class WC_REST_Authentication {
return $result;
}
private function is_graphql_request()
{
return StringUtil::ends_with($_SERVER['PHP_SELF'], '/wc/v4/api');
}
public function current_user_has_permission(string $permission) {
if(!$this->user) {
return false;
}
$actual_permission = $this->user->permissions;
switch($permission) {
case 'read':
return 'read' === $actual_permission || 'read_write' === $actual_permission;
case 'write':
return 'write' === $actual_permission || 'read_write' === $actual_permission;
}
throw new Exception("Unknown permission: " . $permission);
}
}
new WC_REST_Authentication();
WC_REST_Authentication::instance();

View File

@ -167,6 +167,7 @@ final class WooCommerce {
$this->define_tables();
$this->includes();
$this->init_hooks();
Automattic\WooCommerce\Internal\GraphQL\Main::initialize();
}
/**

View File

@ -7,6 +7,8 @@ namespace Automattic\WooCommerce;
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\DownloadPermissionsAdjusterServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\GraphqlInfrastructureServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\GraphqlTypesServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider;
/**
@ -33,8 +35,10 @@ final class Container implements \Psr\Container\ContainerInterface {
* @var string[]
*/
private $service_providers = array(
ProxiesServiceProvider::class,
DownloadPermissionsAdjusterServiceProvider::class,
GraphqlInfrastructureServiceProvider::class,
GraphqlTypesServiceProvider::class,
ProxiesServiceProvider::class
);
/**

View File

@ -0,0 +1,35 @@
<?php
/**
* GraphqlInfrastructureServiceProvider class file.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\GraphQL\RootMutationType;
use Automattic\WooCommerce\Internal\GraphQL\RootQueryType;
/**
* Service provider for the infrastructure classes in the Automattic\WooCommerce\Internal\GraphQL namespace.
*/
class GraphqlInfrastructureServiceProvider extends AbstractServiceProvider {
/**
* The classes/interfaces that are serviced by this service provider.
*
* @var array
*/
protected $provides = array(
RootQueryType::class,
RootMutationType::class,
);
/**
* Register the classes.
*/
public function register() {
foreach ( $this->provides as $class_name ) {
$this->share_with_auto_arguments( $class_name );
}
}
}

View File

@ -0,0 +1,45 @@
<?php
/**
* GraphqlTypesServiceProvider class file.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\GraphQL\EnumTypes\ProductAttributeOrderBy;
use Automattic\WooCommerce\Internal\GraphQL\InputTypes\AddProductAttributeTermInputType;
use Automattic\WooCommerce\Internal\GraphQL\MutationTypes\AddProductAttributeTerm;
use Automattic\WooCommerce\Internal\GraphQL\QueryTypes\ProductAttribute;
use Automattic\WooCommerce\Internal\GraphQL\QueryTypes\ProductAttributes;
use Automattic\WooCommerce\Internal\GraphQL\QueryTypes\ProductAttributeTerm;
use Automattic\WooCommerce\Internal\GraphQL\QueryTypes\ProductAttributeTerms;
/**
* Service provider for the type definition classes in the Automattic\WooCommerce\Internal\GraphQL namespace.
*/
class GraphqlTypesServiceProvider extends AbstractServiceProvider {
/**
* The classes/interfaces that are serviced by this service provider.
*
* @var array
*/
protected $provides = array(
ProductAttribute::class,
ProductAttributes::class,
ProductAttributeOrderBy::class,
ProductAttributeTerm::class,
ProductAttributeTerms::class,
AddProductAttributeTerm::class,
AddProductAttributeTermInputType::class,
);
/**
* Register the classes.
*/
public function register() {
foreach ( $this->provides as $class_name ) {
$this->share( $class_name );
}
}
}

View File

@ -0,0 +1,69 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use GraphQL\Error\ClientAware;
/**
* ApiException class.
*
* Queries and mutations should throw an exception of this type when something prevents the completion of the
* operation. The supplied message will be returned in the response, and the HTTP status of the response
* will depend on the specified category (note that the category must be one of the keys of
* Main::$status_codes_by_error_category)
*
* For authorization errors the query/mutation code can do just "throw ApiException::Unauthorized()".
*
* @package Automattic\WooCommerce\Internal\GraphQL
*/
class ApiException extends \Exception implements ClientAware {
/**
* The category of the error.
*
* @var string
*/
private $category;
/**
* ApiException constructor.
*
* @param string $message The error message to be included in the response.
* @param string $category The error category. Must be one of the keys of Main::$status_codes_by_error_category.
* @param int $code The error code, unused by the GraphQL infrastructure.
* @param Throwable|null $previous The previous exception, unused by the GraphQL infrastructure.
*/
public function __construct( $message = '', $category = 'request', $code = 0, Throwable $previous = null ) {
parent::__construct( $message, $code, $previous );
$this->category = $category;
}
/**
* This must be true for the error message to be included in the response.
*
* @return bool True, so the error message will be included in the response.
*/
public function isClientSafe() {
return true;
}
/**
* Returns the category of the error.
*
* @return string The category of the error.
*/
public function getCategory() {
return $this->category;
}
/**
* Returns a new ApiException object with the "authorization" category.
*
* @param null|string $message The error message to be included in the response.
* @return ApiException The new instance of the exception.
*/
public static function Unauthorized( $message = null ) {
return new ApiException( is_null( $message ) ? "You don't have permission for the requested operation" : $message, 'authorization' );
}
}

View File

@ -0,0 +1,112 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use GraphQL\Type\Definition\EnumType;
/**
* Base class for the GraphQL enumeration types.
*
To create a new enumeration type:
*
* 1. Add a new class inheriting this one in the "EnumerationTypes" folder/namespace.
* The name of the class should be the same of the mutation plus "InputType".
* 2. Add the class name to "$provides" in "GraphqlTypesServiceProvider"
* so that it can be resolved with the dependency injection container.
*/
abstract class BaseEnumType extends EnumType {
/**
* An instance of the dependency injection container.
* Can be used by derived classes.
*
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* Creates a new instance of the class.
*/
public function __construct() {
$this->container = wc_get_container();
$config = array(
'name' => $this->get_name(),
'description' => $this->get_description(),
'values' => $this->get_enum_values(),
);
parent::__construct( $config );
}
/**
* Get the GraphQL name of the type.
*
* The default name is the one of the class without the "Type" suffix.
* Derived classes can override this but they shouldn't without a good reason.
*
* @return string The GraphQL name of the type.
*/
public function get_name() {
return $this->tryInferName();
}
/**
* Returns a comma separated list of the value names, each enclosed in "`".
* Derived classes can use this for field descriptions.
*
* @return string A comma separated list of the value names, each enclosed in "`".
*/
public function get_comma_separated_value_names() {
return '`' . implode( '`, `', $this->get_enum_value_names() ) . '`';
}
/**
* Get the names of the enumeration values.
*
* @return array The names of the enumeration values.
*/
public function get_enum_value_names() {
$enum_values = $this->get_enum_values();
$names = array();
foreach ( $enum_values as $key => $value ) {
if ( is_string( $value ) ) {
$names[] = $value;
} elseif ( is_string( $key ) ) {
$names[] = $key;
} else {
$names[] = $value['name'];
}
}
return $names;
}
/**
* Get the GraphQL description of the enumeration type.
*
* @return string The GraphQL description of the enumeration type.
*/
abstract public function get_description();
/**
* Get the definition of the enumeration values.
*
* Example:
*
* array(
* 'value_1' => array(
* 'description' => 'The meaning of the first value.',
* ),
* 'value_2' => array(
* 'description' => 'The meaning of the second value.',
* );
*
* See https://webonyx.github.io/graphql-php/type-system/enum-types/ for the full syntax.
*
* @return array The definition of the enumeration values.
*/
abstract public function get_enum_values();
}

View File

@ -0,0 +1,90 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use Automattic\WooCommerce\Utilities\StringUtil;
use GraphQL\Type\Definition\InputObjectType;
/**
* Base class for the GraphQL input types for mutations.
*
* To create a new input type:
*
* 1. Add a new class inheriting this one in the "InputTypes" folder/namespace.
* The name of the class should be the same of the mutation plus "InputType".
* 2. Add the class name to "$provides" in "GraphqlTypesServiceProvider"
* so that it can be resolved with the dependency injection container.
*/
abstract class BaseInputType extends InputObjectType {
/**
* An instance of the dependency injection container.
* Can be used by derived classes.
*
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* Creates a new instance of the class.
*/
public function __construct() {
$this->container = wc_get_container();
$config = array(
'name' => $this->get_name(),
'description' => $this->get_description(),
'fields' => $this->get_fields(),
);
parent::__construct( $config );
}
/**
* Get the GraphQL name of the input type.
*
* The default name is the one of the class without the "Type" suffix.
* Derived classes can override this but they shouldn't without a good reason.
*
* @return string The GraphQL name of the input type.
*/
public function get_name() {
return $this->tryInferName();
}
/**
* Get the GraphQL description of the input type.
*
* By default the name is "Input type for the (class name without InputType suffix) mutation."
* Derived classes can override this if necessary.
*
* @return string The GraphQL description of the enumeration type.
*/
public function get_description() {
$my_class_name = StringUtil::class_name_without_namespace( get_class( $this ) );
$mutation_name = preg_replace( '~InputType$~', '', $my_class_name );
return "Input type for the {$mutation_name} mutation.";
}
/**
* Get the fields of the input type.
*
* Example:
*
* array(
* 'name' => array(
* 'type' => Type::nonNull( Type::string() ),
* 'description' => 'Name of the object being created.',
* ),
* 'kind' => array(
* 'type' => $this->container->get( KindEnumType::class ),
* 'description' => 'Kind of the object being created. Possible types: ' . $this->container->get( KindEnumType::class )->get_enum_value_names(),
* )
* );
*
* See "Input object type" in https://webonyx.github.io/graphql-php/type-system/input-types/ for the full syntax.
*
* @return mixed
*/
abstract public function get_fields();
}

View File

@ -0,0 +1,134 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use Automattic\WooCommerce\Utilities\StringUtil;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
/**
* Base class for mutation types.
*
* To create a new mutation:
*
* 1. Add a new class inheriting this one in the "MutationTypes" folder/namespace.
* 2. Add the class name to "get_object_type_classes" in "RootMutationType".
* 3. Add the class name to "$provides" in "GraphqlTypesServiceProvider"
* so that it can be resolved with the dependency injection container.
*/
abstract class BaseMutationType extends ObjectType {
/**
* An instance of the dependency injection container.
* Can be used by derived classes.
*
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* Creates a new instance of the class.
*/
public function __construct() {
$this->container = wc_get_container();
$config = array(
'name' => $this->get_name(),
'description' => $this->get_description(),
'fields' => $this->get_fields(),
'args' => $this->get_args(),
);
parent::__construct( $config );
}
/**
* Get the GraphQL name of the mutation type.
*
* The default name is the one of the class without the "Type" suffix.
* Derived classes can override this but they shouldn't without a good reason.
*
* @return string The GraphQL name of the mutation type.
*/
public function get_name() {
return $this->tryInferName();
}
/**
* Get the definition of the GraphQL arguments that the mutation will accept for its execution.
*
* By default only one argument will be accepted, its name will be "input"
* and its type will be defined by a class with the same name plus the "InputType" suffix,
* derived classes can add more arguments or replace the "input" argument as appropriate.
*
* See "Field arguments" in https://webonyx.github.io/graphql-php/type-system/object-types/ for the full syntax.
*
* @return array Definition of the GraphQL arguments that the mutation accepts for its execution.
*/
public function get_args() {
$my_class_name = StringUtil::class_name_without_namespace( get_class( $this ) );
$input_type_name = $my_class_name . 'InputType';
$input_type = Main::resolve_type( $input_type_name );
return array(
'input' => $input_type,
);
}
/**
* Get the definition GraphQL fields of the mutation type
* (the fields of the object that will be returned when the mutation completes successfully).
*
* By default only the "id" field of the created object will be returned,
* derived classes can override this as needed.
*
* See "Mutations" in https://webonyx.github.io/graphql-php/type-system/input-types/ for the full syntax.
*
* @return array The definition fields of the object returned by the mutation.
*/
public function get_fields() {
return array(
'id' => array(
'type' => Type::nonNull( Type::int() ),
'description' => 'The unique identifier of the resource created.',
),
);
}
/**
* Get the GraphQL description of the mutation type.
*
* @return string The GraphQL description of the mutation type.
*/
abstract public function get_description();
/**
* Execute the operation defined for the mutation.
*
* The method will receive an array with argument values consistent with the definition returned by "get_args"
* (for the default implementation it will only have one key, "input", which will in turn hold an array of data)
* and must return an object/array with public properties/keys consistent with the definition returned by "get_fields"
* (for the default implementation this will have one single property/key, "id", holding a number).
*
* Values for individual fields in the output can alternatively be obtained from the generated object if they define
* a "resolve" callback, e.g.:
*
* 'id' => array(
* 'type' => Type::nonNull( Type::id() ),
* 'description' => 'Unique identifier for the resource created.',
* 'resolve' => function( $output_from_execute ) {
* return $output_from_execute['item_id'];
* },
* ),
*
* If the operation can't be completed the method must throw an ApiException. For authorization errors
* it can be just "throw ApiException::Unauthorized()".
*
* @param array $args Arguments for the mutation operation.
* @param mixed $context Context for the mutation operation, currently unused.
* @param ResolveInfo $info The resolve info passed from the GraphQL engine.
* @return array The result of the mutation execution.
*/
abstract public function execute( $args, $context, ResolveInfo $info);
}

View File

@ -0,0 +1,192 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
/**
* Base class for queries that return a list of items.
*
* The arguments for the query will always be "offset" and "count" (more can be added via "get_extra_args")
* and the returned object will always have two fields: "total_count" (a number)
* and "items" (a list of items whose fields will be defined by the type specified by "get_item_type_class_name").
*
* Derived classes should be a name in plural, and the type name returned by "get_item_type_class_name"
* should be the same name in singular, unless there's a good reason to follow a different approach
* in some particular case.
*
* To create a new list query, proceed as when inheriting from BaseQueryType.
*/
abstract class BaseQueryListType extends BaseQueryType {
/**
* Maximum amount of results that will be returned if no "count" argument is supplied.
*/
const MAX_RESULTS_PER_QUERY = 100;
/**
* Holds the instance of the class defining the type of the objects returned.
*
* @var mixed
*/
protected $object_type_instance;
/**
* Creates a new instance of the class.
*/
public function __construct() {
$this->container = wc_get_container();
$this->object_type_instance = $this->container->get( $this->get_item_type_class_name() );
parent::__construct( $this->get_args() );
}
/**
* Get the GraphQL description of the query.
*
* By default it will be "A collection of (object type name)", derived classes can override this if necesessary.
*
* @return string The GraphQL description of the query.
*/
public function get_description() {
$object_type_name = $this->container->get( $this->get_item_type_class_name() )->get_name();
return 'A collection of ' . $object_type_name . '.';
}
/**
* Get the definition GraphQL fields of the query type
* (the fields of the object that will be returned when the query completes successfully).
* These are always "total_count" (a number) and "items" (a list of objects whose fields will be defined
* by the object specified by "get_item_type_class_name").
*
* See "get_fields" in "BaseQueryType" for an example.
* See "Field configuration options" in https://webonyx.github.io/graphql-php/type-system/object-types/ for the full syntax.
*
* @return array[]
*/
public function get_fields() {
return array(
'total_count' => array(
'type' => Type::int(),
'description' => 'The total count.',
),
'items' => array(
'type' => Type::listOf( $this->object_type_instance ),
'description' => 'The items themselves.',
),
);
}
/**
* Get the definition of the GraphQL arguments that the query will accept for its resolution.
*
* The arguments will always include "offset" (how many items to skip from the data store before
* including items in the output) and "count" (the maximum number of items to return).
* Extra arguments can be added via "get_extra_args".
*
* @return array Definition of the GraphQL arguments that the query accepts for its resolution.
*/
public function get_args() {
$args = array(
'offset' => array(
'type' => Type::int(),
'description' => 'Specifies how many items to skip from the data store before including items in the output.',
'defaultValue' => 0,
),
'count' => array(
'type' => Type::int(),
'description' => 'Specifies the maximum number of items to return.',
'defaultValue' => self::MAX_RESULTS_PER_QUERY,
),
);
return array_merge( $args, $this->get_extra_args() );
}
/**
* Specifies any extra arguments that the query will accept besides "offset" and "count",
* none by default.
*
* See "get_args" in "BaseQueryType" for an example.
* See "Field arguments" in https://webonyx.github.io/graphql-php/type-system/object-types/ for the full syntax.
*
* @return array Extra arguments that the query will accept besides "offset" and "count".
*/
public function get_extra_args() {
return array();
}
/**
* Resolve the query defined by the type.
*
* The actual resolution will be delegated to the "resolve_total_count" and "resolve_items" methods.
*
* @param array $args Arguments for the query operation.
* @param mixed $context Context for the query operation, currently unused.
* @param ResolveInfo $info The resolve info passed from the GraphQL engine.
* @return array The result of the query execution.
*/
public function resolve( $args, $context, ResolveInfo $info ) {
$extra_args = $args;
unset( $extra_args['offset'] );
unset( $extra_args['count'] );
$result = array();
$field_selection = $info->getFieldSelection();
if ( array_key_exists( 'total_count', $field_selection ) ) {
$result['total_count'] = $this->resolve_total_count( $extra_args );
}
if ( array_key_exists( 'items', $field_selection ) ) {
$result['items'] = $this->resolve_items(
$args['offset'],
$args['count'],
$extra_args,
$field_selection['items']
);
}
return $result;
}
/**
* Get the name of the class defining the type of the items that the query will return
* (must be a query type class implementing BaseQueryType).
*
* That name should be the same as the class but in singular, unless there's a good reason
* to follow a different approach in some particular case.
*
* @return string Full (namespaced) name of the class defining the type of the items that the query will return.
*/
abstract public function get_item_type_class_name();
/**
* Get the count of existing items of the type defined by "get_item_type_class_name".
* This method will be executed only if the query requests the "total_count" field.
*
* The method will receive an array with argument values consistent with the definition returned by "get_extra_args"
* (for the default implementation it will just be an empty array) and must return a count of items according
* to these arguments (or the absolute total of existing items if there are none).
*
* @param array $extra_args Any arguments for counting the existing items.
* @return int The count of existing items.
*/
abstract public function resolve_total_count( $extra_args );
/**
* Resolve the query defined by the type.
* This method will be executed only if the query requests the "items" field.
*
* This works pretty much as BaseObjectType::resolve, except that it returns an array of objects/arrays
* (of the type defined by "get_item_type_class_name") according to the offset, count and extra arguments
* supplied.
*
* @param int $offset How many items to skip from the data store before including items in the output.
* @param int $count Maximum amount of items to return.
* @param array $extra_args Any extra arguments for the query, consistent with the definition specified by "get_extra_args".
* @param array $field_selection The result from executing $info->getFieldSelection()['items'] in "resolve".
* @return array An array of objects/arrays, each representing an item defined by the type specified by "get_item_type_class_name".
*/
abstract public function resolve_items( $offset, $count, $extra_args, $field_selection );
}

View File

@ -0,0 +1,134 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
/**
* Base class for query types.
*
* To create a new query:
*
* 1. Add a new class inheriting this one in the "QueryTypes" folder/namespace.
* 2. Add the class name to "get_object_type_classes" in "RootQueryType".
* 3. Add the class name to "$provides" in "GraphqlTypesServiceProvider"
* so that it can be resolved with the dependency injection container.
*/
abstract class BaseQueryType extends ObjectType {
/**
* An instance of the dependency injection container.
* Can be used by derived classes.
*
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* Creates a new instance of the class.
*/
public function __construct() {
$this->container = wc_get_container();
$config = array(
'name' => $this->get_name(),
'description' => $this->get_description(),
'fields' => $this->get_fields(),
'args' => $this->get_args(),
);
parent::__construct( $config );
}
/**
* Get the GraphQL name of the query type.
*
* The default name is the one of the class without the "Type" suffix.
* Derived classes can override this but they shouldn't without a good reason.
*
* @return string The GraphQL name of the query type.
*/
public function get_name() {
return $this->tryInferName();
}
/**
* Get the definition of the GraphQL arguments that the query will accept for its resolution.
* By default no arguments will be accepted.
*
* Example:
*
* array(
* 'id' => array(
* 'type' => Type::nonNull( Type::id() ),
* 'description' => 'Unique identifier of the resource to be obtained.',
* )
* );
*
* See "Field arguments" in https://webonyx.github.io/graphql-php/type-system/object-types/ for the full syntax.
*
* @return array Definition of the GraphQL arguments that the query accepts for its resolution.
*/
public function get_args() {
return array();
}
/**
* Get the definition GraphQL fields of the query type
* (the fields of the object that will be returned when the query completes successfully).
*
* Example:
*
* array(
* 'id' => array(
* 'type' => Type::nonNull( Type::id() ),
* 'description' => 'Unique identifier of the resource.',
* ),
* 'name' => array(
* 'type' => Type::nonNull( Type::string() ),
* 'description' => 'Name of the resource.',
* ),
* );
*
* See "Field configuration options" in https://webonyx.github.io/graphql-php/type-system/object-types/ for the full syntax.
*
* @return array The definition fields of the object returned by the query.
*/
abstract public function get_fields();
/**
* Get the GraphQL description of the query type.
*
* @return string The GraphQL description of the query type.
*/
abstract public function get_description();
/**
* Resolve the query defined by the type.
*
* The method will receive an array with argument values consistent with the definition returned by "get_args"
* (for the default implementation it will just be an empty array)
* and must return an object/array with public properties/keys consistent with the definition returned by "get_fields".
*
* Values for individual fields in the output can alternatively be obtained from the generated object if they define
* a "resolve" callback, e.g.:
*
* 'id' => array(
* 'type' => Type::nonNull( Type::id() ),
* 'description' => 'Unique identifier of the resource.',
* 'resolve' => function( $output_from_resolve ) {
* return $output_from_resolve['item_id'];
* },
* ),
*
* If the query can't be completed the method must throw an ApiException. For authorization errors
* it can be just "throw ApiException::Unauthorized()".
*
* @param array $args Arguments for the query operation.
* @param mixed $context Context for the query operation, currently unused.
* @param ResolveInfo $info The resolve info passed from the GraphQL engine.
* @return array The result of the query execution.
*/
abstract public function resolve( $args, $context, ResolveInfo $info);
}

View File

@ -0,0 +1,139 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\ResolveInfo;
/**
* Base class for defining the query and mutation root types for the API.
*/
abstract class BaseRootType extends ObjectType {
/**
* An instance of the dependency injection container.
*
* @var \Psr\Container\ContainerInterface
*/
private $container;
/**
* Creates a new instance of the class.
*/
public function __construct() {
$this->container = wc_get_container();
$config = array(
'name' => $this->get_name(),
'description' => $this->get_description(),
'fields' => $this->get_fields(),
'resolveField' => function( $value, $args, $context, ResolveInfo $info ) {
return $this->resolve_field( $value, $args, $context, $info );
},
);
parent::__construct( $config );
}
/**
* Resolve a given query or execute a given mutation.
* This is done by first obtaining the instance of the type corresponding to the requested operation
* (by asking the dependency injection container for a class with the same name), and then
* executing the "resolve" or "execute" method on it.
*
* Authorization is done by checking that there's an authenticated user with read/write permissions as appropriate
* (read for queries, write for mutations), the same mechanism of consumer key+consumer secret from the WooCommerce
* REST API is used (the very same set of credentials, actually). Additionally there's a
* "woocommerce_graphql_check_permissions" hook that works similarly to "woocommerce_rest_check_permissions".
*
* @param mixed $value Root value supplied to "GraphQL::executeQuery", currently unused.
* @param array $args Arguments for the resolution/execution.
* @param mixed $context Context object supplied to "GraphQL::executeQuery", currently unused.
* @param ResolveInfo $info Resolve info generated by "GraphQL::executeQuery".
* @return mixed Result of executing "resolve" or "execute" on the target query/mutation instance.
* @throws ApiException User lacks required permissions, or exception thrown by "resolve"/"execute" on the target query/mutation instance.
*/
private function resolve_field( $value, $args, $context, ResolveInfo $info ) {
$user_has_permission = \WC_REST_Authentication::instance()->current_user_has_permission( $this->get_required_permission() );
// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
/**
* Filter to give or remove the permission to execute a given GraphQL query/mutation to the current user.
*
* @since ?
*
* @param bool Whether the user has initially the permission or not based on the existing authorization data for the user.
* @param string The name of the query or mutation that the user wants to execute.
* @param string The type of operation to perform, "read" or "write".
* @param array The GraphQL arguments supplied for the execution of the query or mutation.
* @return bool True if the user should be allowed to perform the query or mutation, false otherwise.
*/
$user_has_permission = apply_filters( 'woocommerce_graphql_check_permissions', $user_has_permission, $info->fieldName, $this->get_required_permission(), $args );
if ( ! $user_has_permission ) {
throw ApiException::Unauthorized();
}
$resolve_method = $this->get_resolve_method_name();
return Main::resolve_type( $info->fieldName )->$resolve_method( $args, $context, $info );
// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
}
/**
* Get the fields for the root query/mutation.
* There will be one for each instance of the classes returned by "get_object_type_classes".
*
* @return array
*/
private function get_fields() {
$fields = array();
$class_names = $this->get_object_type_classes();
foreach ( $class_names as $class_name ) {
$type_object = $this->container->get( $class_name );
$fields[ $type_object->get_name() ] = array(
'type' => $type_object,
'description' => $type_object->get_description(),
'args' => $type_object->get_args(),
);
}
return $fields;
}
/**
* Get the name of this root query, will be either "Query" or "Mutation".
*
* @return string Name of this root query.
*/
abstract protected function get_name();
/**
* Get the description of this root query.
*
* @return string Description of this root query.
*/
abstract protected function get_description();
/**
* Get the required permission for the queries/mutations in this root query, will be either "read" or "write".
*
* @return string Required permission for the queries in this root query.
*/
abstract protected function get_required_permission();
/**
* Get the method to execute when resolving a query or mutation, will be either "resolve" or "execute".
*
* @return string Method to execute when resolving a query or mutation.
*/
abstract protected function get_resolve_method_name();
/**
* Get the full names of the classes defining the queries/mutations for this root query.
*
* @return array Full names of the classes defining the queries/mutations for this root query.
*/
abstract protected function get_object_type_classes();
}

View File

@ -0,0 +1,42 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL\EnumTypes;
use Automattic\WooCommerce\Internal\GraphQL\BaseEnumType;
/**
* Class for the ProductAttributeOrderBy enumeration type.
*/
class ProductAttributeOrderBy extends BaseEnumType {
/**
* Get the description of the type.
*
* @return string The description of the type.
*/
public function get_description() {
return 'Default sort order for product attribute terms.';
}
/**
* Get the possible values for the enumeration.
*
* @return array The possible values for the enumeration.
*/
public function get_enum_values() {
return array(
'menu_order' => array(
'description' => 'Order by name.',
),
'name' => array(
'description' => 'Order by name.',
),
'name_num' => array(
'description' => 'Order by name (numeric).',
),
'id' => array(
'description' => 'Order by id.',
),
);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL\InputTypes;
use Automattic\WooCommerce\Internal\GraphQL\BaseInputType;
use GraphQL\Type\Definition\Type;
/**
* Class for the AddProductAttributeTerm input type.
*/
class AddProductAttributeTermInputType extends BaseInputType {
/**
* Get the fields of the type.
*
* @return array Fields of the type.
*/
public function get_fields() {
return array(
'attribute_id' => array(
'type' => Type::nonNull( Type::id() ),
'description' => 'Unique identifier of the product attribute the term will be added to.',
),
'name' => array(
'type' => Type::nonNull( Type::string() ),
'description' => 'Term name.',
),
'slug' => array(
'type' => Type::string(),
'description' => 'An alphanumeric identifier for the resource unique to its type.',
),
'description' => array(
'type' => Type::string(),
'description' => 'HTML description of the resource.',
),
// TODO: Add 'menu order' when implementing insertion in AddProductAttributeTerm.
/*
'menu_order' => [
'type' => Type::int(),
'description' => 'Menu order, used to custom sort the resource.'
]
*/
);
}
}

View File

@ -0,0 +1,195 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use GraphQL\GraphQL;
use GraphQL\Type\Schema;
use GraphQL\Error\ClientAware;
use GraphQL\Error\Debug;
defined( 'ABSPATH' ) || exit;
/**
* The main entry point class for the GraphQL API.
*
* The GraphQL API is enabled when the "woocommerce_graphql_api_enabled" option has a value of "yes",
* this option can be set via UI from Woo settings - Advanced - GraphQL API.
*
* Once "initialize" is called the API is available via POST at endpoint /wp-json/wc/graphql/api.
* Authorization is done in the "RootQueryType" and "RootMutationType" classes.
*
* Request bodies must consist of a JSON object having a "query" key and optionally a "variables" key.
*
* By default unexpected exceptions (those that are not ApiException) will be returned as a generic
* "Internal server error" message, see "get_debug_config" method for how to enable verbose errors mode.
*/
class Main {
/**
* An instance of the dependency injection container.
*
* @var \Psr\Container\ContainerInterface
*/
private static $container;
/**
* Namespaces (relative to the one of this class) that contain GraphQL related classes
* to be resolved via dependency injection container.
*
* @var string List of namespaces.
*/
private static $subnamespaces = array( 'EnumTypes', 'QueryTypes', 'MutationTypes', 'InputTypes' );
/**
* Array mapping "ApiException" categories to HTTP response status codes.
* Thrown "ApiException"s should always have a category matching one of the keys of this array.
*
* @var array Array of "ApiException" category => HTTP response status code.
*/
private static $status_codes_by_error_category = array(
'request' => 400,
'graphql' => 400,
'authorization' => 401,
'internal' => 500,
);
/**
* Initialize the GraphQL API entry point.
*/
public static function initialize() {
if ( 'yes' !== get_option( 'woocommerce_graphql_api_enabled' ) ) {
return;
}
self::$container = wc_get_container();
add_action(
'rest_api_init',
function () {
register_rest_route(
'wc/graphql',
'api',
array(
'methods' => 'POST',
'callback' => function( $request ) {
return call_user_func( self::class . '::handle_request', $request );
},
'permission_callback' => '__return_true',
)
);
}
);
}
/**
* Get an instance of a class given its non-namespaced name.
*
* @param string $name Non-namespaced ame of the class to get an instance from.
* @return mixed Instance of the requested class.
* @throws \Exception The type can't be resolved.
*/
public static function resolve_type( $name ) {
foreach ( self::$subnamespaces as $namespace ) {
$full_name = __NAMESPACE__ . '\\' . $namespace . '\\' . $name;
if ( self::$container->has( $full_name ) ) {
return self::$container->get( $full_name );
}
}
throw new \Exception( "There's no way to resolve the type '" . $name . "'." );
}
/**
* Handle a HTTP request for the GraphQL API entry point.
*
* @param \WP_REST_Request $request Request to handle.
* @return array|\WP_REST_Response Response to return to the client.
* @throws ApiException Does not actually throw any exception, but phpcs needs this comment.
*/
private static function handle_request( \WP_REST_Request $request ) {
$error_category = null;
try {
$input = json_decode( $request->get_body(), true );
if ( is_null( $input ) ) {
throw new ApiException( 'Invalid input JSON: ' . json_last_error_msg() );
}
if ( ! isset( $input['query'] ) ) {
throw new ApiException( "Invalid input JSON: no 'query' element" );
}
$query = $input['query'];
$variable_values = isset( $input['variables'] ) ? $input['variables'] : null;
$schema = new Schema(
array(
'query' => self::$container->get( RootQueryType::class ),
'mutation' => self::$container->get( RootMutationType::class ),
'typeLoader' => function( $name ) {
return self::resolve_type( $name );
},
)
);
$result = GraphQL::executeQuery( $schema, $query, null, null, $variable_values );
if ( ! empty( $result->errors ) ) {
$error_category = current( $result->errors )->getCategory();
}
$output = $result->toArray( self::get_debug_config() );
} catch ( \Exception $e ) {
$error_category = $e instanceof ClientAware ? $e->getCategory() : 'internal';
if ( self::get_debug_config() ) {
$output = array(
'errors' => array(
array(
'message' => $e->getMessage(),
'category' => $error_category,
'trace' => $e->getTrace(),
),
),
);
} else {
$output = array(
'errors' => array(
array(
'message' => 'Internal server error',
'category' => $error_category,
),
),
);
}
}
if ( $error_category ) {
return new \WP_REST_Response( $output, self::$status_codes_by_error_category[ $error_category ] );
} else {
return $output;
}
}
/**
* Get debug flags for GraphQL::executeQuery()->toArray.
*
* When debug flags are returned error output will always include the exception error message
* (even for exceptions that are not ApiException) and the output will also include
* a full stack trace.
*
* Debug flags are returned when "?verbose_errors" is added to the query string AND
* either the user is in the "administrator" role OR the WP_DEBUG constant is set.
*
* @return false|int Debug flags or false.
*/
private static function get_debug_config() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['verbose_errors'] ) ) {
return false;
}
if ( wc_current_user_has_role( 'administrator' ) || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) {
return Debug::INCLUDE_DEBUG_MESSAGE | Debug::INCLUDE_TRACE;
}
return false;
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL\MutationTypes;
use Automattic\WooCommerce\Internal\GraphQL\ApiException;
use Automattic\WooCommerce\Internal\GraphQL\BaseMutationType;
use GraphQL\Type\Definition\ResolveInfo;
/**
* Class for the AddProductAttributeTerm mutation type.
*/
class AddProductAttributeTerm extends BaseMutationType {
/**
* Get the description of the type.
*
* @return string The description of the type.
*/
public function get_description() {
return 'Creates a new product attribute term.';
}
/**
* Execute the mutation.
*
* @param array $args Arguments for the mutation operation.
* @param mixed $context Context passed from the caller, currently unused.
* @param ResolveInfo $info The resolve info passed from the GraphQL engine.
* @return array The result of the mutation execution.
* @throws ApiException Can't execute the mutation/error when executing the mutation.
*/
public function execute( $args, $context, ResolveInfo $info ) {
// TODO: Implement inserting 'menu order' too.
$input = $args['input'];
$attribute = wc_get_attribute( $input['attribute_id'] );
if ( is_null( $attribute ) ) {
throw new ApiException( 'Invalid attribute id' );
}
$insert_args = array();
if ( isset( $input['description'] ) ) {
$insert_args['description'] = $input['description'];
}
if ( isset( $input['slug'] ) ) {
$insert_args['slug'] = $input['slug'];
}
$term = wp_insert_term( $input['name'], $attribute->slug, $insert_args );
if ( is_wp_error( $term ) ) {
throw new ApiException( "Can't create term: " . $term->get_error_message() );
}
return array( 'id' => $term['term_id'] );
}
}

View File

@ -0,0 +1,125 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL\QueryTypes;
use Automattic\WooCommerce\Internal\GraphQL\ApiException;
use Automattic\WooCommerce\Internal\GraphQL\BaseQueryType;
use Automattic\WooCommerce\Internal\GraphQL\QueryTypes\ProductAttributeTerms;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
use Automattic\WooCommerce\Internal\GraphQL\EnumTypes\ProductAttributeOrderBy;
/**
* Class for the ProductAttribute query type.
*/
class ProductAttribute extends BaseQueryType {
/**
* Get the description of the query.
*
* @return string The description of the query.
*/
public function get_description() {
return 'A product attribute.';
}
/**
* Get the fields of the query.
*
* @return array The fields of the query.
*/
public function get_fields() {
$terms_args = $this->container->get( ProductAttributeTerms::class )->get_args();
unset( $terms_args['taxonomy'] );
$order_by_instance = $this->container->get( ProductAttributeOrderBy::class );
$order_by_comma_separated = $order_by_instance->get_comma_separated_value_names();
$order_by_default = $order_by_instance->get_enum_value_names();
return array(
'id' => array(
'type' => Type::nonNull( Type::id() ),
'description' => 'Unique identifier for the resource.',
),
'name' => array(
'type' => Type::nonNull( Type::string() ),
'description' => 'Attribute name.',
),
'slug' => array(
'type' => Type::string(),
'description' => 'An alphanumeric identifier for the resource unique to its type.',
),
'type' => array(
'type' => Type::string(),
'description' => 'Type of attribute. By default only `select` is supported.',
),
'order_by' => array(
'type' => $this->container->get( ProductAttributeOrderBy::class ),
'description' => 'Default sort order. Options: ' . $order_by_comma_separated . '. Default is `' . $order_by_default . '`.',
),
'has_archives' => array(
'type' => Type::boolean(),
'description' => 'Enable/Disable attribute archives. Default is `false`.',
),
'terms' => array(
'type' => $this->container->get( ProductAttributeTerms::class ),
'description' => 'The terms for this attribute.',
'args' => $terms_args,
'resolve' => function( $resolved_attribute, $args, $context, ResolveInfo $info ) {
$args['taxonomy'] = $resolved_attribute['slug'];
return $this->container->get( ProductAttributeTerms::class )->resolve( $args, $context, $info );
},
),
);
}
/**
* Get the arguments for the query.
*
* @return array The arguments for the query.
*/
public function get_args() {
return array(
'id' => array(
'type' => Type::nonNull( Type::id() ),
'description' => 'Unique identifier for the resource.',
),
);
}
/**
* Execute the query.
*
* @param array $args Arguments for the query.
* @param mixed $context Context passed from the caller, currently unused.
* @param ResolveInfo $info The resolve info passed from the GraphQL engine.
* @return array The result of the query execution.
* @throws ApiException Can't execute the query/error when executing the query.
*/
public function resolve( $args, $context, ResolveInfo $info ) {
$attribute = (array) wc_get_attribute( $args['id'] );
if ( is_null( $attribute ) ) {
throw new ApiException( "Can't get this term" );
}
if ( array_key_exists( 'terms', $info->getFieldSelection() ) ) {
$terms = get_terms(
$attribute->slug,
array(
'hide_empty' => false,
'fields' => 'all',
'count' => true,
)
);
$attribute['terms'] = array_map(
function( $term ) {
return (array) $term;
},
$terms
);
}
return $attribute;
}
}

View File

@ -0,0 +1,97 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL\QueryTypes;
use Automattic\WooCommerce\Internal\GraphQL\ApiException;
use Automattic\WooCommerce\Internal\GraphQL\BaseQueryType;
use GraphQL\Type\Definition\ResolveInfo;
use GraphQL\Type\Definition\Type;
/**
* Class for the ProductAttributeTerm query type.
*/
class ProductAttributeTerm extends BaseQueryType {
/**
* Get the description of the query.
*
* @return string The description of the query.
*/
public function get_description() {
return 'A term of a product attribute.';
}
/**
* Get the fields of the query.
*
* @return array The fields of the query.
*/
public function get_fields() {
return array(
'id' => array(
'type' => Type::nonNull( Type::id() ),
'description' => 'Unique identifier for the resource.',
'resolve' => function( $resolved_term ) {
return $resolved_term['term_id'];
},
),
'name' => array(
'type' => Type::nonNull( Type::string() ),
'description' => 'Term name',
),
'slug' => array(
'type' => Type::string(),
'description' => 'An alphanumeric identifier for the resource unique to its type.',
),
'description' => array(
'type' => Type::string(),
'description' => 'HTML description of the resource.',
),
'menu_order' => array(
'type' => Type::int(),
'description' => 'Menu order, used to custom sort the resource.',
),
'count' => array(
'type' => Type::int(),
'description' => 'Number of published products for the resource.',
),
'taxonomy' => array(
'type' => Type::string(),
'description' => 'The taxonomy this term belongs to.',
),
);
}
/**
* Get the arguments for the query.
*
* @return array The arguments for the query.
*/
public function get_args() {
return array(
'id' => array(
'type' => Type::nonNull( Type::id() ),
'description' => 'Unique identifier for the resource.',
),
);
}
/**
* Execute the query.
*
* @param array $args Arguments for the query.
* @param mixed $context Context passed from the caller, currently unused.
* @param ResolveInfo $info The resolve info passed from the GraphQL engine.
* @return array The result of the query execution.
* @throws ApiException Can't execute the query/error when executing the query.
*/
public function resolve( $args, $context, ResolveInfo $info ) {
$term = get_term( $args['id'], '', ARRAY_A );
if ( ! is_array( $term ) ) {
throw new ApiException( "Can't get this term" );
}
return $term;
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL\QueryTypes;
use Automattic\WooCommerce\Internal\GraphQL\ApiException;
use Automattic\WooCommerce\Internal\GraphQL\BaseQueryListType;
use GraphQL\Type\Definition\Type;
/**
* Class for the ProductAttributeTerms list query type.
*/
class ProductAttributeTerms extends BaseQueryListType {
/**
* Get the type of the items that the returned list will contain.
*
* @return string The type of the items that the returned list will contain.
*/
public function get_item_type_class_name() {
return ProductAttributeTerm::class;
}
/**
* Extra arguments for the query (other than "offset" and "count").
*
* @return array $extra_args Any extra arguments for the query.
*/
public function get_extra_args() {
return array(
'taxonomy' => array(
'type' => Type::string(),
'description' => 'Return only the terms for the given taxonomy.',
),
);
}
/**
* Get the total count of available items.
*
* @param array $extra_args Any extra arguments passed to the query (other than "offset" and "count").
* @return int The total count of available items.
*/
public function resolve_total_count( $extra_args ) {
return $this->resolve_terms(
array(
'hide_empty' => false,
'fields' => 'count',
),
$extra_args
);
}
/**
* Resolve the query.
*
* @param int $offset How many items to skip.
* @param int $count How many items to return.
* @param array $extra_args Any extra arguments passed to the query (other than "offset" and "count").
* @param array $field_selection The part of the ResolveInfo object wupplied by the GraphQL engine that corresponds to the 'items' object.
* @return array The list of items resolved.
* @throws ApiException When resolve_terms doesn't retun an array.
*/
public function resolve_items( $offset, $count, $extra_args, $field_selection ) {
$result = $this->resolve_terms(
array(
'hide_empty' => false,
'fields' => 'all',
'offset' => $offset,
'number' => $count,
),
$extra_args
);
if ( ! is_array( $result ) ) {
throw new ApiException( "Can't get terms" );
}
return array_map(
function( $term ) {
return (array) $term;
},
$result
);
}
/**
* Auxiliary method to fetch the terms according to the supplied arguments.
*
* @param array $args_for_get_terms Arguments for the get_terms call.
* @param array $extra_args Extra arguments supplied for the query.
* @return mixed The result of executing get_terms.
* @throws ApiException When get_terms returns an error.
*/
private function resolve_terms( $args_for_get_terms, $extra_args ) {
if ( isset( $extra_args['taxonomy'] ) ) {
$args_for_get_terms['taxonomy'] = $extra_args['taxonomy'];
}
$result = get_terms( $args_for_get_terms );
if ( $result instanceof \WP_Error ) {
throw new ApiException( $result->get_error_message() );
}
return $result;
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL\QueryTypes;
use Automattic\WooCommerce\Internal\GraphQL\BaseQueryListType;
/**
* Class for the ProductAttributes list query type.
*/
class ProductAttributes extends BaseQueryListType {
/**
* Get the type of the items that the returned list will contain.
*
* @return string The type of the items that the returned list will contain.
*/
public function get_item_type_class_name() {
return ProductAttribute::class;
}
/**
* Get the total count of available items.
*
* @param array $extra_args Any extra arguments passed to the query (other than "offset" and "count").
* @return int The total count of available items.
*/
public function resolve_total_count( $extra_args ) {
global $wpdb;
$sql = "SELECT COUNT(1) FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name != '' ORDER BY attribute_name ASC";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
return $wpdb->get_var( $sql );
}
/**
* Resolve the query.
*
* @param int $offset How many items to skip.
* @param int $count How many items to return.
* @param array $extra_args Any extra arguments passed to the query (other than "offset" and "count").
* @param array $field_selection The part of the ResolveInfo object wupplied by the GraphQL engine that corresponds to the 'items' object.
* @return array The list of items resolved.
*/
public function resolve_items( $offset, $count, $extra_args, $field_selection ) {
global $wpdb;
$sql = $wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}woocommerce_attribute_taxonomies WHERE attribute_name != '' ORDER BY attribute_name ASC LIMIT %d OFFSET %d",
$count,
$offset
);
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$rows = $wpdb->get_results( $sql, ARRAY_A );
return array_map(
function( $row ) {
return array(
'id' => $row['attribute_id'],
'name' => $row['attribute_label'],
'slug' => wc_attribute_taxonomy_name( $row['attribute_name'] ),
'type' => $row['attribute_type'],
'order_by' => $row['attribute_orderby'],
'has_archives' => (bool) $row['attribute_public'],
);
},
$rows
);
}
}

View File

@ -0,0 +1,61 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use Automattic\WooCommerce\Internal\GraphQL\MutationTypes\AddProductAttributeTerm;
/**
* Defines the root mutation type for the GraphQL API.
*
* All the work is actually done in the "BaseRootType" class,
* see its code for details.
*/
class RootMutationType extends BaseRootType {
/**
* Gets the classes that define mutation operations.
*
* @return string[] The classes that define mutation operations.
*/
protected function get_object_type_classes() {
return array(
AddProductAttributeTerm::class,
);
}
/**
* Gets the name of this root query.
*
* @return string The name of this root query.
*/
protected function get_name() {
return 'Mutation';
}
/**
* Gets the description of this root query.
*
* @return string The description of this root query.
*/
protected function get_description() {
return 'The root query for implementing GraphQL mutations.';
}
/**
* Gets the required permission for the mutations defined in this root query.
*
* @return string The required permission for the mutations defined in this root query.
*/
protected function get_required_permission() {
return 'write';
}
/**
* Gets the name of the method to execute in the target mutation type classes.
*
* @return string The name of the method to execute in the target mutation type classes.
*/
protected function get_resolve_method_name() {
return 'execute';
}
}

View File

@ -0,0 +1,67 @@
<?php
namespace Automattic\WooCommerce\Internal\GraphQL;
use Automattic\WooCommerce\Internal\GraphQL\QueryTypes\ProductAttribute;
use Automattic\WooCommerce\Internal\GraphQL\QueryTypes\ProductAttributes;
use Automattic\WooCommerce\Internal\GraphQL\QueryTypes\ProductAttributeTerm;
use Automattic\WooCommerce\Internal\GraphQL\QueryTypes\ProductAttributeTerms;
/**
* Defines the root query type for the GraphQL API.
*
* All the work is actually done in the "BaseRootType" class,
* see its code for details.
*/
class RootQueryType extends BaseRootType {
/**
* Gets the classes that define query operations.
*
* @return string[] The classes that define query operations.
*/
protected function get_object_type_classes() {
return array(
ProductAttribute::class,
ProductAttributes::class,
ProductAttributeTerm::class,
ProductAttributeTerms::class,
);
}
/**
* Gets the name of this root query.
*
* @return string The name of this root query.
*/
protected function get_name() {
return 'Query';
}
/**
* Gets the description of this root query.
*
* @return string The description of this root query.
*/
protected function get_description() {
return 'The query root of WooCommerce GraphQL API.';
}
/**
* Gets the required permission for the queries defined in this root query.
*
* @return string The required permission for the queries defined in this root query.
*/
protected function get_required_permission() {
return 'read';
}
/**
* Gets the name of the method to execute in the target query type classes.
*
* @return string The name of the method to execute in the target query type classes.
*/
protected function get_resolve_method_name() {
return 'resolve';
}
}

View File

@ -57,4 +57,15 @@ final class StringUtil {
return strcasecmp( $string, $ends_with ) === 0;
}
/**
* Get a class name without namespace from a full class name.
*
* @param string $full_class_name Full (namespaced) class name.
* @return string Class name without the namespace.
*/
public static function class_name_without_namespace( string $full_class_name ) {
$class_name_parts = explode( '\\', $full_class_name );
return array_pop( $class_name_parts );
}
}