Commit 6a5d8e8c7d3 for woocommerce
commit 6a5d8e8c7d3fc7d4999da4dcd3c77a32b3c715a0
Author: Néstor Soriano <konamiman@konamiman.com>
Date: Wed Apr 22 15:40:02 2026 +0200
Internalize the Webonyx GraphQL package (#64317)
Also make lib build scripts lock-respecting by default
diff --git a/plugins/woocommerce/bin/build-lib.sh b/plugins/woocommerce/bin/build-lib.sh
index d4e73015af2..4e679bbce39 100755
--- a/plugins/woocommerce/bin/build-lib.sh
+++ b/plugins/woocommerce/bin/build-lib.sh
@@ -26,8 +26,21 @@ output 6 "Building lib package"
rm -rf lib/packages lib/classes
mkdir lib/packages lib/classes
-# Running update on the lib package will automatically run Mozart
-composer update -d ./lib
+# Prefer `composer install` so pinned versions in lib/composer.lock are respected
+# and routine rebuilds don't silently bump unrelated dependencies. Fall back to
+# `composer update` only when the lock is out of sync with composer.json — i.e. a
+# developer has just added or bumped a package on purpose. Pass --update to force
+# an update (useful to pick up in-range upstream releases without editing composer.json).
+if [ "${1:-}" = "--update" ] || ! composer validate -d ./lib --check-lock --quiet; then
+ composer update -d ./lib
+else
+ composer install -d ./lib
+fi
+
+# Re-apply manual patches that Mozart overwrites on rebuild (see lib/README.md).
+if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
+ git restore lib/packages/Detection/MobileDetect.php 2>/dev/null || true
+fi
output 6 "Updating autoload files"
diff --git a/plugins/woocommerce/bin/post-update.sh b/plugins/woocommerce/bin/post-update.sh
index c6ffaf9259f..716bb2831f7 100644
--- a/plugins/woocommerce/bin/post-update.sh
+++ b/plugins/woocommerce/bin/post-update.sh
@@ -2,5 +2,11 @@
# Required for dev and build environments: generate optimized autoloaders, safe to run in background.
composer dump-autoload --optimize --quiet &
-# Required for dev environments: update tooling dependencies, not suitable to run in background.
-composer bin all update --ansi
+# Install tooling dependencies per the pinned bin locks so routine root updates don't
+# cascade-bump unrelated dev tools (phpcs, phpunit, mozart, wp-cli). Falls back to update
+# when a bin's lock is out of sync with its composer.json — i.e. a developer has just added
+# or bumped a bin package on purpose. To intentionally refresh all bin versions without
+# editing composer.json, run `composer bin all update` directly.
+if ! composer bin all install --ansi; then
+ composer bin all update --ansi
+fi
diff --git a/plugins/woocommerce/changelog/pr-64317 b/plugins/woocommerce/changelog/pr-64317
new file mode 100644
index 00000000000..a5f5f59af58
--- /dev/null
+++ b/plugins/woocommerce/changelog/pr-64317
@@ -0,0 +1,4 @@
+Significance: minor
+Type: update
+
+Internalize the webonyx GraphQL package
diff --git a/plugins/woocommerce/composer.json b/plugins/woocommerce/composer.json
index 58fd47b50c0..079e2e31729 100644
--- a/plugins/woocommerce/composer.json
+++ b/plugins/woocommerce/composer.json
@@ -53,7 +53,6 @@
"composer/installers": "^1.9",
"maxmind-db/reader": "^1.11",
"opis/json-schema": "*",
- "webonyx/graphql-php": "^15.31",
"woocommerce/action-scheduler": "3.9.3",
"woocommerce/blueprint": "*",
"woocommerce/email-editor": "*",
diff --git a/plugins/woocommerce/composer.lock b/plugins/woocommerce/composer.lock
index f8d804eac2e..fe6f99f2b90 100644
--- a/plugins/woocommerce/composer.lock
+++ b/plugins/woocommerce/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": "b8069038c6b35a5f88ea33f3b60e9c08",
+ "content-hash": "c9dcd2cbeb75aa25b1b43c237292c908",
"packages": [
{
"name": "automattic/block-delimiter",
@@ -1068,86 +1068,6 @@
},
"time": "2021-05-22T15:57:08+00:00"
},
- {
- "name": "webonyx/graphql-php",
- "version": "v15.32.1",
- "source": {
- "type": "git",
- "url": "https://github.com/webonyx/graphql-php.git",
- "reference": "e8f77f81dbe5de75551137955dd0fd3f779235cf"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/e8f77f81dbe5de75551137955dd0fd3f779235cf",
- "reference": "e8f77f81dbe5de75551137955dd0fd3f779235cf",
- "shasum": ""
- },
- "require": {
- "ext-json": "*",
- "ext-mbstring": "*",
- "php": "^7.4 || ^8"
- },
- "require-dev": {
- "amphp/amp": "^2.6 || ^3",
- "amphp/http-server": "^2.1 || ^3",
- "dms/phpunit-arraysubset-asserts": "dev-master",
- "ergebnis/composer-normalize": "^2.28",
- "friendsofphp/php-cs-fixer": "3.95.1",
- "mll-lab/php-cs-fixer-config": "5.13.0",
- "nyholm/psr7": "^1.5",
- "phpbench/phpbench": "^1.2",
- "phpstan/extension-installer": "^1.1",
- "phpstan/phpstan": "2.1.46",
- "phpstan/phpstan-phpunit": "2.0.16",
- "phpstan/phpstan-strict-rules": "2.0.10",
- "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
- "psr/http-message": "^1 || ^2",
- "react/http": "^1.6",
- "react/promise": "^2.0 || ^3.0",
- "rector/rector": "^2.0",
- "symfony/polyfill-php81": "^1.23",
- "symfony/var-exporter": "^5 || ^6 || ^7 || ^8",
- "thecodingmachine/safe": "^1.3 || ^2 || ^3",
- "ticketswap/phpstan-error-formatter": "1.3.0"
- },
- "suggest": {
- "amphp/amp": "To leverage async resolving on AMPHP platform (v3 with AmpFutureAdapter, v2 with AmpPromiseAdapter)",
- "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
- "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/v15.32.1"
- },
- "funding": [
- {
- "url": "https://github.com/spawnia",
- "type": "github"
- },
- {
- "url": "https://opencollective.com/webonyx-graphql-php",
- "type": "open_collective"
- }
- ],
- "time": "2026-04-21T09:42:39+00:00"
- },
{
"name": "woocommerce/action-scheduler",
"version": "3.9.3",
diff --git a/plugins/woocommerce/lib/README.md b/plugins/woocommerce/lib/README.md
index 3950079fb61..e8ee558776c 100644
--- a/plugins/woocommerce/lib/README.md
+++ b/plugins/woocommerce/lib/README.md
@@ -21,6 +21,19 @@ Composer treats `require` dependencies as transitive while `require-dev` depende
Updating a package is as easy as changing the version in `composer.json` and then running `composer run-script build-lib` from the root directory.
+`build-lib` prefers `composer install` so that routine rebuilds respect `lib/composer.lock`
+and don't silently bump unrelated dependencies to their latest in-range version. It automatically
+switches to `composer update` when `lib/composer.json` has been modified (so a newly added
+or bumped package can refresh the lock). Pass `--update` to force a full update even when
+`composer.json` hasn't changed — useful for picking up in-range upstream releases deliberately:
+
+```sh
+composer run-script build-lib -- --update
+```
+
+The `--` separator tells Composer to forward `--update` to the build script rather than
+interpret it as one of its own options.
+
## Ignoring Packages
If you would like to add a package which does not undergo conflict avoidance you must take steps to ensure it appears in
@@ -30,6 +43,20 @@ the root autoloader.
2. Add package slug to `extra/mozart/excluded-packages` section of `composer.json`
3. Run `composer run-script build-lib` from the root directory (You **should not** see the package in `packages/VendorName/PackageName` or `classes`) - see the note about MobileDetect below.
+## A note about the webonyx/graphql-php library
+
+Mozart rewrites namespace declarations and `use` statements, but it can miss stringified FQCNs
+(class names embedded in string literals). Before shipping a webonyx version bump, audit the
+rebuilt package for any such strings that still reference the bare `GraphQL\` namespace by
+running this from `plugins/woocommerce/`:
+
+```sh
+grep -rn "'GraphQL\\\\\\|\"GraphQL\\\\" lib/packages/GraphQL/
+```
+
+The grep should return no results. If it does, patch the offending file manually and commit it,
+mirroring the MobileDetect workflow below.
+
## A note about the MobileDetect library
The `lib/packages/Detection/MobileDetect.php` file
@@ -37,7 +64,9 @@ The `lib/packages/Detection/MobileDetect.php` file
These fixes are already present in newer versions of the package, but we can't update to any of these versions
because they all require PHP 8. The package version currently in use is the newest one supporting PHP 7.4.
-Therefore, as long as WooCommerce runs in PHP 7.4 and no alternative solution is found for this,
-the changes in `lib/packages/Detection/MobileDetect.php` must be manually reverted after running
-`composer run-script build-lib`. This can be accomplished from the command line by running
+Therefore, as long as WooCommerce runs in PHP 7.4 and no alternative solution is found for this,
+the changes in `lib/packages/Detection/MobileDetect.php` must be reverted after running
+`composer run-script build-lib`. `bin/build-lib.sh` does this automatically via `git restore`
+at the end of the build when run inside a git working tree. If you rebuild outside of git
+(e.g. from a tarball), restore the patched file manually:
`git restore ./plugins/woocommerce/lib/packages/Detection/MobileDetect.php` from the root of the repository.
diff --git a/plugins/woocommerce/lib/composer.json b/plugins/woocommerce/lib/composer.json
index fe2cea4c86d..a91c9fc5d34 100644
--- a/plugins/woocommerce/lib/composer.json
+++ b/plugins/woocommerce/lib/composer.json
@@ -10,7 +10,8 @@
"mobiledetect/mobiledetectlib": "^3.74",
"psr/container": "^1.1",
"pelago/emogrifier": "7.3.0",
- "league/iso3166": "^4.3.3"
+ "league/iso3166": "^4.3.3",
+ "webonyx/graphql-php": "^15.31"
},
"config": {
"platform": {
@@ -33,7 +34,8 @@
"psr/container",
"mobiledetect/mobiledetectlib",
"pelago/emogrifier",
- "league/iso3166"
+ "league/iso3166",
+ "webonyx/graphql-php"
],
"excluded_packages": [
],
diff --git a/plugins/woocommerce/lib/composer.lock b/plugins/woocommerce/lib/composer.lock
index 1d3edbdc38d..224fe20025a 100644
--- a/plugins/woocommerce/lib/composer.lock
+++ b/plugins/woocommerce/lib/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": "b49b67514848dfa15b0edddc31cfc196",
+ "content-hash": "6bc80befedb70a96c4cafa8e061bc629",
"packages": [],
"packages-dev": [
{
@@ -483,6 +483,86 @@
}
],
"time": "2025-01-02T08:10:11+00:00"
+ },
+ {
+ "name": "webonyx/graphql-php",
+ "version": "v15.32.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webonyx/graphql-php.git",
+ "reference": "e8f77f81dbe5de75551137955dd0fd3f779235cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/e8f77f81dbe5de75551137955dd0fd3f779235cf",
+ "reference": "e8f77f81dbe5de75551137955dd0fd3f779235cf",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "php": "^7.4 || ^8"
+ },
+ "require-dev": {
+ "amphp/amp": "^2.6 || ^3",
+ "amphp/http-server": "^2.1 || ^3",
+ "dms/phpunit-arraysubset-asserts": "dev-master",
+ "ergebnis/composer-normalize": "^2.28",
+ "friendsofphp/php-cs-fixer": "3.95.1",
+ "mll-lab/php-cs-fixer-config": "5.13.0",
+ "nyholm/psr7": "^1.5",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "2.1.46",
+ "phpstan/phpstan-phpunit": "2.0.16",
+ "phpstan/phpstan-strict-rules": "2.0.10",
+ "phpunit/phpunit": "^9.5 || ^10.5.21 || ^11",
+ "psr/http-message": "^1 || ^2",
+ "react/http": "^1.6",
+ "react/promise": "^2.0 || ^3.0",
+ "rector/rector": "^2.0",
+ "symfony/polyfill-php81": "^1.23",
+ "symfony/var-exporter": "^5 || ^6 || ^7 || ^8",
+ "thecodingmachine/safe": "^1.3 || ^2 || ^3",
+ "ticketswap/phpstan-error-formatter": "1.3.0"
+ },
+ "suggest": {
+ "amphp/amp": "To leverage async resolving on AMPHP platform (v3 with AmpFutureAdapter, v2 with AmpPromiseAdapter)",
+ "amphp/http-server": "To leverage async resolving with webserver on AMPHP platform",
+ "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/v15.32.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/spawnia",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/webonyx-graphql-php",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2026-04-21T09:42:39+00:00"
}
],
"aliases": [],
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Deferred.php b/plugins/woocommerce/lib/packages/GraphQL/Deferred.php
new file mode 100644
index 00000000000..00880a2e930
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Deferred.php
@@ -0,0 +1,57 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromise;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromiseQueue;
+
+/**
+ * User-facing promise class for deferred field resolution.
+ *
+ * @phpstan-type Executor callable(): mixed
+ */
+class Deferred extends SyncPromise
+{
+ /**
+ * Executor for deferred promises.
+ *
+ * @var (callable(): mixed)|null
+ */
+ protected $executor;
+
+ /**
+ * Create a new Deferred promise and enqueue its execution.
+ *
+ * @api
+ *
+ * @param Executor $executor
+ */
+ public function __construct(callable $executor)
+ {
+ $this->executor = $executor;
+
+ SyncPromiseQueue::enqueue(function (): void {
+ $executor = $this->executor;
+ assert($executor !== null, 'Always set in constructor, this callback runs only once.');
+ $this->executor = null;
+
+ try {
+ $this->resolve($executor());
+ } catch (\Throwable $e) {
+ $this->reject($e);
+ }
+ });
+ }
+
+ /**
+ * Alias for __construct.
+ *
+ * @param Executor $executor
+ *
+ * @deprecated TODO remove in next major version, use new Deferred() instead
+ */
+ public static function create(callable $executor): self
+ {
+ return new self($executor);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/ClientAware.php b/plugins/woocommerce/lib/packages/GraphQL/Error/ClientAware.php
new file mode 100644
index 00000000000..6132a33dc4b
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/ClientAware.php
@@ -0,0 +1,21 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+/**
+ * Implementing ClientAware allows graphql-php to decide if this error is safe to be shown to clients.
+ *
+ * Only errors that both implement this interface and return true from `isClientSafe()`
+ * will retain their original error message during formatting.
+ *
+ * All other errors will have their message replaced with "Internal server error".
+ */
+interface ClientAware
+{
+ /**
+ * Is it safe to show the error message to clients?
+ *
+ * @api
+ */
+ public function isClientSafe(): bool;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/CoercionError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/CoercionError.php
new file mode 100644
index 00000000000..084040dfc72
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/CoercionError.php
@@ -0,0 +1,57 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-type InputPath list<string|int>
+ */
+class CoercionError extends Error
+{
+ /** @var InputPath|null */
+ public ?array $inputPath;
+
+ /** @var mixed whatever invalid value was passed */
+ public $invalidValue;
+
+ /**
+ * @param InputPath|null $inputPath
+ * @param mixed $invalidValue whatever invalid value was passed
+ *
+ * @return static
+ */
+ public static function make(
+ string $message,
+ ?array $inputPath,
+ $invalidValue,
+ ?\Throwable $previous = null
+ ): self {
+ $instance = new static($message, null, null, [], null, $previous);
+ $instance->inputPath = $inputPath;
+ $instance->invalidValue = $invalidValue;
+
+ return $instance;
+ }
+
+ public function printInputPath(): ?string
+ {
+ if ($this->inputPath === null) {
+ return null;
+ }
+
+ $path = '';
+ foreach ($this->inputPath as $segment) {
+ $path .= is_int($segment)
+ ? "[{$segment}]"
+ : ".{$segment}";
+ }
+
+ return $path;
+ }
+
+ public function printInvalidValue(): string
+ {
+ return Utils::printSafeJson($this->invalidValue);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/DebugFlag.php b/plugins/woocommerce/lib/packages/GraphQL/Error/DebugFlag.php
new file mode 100644
index 00000000000..943299bf380
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/DebugFlag.php
@@ -0,0 +1,15 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+/**
+ * Collection of flags for [error debugging](error-handling.md#debugging-tools).
+ */
+final class DebugFlag
+{
+ public const NONE = 0;
+ public const INCLUDE_DEBUG_MESSAGE = 1;
+ public const INCLUDE_TRACE = 2;
+ public const RETHROW_INTERNAL_EXCEPTIONS = 4;
+ public const RETHROW_UNSAFE_EXCEPTIONS = 8;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/Error.php b/plugins/woocommerce/lib/packages/GraphQL/Error/Error.php
new file mode 100644
index 00000000000..a72d276ada4
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/Error.php
@@ -0,0 +1,319 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\SourceLocation;
+
+/**
+ * Describes an Error found during the parse, validate, or
+ * execute phases of performing a Automattic\WooCommerce\Vendor\GraphQL operation. In addition to a message
+ * and stack trace, it also includes information about the locations in a
+ * Automattic\WooCommerce\Vendor\GraphQL document and/or execution result that correspond to the Error.
+ *
+ * When the error was caused by an exception thrown in resolver, original exception
+ * is available via `getPrevious()`.
+ *
+ * Also read related docs on [error handling](error-handling.md)
+ *
+ * Class extends standard PHP `\Exception`, so all standard methods of base `\Exception` class
+ * are available in addition to those listed below.
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Error\ErrorTest
+ */
+class Error extends \Exception implements \JsonSerializable, ClientAware, ProvidesExtensions
+{
+ /**
+ * Lazily initialized.
+ *
+ * @var array<int, SourceLocation>
+ */
+ private array $locations;
+
+ /**
+ * An array describing the JSON-path into the execution response which
+ * corresponds to this error. Only included for errors during execution.
+ * When fields are aliased, the path includes aliases.
+ *
+ * @var list<int|string>|null
+ */
+ public ?array $path;
+
+ /**
+ * An array describing the JSON-path into the execution response which
+ * corresponds to this error. Only included for errors during execution.
+ * This will never include aliases.
+ *
+ * @var list<int|string>|null
+ */
+ public ?array $unaliasedPath;
+
+ /**
+ * An array of Automattic\WooCommerce\Vendor\GraphQL AST Nodes corresponding to this error.
+ *
+ * @var array<Node>|null
+ */
+ public ?array $nodes;
+
+ /**
+ * The source Automattic\WooCommerce\Vendor\GraphQL document for the first location of this error.
+ *
+ * Note that if this Error represents more than one node, the source may not
+ * represent nodes after the first node.
+ */
+ private ?Source $source;
+
+ /** @var array<int, int>|null */
+ private ?array $positions;
+
+ private bool $isClientSafe;
+
+ /** @var array<string, mixed>|null */
+ protected ?array $extensions;
+
+ /**
+ * @param iterable<array-key, Node|null>|Node|null $nodes
+ * @param array<int, int>|null $positions
+ * @param list<int|string>|null $path
+ * @param array<string, mixed>|null $extensions
+ * @param list<int|string>|null $unaliasedPath
+ */
+ public function __construct(
+ string $message = '',
+ $nodes = null,
+ ?Source $source = null,
+ ?array $positions = null,
+ ?array $path = null,
+ ?\Throwable $previous = null,
+ ?array $extensions = null,
+ ?array $unaliasedPath = null
+ ) {
+ parent::__construct($message, 0, $previous);
+
+ // Compute list of blame nodes.
+ if ($nodes instanceof \Traversable) {
+ /** @phpstan-ignore arrayFilter.strict */
+ $this->nodes = array_filter(iterator_to_array($nodes));
+ } elseif (is_array($nodes)) {
+ $this->nodes = array_filter($nodes);
+ } elseif ($nodes !== null) {
+ $this->nodes = [$nodes];
+ } else {
+ $this->nodes = null;
+ }
+
+ $this->source = $source;
+ $this->positions = $positions;
+ $this->path = $path;
+ $this->unaliasedPath = $unaliasedPath;
+
+ if (is_array($extensions) && $extensions !== []) {
+ $this->extensions = $extensions;
+ } elseif ($previous instanceof ProvidesExtensions) {
+ $this->extensions = $previous->getExtensions();
+ } else {
+ $this->extensions = null;
+ }
+
+ $this->isClientSafe = $previous instanceof ClientAware
+ ? $previous->isClientSafe()
+ : $previous === null;
+ }
+
+ /**
+ * Given an arbitrary Error, presumably thrown while attempting to execute a
+ * Automattic\WooCommerce\Vendor\GraphQL operation, produce a new GraphQLError aware of the location in the
+ * document responsible for the original Error.
+ *
+ * @param mixed $error
+ * @param iterable<Node>|Node|null $nodes
+ * @param list<int|string>|null $path
+ * @param list<int|string>|null $unaliasedPath
+ */
+ public static function createLocatedError($error, $nodes = null, ?array $path = null, ?array $unaliasedPath = null): Error
+ {
+ if ($error instanceof self) {
+ if ($error->isLocated()) {
+ return $error;
+ }
+
+ $nodes ??= $error->getNodes();
+ $path ??= $error->getPath();
+ $unaliasedPath ??= $error->getUnaliasedPath();
+ }
+
+ $source = null;
+ $originalError = null;
+ $positions = [];
+ $extensions = [];
+
+ if ($error instanceof self) {
+ $message = $error->getMessage();
+ $originalError = $error;
+ $source = $error->getSource();
+ $positions = $error->getPositions();
+ $extensions = $error->getExtensions();
+ } elseif ($error instanceof InvariantViolation) {
+ $message = $error->getMessage();
+ $originalError = $error->getPrevious() ?? $error;
+ } elseif ($error instanceof \Throwable) {
+ $message = $error->getMessage();
+ $originalError = $error;
+ } else {
+ $message = (string) $error;
+ }
+
+ $nonEmptyMessage = $message === ''
+ ? 'An unknown error occurred.'
+ : $message;
+
+ return new static(
+ $nonEmptyMessage,
+ $nodes,
+ $source,
+ $positions,
+ $path,
+ $originalError,
+ $extensions,
+ $unaliasedPath
+ );
+ }
+
+ protected function isLocated(): bool
+ {
+ $path = $this->getPath();
+ $nodes = $this->getNodes();
+
+ return $path !== null
+ && $path !== []
+ && $nodes !== null
+ && $nodes !== [];
+ }
+
+ public function isClientSafe(): bool
+ {
+ return $this->isClientSafe;
+ }
+
+ public function getSource(): ?Source
+ {
+ return $this->source
+ ??= $this->nodes[0]->loc->source
+ ?? null;
+ }
+
+ /** @return array<int, int> */
+ public function getPositions(): array
+ {
+ if (! isset($this->positions)) {
+ $this->positions = [];
+
+ if (isset($this->nodes)) {
+ foreach ($this->nodes as $node) {
+ if (isset($node->loc->start)) {
+ $this->positions[] = $node->loc->start;
+ }
+ }
+ }
+ }
+
+ return $this->positions;
+ }
+
+ /**
+ * An array of locations within the source Automattic\WooCommerce\Vendor\GraphQL document which correspond to this error.
+ *
+ * Each entry has information about `line` and `column` within source Automattic\WooCommerce\Vendor\GraphQL document:
+ * $location->line;
+ * $location->column;
+ *
+ * Errors during validation often contain multiple locations, for example to
+ * point out to field mentioned in multiple fragments. Errors during execution include a
+ * single location, the field which produced the error.
+ *
+ * @return array<int, SourceLocation>
+ *
+ * @api
+ */
+ public function getLocations(): array
+ {
+ if (! isset($this->locations)) {
+ $positions = $this->getPositions();
+ $source = $this->getSource();
+ $nodes = $this->getNodes();
+
+ $this->locations = [];
+ if ($source !== null && $positions !== []) {
+ foreach ($positions as $position) {
+ $this->locations[] = $source->getLocation($position);
+ }
+ } elseif ($nodes !== null && $nodes !== []) {
+ foreach ($nodes as $node) {
+ if (isset($node->loc->source)) {
+ $this->locations[] = $node->loc->source->getLocation($node->loc->start);
+ }
+ }
+ }
+ }
+
+ return $this->locations;
+ }
+
+ /** @return array<Node>|null */
+ public function getNodes(): ?array
+ {
+ return $this->nodes;
+ }
+
+ /**
+ * Returns an array describing the path from the root value to the field which produced this error.
+ * Only included for execution errors. When fields are aliased, the path includes aliases.
+ *
+ * @return list<int|string>|null
+ *
+ * @api
+ */
+ public function getPath(): ?array
+ {
+ return $this->path;
+ }
+
+ /**
+ * Returns an array describing the path from the root value to the field which produced this error.
+ * Only included for execution errors. This will never include aliases.
+ *
+ * @return list<int|string>|null
+ *
+ * @api
+ */
+ public function getUnaliasedPath(): ?array
+ {
+ return $this->unaliasedPath;
+ }
+
+ /** @return array<string, mixed>|null */
+ public function getExtensions(): ?array
+ {
+ return $this->extensions;
+ }
+
+ /**
+ * Specify data which should be serialized to JSON.
+ *
+ * @see http://php.net/manual/en/jsonserializable.jsonserialize.php
+ *
+ * @return array<string, mixed> data which can be serialized by <b>json_encode</b>,
+ * which is a value of any type other than a resource
+ */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize(): array
+ {
+ return FormattedError::createFromException($this);
+ }
+
+ public function __toString(): string
+ {
+ return FormattedError::printError($this);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/FormattedError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/FormattedError.php
new file mode 100644
index 00000000000..4d0639f5dab
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/FormattedError.php
@@ -0,0 +1,337 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\SourceLocation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use PHPUnit\Framework\Test;
+
+/**
+ * This class is used for [default error formatting](error-handling.md).
+ * It converts PHP exceptions to [spec-compliant errors](https://facebook.github.io/graphql/#sec-Errors)
+ * and provides tools for error debugging.
+ *
+ * @see ExecutionResult
+ *
+ * @phpstan-import-type SerializableError from ExecutionResult
+ * @phpstan-import-type ErrorFormatter from ExecutionResult
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Error\FormattedErrorTest
+ */
+class FormattedError
+{
+ private static string $internalErrorMessage = 'Internal server error';
+
+ /**
+ * Set default error message for internal errors formatted using createFormattedError().
+ * This value can be overridden by passing 3rd argument to `createFormattedError()`.
+ *
+ * @api
+ */
+ public static function setInternalErrorMessage(string $msg): void
+ {
+ self::$internalErrorMessage = $msg;
+ }
+
+ /**
+ * Prints a GraphQLError to a string, representing useful location information
+ * about the error's position in the source.
+ */
+ public static function printError(Error $error): string
+ {
+ $printedLocations = [];
+
+ $nodes = $error->nodes;
+ if (isset($nodes) && $nodes !== []) {
+ foreach ($nodes as $node) {
+ $location = $node->loc;
+ if (isset($location)) {
+ $source = $location->source;
+ if (isset($source)) {
+ $printedLocations[] = self::highlightSourceAtLocation(
+ $source,
+ $source->getLocation($location->start)
+ );
+ }
+ }
+ }
+ } elseif ($error->getSource() !== null && $error->getLocations() !== []) {
+ $source = $error->getSource();
+ foreach ($error->getLocations() as $location) {
+ $printedLocations[] = self::highlightSourceAtLocation($source, $location);
+ }
+ }
+
+ return $printedLocations === []
+ ? $error->getMessage()
+ : implode("\n\n", array_merge([$error->getMessage()], $printedLocations)) . "\n";
+ }
+
+ /**
+ * Render a helpful description of the location of the error in the Automattic\WooCommerce\Vendor\GraphQL
+ * Source document.
+ */
+ private static function highlightSourceAtLocation(Source $source, SourceLocation $location): string
+ {
+ $line = $location->line;
+ $lineOffset = $source->locationOffset->line - 1;
+ $columnOffset = self::getColumnOffset($source, $location);
+ $contextLine = $line + $lineOffset;
+ $contextColumn = $location->column + $columnOffset;
+ $prevLineNum = (string) ($contextLine - 1);
+ $lineNum = (string) $contextLine;
+ $nextLineNum = (string) ($contextLine + 1);
+ $padLen = strlen($nextLineNum);
+
+ $lines = Utils::splitLines($source->body);
+ $lines[0] = self::spaces($source->locationOffset->column - 1) . $lines[0];
+
+ $outputLines = [
+ "{$source->name} ({$contextLine}:{$contextColumn})",
+ $line >= 2 ? (self::leftPad($padLen, $prevLineNum) . ': ' . $lines[$line - 2]) : null,
+ self::leftPad($padLen, $lineNum) . ': ' . $lines[$line - 1],
+ self::spaces(2 + $padLen + $contextColumn - 1) . '^',
+ $line < count($lines) ? self::leftPad($padLen, $nextLineNum) . ': ' . $lines[$line] : null,
+ ];
+
+ return implode("\n", array_filter($outputLines));
+ }
+
+ private static function getColumnOffset(Source $source, SourceLocation $location): int
+ {
+ return $location->line === 1
+ ? $source->locationOffset->column - 1
+ : 0;
+ }
+
+ private static function spaces(int $length): string
+ {
+ return str_repeat(' ', $length);
+ }
+
+ private static function leftPad(int $length, string $str): string
+ {
+ return self::spaces($length - mb_strlen($str)) . $str;
+ }
+
+ /**
+ * Convert any exception to a Automattic\WooCommerce\Vendor\GraphQL spec compliant array.
+ *
+ * This method only exposes the exception message when the given exception
+ * implements the ClientAware interface, or when debug flags are passed.
+ *
+ * For a list of available debug flags @see \Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag constants.
+ *
+ * @return SerializableError
+ *
+ * @api
+ */
+ public static function createFromException(\Throwable $exception, int $debugFlag = DebugFlag::NONE, ?string $internalErrorMessage = null): array
+ {
+ $internalErrorMessage ??= self::$internalErrorMessage;
+
+ $message = $exception instanceof ClientAware && $exception->isClientSafe()
+ ? $exception->getMessage()
+ : $internalErrorMessage;
+
+ $formattedError = ['message' => $message];
+
+ if ($exception instanceof Error) {
+ $locations = array_map(
+ static fn (SourceLocation $loc): array => $loc->toSerializableArray(),
+ $exception->getLocations()
+ );
+ if ($locations !== []) {
+ $formattedError['locations'] = $locations;
+ }
+
+ if ($exception->path !== null && $exception->path !== []) {
+ $formattedError['path'] = $exception->path;
+ }
+ }
+
+ if ($exception instanceof ProvidesExtensions) {
+ $extensions = $exception->getExtensions();
+ if (is_array($extensions) && $extensions !== []) {
+ $formattedError['extensions'] = $extensions;
+ }
+ }
+
+ if ($debugFlag !== DebugFlag::NONE) {
+ $formattedError = self::addDebugEntries($formattedError, $exception, $debugFlag);
+ }
+
+ return $formattedError;
+ }
+
+ /**
+ * Decorates spec-compliant $formattedError with debug entries according to $debug flags.
+ *
+ * @param SerializableError $formattedError
+ * @param int $debugFlag For available flags @see \Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag
+ *
+ * @throws \Throwable
+ *
+ * @return SerializableError
+ */
+ public static function addDebugEntries(array $formattedError, \Throwable $e, int $debugFlag): array
+ {
+ if ($debugFlag === DebugFlag::NONE) {
+ return $formattedError;
+ }
+
+ if (($debugFlag & DebugFlag::RETHROW_INTERNAL_EXCEPTIONS) !== 0) {
+ if (! $e instanceof Error) {
+ throw $e;
+ }
+
+ if ($e->getPrevious() !== null) {
+ throw $e->getPrevious();
+ }
+ }
+
+ $isUnsafe = ! $e instanceof ClientAware || ! $e->isClientSafe();
+
+ if (($debugFlag & DebugFlag::RETHROW_UNSAFE_EXCEPTIONS) !== 0 && $isUnsafe && $e->getPrevious() !== null) {
+ throw $e->getPrevious();
+ }
+
+ if (($debugFlag & DebugFlag::INCLUDE_DEBUG_MESSAGE) !== 0 && $isUnsafe) {
+ $formattedError['extensions']['debugMessage'] = $e->getMessage();
+ }
+
+ if (($debugFlag & DebugFlag::INCLUDE_TRACE) !== 0) {
+ $actualError = $e->getPrevious() ?? $e;
+ if ($e instanceof \ErrorException || $e instanceof \Error) {
+ $formattedError['extensions']['file'] = $e->getFile();
+ $formattedError['extensions']['line'] = $e->getLine();
+ } else {
+ $formattedError['extensions']['file'] = $actualError->getFile();
+ $formattedError['extensions']['line'] = $actualError->getLine();
+ }
+
+ $isTrivial = $e instanceof Error && $e->getPrevious() === null;
+
+ if (! $isTrivial) {
+ $formattedError['extensions']['trace'] = static::toSafeTrace($actualError);
+ }
+ }
+
+ return $formattedError;
+ }
+
+ /**
+ * Prepares final error formatter taking in account $debug flags.
+ *
+ * If initial formatter is not set, FormattedError::createFromException is used.
+ *
+ * @phpstan-param ErrorFormatter|null $formatter
+ */
+ public static function prepareFormatter(?callable $formatter, int $debug): callable
+ {
+ return $formatter === null
+ ? static fn (\Throwable $e): array => static::createFromException($e, $debug)
+ : static fn (\Throwable $e): array => static::addDebugEntries($formatter($e), $e, $debug);
+ }
+
+ /**
+ * Returns error trace as serializable array.
+ *
+ * @return array<int, array{
+ * file?: string,
+ * line?: int,
+ * function?: string,
+ * call?: string,
+ * }>
+ *
+ * @api
+ */
+ public static function toSafeTrace(\Throwable $error): array
+ {
+ $trace = $error->getTrace();
+
+ if (
+ isset($trace[0]['function']) && isset($trace[0]['class'])
+ // Remove invariant entries as they don't provide much value:
+ && ($trace[0]['class'] . '::' . $trace[0]['function'] === 'Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils::invariant')
+ ) {
+ array_shift($trace);
+ } elseif (! isset($trace[0]['file'])) {
+ // Remove root call as it's likely error handler trace:
+ array_shift($trace);
+ }
+
+ $formatted = [];
+ foreach ($trace as $err) {
+ $safeErr = [];
+
+ if (isset($err['file'])) {
+ $safeErr['file'] = $err['file'];
+ }
+
+ if (isset($err['line'])) {
+ $safeErr['line'] = $err['line'];
+ }
+
+ $func = $err['function'];
+ $args = array_map([self::class, 'printVar'], $err['args'] ?? []);
+ $funcStr = $func . '(' . implode(', ', $args) . ')';
+
+ if (isset($err['class'])) {
+ $safeErr['call'] = $err['class'] . '::' . $funcStr;
+ } else {
+ $safeErr['function'] = $funcStr;
+ }
+
+ $formatted[] = $safeErr;
+ }
+
+ return $formatted;
+ }
+
+ /** @param mixed $var */
+ public static function printVar($var): string
+ {
+ if ($var instanceof Type) {
+ return 'GraphQLType: ' . $var->toString();
+ }
+
+ if (is_object($var)) {
+ // Calling `count` on instances of `PHPUnit\Framework\Test` triggers an unintended side effect - see https://github.com/sebastianbergmann/phpunit/issues/5866#issuecomment-2172429263
+ $count = ! $var instanceof Test && $var instanceof \Countable
+ ? '(' . count($var) . ')'
+ : '';
+
+ return 'instance of ' . get_class($var) . $count;
+ }
+
+ if (is_array($var)) {
+ return 'array(' . count($var) . ')';
+ }
+
+ if ($var === '') {
+ return '(empty string)';
+ }
+
+ if (is_string($var)) {
+ return "'" . addcslashes($var, "'") . "'";
+ }
+
+ if (is_bool($var)) {
+ return $var ? 'true' : 'false';
+ }
+
+ if (is_scalar($var)) {
+ return (string) $var;
+ }
+
+ if ($var === null) {
+ return 'null';
+ }
+
+ return gettype($var);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/InvariantViolation.php b/plugins/woocommerce/lib/packages/GraphQL/Error/InvariantViolation.php
new file mode 100644
index 00000000000..c26260662e2
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/InvariantViolation.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+/**
+ * Note:
+ * This exception should not inherit base Error exception as it is raised when there is an error somewhere in
+ * user-land code.
+ */
+class InvariantViolation extends \LogicException {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/ProvidesExtensions.php b/plugins/woocommerce/lib/packages/GraphQL/Error/ProvidesExtensions.php
new file mode 100644
index 00000000000..b21ed004bb1
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/ProvidesExtensions.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+/**
+ * Implementing HasExtensions allows this error to provide additional data to clients.
+ */
+interface ProvidesExtensions
+{
+ /**
+ * Data to include within the "extensions" key of the formatted error.
+ *
+ * @return array<string, mixed>|null
+ */
+ public function getExtensions(): ?array;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/SerializationError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/SerializationError.php
new file mode 100644
index 00000000000..8b6d4880edb
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/SerializationError.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+/**
+ * Thrown when failing to serialize a leaf value.
+ *
+ * Not generally safe for clients, as the wrong given value could
+ * be something not intended to ever be seen by clients.
+ */
+class SerializationError extends \Exception {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/SyntaxError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/SyntaxError.php
new file mode 100644
index 00000000000..b44e94afa42
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/SyntaxError.php
@@ -0,0 +1,18 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
+
+class SyntaxError extends Error
+{
+ public function __construct(Source $source, int $position, string $description)
+ {
+ parent::__construct(
+ "Syntax Error: {$description}",
+ null,
+ $source,
+ [$position]
+ );
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/UserError.php b/plugins/woocommerce/lib/packages/GraphQL/Error/UserError.php
new file mode 100644
index 00000000000..e7ba3ecc47b
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/UserError.php
@@ -0,0 +1,14 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+/**
+ * Caused by Automattic\WooCommerce\Vendor\GraphQL clients and can safely be displayed.
+ */
+class UserError extends \RuntimeException implements ClientAware
+{
+ public function isClientSafe(): bool
+ {
+ return true;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Error/Warning.php b/plugins/woocommerce/lib/packages/GraphQL/Error/Warning.php
new file mode 100644
index 00000000000..38b21f72e9f
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Error/Warning.php
@@ -0,0 +1,122 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Error;
+
+/**
+ * Encapsulates warnings produced by the library.
+ *
+ * Warnings can be suppressed (individually or all) if required.
+ * Also, it is possible to override warning handler (which is **trigger_error()** by default).
+ *
+ * @phpstan-type WarningHandler callable(string $errorMessage, int $warningId, ?int $messageLevel): void
+ */
+final class Warning
+{
+ public const NONE = 0;
+ public const WARNING_ASSIGN = 2;
+ public const WARNING_CONFIG = 4;
+ public const WARNING_FULL_SCHEMA_SCAN = 8;
+ public const WARNING_CONFIG_DEPRECATION = 16;
+ public const WARNING_NOT_A_TYPE = 32;
+ public const ALL = 63;
+
+ private static int $enableWarnings = self::ALL;
+
+ /** @var array<int, true> */
+ private static array $warned = [];
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var WarningHandler|null
+ */
+ private static $warningHandler;
+
+ /**
+ * Sets warning handler which can intercept all system warnings.
+ * When not set, trigger_error() is used to notify about warnings.
+ *
+ * @phpstan-param WarningHandler|null $warningHandler
+ *
+ * @api
+ */
+ public static function setWarningHandler(?callable $warningHandler = null): void
+ {
+ self::$warningHandler = $warningHandler;
+ }
+
+ /**
+ * Suppress warning by id (has no effect when custom warning handler is set).
+ *
+ * @param bool|int $suppress
+ *
+ * @example Warning::suppress(Warning::WARNING_NOT_A_TYPE) suppress a specific warning
+ * @example Warning::suppress(true) suppresses all warnings
+ * @example Warning::suppress(false) enables all warnings
+ *
+ * @api
+ */
+ public static function suppress($suppress = true): void
+ {
+ if ($suppress === true) {
+ self::$enableWarnings = 0;
+ } elseif ($suppress === false) {
+ self::$enableWarnings = self::ALL;
+ // @phpstan-ignore-next-line necessary until we can use proper unions
+ } elseif (is_int($suppress)) {
+ self::$enableWarnings &= ~$suppress;
+ } else {
+ $type = gettype($suppress);
+ throw new \InvalidArgumentException("Expected type bool|int, got {$type}.");
+ }
+ }
+
+ /**
+ * Re-enable previously suppressed warning by id (has no effect when custom warning handler is set).
+ *
+ * @param bool|int $enable
+ *
+ * @example Warning::suppress(Warning::WARNING_NOT_A_TYPE) re-enables a specific warning
+ * @example Warning::suppress(true) re-enables all warnings
+ * @example Warning::suppress(false) suppresses all warnings
+ *
+ * @api
+ */
+ public static function enable($enable = true): void
+ {
+ if ($enable === true) {
+ self::$enableWarnings = self::ALL;
+ } elseif ($enable === false) {
+ self::$enableWarnings = 0;
+ // @phpstan-ignore-next-line necessary until we can use proper unions
+ } elseif (is_int($enable)) {
+ self::$enableWarnings |= $enable;
+ } else {
+ $type = gettype($enable);
+ throw new \InvalidArgumentException("Expected type bool|int, got {$type}.");
+ }
+ }
+
+ public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null): void
+ {
+ $messageLevel ??= \E_USER_WARNING;
+
+ if (self::$warningHandler !== null) {
+ (self::$warningHandler)($errorMessage, $warningId, $messageLevel);
+ } elseif ((self::$enableWarnings & $warningId) > 0 && ! isset(self::$warned[$warningId])) {
+ self::$warned[$warningId] = true;
+ trigger_error($errorMessage, $messageLevel);
+ }
+ }
+
+ public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null): void
+ {
+ $messageLevel ??= \E_USER_WARNING;
+
+ if (self::$warningHandler !== null) {
+ (self::$warningHandler)($errorMessage, $warningId, $messageLevel);
+ } elseif ((self::$enableWarnings & $warningId) > 0) {
+ trigger_error($errorMessage, $messageLevel);
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionContext.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionContext.php
new file mode 100644
index 00000000000..1a18dfcc52a
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionContext.php
@@ -0,0 +1,94 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * Data that must be available at all points during query execution.
+ *
+ * Namely, schema of the type system that is currently executing,
+ * and the fragments defined in the query document.
+ *
+ * @phpstan-import-type FieldResolver from Executor
+ * @phpstan-import-type ArgsMapper from Executor
+ */
+class ExecutionContext
+{
+ public Schema $schema;
+
+ /** @var array<string, FragmentDefinitionNode> */
+ public array $fragments;
+
+ /** @var mixed */
+ public $rootValue;
+
+ /** @var mixed */
+ public $contextValue;
+
+ public OperationDefinitionNode $operation;
+
+ /** @var array<string, mixed> */
+ public array $variableValues;
+
+ /**
+ * @var callable
+ *
+ * @phpstan-var FieldResolver
+ */
+ public $fieldResolver;
+
+ /**
+ * @var callable
+ *
+ * @phpstan-var ArgsMapper
+ */
+ public $argsMapper;
+
+ /** @var list<Error> */
+ public array $errors;
+
+ public PromiseAdapter $promiseAdapter;
+
+ /**
+ * @param array<string, FragmentDefinitionNode> $fragments
+ * @param mixed $rootValue
+ * @param mixed $contextValue
+ * @param array<string, mixed> $variableValues
+ * @param list<Error> $errors
+ *
+ * @phpstan-param FieldResolver $fieldResolver
+ */
+ public function __construct(
+ Schema $schema,
+ array $fragments,
+ $rootValue,
+ $contextValue,
+ OperationDefinitionNode $operation,
+ array $variableValues,
+ array $errors,
+ callable $fieldResolver,
+ callable $argsMapper,
+ PromiseAdapter $promiseAdapter
+ ) {
+ $this->schema = $schema;
+ $this->fragments = $fragments;
+ $this->rootValue = $rootValue;
+ $this->contextValue = $contextValue;
+ $this->operation = $operation;
+ $this->variableValues = $variableValues;
+ $this->errors = $errors;
+ $this->fieldResolver = $fieldResolver;
+ $this->argsMapper = $argsMapper;
+ $this->promiseAdapter = $promiseAdapter;
+ }
+
+ public function addError(Error $error): void
+ {
+ $this->errors[] = $error;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionResult.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionResult.php
new file mode 100644
index 00000000000..451f0c5ad7e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutionResult.php
@@ -0,0 +1,186 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\FormattedError;
+
+/**
+ * Returned after [query execution](executing-queries.md).
+ * Represents both - result of successful execution and of a failed one
+ * (with errors collected in `errors` prop).
+ *
+ * Could be converted to [spec-compliant](https://facebook.github.io/graphql/#sec-Response-Format)
+ * serializable array using `toArray()`.
+ *
+ * @phpstan-type SerializableError array{
+ * message: string,
+ * locations?: array<int, array{line: int, column: int}>,
+ * path?: array<int, int|string>,
+ * extensions?: array<string, mixed>
+ * }
+ * @phpstan-type SerializableErrors list<SerializableError>
+ * @phpstan-type SerializableResult array{
+ * data?: array<string, mixed>,
+ * errors?: SerializableErrors,
+ * extensions?: array<string, mixed>
+ * }
+ * @phpstan-type ErrorFormatter callable(\Throwable): SerializableError
+ * @phpstan-type ErrorsHandler callable(list<Error> $errors, ErrorFormatter $formatter): SerializableErrors
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Executor\ExecutionResultTest
+ */
+class ExecutionResult implements \JsonSerializable
+{
+ /**
+ * Data collected from resolvers during query execution.
+ *
+ * @api
+ *
+ * @var array<string, mixed>|null
+ */
+ public ?array $data = null;
+
+ /**
+ * Errors registered during query execution.
+ *
+ * If an error was caused by exception thrown in resolver, $error->getPrevious() would
+ * contain original exception.
+ *
+ * @api
+ *
+ * @var list<Error>
+ */
+ public array $errors = [];
+
+ /**
+ * User-defined serializable array of extensions included in serialized result.
+ *
+ * @api
+ *
+ * @var array<string, mixed>|null
+ */
+ public ?array $extensions = null;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var ErrorFormatter|null
+ */
+ private $errorFormatter;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var ErrorsHandler|null
+ */
+ private $errorsHandler;
+
+ /**
+ * @param array<string, mixed>|null $data
+ * @param list<Error> $errors
+ * @param array<string, mixed> $extensions
+ */
+ public function __construct(?array $data = null, array $errors = [], array $extensions = [])
+ {
+ $this->data = $data;
+ $this->errors = $errors;
+ $this->extensions = $extensions;
+ }
+
+ /**
+ * Define custom error formatting (must conform to http://facebook.github.io/graphql/#sec-Errors).
+ *
+ * Expected signature is: function (Automattic\WooCommerce\Vendor\GraphQL\Error\Error $error): array
+ *
+ * Default formatter is "Automattic\WooCommerce\Vendor\GraphQL\Error\FormattedError::createFromException"
+ *
+ * Expected returned value must be an array:
+ * array(
+ * 'message' => 'errorMessage',
+ * // ... other keys
+ * );
+ *
+ * @phpstan-param ErrorFormatter|null $errorFormatter
+ *
+ * @api
+ */
+ public function setErrorFormatter(?callable $errorFormatter): self
+ {
+ $this->errorFormatter = $errorFormatter;
+
+ return $this;
+ }
+
+ /**
+ * Define custom logic for error handling (filtering, logging, etc).
+ *
+ * Expected handler signature is:
+ * fn (array $errors, callable $formatter): array
+ *
+ * Default handler is:
+ * fn (array $errors, callable $formatter): array => array_map($formatter, $errors)
+ *
+ * @phpstan-param ErrorsHandler|null $errorsHandler
+ *
+ * @api
+ */
+ public function setErrorsHandler(?callable $errorsHandler): self
+ {
+ $this->errorsHandler = $errorsHandler;
+
+ return $this;
+ }
+
+ /** @phpstan-return SerializableResult */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize(): array
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * Converts Automattic\WooCommerce\Vendor\GraphQL query result to spec-compliant serializable array using provided
+ * errors handler and formatter.
+ *
+ * If debug argument is passed, output of error formatter is enriched which debugging information
+ * ("debugMessage", "trace" keys depending on flags).
+ *
+ * $debug argument must sum of flags from @see \Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag
+ *
+ * @phpstan-return SerializableResult
+ *
+ * @api
+ */
+ public function toArray(int $debug = DebugFlag::NONE): array
+ {
+ $result = [];
+
+ if ($this->errors !== []) {
+ $errorsHandler = $this->errorsHandler
+ ?? static fn (array $errors, callable $formatter): array => array_map($formatter, $errors);
+
+ /** @phpstan-var SerializableErrors */
+ $handledErrors = $errorsHandler(
+ $this->errors,
+ FormattedError::prepareFormatter($this->errorFormatter, $debug)
+ );
+
+ // While we know that there were errors initially, they might have been discarded
+ if ($handledErrors !== []) {
+ $result['errors'] = $handledErrors;
+ }
+ }
+
+ if ($this->data !== null) {
+ $result['data'] = $this->data;
+ }
+
+ if ($this->extensions !== null && $this->extensions !== []) {
+ $result['extensions'] = $this->extensions;
+ }
+
+ return $result;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Executor.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Executor.php
new file mode 100644
index 00000000000..0783f1c7388
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Executor.php
@@ -0,0 +1,219 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * Implements the "Evaluating requests" section of the Automattic\WooCommerce\Vendor\GraphQL specification.
+ *
+ * @phpstan-type ArgsMapper callable(array<string, mixed>, FieldDefinition, FieldNode, mixed): mixed
+ * @phpstan-type FieldResolver callable(mixed, array<string, mixed>, mixed, ResolveInfo): mixed
+ * @phpstan-type ImplementationFactory callable(PromiseAdapter, Schema, DocumentNode, mixed, mixed, array<mixed>, ?string, callable, callable): ExecutorImplementation
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Executor\ExecutorTest
+ */
+class Executor
+{
+ /**
+ * @var callable
+ *
+ * @phpstan-var FieldResolver
+ */
+ private static $defaultFieldResolver = [self::class, 'defaultFieldResolver'];
+
+ /**
+ * @var callable
+ *
+ * @phpstan-var ArgsMapper
+ */
+ private static $defaultArgsMapper = [self::class, 'defaultArgsMapper'];
+
+ private static ?PromiseAdapter $defaultPromiseAdapter;
+
+ /**
+ * @var callable
+ *
+ * @phpstan-var ImplementationFactory
+ */
+ private static $implementationFactory = [ReferenceExecutor::class, 'create'];
+
+ /** @phpstan-return FieldResolver */
+ public static function getDefaultFieldResolver(): callable
+ {
+ return self::$defaultFieldResolver;
+ }
+
+ /**
+ * Set a custom default resolve function.
+ *
+ * @phpstan-param FieldResolver $fieldResolver
+ */
+ public static function setDefaultFieldResolver(callable $fieldResolver): void
+ {
+ self::$defaultFieldResolver = $fieldResolver;
+ }
+
+ /** @phpstan-return ArgsMapper */
+ public static function getDefaultArgsMapper(): callable
+ {
+ return self::$defaultArgsMapper;
+ }
+
+ /** @phpstan-param ArgsMapper $argsMapper */
+ public static function setDefaultArgsMapper(callable $argsMapper): void
+ {
+ self::$defaultArgsMapper = $argsMapper;
+ }
+
+ public static function getDefaultPromiseAdapter(): PromiseAdapter
+ {
+ return self::$defaultPromiseAdapter ??= new SyncPromiseAdapter();
+ }
+
+ /** Set a custom default promise adapter. */
+ public static function setDefaultPromiseAdapter(?PromiseAdapter $defaultPromiseAdapter = null): void
+ {
+ self::$defaultPromiseAdapter = $defaultPromiseAdapter;
+ }
+
+ /** @phpstan-return ImplementationFactory */
+ public static function getImplementationFactory(): callable
+ {
+ return self::$implementationFactory;
+ }
+
+ /**
+ * Set a custom executor implementation factory.
+ *
+ * @phpstan-param ImplementationFactory $implementationFactory
+ */
+ public static function setImplementationFactory(callable $implementationFactory): void
+ {
+ self::$implementationFactory = $implementationFactory;
+ }
+
+ /**
+ * Executes DocumentNode against given $schema.
+ *
+ * Always returns ExecutionResult and never throws.
+ * All errors which occur during operation execution are collected in `$result->errors`.
+ *
+ * @param mixed $rootValue
+ * @param mixed $contextValue
+ * @param array<string, mixed>|null $variableValues
+ *
+ * @phpstan-param FieldResolver|null $fieldResolver
+ *
+ * @api
+ *
+ * @throws InvariantViolation
+ */
+ public static function execute(
+ Schema $schema,
+ DocumentNode $documentNode,
+ $rootValue = null,
+ $contextValue = null,
+ ?array $variableValues = null,
+ ?string $operationName = null,
+ ?callable $fieldResolver = null
+ ): ExecutionResult {
+ $promiseAdapter = new SyncPromiseAdapter();
+
+ $result = static::promiseToExecute(
+ $promiseAdapter,
+ $schema,
+ $documentNode,
+ $rootValue,
+ $contextValue,
+ $variableValues,
+ $operationName,
+ $fieldResolver
+ );
+
+ return $promiseAdapter->wait($result);
+ }
+
+ /**
+ * Same as execute(), but requires promise adapter and returns a promise which is always
+ * fulfilled with an instance of ExecutionResult and never rejected.
+ *
+ * Useful for async PHP platforms.
+ *
+ * @param mixed $rootValue
+ * @param mixed $contextValue
+ * @param array<string, mixed>|null $variableValues
+ *
+ * @phpstan-param FieldResolver|null $fieldResolver
+ * @phpstan-param ArgsMapper|null $argsMapper
+ *
+ * @api
+ */
+ public static function promiseToExecute(
+ PromiseAdapter $promiseAdapter,
+ Schema $schema,
+ DocumentNode $documentNode,
+ $rootValue = null,
+ $contextValue = null,
+ ?array $variableValues = null,
+ ?string $operationName = null,
+ ?callable $fieldResolver = null,
+ ?callable $argsMapper = null
+ ): Promise {
+ $executor = (self::$implementationFactory)(
+ $promiseAdapter,
+ $schema,
+ $documentNode,
+ $rootValue,
+ $contextValue,
+ $variableValues ?? [],
+ $operationName,
+ $fieldResolver ?? self::$defaultFieldResolver,
+ $argsMapper ?? self::$defaultArgsMapper,
+ );
+
+ return $executor->doExecute();
+ }
+
+ /**
+ * If a resolve function is not given, then a default resolve behavior is used
+ * which takes the property of the root value of the same name as the field
+ * and returns it as the result, or if it's a function, returns the result
+ * of calling that function while passing along args and context.
+ *
+ * @param mixed $objectLikeValue
+ * @param array<string, mixed> $args
+ * @param mixed $contextValue
+ *
+ * @return mixed
+ */
+ public static function defaultFieldResolver($objectLikeValue, array $args, $contextValue, ResolveInfo $info)
+ {
+ $property = Utils::extractKey($objectLikeValue, $info->fieldName);
+
+ return $property instanceof \Closure
+ ? $property($objectLikeValue, $args, $contextValue, $info)
+ : $property;
+ }
+
+ /**
+ * @template T of array<string, mixed>
+ *
+ * @param T $args
+ *
+ * @return T
+ */
+ public static function defaultArgsMapper(array $args): array
+ {
+ return $args;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutorImplementation.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutorImplementation.php
new file mode 100644
index 00000000000..16dd09e116b
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/ExecutorImplementation.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+
+interface ExecutorImplementation
+{
+ /** Returns promise of {@link ExecutionResult}. Promise should always resolve, never reject. */
+ public function doExecute(): Promise;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpFutureAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpFutureAdapter.php
new file mode 100644
index 00000000000..782aeda42a8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpFutureAdapter.php
@@ -0,0 +1,181 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
+
+use Amp\DeferredFuture;
+use Amp\Future;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+
+use function Amp\async;
+use function Amp\Future\await;
+
+/**
+ * Allows integration with amphp/amp v3 (fiber-based futures).
+ *
+ * @see https://amphp.org/amp
+ */
+class AmpFutureAdapter implements PromiseAdapter
+{
+ public function isThenable($value): bool
+ {
+ return $value instanceof Future;
+ }
+
+ /** @throws InvariantViolation */
+ public function convertThenable($thenable): Promise
+ {
+ return new Promise($thenable, $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
+ {
+ $future = $promise->adoptedPromise;
+ assert($future instanceof Future);
+
+ $next = async(static function () use ($future, $onFulfilled, $onRejected) {
+ try {
+ $value = $future->await();
+ } catch (\Throwable $reason) {
+ if ($onRejected === null) {
+ throw $reason;
+ }
+
+ return static::unwrapResult($onRejected($reason));
+ }
+
+ if ($onFulfilled === null) {
+ return $value;
+ }
+
+ return static::unwrapResult($onFulfilled($value));
+ });
+
+ return new Promise($next, $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function create(callable $resolver): Promise
+ {
+ $deferred = new DeferredFuture();
+
+ try {
+ $resolver(
+ static function ($value) use ($deferred): void {
+ static::resolveDeferred($deferred, $value);
+ },
+ static function (\Throwable $exception) use ($deferred): void {
+ $deferred->error($exception);
+ }
+ );
+ } catch (\Throwable $exception) {
+ $deferred->error($exception);
+ }
+
+ return new Promise($deferred->getFuture(), $this);
+ }
+
+ /**
+ * @throws \Error
+ * @throws InvariantViolation
+ */
+ public function createFulfilled($value = null): Promise
+ {
+ if ($value instanceof Promise) {
+ return $value;
+ }
+
+ if ($value instanceof Future) {
+ return new Promise($value, $this);
+ }
+
+ return new Promise(Future::complete($value), $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function createRejected(\Throwable $reason): Promise
+ {
+ return new Promise(Future::error($reason), $this);
+ }
+
+ /**
+ * @throws \Error
+ * @throws InvariantViolation
+ */
+ public function all(iterable $promisesOrValues): Promise
+ {
+ $items = is_array($promisesOrValues)
+ ? $promisesOrValues
+ : iterator_to_array($promisesOrValues);
+
+ /** @var array<Future<mixed>> $futures */
+ $futures = [];
+
+ foreach ($items as $key => $item) {
+ if ($item instanceof Promise) {
+ $item = $item->adoptedPromise;
+ }
+
+ if ($item instanceof Future) {
+ $futures[$key] = $item;
+ }
+ }
+
+ $combined = async(static function () use ($items, $futures): array {
+ if ($futures === []) {
+ return $items;
+ }
+
+ $resolved = await($futures);
+
+ return array_replace($items, $resolved);
+ });
+
+ return new Promise($combined, $this);
+ }
+
+ /**
+ * @param DeferredFuture<mixed> $deferred
+ * @param mixed $value
+ */
+ protected static function resolveDeferred(DeferredFuture $deferred, $value): void
+ {
+ if ($value instanceof Promise) {
+ $value = $value->adoptedPromise;
+ }
+
+ if ($value instanceof Future) {
+ async(static function () use ($deferred, $value): void {
+ try {
+ $deferred->complete($value->await());
+ } catch (\Throwable $exception) {
+ $deferred->error($exception);
+ }
+ });
+
+ return;
+ }
+
+ $deferred->complete($value);
+ }
+
+ /**
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ protected static function unwrapResult($value)
+ {
+ if ($value instanceof Promise) {
+ $value = $value->adoptedPromise;
+ }
+
+ if ($value instanceof Future) {
+ return $value->await();
+ }
+
+ return $value;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpPromiseAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpPromiseAdapter.php
new file mode 100644
index 00000000000..35d03aaf266
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/AmpPromiseAdapter.php
@@ -0,0 +1,151 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
+
+use Amp\Deferred;
+use Amp\Failure;
+use Amp\Promise as AmpPromise;
+use Amp\Success;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+
+use function Amp\Promise\all;
+
+class AmpPromiseAdapter implements PromiseAdapter
+{
+ public function isThenable($value): bool
+ {
+ return $value instanceof AmpPromise;
+ }
+
+ /** @throws InvariantViolation */
+ public function convertThenable($thenable): Promise
+ {
+ return new Promise($thenable, $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
+ {
+ $deferred = new Deferred();
+ $onResolve = static function (?\Throwable $reason, $value) use ($onFulfilled, $onRejected, $deferred): void {
+ if ($reason === null && $onFulfilled !== null) {
+ self::resolveWithCallable($deferred, $onFulfilled, $value);
+ } elseif ($reason === null) {
+ $deferred->resolve($value);
+ } elseif ($onRejected !== null) {
+ self::resolveWithCallable($deferred, $onRejected, $reason);
+ } else {
+ $deferred->fail($reason);
+ }
+ };
+
+ $ampPromise = $promise->adoptedPromise;
+ assert($ampPromise instanceof AmpPromise);
+ $ampPromise->onResolve($onResolve);
+
+ return new Promise($deferred->promise(), $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function create(callable $resolver): Promise
+ {
+ $deferred = new Deferred();
+
+ $resolver(
+ static function ($value) use ($deferred): void {
+ $deferred->resolve($value);
+ },
+ static function (\Throwable $exception) use ($deferred): void {
+ $deferred->fail($exception);
+ }
+ );
+
+ return new Promise($deferred->promise(), $this);
+ }
+
+ /**
+ * @throws \Error
+ * @throws InvariantViolation
+ */
+ public function createFulfilled($value = null): Promise
+ {
+ $promise = new Success($value);
+
+ return new Promise($promise, $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function createRejected(\Throwable $reason): Promise
+ {
+ $promise = new Failure($reason);
+
+ return new Promise($promise, $this);
+ }
+
+ /**
+ * @throws \Error
+ * @throws InvariantViolation
+ */
+ public function all(iterable $promisesOrValues): Promise
+ {
+ /** @var array<AmpPromise<mixed>> $promises */
+ $promises = [];
+ foreach ($promisesOrValues as $key => $item) {
+ if ($item instanceof Promise) {
+ $ampPromise = $item->adoptedPromise;
+ assert($ampPromise instanceof AmpPromise);
+ $promises[$key] = $ampPromise;
+ } elseif ($item instanceof AmpPromise) {
+ $promises[$key] = $item;
+ }
+ }
+
+ $deferred = new Deferred();
+
+ all($promises)->onResolve(static function (?\Throwable $reason, ?array $values) use ($promisesOrValues, $deferred): void {
+ if ($reason === null) {
+ assert(is_array($values), 'Either $reason or $values must be passed');
+
+ $promisesOrValuesArray = is_array($promisesOrValues)
+ ? $promisesOrValues
+ : iterator_to_array($promisesOrValues);
+ $resolvedValues = array_replace($promisesOrValuesArray, $values);
+ $deferred->resolve($resolvedValues);
+
+ return;
+ }
+
+ $deferred->fail($reason);
+ });
+
+ return new Promise($deferred->promise(), $this);
+ }
+
+ /**
+ * @template TArgument
+ * @template TResult of AmpPromise<mixed>
+ *
+ * @param Deferred<TResult> $deferred
+ * @param callable(TArgument): TResult $callback
+ * @param TArgument $argument
+ */
+ private static function resolveWithCallable(Deferred $deferred, callable $callback, $argument): void
+ {
+ try {
+ $result = $callback($argument);
+ } catch (\Throwable $exception) {
+ $deferred->fail($exception);
+
+ return;
+ }
+
+ if ($result instanceof Promise) {
+ /** @var TResult $result */
+ $result = $result->adoptedPromise;
+ }
+
+ $deferred->resolve($result);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/ReactPromiseAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/ReactPromiseAdapter.php
new file mode 100644
index 00000000000..4db5e2ccadf
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/ReactPromiseAdapter.php
@@ -0,0 +1,80 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+use React\Promise\Promise as ReactPromise;
+use React\Promise\PromiseInterface as ReactPromiseInterface;
+
+use function React\Promise\all;
+use function React\Promise\reject;
+use function React\Promise\resolve;
+
+class ReactPromiseAdapter implements PromiseAdapter
+{
+ public function isThenable($value): bool
+ {
+ return $value instanceof ReactPromiseInterface;
+ }
+
+ /** @throws InvariantViolation */
+ public function convertThenable($thenable): Promise
+ {
+ return new Promise($thenable, $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
+ {
+ $reactPromise = $promise->adoptedPromise;
+ assert($reactPromise instanceof ReactPromiseInterface);
+
+ return new Promise($reactPromise->then($onFulfilled, $onRejected), $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function create(callable $resolver): Promise
+ {
+ $reactPromise = new ReactPromise($resolver);
+
+ return new Promise($reactPromise, $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function createFulfilled($value = null): Promise
+ {
+ $reactPromise = resolve($value);
+
+ return new Promise($reactPromise, $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function createRejected(\Throwable $reason): Promise
+ {
+ $reactPromise = reject($reason);
+
+ return new Promise($reactPromise, $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function all(iterable $promisesOrValues): Promise
+ {
+ foreach ($promisesOrValues as &$promiseOrValue) {
+ if ($promiseOrValue instanceof Promise) {
+ $promiseOrValue = $promiseOrValue->adoptedPromise;
+ }
+ }
+
+ $promisesOrValuesArray = is_array($promisesOrValues)
+ ? $promisesOrValues
+ : iterator_to_array($promisesOrValues);
+ $reactPromise = all($promisesOrValuesArray)->then(static fn (array $values): array => array_map(
+ static fn ($key) => $values[$key],
+ array_keys($promisesOrValuesArray),
+ ));
+
+ return new Promise($reactPromise, $this);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromise.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromise.php
new file mode 100644
index 00000000000..b1c039e47c8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromise.php
@@ -0,0 +1,212 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+
+/**
+ * Synchronous promise implementation following Promises A+ spec.
+ *
+ * Uses a hybrid approach for optimal memory and performance:
+ * - Lightweight closures in queue (fast execution)
+ * - Heavy payload (callbacks) stored on promise objects and cleared after use
+ *
+ * Library users should use @see \Automattic\WooCommerce\Vendor\GraphQL\Deferred to create promises.
+ *
+ * @phpstan-type Executor callable(): mixed
+ */
+class SyncPromise
+{
+ /**
+ * TODO remove in next major version.
+ *
+ * @deprecated Use SyncPromiseQueue::run() instead
+ */
+ public static function runQueue(): void
+ {
+ SyncPromiseQueue::run();
+ }
+
+ /**
+ * TODO remove in next major version.
+ *
+ * @deprecated Use SyncPromiseQueue methods instead
+ *
+ * @return \SplQueue<callable(): void>
+ */
+ public static function getQueue(): \SplQueue
+ {
+ return SyncPromiseQueue::queue();
+ }
+
+ public const PENDING = 0;
+ public const FULFILLED = 1;
+ public const REJECTED = 2;
+
+ /**
+ * Current promise state.
+ *
+ * @var 0|1|2
+ */
+ public int $state = self::PENDING;
+
+ /**
+ * Resolved value or rejection reason.
+ *
+ * @var mixed
+ */
+ public $result;
+
+ /**
+ * Promises created in `then` method awaiting resolution.
+ *
+ * @var array<
+ * int,
+ * array{
+ * self,
+ * (callable(mixed): mixed)|null,
+ * (callable(\Throwable): mixed)|null,
+ * },
+ * >
+ */
+ protected array $waiting = [];
+
+ /**
+ * @param mixed $value
+ *
+ * @throws \Exception
+ */
+ public function resolve($value): self
+ {
+ switch ($this->state) {
+ case self::PENDING:
+ if ($value === $this) {
+ throw new \Exception('Cannot resolve promise with self.');
+ }
+
+ if (is_object($value) && method_exists($value, 'then')) {
+ $value->then(
+ function ($resolvedValue): void {
+ $this->resolve($resolvedValue);
+ },
+ function (\Throwable $reason): void {
+ $this->reject($reason);
+ }
+ );
+
+ return $this;
+ }
+
+ $this->state = self::FULFILLED;
+ $this->result = $value;
+ $this->enqueueWaitingPromises();
+ break;
+ case self::FULFILLED:
+ if ($this->result !== $value) {
+ throw new \Exception('Cannot change value of fulfilled promise.');
+ }
+
+ break;
+ case self::REJECTED:
+ throw new \Exception('Cannot resolve rejected promise.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * @throws \Exception
+ *
+ * @return $this
+ */
+ public function reject(\Throwable $reason): self
+ {
+ switch ($this->state) {
+ case self::PENDING:
+ $this->state = self::REJECTED;
+ $this->result = $reason;
+ $this->enqueueWaitingPromises();
+ break;
+ case self::REJECTED:
+ if ($reason !== $this->result) {
+ throw new \Exception('Cannot change rejection reason.');
+ }
+
+ break;
+ case self::FULFILLED:
+ throw new \Exception('Cannot reject fulfilled promise.');
+ }
+
+ return $this;
+ }
+
+ /**
+ * @param (callable(mixed): mixed)|null $onFulfilled
+ * @param (callable(\Throwable): mixed)|null $onRejected
+ *
+ * @throws InvariantViolation
+ */
+ public function then(?callable $onFulfilled = null, ?callable $onRejected = null): self
+ {
+ if ($this->state === self::REJECTED
+ && $onRejected === null
+ ) {
+ return $this;
+ }
+
+ if ($this->state === self::FULFILLED
+ && $onFulfilled === null
+ ) {
+ return $this;
+ }
+
+ $child = new self();
+
+ $this->waiting[] = [$child, $onFulfilled, $onRejected];
+
+ if ($this->state !== self::PENDING) {
+ $this->enqueueWaitingPromises();
+ }
+
+ return $child;
+ }
+
+ /** @throws InvariantViolation */
+ private function enqueueWaitingPromises(): void
+ {
+ if ($this->state === self::PENDING) {
+ throw new InvariantViolation('Cannot enqueue derived promises when parent is still pending.');
+ }
+
+ $waiting = $this->waiting;
+ if ($waiting === []) {
+ return;
+ }
+
+ $this->waiting = [];
+
+ $result = $this->result;
+
+ SyncPromiseQueue::enqueue(static function () use ($waiting, $result): void {
+ foreach ($waiting as [$child, $onFulfilled, $onRejected]) {
+ try {
+ if ($result instanceof \Throwable) {
+ if ($onRejected === null) {
+ $child->reject($result);
+ } else {
+ $child->resolve($onRejected($result));
+ }
+ } else {
+ $child->resolve(
+ $onFulfilled === null
+ ? $result
+ : $onFulfilled($result)
+ );
+ }
+ } catch (\Throwable $e) {
+ $child->reject($e);
+ }
+ }
+ });
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseAdapter.php
new file mode 100644
index 00000000000..44ff4f56342
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseAdapter.php
@@ -0,0 +1,164 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Deferred;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * Allows changing order of field resolution even in sync environments
+ * (by leveraging queue of deferreds and promises).
+ */
+class SyncPromiseAdapter implements PromiseAdapter
+{
+ public function isThenable($value): bool
+ {
+ return $value instanceof SyncPromise;
+ }
+
+ /** @throws InvariantViolation */
+ public function convertThenable($thenable): Promise
+ {
+ if (! $thenable instanceof SyncPromise) {
+ // End-users should always use Deferred, not SyncPromise directly
+ $deferredClass = Deferred::class;
+ $safeThenable = Utils::printSafe($thenable);
+ throw new InvariantViolation("Expected instance of {$deferredClass}, got {$safeThenable}.");
+ }
+
+ return new Promise($thenable, $this);
+ }
+
+ /** @throws InvariantViolation */
+ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise
+ {
+ $syncPromise = $promise->adoptedPromise;
+ assert($syncPromise instanceof SyncPromise);
+
+ return new Promise($syncPromise->then($onFulfilled, $onRejected), $this);
+ }
+
+ /**
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public function create(callable $resolver): Promise
+ {
+ $syncPromise = new SyncPromise();
+
+ try {
+ $resolver(
+ [$syncPromise, 'resolve'],
+ [$syncPromise, 'reject']
+ );
+ } catch (\Throwable $e) {
+ $syncPromise->reject($e);
+ }
+
+ return new Promise($syncPromise, $this);
+ }
+
+ /**
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public function createFulfilled($value = null): Promise
+ {
+ $syncPromise = new SyncPromise();
+
+ return new Promise($syncPromise->resolve($value), $this);
+ }
+
+ /**
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public function createRejected(\Throwable $reason): Promise
+ {
+ $syncPromise = new SyncPromise();
+
+ return new Promise($syncPromise->reject($reason), $this);
+ }
+
+ /**
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public function all(iterable $promisesOrValues): Promise
+ {
+ $all = new SyncPromise();
+
+ $total = is_array($promisesOrValues)
+ ? count($promisesOrValues)
+ : iterator_count($promisesOrValues);
+ $count = 0;
+ $result = [];
+
+ $resolveAllWhenFinished = function () use (&$count, &$total, $all, &$result): void {
+ if ($count === $total) {
+ $all->resolve($result);
+ }
+ };
+
+ foreach ($promisesOrValues as $index => $promiseOrValue) {
+ if ($promiseOrValue instanceof Promise) {
+ $result[$index] = null;
+ $promiseOrValue->then(
+ static function ($value) use (&$result, $index, &$count, &$resolveAllWhenFinished): void {
+ $result[$index] = $value;
+ ++$count;
+ $resolveAllWhenFinished();
+ },
+ [$all, 'reject']
+ );
+ continue;
+ }
+
+ $result[$index] = $promiseOrValue;
+ ++$count;
+ }
+
+ $resolveAllWhenFinished();
+
+ return new Promise($all, $this);
+ }
+
+ /**
+ * Synchronously wait when promise completes.
+ *
+ * @throws InvariantViolation
+ *
+ * @return mixed
+ */
+ public function wait(Promise $promise)
+ {
+ $this->beforeWait($promise);
+
+ $syncPromise = $promise->adoptedPromise;
+ assert($syncPromise instanceof SyncPromise);
+
+ while ($syncPromise->state === SyncPromise::PENDING) {
+ SyncPromiseQueue::run();
+ $this->onWait($promise);
+ }
+
+ if ($syncPromise->state === SyncPromise::FULFILLED) {
+ return $syncPromise->result;
+ }
+
+ if ($syncPromise->state === SyncPromise::REJECTED) {
+ throw $syncPromise->result;
+ }
+
+ throw new InvariantViolation('Could not resolve promise.');
+ }
+
+ /** Execute just before starting to run promise completion. */
+ protected function beforeWait(Promise $promise): void {}
+
+ /** Execute while running promise completion. */
+ protected function onWait(Promise $promise): void {}
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseQueue.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseQueue.php
new file mode 100644
index 00000000000..3d0cbbe4a8d
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Adapter/SyncPromiseQueue.php
@@ -0,0 +1,74 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter;
+
+/**
+ * Queue for deferred execution of SyncPromise tasks.
+ *
+ * Owns the shared queue and provides the run loop for processing promises.
+ *
+ * @api
+ *
+ * @phpstan-type Task callable(): void
+ */
+class SyncPromiseQueue
+{
+ /**
+ * Adds a task to the queue.
+ *
+ * @param Task $task
+ *
+ * @api
+ */
+ public static function enqueue(callable $task): void
+ {
+ self::queue()->enqueue($task);
+ }
+
+ /**
+ * Process all queued promises until the queue is empty.
+ *
+ * @api
+ */
+ public static function run(): void
+ {
+ $queue = self::queue();
+ while (! $queue->isEmpty()) {
+ $task = $queue->dequeue();
+ $task();
+ }
+ }
+
+ /**
+ * Check if the queue is empty.
+ *
+ * @api
+ */
+ public static function isEmpty(): bool
+ {
+ return self::queue()->isEmpty();
+ }
+
+ /**
+ * Return the number of tasks in the queue.
+ *
+ * @api
+ */
+ public static function count(): int
+ {
+ return self::queue()->count();
+ }
+
+ /**
+ * TODO change to protected in next major version.
+ *
+ * @return \SplQueue<Task>
+ */
+ public static function queue(): \SplQueue
+ {
+ /** @var \SplQueue<Task>|null $queue */
+ static $queue;
+
+ return $queue ??= new \SplQueue();
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Promise.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Promise.php
new file mode 100644
index 00000000000..cce025f2ab6
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/Promise.php
@@ -0,0 +1,41 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise;
+
+use Amp\Future as AmpFuture;
+use Amp\Promise as AmpPromise;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromise;
+use React\Promise\PromiseInterface as ReactPromise;
+
+/**
+ * Convenience wrapper for promises represented by Promise Adapter.
+ */
+class Promise
+{
+ /** @var SyncPromise|ReactPromise<mixed>|AmpFuture<mixed>|AmpPromise<mixed> */
+ public $adoptedPromise;
+
+ private PromiseAdapter $adapter;
+
+ /**
+ * @param mixed $adoptedPromise
+ *
+ * @throws InvariantViolation
+ */
+ public function __construct($adoptedPromise, PromiseAdapter $adapter)
+ {
+ if ($adoptedPromise instanceof self) {
+ $selfClass = self::class;
+ throw new InvariantViolation("Expected promise from adapted system, got {$selfClass}.");
+ }
+
+ $this->adoptedPromise = $adoptedPromise;
+ $this->adapter = $adapter;
+ }
+
+ public function then(?callable $onFulfilled = null, ?callable $onRejected = null): Promise
+ {
+ return $this->adapter->then($this, $onFulfilled, $onRejected);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/PromiseAdapter.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/PromiseAdapter.php
new file mode 100644
index 00000000000..ae331ad10e0
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Promise/PromiseAdapter.php
@@ -0,0 +1,72 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise;
+
+/**
+ * Provides a means for integration of async PHP platforms ([related docs](data-fetching.md#async-php)).
+ */
+interface PromiseAdapter
+{
+ /**
+ * Is the value a promise or a deferred of the underlying platform?
+ *
+ * @param mixed $value
+ *
+ * @api
+ */
+ public function isThenable($value): bool;
+
+ /**
+ * Converts thenable of the underlying platform into Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise instance.
+ *
+ * @param mixed $thenable
+ *
+ * @api
+ */
+ public function convertThenable($thenable): Promise;
+
+ /**
+ * Accepts our Promise wrapper, extracts adopted promise out of it and executes actual `then` logic described
+ * in Promises/A+ specs. Then returns new wrapped instance of Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise.
+ *
+ * @api
+ */
+ public function then(Promise $promise, ?callable $onFulfilled = null, ?callable $onRejected = null): Promise;
+
+ /**
+ * Creates a Promise from the given resolver callable.
+ *
+ * @param callable(callable $resolve, callable $reject): void $resolver
+ *
+ * @api
+ */
+ public function create(callable $resolver): Promise;
+
+ /**
+ * Creates a fulfilled Promise for a value if the value is not a promise.
+ *
+ * @param mixed $value
+ *
+ * @api
+ */
+ public function createFulfilled($value = null): Promise;
+
+ /**
+ * Creates a rejected promise for a reason if the reason is not a promise.
+ *
+ * If the provided reason is a promise, then it is returned as-is.
+ *
+ * @api
+ */
+ public function createRejected(\Throwable $reason): Promise;
+
+ /**
+ * Given an iterable of promises (or values), returns a promise that is fulfilled when all the
+ * items in the iterable are fulfilled.
+ *
+ * @param iterable<Promise|mixed> $promisesOrValues
+ *
+ * @api
+ */
+ public function all(iterable $promisesOrValues): Promise;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/PromiseExecutor.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/PromiseExecutor.php
new file mode 100644
index 00000000000..0416561bb14
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/PromiseExecutor.php
@@ -0,0 +1,20 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+
+class PromiseExecutor implements ExecutorImplementation
+{
+ private Promise $result;
+
+ public function __construct(Promise $result)
+ {
+ $this->result = $result;
+ }
+
+ public function doExecute(): Promise
+ {
+ return $this->result;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ReferenceExecutor.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ReferenceExecutor.php
new file mode 100644
index 00000000000..707434168db
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/ReferenceExecutor.php
@@ -0,0 +1,1501 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Warning;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\AbstractType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\LeafType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\OutputType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-import-type FieldResolver from Executor
+ * @phpstan-import-type Path from ResolveInfo
+ * @phpstan-import-type ArgsMapper from Executor
+ *
+ * @phpstan-type Fields \ArrayObject<string, \ArrayObject<int, FieldNode>>
+ */
+class ReferenceExecutor implements ExecutorImplementation
+{
+ protected static \stdClass $UNDEFINED;
+
+ protected ExecutionContext $exeContext;
+
+ /**
+ * @var \SplObjectStorage<
+ * ObjectType,
+ * \SplObjectStorage<
+ * \ArrayObject<int, FieldNode>,
+ * \ArrayObject<
+ * string,
+ * \ArrayObject<int, FieldNode>
+ * >
+ * >
+ * >
+ */
+ protected \SplObjectStorage $subFieldCache;
+
+ /**
+ * @var \SplObjectStorage<
+ * FieldDefinition,
+ * \SplObjectStorage<FieldNode, mixed>
+ * >
+ */
+ protected \SplObjectStorage $fieldArgsCache;
+
+ protected FieldDefinition $schemaMetaFieldDef;
+
+ protected FieldDefinition $typeMetaFieldDef;
+
+ protected FieldDefinition $typeNameMetaFieldDef;
+
+ protected function __construct(ExecutionContext $context)
+ {
+ if (! isset(static::$UNDEFINED)) {
+ static::$UNDEFINED = Utils::undefined();
+ }
+
+ $this->exeContext = $context;
+ $this->subFieldCache = new \SplObjectStorage();
+ $this->fieldArgsCache = new \SplObjectStorage();
+ }
+
+ /**
+ * @param mixed $rootValue
+ * @param mixed $contextValue
+ * @param array<string, mixed> $variableValues
+ *
+ * @phpstan-param FieldResolver $fieldResolver
+ * @phpstan-param ArgsMapper $argsMapper
+ *
+ * @throws \Exception
+ */
+ public static function create(
+ PromiseAdapter $promiseAdapter,
+ Schema $schema,
+ DocumentNode $documentNode,
+ $rootValue,
+ $contextValue,
+ array $variableValues,
+ ?string $operationName,
+ callable $fieldResolver,
+ ?callable $argsMapper = null // TODO make non-optional in next major release
+ ): ExecutorImplementation {
+ $exeContext = static::buildExecutionContext(
+ $schema,
+ $documentNode,
+ $rootValue,
+ $contextValue,
+ $variableValues,
+ $operationName,
+ $fieldResolver,
+ $argsMapper ?? Executor::getDefaultArgsMapper(),
+ $promiseAdapter,
+ );
+
+ if (is_array($exeContext)) {
+ $executionResult = new ExecutionResult(null, $exeContext);
+ $fulfilledPromise = $promiseAdapter->createFulfilled($executionResult);
+
+ return new PromiseExecutor($fulfilledPromise);
+ }
+
+ return new static($exeContext);
+ }
+
+ /**
+ * Constructs an ExecutionContext object from the arguments passed to execute,
+ * which we will pass throughout the other execution methods.
+ *
+ * @param mixed $rootValue
+ * @param mixed $contextValue
+ * @param array<string, mixed> $rawVariableValues
+ *
+ * @phpstan-param FieldResolver $fieldResolver
+ *
+ * @throws \Exception
+ *
+ * @return ExecutionContext|list<Error>
+ */
+ protected static function buildExecutionContext(
+ Schema $schema,
+ DocumentNode $documentNode,
+ $rootValue,
+ $contextValue,
+ array $rawVariableValues,
+ ?string $operationName,
+ callable $fieldResolver,
+ callable $argsMapper,
+ PromiseAdapter $promiseAdapter
+ ) {
+ /** @var list<Error> $errors */
+ $errors = [];
+
+ /** @var array<string, FragmentDefinitionNode> $fragments */
+ $fragments = [];
+
+ /** @var OperationDefinitionNode|null $operation */
+ $operation = null;
+
+ /** @var bool $hasMultipleAssumedOperations */
+ $hasMultipleAssumedOperations = false;
+
+ foreach ($documentNode->definitions as $definition) {
+ switch (true) {
+ case $definition instanceof OperationDefinitionNode:
+ if ($operationName === null && $operation !== null) {
+ $hasMultipleAssumedOperations = true;
+ }
+
+ if (
+ $operationName === null
+ || (isset($definition->name) && $definition->name->value === $operationName)
+ ) {
+ $operation = $definition;
+ }
+
+ break;
+ case $definition instanceof FragmentDefinitionNode:
+ $fragments[$definition->name->value] = $definition;
+ break;
+ }
+ }
+
+ if ($operation === null) {
+ $message = $operationName === null
+ ? 'Must provide an operation.'
+ : "Unknown operation named \"{$operationName}\".";
+ $errors[] = new Error($message);
+ } elseif ($hasMultipleAssumedOperations) {
+ $errors[] = new Error(
+ 'Must provide operation name if query contains multiple operations.'
+ );
+ }
+
+ $variableValues = null;
+ if ($operation !== null) {
+ [$coercionErrors, $coercedVariableValues] = Values::getVariableValues(
+ $schema,
+ $operation->variableDefinitions,
+ $rawVariableValues
+ );
+ if ($coercionErrors === null) {
+ $variableValues = $coercedVariableValues;
+ } else {
+ $errors = array_merge($errors, $coercionErrors);
+ }
+ }
+
+ if ($errors !== []) {
+ return $errors;
+ }
+
+ assert($operation instanceof OperationDefinitionNode, 'Has operation if no errors.');
+ assert(is_array($variableValues), 'Has variables if no errors.');
+
+ return new ExecutionContext(
+ $schema,
+ $fragments,
+ $rootValue,
+ $contextValue,
+ $operation,
+ $variableValues,
+ $errors,
+ $fieldResolver,
+ $argsMapper,
+ $promiseAdapter
+ );
+ }
+
+ /**
+ * @throws \Exception
+ * @throws Error
+ */
+ public function doExecute(): Promise
+ {
+ // Return a Promise that will eventually resolve to the data described by
+ // the "Response" section of the Automattic\WooCommerce\Vendor\GraphQL specification.
+ //
+ // If errors are encountered while executing a Automattic\WooCommerce\Vendor\GraphQL field, only that
+ // field and its descendants will be omitted, and sibling fields will still
+ // be executed. An execution which encounters errors will still result in a
+ // resolved Promise.
+ $data = $this->executeOperation($this->exeContext->operation, $this->exeContext->rootValue);
+ $result = $this->buildResponse($data);
+
+ // Note: we deviate here from the reference implementation a bit by always returning promise
+ // But for the "sync" case it is always fulfilled
+
+ $promise = $this->getPromise($result);
+ if ($promise !== null) {
+ return $promise;
+ }
+
+ return $this->exeContext->promiseAdapter->createFulfilled($result);
+ }
+
+ /**
+ * @param mixed $data
+ *
+ * @return ExecutionResult|Promise
+ */
+ protected function buildResponse($data)
+ {
+ if ($data instanceof Promise) {
+ return $data->then(fn ($resolved) => $this->buildResponse($resolved));
+ }
+
+ $promiseAdapter = $this->exeContext->promiseAdapter;
+ if ($promiseAdapter->isThenable($data)) {
+ return $promiseAdapter->convertThenable($data)
+ ->then(fn ($resolved) => $this->buildResponse($resolved));
+ }
+
+ if ($data !== null) {
+ $data = (array) $data;
+ }
+
+ return new ExecutionResult($data, $this->exeContext->errors);
+ }
+
+ /**
+ * Implements the "Evaluating operations" section of the spec.
+ *
+ * @param mixed $rootValue
+ *
+ * @throws \Exception
+ *
+ * @return array<mixed>|Promise|\stdClass|null
+ */
+ protected function executeOperation(OperationDefinitionNode $operation, $rootValue)
+ {
+ $type = $this->getOperationRootType($this->exeContext->schema, $operation);
+ $fields = $this->collectFields($type, $operation->selectionSet, new \ArrayObject(), new \ArrayObject());
+ $path = [];
+ $unaliasedPath = [];
+ // Errors from sub-fields of a NonNull type may propagate to the top level,
+ // at which point we still log the error and null the parent field, which
+ // in this case is the entire response.
+ //
+ // Similar to completeValueCatchingError.
+ try {
+ $result = $operation->operation === 'mutation'
+ ? $this->executeFieldsSerially($type, $rootValue, $path, $unaliasedPath, $fields, $this->exeContext->contextValue)
+ : $this->executeFields($type, $rootValue, $path, $unaliasedPath, $fields, $this->exeContext->contextValue);
+
+ $promise = $this->getPromise($result);
+ if ($promise !== null) {
+ return $promise->then(null, [$this, 'onError']);
+ }
+
+ return $result;
+ } catch (Error $error) {
+ $this->exeContext->addError($error);
+
+ return null;
+ }
+ }
+
+ /** @param mixed $error */
+ public function onError($error): ?Promise
+ {
+ if ($error instanceof Error) {
+ $this->exeContext->addError($error);
+
+ return $this->exeContext->promiseAdapter->createFulfilled();
+ }
+
+ return null;
+ }
+
+ /**
+ * Extracts the root type of the operation from the schema.
+ *
+ * @throws \Exception
+ * @throws Error
+ */
+ protected function getOperationRootType(Schema $schema, OperationDefinitionNode $operation): ObjectType
+ {
+ switch ($operation->operation) {
+ case 'query':
+ $queryType = $schema->getQueryType();
+ if ($queryType === null) {
+ throw new Error('Schema does not define the required query root type.', [$operation]);
+ }
+
+ return $queryType;
+
+ case 'mutation':
+ $mutationType = $schema->getMutationType();
+ if ($mutationType === null) {
+ throw new Error('Schema is not configured for mutations.', [$operation]);
+ }
+
+ return $mutationType;
+
+ case 'subscription':
+ $subscriptionType = $schema->getSubscriptionType();
+ if ($subscriptionType === null) {
+ throw new Error('Schema is not configured for subscriptions.', [$operation]);
+ }
+
+ return $subscriptionType;
+
+ default:
+ throw new Error('Can only execute queries, mutations and subscriptions.', [$operation]);
+ }
+ }
+
+ /**
+ * Given a selectionSet, adds all fields in that selection to
+ * the passed in map of fields, and returns it at the end.
+ *
+ * CollectFields requires the "runtime type" of an object. For a field which
+ * returns an Interface or Union type, the "runtime type" will be the actual
+ * Object type returned by that field.
+ *
+ * @param \ArrayObject<string, true> $visitedFragmentNames
+ *
+ * @phpstan-param Fields $fields
+ *
+ * @throws \Exception
+ * @throws Error
+ *
+ * @phpstan-return Fields
+ */
+ protected function collectFields(
+ ObjectType $runtimeType,
+ SelectionSetNode $selectionSet,
+ \ArrayObject $fields,
+ \ArrayObject $visitedFragmentNames
+ ): \ArrayObject {
+ $exeContext = $this->exeContext;
+ foreach ($selectionSet->selections as $selection) {
+ switch (true) {
+ case $selection instanceof FieldNode:
+ if (! $this->shouldIncludeNode($selection)) {
+ break;
+ }
+
+ $name = static::getFieldEntryKey($selection);
+ $fields[$name] ??= new \ArrayObject();
+ $fields[$name][] = $selection;
+ break;
+ case $selection instanceof InlineFragmentNode:
+ if (
+ ! $this->shouldIncludeNode($selection)
+ || ! $this->doesFragmentConditionMatch($selection, $runtimeType)
+ ) {
+ break;
+ }
+
+ $this->collectFields(
+ $runtimeType,
+ $selection->selectionSet,
+ $fields,
+ $visitedFragmentNames
+ );
+ break;
+ case $selection instanceof FragmentSpreadNode:
+ $fragName = $selection->name->value;
+
+ if (isset($visitedFragmentNames[$fragName]) || ! $this->shouldIncludeNode($selection)) {
+ break;
+ }
+
+ $visitedFragmentNames[$fragName] = true;
+
+ if (! isset($exeContext->fragments[$fragName])) {
+ break;
+ }
+
+ $fragment = $exeContext->fragments[$fragName];
+ if (! $this->doesFragmentConditionMatch($fragment, $runtimeType)) {
+ break;
+ }
+
+ $this->collectFields(
+ $runtimeType,
+ $fragment->selectionSet,
+ $fields,
+ $visitedFragmentNames
+ );
+ break;
+ }
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Determines if a field should be included based on the @include and @skip
+ * directives, where @skip has higher precedence than @include.
+ *
+ * @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
+ *
+ * @throws \Exception
+ * @throws Error
+ */
+ protected function shouldIncludeNode(SelectionNode $node): bool
+ {
+ $variableValues = $this->exeContext->variableValues;
+
+ $schema = $this->exeContext->schema;
+
+ $skip = Values::getDirectiveValues(
+ Directive::skipDirective(),
+ $node,
+ $variableValues,
+ $schema,
+ );
+ if (isset($skip['if']) && $skip['if'] === true) {
+ return false;
+ }
+
+ $include = Values::getDirectiveValues(
+ Directive::includeDirective(),
+ $node,
+ $variableValues,
+ $schema,
+ );
+
+ return ! isset($include['if']) || $include['if'] !== false;
+ }
+
+ /** Implements the logic to compute the key of a given fields entry. */
+ protected static function getFieldEntryKey(FieldNode $node): string
+ {
+ return $node->alias->value
+ ?? $node->name->value;
+ }
+
+ /**
+ * Determines if a fragment is applicable to the given type.
+ *
+ * @param FragmentDefinitionNode|InlineFragmentNode $fragment
+ *
+ * @throws \Exception
+ */
+ protected function doesFragmentConditionMatch(Node $fragment, ObjectType $type): bool
+ {
+ $typeConditionNode = $fragment->typeCondition;
+ if ($typeConditionNode === null) {
+ return true;
+ }
+
+ $conditionalType = AST::typeFromAST([$this->exeContext->schema, 'getType'], $typeConditionNode);
+ if ($conditionalType === $type) {
+ return true;
+ }
+
+ if ($conditionalType instanceof AbstractType) {
+ return $this->exeContext->schema->isSubType($conditionalType, $type);
+ }
+
+ return false;
+ }
+
+ /**
+ * Implements the "Evaluating selection sets" section of the spec for "write" mode.
+ *
+ * @param mixed $rootValue
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ * @param mixed $contextValue
+ *
+ * @phpstan-param Fields $fields
+ *
+ * @return array<mixed>|Promise|\stdClass
+ */
+ protected function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, array $unaliasedPath, \ArrayObject $fields, $contextValue)
+ {
+ $result = $this->promiseReduce(
+ array_keys($fields->getArrayCopy()),
+ function ($results, $responseName) use ($contextValue, $path, $unaliasedPath, $parentType, $rootValue, $fields) {
+ $fieldNodes = $fields[$responseName];
+ assert($fieldNodes instanceof \ArrayObject, 'The keys of $fields populate $responseName');
+
+ $result = $this->resolveField(
+ $parentType,
+ $rootValue,
+ $fieldNodes,
+ $responseName,
+ $path,
+ $unaliasedPath,
+ $this->maybeScopeContext($contextValue)
+ );
+ if ($result === static::$UNDEFINED) {
+ return $results;
+ }
+
+ $promise = $this->getPromise($result);
+ if ($promise !== null) {
+ return $promise->then(static function ($resolvedResult) use ($responseName, $results): array {
+ $results[$responseName] = $resolvedResult;
+
+ return $results;
+ });
+ }
+
+ $results[$responseName] = $result;
+
+ return $results;
+ },
+ []
+ );
+
+ $promise = $this->getPromise($result);
+ if ($promise !== null) {
+ return $result->then(
+ static fn ($resolvedResults) => static::fixResultsIfEmptyArray($resolvedResults)
+ );
+ }
+
+ return static::fixResultsIfEmptyArray($result);
+ }
+
+ /**
+ * Resolves the field on the given root value.
+ *
+ * In particular, this figures out the value that the field returns
+ * by calling its resolve function, then calls completeValue to complete promises,
+ * serialize scalars, or execute the sub-selection-set for objects.
+ *
+ * @param mixed $rootValue
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ * @param mixed $contextValue
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ *
+ * @phpstan-param Path $path
+ * @phpstan-param Path $unaliasedPath
+ *
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return array<mixed>|\Throwable|mixed|null
+ */
+ protected function resolveField(
+ ObjectType $parentType,
+ $rootValue,
+ \ArrayObject $fieldNodes,
+ string $responseName,
+ array $path,
+ array $unaliasedPath,
+ $contextValue
+ ) {
+ $exeContext = $this->exeContext;
+
+ $fieldNode = $fieldNodes[0];
+ assert($fieldNode instanceof FieldNode, '$fieldNodes is non-empty');
+
+ $fieldName = $fieldNode->name->value;
+ $fieldDef = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
+ if ($fieldDef === null || ! $fieldDef->isVisible()) {
+ return static::$UNDEFINED;
+ }
+
+ $path[] = $responseName;
+ $unaliasedPath[] = $fieldName;
+
+ $returnType = $fieldDef->getType();
+ // The resolve function's optional 3rd argument is a context value that
+ // is provided to every resolve function within an execution. It is commonly
+ // used to represent an authenticated user, or request-specific caches.
+ // The resolve function's optional 4th argument is a collection of
+ // information about the current execution state.
+ $info = new ResolveInfo(
+ $fieldDef,
+ $fieldNodes,
+ $parentType,
+ $path,
+ $exeContext->schema,
+ $exeContext->fragments,
+ $exeContext->rootValue,
+ $exeContext->operation,
+ $exeContext->variableValues,
+ $unaliasedPath
+ );
+
+ $resolveFn = $fieldDef->resolveFn
+ ?? $parentType->resolveFieldFn
+ ?? $this->exeContext->fieldResolver;
+
+ $argsMapper = $fieldDef->argsMapper
+ ?? $parentType->argsMapper
+ ?? $this->exeContext->argsMapper;
+
+ // Get the resolve function, regardless of if its result is normal
+ // or abrupt (error).
+ $result = $this->resolveFieldValueOrError(
+ $fieldDef,
+ $fieldNode,
+ $resolveFn,
+ $argsMapper,
+ $rootValue,
+ $info,
+ $contextValue
+ );
+
+ return $this->completeValueCatchingError(
+ $returnType,
+ $fieldNodes,
+ $info,
+ $path,
+ $unaliasedPath,
+ $result,
+ $contextValue
+ );
+ }
+
+ /**
+ * This method looks up the field on the given type definition.
+ *
+ * It has special casing for the two introspection fields, __schema
+ * and __typename. __typename is special because it can always be
+ * queried as a field, even in situations where no other fields
+ * are allowed, like on a Union. __schema could get automatically
+ * added to the query type, but that would require mutating type
+ * definitions, which would cause issues.
+ *
+ * @throws InvariantViolation
+ */
+ protected function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName): ?FieldDefinition
+ {
+ $this->schemaMetaFieldDef ??= Introspection::schemaMetaFieldDef();
+ $this->typeMetaFieldDef ??= Introspection::typeMetaFieldDef();
+ $this->typeNameMetaFieldDef ??= Introspection::typeNameMetaFieldDef();
+
+ $queryType = $schema->getQueryType();
+
+ if ($fieldName === $this->schemaMetaFieldDef->name
+ && $queryType === $parentType
+ ) {
+ return $this->schemaMetaFieldDef;
+ }
+
+ if ($fieldName === $this->typeMetaFieldDef->name
+ && $queryType === $parentType
+ ) {
+ return $this->typeMetaFieldDef;
+ }
+
+ if ($fieldName === $this->typeNameMetaFieldDef->name) {
+ return $this->typeNameMetaFieldDef;
+ }
+
+ return $parentType->findField($fieldName);
+ }
+
+ /**
+ * Isolates the "ReturnOrAbrupt" behavior to not de-opt the `resolveField` function.
+ * Returns the result of resolveFn or the abrupt-return Error object.
+ *
+ * @param mixed $rootValue
+ * @param mixed $contextValue
+ *
+ * @phpstan-param FieldResolver $resolveFn
+ *
+ * @return \Throwable|Promise|mixed
+ */
+ protected function resolveFieldValueOrError(
+ FieldDefinition $fieldDef,
+ FieldNode $fieldNode,
+ callable $resolveFn,
+ callable $argsMapper,
+ $rootValue,
+ ResolveInfo $info,
+ $contextValue
+ ) {
+ try {
+ // Build a map of arguments from the field.arguments AST, using the
+ // variables scope to fulfill any variable references.
+ // @phpstan-ignore-next-line generics of SplObjectStorage are not inferred from empty instantiation
+ $this->fieldArgsCache[$fieldDef] ??= new \SplObjectStorage();
+
+ $args = $this->fieldArgsCache[$fieldDef][$fieldNode] ??= $argsMapper(Values::getArgumentValues(
+ $fieldDef,
+ $fieldNode,
+ $this->exeContext->variableValues,
+ $this->exeContext->schema,
+ ), $fieldDef, $fieldNode, $contextValue);
+
+ return $resolveFn($rootValue, $args, $contextValue, $info);
+ } catch (\Throwable $error) {
+ return $error;
+ }
+ }
+
+ /**
+ * This is a small wrapper around completeValue which detects and logs errors
+ * in the execution context.
+ *
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ * @param mixed $contextValue
+ * @param mixed $result
+ *
+ * @phpstan-param Path $path
+ * @phpstan-param Path $unaliasedPath
+ *
+ * @throws Error
+ *
+ * @return array<mixed>|Promise|\stdClass|null
+ */
+ protected function completeValueCatchingError(
+ Type $returnType,
+ \ArrayObject $fieldNodes,
+ ResolveInfo $info,
+ array $path,
+ array $unaliasedPath,
+ $result,
+ $contextValue
+ ) {
+ // Otherwise, error protection is applied, logging the error and resolving
+ // a null value for this field if one is encountered.
+ try {
+ $promise = $this->getPromise($result);
+ if ($promise !== null) {
+ $completed = $promise->then(fn (&$resolved) => $this->completeValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $resolved, $contextValue));
+ } else {
+ $completed = $this->completeValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue);
+ }
+
+ $promise = $this->getPromise($completed);
+ if ($promise !== null) {
+ return $promise->then(null, function ($error) use ($fieldNodes, $path, $unaliasedPath, $returnType): void {
+ $this->handleFieldError($error, $fieldNodes, $path, $unaliasedPath, $returnType);
+ });
+ }
+
+ return $completed;
+ } catch (\Throwable $err) {
+ $this->handleFieldError($err, $fieldNodes, $path, $unaliasedPath, $returnType);
+
+ return null;
+ }
+ }
+
+ /**
+ * @param mixed $rawError
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ *
+ * @throws Error
+ */
+ protected function handleFieldError($rawError, \ArrayObject $fieldNodes, array $path, array $unaliasedPath, Type $returnType): void
+ {
+ $error = Error::createLocatedError(
+ $rawError,
+ $fieldNodes,
+ $path,
+ $unaliasedPath
+ );
+
+ // If the field type is non-nullable, then it is resolved without any
+ // protection from errors, however it still properly locates the error.
+ if ($returnType instanceof NonNull) {
+ throw $error;
+ }
+
+ // Otherwise, error protection is applied, logging the error and resolving
+ // a null value for this field if one is encountered.
+ $this->exeContext->addError($error);
+ }
+
+ /**
+ * Implements the instructions for completeValue as defined in the
+ * "Field entries" section of the spec.
+ *
+ * If the field type is Non-Null, then this recursively completes the value
+ * for the inner type. It throws a field error if that completion returns null,
+ * as per the "Nullability" section of the spec.
+ *
+ * If the field type is a List, then this recursively completes the value
+ * for the inner type on each item in the list.
+ *
+ * If the field type is a Scalar or Enum, ensures the completed value is a legal
+ * value of the type by calling the `serialize` method of Automattic\WooCommerce\Vendor\GraphQL type
+ * definition.
+ *
+ * If the field is an abstract type, determine the runtime type of the value
+ * and then complete based on that type.
+ *
+ * Otherwise, the field type expects a sub-selection set, and will complete the
+ * value by evaluating all sub-selections.
+ *
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ * @param mixed $result
+ * @param mixed $contextValue
+ *
+ * @throws \Throwable
+ * @throws Error
+ *
+ * @return array<mixed>|mixed|Promise|null
+ */
+ protected function completeValue(
+ Type $returnType,
+ \ArrayObject $fieldNodes,
+ ResolveInfo $info,
+ array $path,
+ array $unaliasedPath,
+ $result,
+ $contextValue
+ ) {
+ // If result is an Error, throw a located error.
+ if ($result instanceof \Throwable) {
+ throw $result;
+ }
+
+ // If field type is NonNull, complete for inner type, and throw field error
+ // if result is null.
+ if ($returnType instanceof NonNull) {
+ $completed = $this->completeValue(
+ $returnType->getWrappedType(),
+ $fieldNodes,
+ $info,
+ $path,
+ $unaliasedPath,
+ $result,
+ $contextValue
+ );
+ if ($completed === null) {
+ throw new InvariantViolation("Cannot return null for non-nullable field \"{$info->parentType}.{$info->fieldName}\".");
+ }
+
+ return $completed;
+ }
+
+ if ($result === null) {
+ return null;
+ }
+
+ // If field type is List, complete each item in the list with the inner type
+ if ($returnType instanceof ListOfType) {
+ if (! is_iterable($result)) {
+ $resultType = gettype($result);
+
+ throw new InvariantViolation("Expected field {$info->parentType}.{$info->fieldName} to return iterable, but got: {$resultType}.");
+ }
+
+ return $this->completeListValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue);
+ }
+
+ assert($returnType instanceof NamedType, 'Wrapping types should return early');
+
+ // Account for invalid schema definition when typeLoader returns different
+ // instance than `resolveType` or $field->getType() or $arg->getType()
+ assert(
+ $returnType === $this->exeContext->schema->getType($returnType->name)
+ || Type::isBuiltInScalar($returnType),
+ SchemaValidationContext::duplicateType($this->exeContext->schema, "{$info->parentType}.{$info->fieldName}", $returnType->name)
+ );
+
+ if ($returnType instanceof LeafType) {
+ if (Type::isBuiltInScalar($returnType)) {
+ $schemaType = $this->exeContext->schema->getType($returnType->name);
+ assert($schemaType instanceof LeafType, "Schema must provide a LeafType for built-in scalar \"{$returnType->name}\".");
+ $returnType = $schemaType;
+ }
+
+ return $this->completeLeafValue($returnType, $result);
+ }
+
+ if ($returnType instanceof AbstractType) {
+ return $this->completeAbstractValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue);
+ }
+
+ // Field type must be and Object, Interface or Union and expect sub-selections.
+ if ($returnType instanceof ObjectType) {
+ return $this->completeObjectValue($returnType, $fieldNodes, $info, $path, $unaliasedPath, $result, $contextValue);
+ }
+
+ $safeReturnType = Utils::printSafe($returnType);
+ throw new \RuntimeException("Cannot complete value of unexpected type {$safeReturnType}.");
+ }
+
+ /** @param mixed $value */
+ protected function isPromise($value): bool
+ {
+ return $value instanceof Promise
+ || $this->exeContext->promiseAdapter->isThenable($value);
+ }
+
+ /**
+ * Only returns the value if it acts like a Promise, i.e. has a "then" function,
+ * otherwise returns null.
+ *
+ * @param mixed $value
+ */
+ protected function getPromise($value): ?Promise
+ {
+ if ($value === null || $value instanceof Promise) {
+ return $value;
+ }
+
+ $promiseAdapter = $this->exeContext->promiseAdapter;
+ if ($promiseAdapter->isThenable($value)) {
+ return $promiseAdapter->convertThenable($value);
+ }
+
+ return null;
+ }
+
+ /**
+ * Similar to array_reduce(), however the reducing callback may return
+ * a Promise, in which case reduction will continue after each promise resolves.
+ *
+ * If the callback does not return a Promise, then this function will also not
+ * return a Promise.
+ *
+ * @param array<mixed> $values
+ * @param Promise|mixed|null $initialValue
+ *
+ * @return Promise|mixed|null
+ */
+ protected function promiseReduce(array $values, callable $callback, $initialValue)
+ {
+ return array_reduce(
+ $values,
+ function ($previous, $value) use ($callback) {
+ $promise = $this->getPromise($previous);
+ if ($promise !== null) {
+ return $promise->then(static fn ($resolved) => $callback($resolved, $value));
+ }
+
+ return $callback($previous, $value);
+ },
+ $initialValue
+ );
+ }
+
+ /**
+ * Complete a list value by completing each item in the list with the inner type.
+ *
+ * @param ListOfType<Type&OutputType> $returnType
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ * @param iterable<mixed> $results
+ * @param mixed $contextValue
+ *
+ * @throws Error
+ *
+ * @return array<mixed>|Promise|\stdClass
+ */
+ protected function completeListValue(
+ ListOfType $returnType,
+ \ArrayObject $fieldNodes,
+ ResolveInfo $info,
+ array $path,
+ array $unaliasedPath,
+ iterable $results,
+ $contextValue
+ ) {
+ $itemType = $returnType->getWrappedType();
+
+ $i = 0;
+ $containsPromise = false;
+ $completedItems = [];
+ foreach ($results as $item) {
+ $itemPath = [...$path, $i];
+ $info->path = $itemPath;
+ $itemUnaliasedPath = [...$unaliasedPath, $i];
+ $info->unaliasedPath = $itemUnaliasedPath;
+ ++$i;
+
+ $completedItem = $this->completeValueCatchingError($itemType, $fieldNodes, $info, $itemPath, $itemUnaliasedPath, $item, $contextValue);
+
+ if (! $containsPromise && $this->getPromise($completedItem) !== null) {
+ $containsPromise = true;
+ }
+
+ $completedItems[] = $completedItem;
+ }
+
+ return $containsPromise
+ ? $this->exeContext->promiseAdapter->all($completedItems)
+ : $completedItems;
+ }
+
+ /**
+ * Complete a Scalar or Enum by serializing to a valid value, throwing if serialization is not possible.
+ *
+ * @param mixed $result
+ *
+ * @throws \Exception
+ *
+ * @return mixed
+ */
+ protected function completeLeafValue(LeafType $returnType, $result)
+ {
+ try {
+ return $returnType->serialize($result);
+ } catch (\Throwable $error) {
+ $safeReturnType = Utils::printSafe($returnType);
+ $safeResult = Utils::printSafe($result);
+ throw new InvariantViolation("Expected a value of type {$safeReturnType} but received: {$safeResult}. {$error->getMessage()}", 0, $error);
+ }
+ }
+
+ /**
+ * Complete a value of an abstract type by determining the runtime object type
+ * of that value, then complete the value for that type.
+ *
+ * @param AbstractType&Type $returnType
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ * @param mixed $result
+ * @param mixed $contextValue
+ *
+ * @throws \Exception
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return array<mixed>|Promise|\stdClass
+ */
+ protected function completeAbstractValue(
+ AbstractType $returnType,
+ \ArrayObject $fieldNodes,
+ ResolveInfo $info,
+ array $path,
+ array $unaliasedPath,
+ $result,
+ $contextValue
+ ) {
+ $result = $returnType->resolveValue($result, $contextValue, $info);
+ $typeCandidate = $returnType->resolveType($result, $contextValue, $info);
+
+ if ($typeCandidate === null) {
+ $runtimeType = static::defaultTypeResolver($result, $contextValue, $info, $returnType);
+ } elseif (! is_string($typeCandidate) && is_callable($typeCandidate)) {
+ $runtimeType = $typeCandidate();
+ } else {
+ $runtimeType = $typeCandidate;
+ }
+
+ $promise = $this->getPromise($runtimeType);
+ if ($promise !== null) {
+ return $promise->then(fn ($resolvedRuntimeType) => $this->completeObjectValue(
+ $this->ensureValidRuntimeType(
+ $resolvedRuntimeType,
+ $returnType,
+ $info,
+ $result
+ ),
+ $fieldNodes,
+ $info,
+ $path,
+ $unaliasedPath,
+ $result,
+ $contextValue
+ ));
+ }
+
+ return $this->completeObjectValue(
+ $this->ensureValidRuntimeType(
+ $runtimeType,
+ $returnType,
+ $info,
+ $result
+ ),
+ $fieldNodes,
+ $info,
+ $path,
+ $unaliasedPath,
+ $result,
+ $contextValue
+ );
+ }
+
+ /**
+ * If a resolveType function is not given, then a default resolve behavior is
+ * used which attempts two strategies:.
+ *
+ * First, See if the provided value has a `__typename` field defined, if so, use
+ * that value as name of the resolved type.
+ *
+ * Otherwise, test each possible type for the abstract type by calling
+ * isTypeOf for the object being coerced, returning the first type that matches.
+ *
+ * @param mixed|null $value
+ * @param mixed|null $contextValue
+ * @param AbstractType&Type $abstractType
+ *
+ * @throws InvariantViolation
+ *
+ * @return Promise|Type|string|null
+ */
+ protected function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType)
+ {
+ $typename = Utils::extractKey($value, '__typename');
+ if (is_string($typename)) {
+ return $typename;
+ }
+
+ if ($abstractType instanceof InterfaceType && isset($info->schema->getConfig()->typeLoader)) {
+ $safeValue = Utils::printSafe($value);
+ Warning::warnOnce(
+ "Automattic\WooCommerce\Vendor\GraphQL Interface Type `{$abstractType->name}` returned `null` from its `resolveType` function for value: {$safeValue}. Switching to slow resolution method using `isTypeOf` of all possible implementations. It requires full schema scan and degrades query performance significantly. Make sure your `resolveType` function always returns a valid implementation or throws.",
+ Warning::WARNING_FULL_SCHEMA_SCAN
+ );
+ }
+
+ $possibleTypes = $info->schema->getPossibleTypes($abstractType);
+ $promisedIsTypeOfResults = [];
+ foreach ($possibleTypes as $index => $type) {
+ $isTypeOfResult = $type->isTypeOf($value, $contextValue, $info);
+ if ($isTypeOfResult === null) {
+ continue;
+ }
+
+ $promise = $this->getPromise($isTypeOfResult);
+ if ($promise !== null) {
+ $promisedIsTypeOfResults[$index] = $promise;
+ } elseif ($isTypeOfResult === true) {
+ return $type;
+ }
+ }
+
+ if ($promisedIsTypeOfResults !== []) {
+ return $this->exeContext->promiseAdapter
+ ->all($promisedIsTypeOfResults)
+ ->then(static function ($isTypeOfResults) use ($possibleTypes): ?ObjectType {
+ foreach ($isTypeOfResults as $index => $result) {
+ if ($result) {
+ return $possibleTypes[$index];
+ }
+ }
+
+ return null;
+ });
+ }
+
+ return null;
+ }
+
+ /**
+ * Complete an Object value by executing all sub-selections.
+ *
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ * @param mixed $result
+ * @param mixed $contextValue
+ *
+ * @throws \Exception
+ * @throws Error
+ *
+ * @return array<mixed>|Promise|\stdClass
+ */
+ protected function completeObjectValue(
+ ObjectType $returnType,
+ \ArrayObject $fieldNodes,
+ ResolveInfo $info,
+ array $path,
+ array $unaliasedPath,
+ $result,
+ $contextValue
+ ) {
+ // If there is an isTypeOf predicate function, call it with the
+ // current result. If isTypeOf returns false, then raise an error rather
+ // than continuing execution.
+ $isTypeOf = $returnType->isTypeOf($result, $contextValue, $info);
+ if ($isTypeOf !== null) {
+ $promise = $this->getPromise($isTypeOf);
+ if ($promise !== null) {
+ return $promise->then(function ($isTypeOfResult) use (
+ $contextValue,
+ $returnType,
+ $fieldNodes,
+ $path,
+ $unaliasedPath,
+ $result
+ ) {
+ if (! $isTypeOfResult) {
+ throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
+ }
+
+ return $this->collectAndExecuteSubfields(
+ $returnType,
+ $fieldNodes,
+ $path,
+ $unaliasedPath,
+ $result,
+ $contextValue
+ );
+ });
+ }
+
+ assert(is_bool($isTypeOf), 'Promise would return early');
+ if (! $isTypeOf) {
+ throw $this->invalidReturnTypeError($returnType, $result, $fieldNodes);
+ }
+ }
+
+ return $this->collectAndExecuteSubfields(
+ $returnType,
+ $fieldNodes,
+ $path,
+ $unaliasedPath,
+ $result,
+ $contextValue
+ );
+ }
+
+ /**
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ * @param mixed $result
+ */
+ protected function invalidReturnTypeError(
+ ObjectType $returnType,
+ $result,
+ \ArrayObject $fieldNodes
+ ): Error {
+ $safeResult = Utils::printSafe($result);
+
+ return new Error(
+ "Expected value of type \"{$returnType->name}\" but got: {$safeResult}.",
+ $fieldNodes
+ );
+ }
+
+ /**
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ * @param mixed $result
+ * @param mixed $contextValue
+ *
+ * @throws \Exception
+ * @throws Error
+ *
+ * @return array<mixed>|Promise|\stdClass
+ */
+ protected function collectAndExecuteSubfields(
+ ObjectType $returnType,
+ \ArrayObject $fieldNodes,
+ array $path,
+ array $unaliasedPath,
+ $result,
+ $contextValue
+ ) {
+ $subFieldNodes = $this->collectSubFields($returnType, $fieldNodes);
+
+ return $this->executeFields($returnType, $result, $path, $unaliasedPath, $subFieldNodes, $contextValue);
+ }
+
+ /**
+ * A memoized collection of relevant subfields with regard to the return
+ * type. Memoizing ensures the subfields are not repeatedly calculated, which
+ * saves overhead when resolving lists of values.
+ *
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ *
+ * @throws \Exception
+ * @throws Error
+ *
+ * @phpstan-return Fields
+ */
+ protected function collectSubFields(ObjectType $returnType, \ArrayObject $fieldNodes): \ArrayObject
+ {
+ // @phpstan-ignore-next-line generics of SplObjectStorage are not inferred from empty instantiation
+ $returnTypeCache = $this->subFieldCache[$returnType] ??= new \SplObjectStorage();
+
+ if (! isset($returnTypeCache[$fieldNodes])) {
+ // Collect sub-fields to execute to complete this value.
+ $subFieldNodes = new \ArrayObject();
+ $visitedFragmentNames = new \ArrayObject();
+ foreach ($fieldNodes as $fieldNode) {
+ if (isset($fieldNode->selectionSet)) {
+ $subFieldNodes = $this->collectFields(
+ $returnType,
+ $fieldNode->selectionSet,
+ $subFieldNodes,
+ $visitedFragmentNames
+ );
+ }
+ }
+
+ $returnTypeCache[$fieldNodes] = $subFieldNodes;
+ }
+
+ return $returnTypeCache[$fieldNodes];
+ }
+
+ /**
+ * Implements the "Evaluating selection sets" section of the spec for "read" mode.
+ *
+ * @param mixed $rootValue
+ * @param list<string|int> $path
+ * @param list<string|int> $unaliasedPath
+ * @param mixed $contextValue
+ *
+ * @phpstan-param Fields $fields
+ *
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return Promise|\stdClass|array<mixed>
+ */
+ protected function executeFields(ObjectType $parentType, $rootValue, array $path, array $unaliasedPath, \ArrayObject $fields, $contextValue)
+ {
+ $containsPromise = false;
+ $results = [];
+ foreach ($fields as $responseName => $fieldNodes) {
+ $result = $this->resolveField(
+ $parentType,
+ $rootValue,
+ $fieldNodes,
+ $responseName,
+ $path,
+ $unaliasedPath,
+ $this->maybeScopeContext($contextValue)
+ );
+ if ($result === static::$UNDEFINED) {
+ continue;
+ }
+
+ if (! $containsPromise && $this->isPromise($result)) {
+ $containsPromise = true;
+ }
+
+ $results[$responseName] = $result;
+ }
+
+ // If there are no promises, we can just return the object
+ if (! $containsPromise) {
+ return static::fixResultsIfEmptyArray($results);
+ }
+
+ // Otherwise, results is a map from field name to the result of resolving that
+ // field, which is possibly a promise. Return a promise that will return this
+ // same map, but with any promises replaced with the values they resolved to.
+ return $this->promiseForAssocArray($results);
+ }
+
+ /**
+ * Differentiate empty objects from empty lists.
+ *
+ * @see https://github.com/webonyx/graphql-php/issues/59
+ *
+ * @param array<mixed>|mixed $results
+ *
+ * @return non-empty-array<mixed>|\stdClass|mixed
+ */
+ protected static function fixResultsIfEmptyArray($results)
+ {
+ if ($results === []) {
+ return new \stdClass();
+ }
+
+ return $results;
+ }
+
+ /**
+ * Transform an associative array with Promises to a Promise which resolves to an
+ * associative array where all Promises were resolved.
+ *
+ * @param array<string, Promise|mixed> $assoc
+ */
+ protected function promiseForAssocArray(array $assoc): Promise
+ {
+ $keys = array_keys($assoc);
+ $valuesAndPromises = array_values($assoc);
+ $promise = $this->exeContext->promiseAdapter->all($valuesAndPromises);
+
+ return $promise->then(static function ($values) use ($keys) {
+ $resolvedResults = [];
+ foreach ($values as $i => $value) {
+ $resolvedResults[$keys[$i]] = $value;
+ }
+
+ return static::fixResultsIfEmptyArray($resolvedResults);
+ });
+ }
+
+ /**
+ * @param mixed $runtimeTypeOrName
+ * @param AbstractType&Type $returnType
+ * @param mixed $result
+ *
+ * @throws InvariantViolation
+ */
+ protected function ensureValidRuntimeType(
+ $runtimeTypeOrName,
+ AbstractType $returnType,
+ ResolveInfo $info,
+ $result
+ ): ObjectType {
+ $runtimeType = is_string($runtimeTypeOrName)
+ ? $this->exeContext->schema->getType($runtimeTypeOrName)
+ : $runtimeTypeOrName;
+
+ if (! $runtimeType instanceof ObjectType) {
+ $safeResult = Utils::printSafe($result);
+ $notObjectType = Utils::printSafe($runtimeType);
+ throw new InvariantViolation("Abstract type {$returnType} must resolve to an Object type at runtime for field {$info->parentType}.{$info->fieldName} with value {$safeResult}, received \"{$notObjectType}\". Either the {$returnType} type should provide a \"resolveType\" function or each possible type should provide an \"isTypeOf\" function.");
+ }
+
+ if (! $this->exeContext->schema->isSubType($returnType, $runtimeType)) {
+ throw new InvariantViolation("Runtime Object type \"{$runtimeType}\" is not a possible type for \"{$returnType}\".");
+ }
+
+ assert(
+ $this->exeContext->schema->getType($runtimeType->name) !== null,
+ "Schema does not contain type \"{$runtimeType}\". This can happen when an object type is only referenced indirectly through abstract types and never directly through fields.List the type in the option \"types\" during schema construction, see https://webonyx.github.io/graphql-php/schema-definition/#configuration-options."
+ );
+
+ assert(
+ $runtimeType === $this->exeContext->schema->getType($runtimeType->name),
+ "Schema must contain unique named types but contains multiple types named \"{$runtimeType}\". Make sure that `resolveType` function of abstract type \"{$returnType}\" returns the same type instance as referenced anywhere else within the schema (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry)."
+ );
+
+ return $runtimeType;
+ }
+
+ /**
+ * @param mixed $contextValue
+ *
+ * @return mixed
+ */
+ private function maybeScopeContext($contextValue)
+ {
+ if ($contextValue instanceof ScopedContext) {
+ return $contextValue->clone();
+ }
+
+ return $contextValue;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/ScopedContext.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/ScopedContext.php
new file mode 100644
index 00000000000..8fcb29ed083
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/ScopedContext.php
@@ -0,0 +1,13 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
+
+/**
+ * When the object passed as `$contextValue` to Automattic\WooCommerce\Vendor\GraphQL execution implements this,
+ * its `clone()` method will be called before passing the context down to a field.
+ * This allows passing information to child fields in the query tree without affecting sibling or parent fields.
+ */
+interface ScopedContext
+{
+ public function clone(): self;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Executor/Values.php b/plugins/woocommerce/lib/packages/GraphQL/Executor/Values.php
new file mode 100644
index 00000000000..f99a742a8db
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Executor/Values.php
@@ -0,0 +1,279 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Executor;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Value;
+
+/**
+ * @see ArgumentNode - force IDE import
+ *
+ * @phpstan-import-type ArgumentNodeValue from ArgumentNode
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Executor\ValuesTest
+ */
+class Values
+{
+ /**
+ * Prepares an object map of variables of the correct type based on the provided
+ * variable definitions and arbitrary input. If the input cannot be coerced
+ * to match the variable definitions, an Error will be thrown.
+ *
+ * @param NodeList<VariableDefinitionNode> $varDefNodes
+ * @param array<string, mixed> $rawVariableValues
+ *
+ * @throws \Exception
+ *
+ * @return array{array<int, Error>, null}|array{null, array<string, mixed>}
+ */
+ public static function getVariableValues(Schema $schema, NodeList $varDefNodes, array $rawVariableValues): array
+ {
+ $errors = [];
+ $coercedValues = [];
+ foreach ($varDefNodes as $varDefNode) {
+ $varName = $varDefNode->variable->name->value;
+ $varType = AST::typeFromAST([$schema, 'getType'], $varDefNode->type);
+
+ if (! Type::isInputType($varType)) {
+ // Must use input types for variables. This should be caught during
+ // validation, however is checked again here for safety.
+ $typeStr = Printer::doPrint($varDefNode->type);
+ $errors[] = new Error(
+ "Variable \"\${$varName}\" expected value of type \"{$typeStr}\" which cannot be used as an input type.",
+ [$varDefNode->type]
+ );
+ } else {
+ $hasValue = array_key_exists($varName, $rawVariableValues);
+ $value = $hasValue
+ ? $rawVariableValues[$varName]
+ : Utils::undefined();
+
+ if (! $hasValue && ($varDefNode->defaultValue !== null)) {
+ // If no value was provided to a variable with a default value,
+ // use the default value.
+ $coercedValues[$varName] = AST::valueFromAST($varDefNode->defaultValue, $varType);
+ } elseif ((! $hasValue || $value === null) && ($varType instanceof NonNull)) {
+ // If no value or a nullish value was provided to a variable with a
+ // non-null type (required), produce an error.
+ $safeVarType = Utils::printSafe($varType);
+ $message = $hasValue
+ ? "Variable \"\${$varName}\" of non-null type \"{$safeVarType}\" must not be null."
+ : "Variable \"\${$varName}\" of required type \"{$safeVarType}\" was not provided.";
+ $errors[] = new Error($message, [$varDefNode]);
+ } elseif ($hasValue) {
+ if ($value === null) {
+ // If the explicit value `null` was provided, an entry in the coerced
+ // values must exist as the value `null`.
+ $coercedValues[$varName] = null;
+ } else {
+ // Otherwise, a non-null value was provided, coerce it to the expected
+ // type or report an error if coercion fails.
+ $coerced = Value::coerceInputValue($value, $varType, null, $schema);
+
+ $coercionErrors = $coerced['errors'];
+ if ($coercionErrors !== null) {
+ foreach ($coercionErrors as $coercionError) {
+ $invalidValue = $coercionError->printInvalidValue();
+
+ $inputPath = $coercionError->printInputPath();
+ $pathMessage = $inputPath !== null
+ ? " at \"{$varName}{$inputPath}\""
+ : '';
+
+ $errors[] = new Error(
+ "Variable \"\${$varName}\" got invalid value {$invalidValue}{$pathMessage}; {$coercionError->getMessage()}",
+ $varDefNode,
+ $coercionError->getSource(),
+ $coercionError->getPositions(),
+ $coercionError->getPath(),
+ $coercionError,
+ $coercionError->getExtensions()
+ );
+ }
+ } else {
+ $coercedValues[$varName] = $coerced['value'];
+ }
+ }
+ }
+ }
+ }
+
+ return $errors === []
+ ? [null, $coercedValues]
+ : [$errors, null];
+ }
+
+ /**
+ * Prepares an object map of argument values given a directive definition
+ * and an AST node which may contain directives. Optionally also accepts a map
+ * of variable values.
+ *
+ * If the directive does not exist on the node, returns undefined.
+ *
+ * @param EnumTypeDefinitionNode|EnumTypeExtensionNode|EnumValueDefinitionNode|FieldDefinitionNode|FieldNode|FragmentDefinitionNode|FragmentSpreadNode|InlineFragmentNode|InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode|InputValueDefinitionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode|ObjectTypeDefinitionNode|ObjectTypeExtensionNode|OperationDefinitionNode|ScalarTypeDefinitionNode|ScalarTypeExtensionNode|SchemaExtensionNode|UnionTypeDefinitionNode|UnionTypeExtensionNode|VariableDefinitionNode $node
+ * @param array<string, mixed>|null $variableValues
+ *
+ * @throws \Exception
+ * @throws Error
+ *
+ * @return array<string, mixed>|null
+ */
+ public static function getDirectiveValues(Directive $directiveDef, Node $node, ?array $variableValues = null, ?Schema $schema = null): ?array
+ {
+ $directiveDefName = $directiveDef->name;
+
+ foreach ($node->directives as $directive) {
+ if ($directive->name->value === $directiveDefName) {
+ return self::getArgumentValues($directiveDef, $directive, $variableValues, $schema);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Prepares an object map of argument values given a list of argument
+ * definitions and list of argument AST nodes.
+ *
+ * @param FieldDefinition|Directive $def
+ * @param FieldNode|DirectiveNode $node
+ * @param array<string, mixed>|null $variableValues
+ *
+ * @throws \Exception
+ * @throws Error
+ *
+ * @return array<string, mixed>
+ */
+ public static function getArgumentValues($def, Node $node, ?array $variableValues = null, ?Schema $schema = null): array
+ {
+ if ($def->args === []) {
+ return [];
+ }
+
+ /** @var array<string, ArgumentNodeValue> $argumentValueMap */
+ $argumentValueMap = [];
+
+ // Might not be defined when an AST from JS is used
+ if (isset($node->arguments)) {
+ foreach ($node->arguments as $argumentNode) {
+ $argumentValueMap[$argumentNode->name->value] = $argumentNode->value;
+ }
+ }
+
+ return static::getArgumentValuesForMap($def, $argumentValueMap, $variableValues, $node, $schema);
+ }
+
+ /**
+ * @param FieldDefinition|Directive $def
+ * @param array<string, ArgumentNodeValue> $argumentValueMap
+ * @param array<string, mixed>|null $variableValues
+ *
+ * @throws \Exception
+ * @throws Error
+ *
+ * @return array<string, mixed>
+ */
+ public static function getArgumentValuesForMap($def, array $argumentValueMap, ?array $variableValues = null, ?Node $referenceNode = null, ?Schema $schema = null): array
+ {
+ /** @var array<string, mixed> $coercedValues */
+ $coercedValues = [];
+
+ foreach ($def->args as $argumentDefinition) {
+ $name = $argumentDefinition->name;
+ $argType = $argumentDefinition->getType();
+ $argumentValueNode = $argumentValueMap[$name] ?? null;
+
+ if ($argumentValueNode instanceof VariableNode) {
+ $variableName = $argumentValueNode->name->value;
+ $hasValue = $variableValues !== null && array_key_exists($variableName, $variableValues);
+ $isNull = $hasValue && $variableValues[$variableName] === null;
+ } else {
+ $hasValue = $argumentValueNode !== null;
+ $isNull = $argumentValueNode instanceof NullValueNode;
+ }
+
+ if (! $hasValue && $argumentDefinition->defaultValueExists()) {
+ // If no argument was provided where the definition has a default value,
+ // use the default value.
+ $coercedValues[$name] = $argumentDefinition->defaultValue;
+ } elseif ((! $hasValue || $isNull) && ($argType instanceof NonNull)) {
+ // If no argument or a null value was provided to an argument with a
+ // non-null type (required), produce a field error.
+ $safeArgType = Utils::printSafe($argType);
+
+ if ($isNull) {
+ throw new Error("Argument \"{$name}\" of non-null type \"{$safeArgType}\" must not be null.", $referenceNode);
+ }
+
+ if ($argumentValueNode instanceof VariableNode) {
+ throw new Error("Argument \"{$name}\" of required type \"{$safeArgType}\" was provided the variable \"\${$argumentValueNode->name->value}\" which was not provided a runtime value.", [$argumentValueNode]);
+ }
+
+ throw new Error("Argument \"{$name}\" of required type \"{$safeArgType}\" was not provided.", $referenceNode);
+ } elseif ($hasValue) {
+ assert($argumentValueNode instanceof Node);
+
+ if ($argumentValueNode instanceof NullValueNode) {
+ // If the explicit value `null` was provided, an entry in the coerced
+ // values must exist as the value `null`.
+ $coercedValues[$name] = null;
+ } elseif ($argumentValueNode instanceof VariableNode) {
+ $variableName = $argumentValueNode->name->value;
+ // Note: This does no further checking that this variable is correct.
+ // This assumes that this query has been validated and the variable
+ // usage here is of the correct type.
+ $coercedValues[$name] = $variableValues[$variableName] ?? null;
+ } else {
+ $coercedValue = AST::valueFromAST($argumentValueNode, $argType, $variableValues, $schema);
+ if (Utils::undefined() === $coercedValue) {
+ // Note: ValuesOfCorrectType validation should catch this before
+ // execution. This is a runtime check to ensure execution does not
+ // continue with an invalid argument value.
+ $invalidValue = Printer::doPrint($argumentValueNode);
+ throw new Error("Argument \"{$name}\" has invalid value {$invalidValue}.", [$argumentValueNode]);
+ }
+
+ $coercedValues[$name] = $coercedValue;
+ }
+ }
+ }
+
+ return $coercedValues;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/GraphQL.php b/plugins/woocommerce/lib/packages/GraphQL/GraphQL.php
new file mode 100644
index 00000000000..b467d5f05a8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/GraphQL.php
@@ -0,0 +1,265 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Executor;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema as SchemaType;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryComplexity;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValidationRule;
+
+/**
+ * This is the primary facade for fulfilling Automattic\WooCommerce\Vendor\GraphQL operations.
+ * See [related documentation](executing-queries.md).
+ *
+ * @phpstan-import-type ArgsMapper from Executor
+ * @phpstan-import-type FieldResolver from Executor
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\GraphQLTest
+ */
+class GraphQL
+{
+ /**
+ * Executes graphql query.
+ *
+ * More sophisticated Automattic\WooCommerce\Vendor\GraphQL servers, such as those which persist queries,
+ * may wish to separate the validation and execution phases to a static time
+ * tooling step, and a server runtime step.
+ *
+ * Available options:
+ *
+ * schema:
+ * The Automattic\WooCommerce\Vendor\GraphQL type system to use when validating and executing a query.
+ * source:
+ * A Automattic\WooCommerce\Vendor\GraphQL language formatted string representing the requested operation.
+ * rootValue:
+ * The value provided as the first argument to resolver functions on the top
+ * level type (e.g. the query object type).
+ * contextValue:
+ * The context value is provided as an argument to resolver functions after
+ * field arguments. It is used to pass shared information useful at any point
+ * during executing this query, for example the currently logged in user and
+ * connections to databases or other services.
+ * If the passed object implements the `ScopedContext` interface,
+ * its `clone()` method will be called before passing the context down to a field.
+ * This allows passing information to child fields in the query tree without affecting sibling or parent fields.
+ * variableValues:
+ * A mapping of variable name to runtime value to use for all variables
+ * defined in the requestString.
+ * operationName:
+ * The name of the operation to use if requestString contains multiple
+ * possible operations. Can be omitted if requestString contains only
+ * one operation.
+ * fieldResolver:
+ * A resolver function to use when one is not provided by the schema.
+ * If not provided, the default field resolver is used (which looks for a
+ * value on the source value with the field's name).
+ * validationRules:
+ * A set of rules for query validation step. Default value is all available rules.
+ * Empty array would allow to skip query validation (may be convenient for persisted
+ * queries which are validated before persisting and assumed valid during execution)
+ *
+ * @param string|DocumentNode $source
+ * @param mixed $rootValue
+ * @param mixed $contextValue
+ * @param array<string, mixed>|null $variableValues
+ * @param array<ValidationRule>|null $validationRules
+ *
+ * @api
+ *
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public static function executeQuery(
+ SchemaType $schema,
+ $source,
+ $rootValue = null,
+ $contextValue = null,
+ ?array $variableValues = null,
+ ?string $operationName = null,
+ ?callable $fieldResolver = null,
+ ?array $validationRules = null
+ ): ExecutionResult {
+ $promiseAdapter = new SyncPromiseAdapter();
+
+ $promise = self::promiseToExecute(
+ $promiseAdapter,
+ $schema,
+ $source,
+ $rootValue,
+ $contextValue,
+ $variableValues,
+ $operationName,
+ $fieldResolver,
+ $validationRules
+ );
+
+ return $promiseAdapter->wait($promise);
+ }
+
+ /**
+ * Same as executeQuery(), but requires PromiseAdapter and always returns a Promise.
+ * Useful for Async PHP platforms.
+ *
+ * @param string|DocumentNode $source
+ * @param mixed $rootValue
+ * @param mixed $context
+ * @param array<string, mixed>|null $variableValues
+ * @param array<ValidationRule>|null $validationRules Defaults to using all available rules
+ *
+ * @api
+ *
+ * @throws \Exception
+ */
+ public static function promiseToExecute(
+ PromiseAdapter $promiseAdapter,
+ SchemaType $schema,
+ $source,
+ $rootValue = null,
+ $context = null,
+ ?array $variableValues = null,
+ ?string $operationName = null,
+ ?callable $fieldResolver = null,
+ ?array $validationRules = null
+ ): Promise {
+ try {
+ $documentNode = $source instanceof DocumentNode
+ ? $source
+ : Parser::parse(new Source($source, 'GraphQL'));
+
+ if ($validationRules === null) {
+ $queryComplexity = DocumentValidator::getRule(QueryComplexity::class);
+ assert($queryComplexity instanceof QueryComplexity, 'should not register a different rule for QueryComplexity');
+
+ $queryComplexity->setRawVariableValues($variableValues);
+ } else {
+ foreach ($validationRules as $rule) {
+ if ($rule instanceof QueryComplexity) {
+ $rule->setRawVariableValues($variableValues);
+ }
+ }
+ }
+
+ $validationErrors = DocumentValidator::validate($schema, $documentNode, $validationRules);
+
+ if ($validationErrors !== []) {
+ return $promiseAdapter->createFulfilled(
+ new ExecutionResult(null, $validationErrors)
+ );
+ }
+
+ return Executor::promiseToExecute(
+ $promiseAdapter,
+ $schema,
+ $documentNode,
+ $rootValue,
+ $context,
+ $variableValues,
+ $operationName,
+ $fieldResolver
+ );
+ } catch (Error $e) {
+ return $promiseAdapter->createFulfilled(
+ new ExecutionResult(null, [$e])
+ );
+ }
+ }
+
+ /**
+ * Returns directives defined in Automattic\WooCommerce\Vendor\GraphQL spec.
+ *
+ * @deprecated use {@see Directive::builtInDirectives()}
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<string, Directive>
+ *
+ * @api
+ */
+ public static function getStandardDirectives(): array
+ {
+ return Directive::builtInDirectives();
+ }
+
+ /**
+ * Returns built-in scalar types defined in Automattic\WooCommerce\Vendor\GraphQL spec.
+ *
+ * @deprecated use {@see Type::builtInScalars()}
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<string, ScalarType>
+ *
+ * @api
+ */
+ public static function getStandardTypes(): array
+ {
+ return Type::builtInScalars();
+ }
+
+ /**
+ * Replaces standard types with types from this list (matching by name).
+ *
+ * Standard types not listed here remain untouched.
+ *
+ * @deprecated prefer per-schema scalar overrides via {@see \Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig::$types} or {@see \Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig::$typeLoader}
+ *
+ * @param array<string, ScalarType> $types
+ *
+ * @api
+ *
+ * @throws InvariantViolation
+ */
+ public static function overrideStandardTypes(array $types): void
+ {
+ Type::overrideStandardTypes($types);
+ }
+
+ /**
+ * Returns standard validation rules implementing Automattic\WooCommerce\Vendor\GraphQL spec.
+ *
+ * @return array<class-string<ValidationRule>, ValidationRule>
+ *
+ * @api
+ */
+ public static function getStandardValidationRules(): array
+ {
+ return DocumentValidator::defaultRules();
+ }
+
+ /**
+ * Set default resolver implementation.
+ *
+ * @phpstan-param FieldResolver $fn
+ *
+ * @api
+ */
+ public static function setDefaultFieldResolver(callable $fn): void
+ {
+ Executor::setDefaultFieldResolver($fn);
+ }
+
+ /**
+ * Set default args mapper implementation.
+ *
+ * @phpstan-param ArgsMapper $fn
+ *
+ * @api
+ */
+ public static function setDefaultArgsMapper(callable $fn): void
+ {
+ Executor::setDefaultArgsMapper($fn);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ArgumentNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ArgumentNode.php
new file mode 100644
index 00000000000..25e3625aff7
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ArgumentNode.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * @phpstan-type ArgumentNodeValue VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode
+ */
+class ArgumentNode extends Node
+{
+ public string $kind = NodeKind::ARGUMENT;
+
+ /** @phpstan-var ArgumentNodeValue */
+ public ValueNode $value;
+
+ public NameNode $name;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/BooleanValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/BooleanValueNode.php
new file mode 100644
index 00000000000..c3b8f3d8785
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/BooleanValueNode.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class BooleanValueNode extends Node implements ValueNode
+{
+ public string $kind = NodeKind::BOOLEAN;
+
+ public bool $value;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DefinitionNode.php
new file mode 100644
index 00000000000..f4baccf9f05
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DefinitionNode.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type DefinitionNode =
+ * | ExecutableDefinitionNode
+ * | TypeSystemDefinitionNode
+ * | TypeSystemExtensionNode;.
+ */
+interface DefinitionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveDefinitionNode.php
new file mode 100644
index 00000000000..7e86d513e90
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveDefinitionNode.php
@@ -0,0 +1,26 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class DirectiveDefinitionNode extends Node implements TypeSystemDefinitionNode
+{
+ public string $kind = NodeKind::DIRECTIVE_DEFINITION;
+
+ public NameNode $name;
+
+ public ?StringValueNode $description = null;
+
+ /** @var NodeList<InputValueDefinitionNode> */
+ public NodeList $arguments;
+
+ public bool $repeatable;
+
+ /** @var NodeList<NameNode> */
+ public NodeList $locations;
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->arguments ??= new NodeList([]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveNode.php
new file mode 100644
index 00000000000..d487d8e6ed2
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DirectiveNode.php
@@ -0,0 +1,19 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class DirectiveNode extends Node
+{
+ public string $kind = NodeKind::DIRECTIVE;
+
+ public NameNode $name;
+
+ /** @var NodeList<ArgumentNode> */
+ public NodeList $arguments;
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->arguments ??= new NodeList([]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DocumentNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DocumentNode.php
new file mode 100644
index 00000000000..f71632ef5d5
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/DocumentNode.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class DocumentNode extends Node
+{
+ public string $kind = NodeKind::DOCUMENT;
+
+ /** @var NodeList<DefinitionNode&Node> */
+ public NodeList $definitions;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeDefinitionNode.php
new file mode 100644
index 00000000000..ed520d6b1fb
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeDefinitionNode.php
@@ -0,0 +1,29 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class EnumTypeDefinitionNode extends Node implements TypeDefinitionNode
+{
+ public string $kind = NodeKind::ENUM_TYPE_DEFINITION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<EnumValueDefinitionNode> */
+ public NodeList $values;
+
+ public ?StringValueNode $description = null;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->directives ??= new NodeList([]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeExtensionNode.php
new file mode 100644
index 00000000000..1795e77588b
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumTypeExtensionNode.php
@@ -0,0 +1,27 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class EnumTypeExtensionNode extends Node implements TypeExtensionNode
+{
+ public string $kind = NodeKind::ENUM_TYPE_EXTENSION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<EnumValueDefinitionNode> */
+ public NodeList $values;
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->directives ??= new NodeList([]);
+ }
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueDefinitionNode.php
new file mode 100644
index 00000000000..474b8519208
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueDefinitionNode.php
@@ -0,0 +1,15 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class EnumValueDefinitionNode extends Node
+{
+ public string $kind = NodeKind::ENUM_VALUE_DEFINITION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public ?StringValueNode $description = null;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueNode.php
new file mode 100644
index 00000000000..d30003b7bca
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/EnumValueNode.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class EnumValueNode extends Node implements ValueNode
+{
+ public string $kind = NodeKind::ENUM;
+
+ public string $value;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ExecutableDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ExecutableDefinitionNode.php
new file mode 100644
index 00000000000..974e5589d2b
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ExecutableDefinitionNode.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type ExecutableDefinitionNode =
+ * | OperationDefinitionNode
+ * | FragmentDefinitionNode;.
+ */
+interface ExecutableDefinitionNode extends DefinitionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldDefinitionNode.php
new file mode 100644
index 00000000000..b88577175f7
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldDefinitionNode.php
@@ -0,0 +1,21 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class FieldDefinitionNode extends Node
+{
+ public string $kind = NodeKind::FIELD_DEFINITION;
+
+ public NameNode $name;
+
+ /** @var NodeList<InputValueDefinitionNode> */
+ public NodeList $arguments;
+
+ /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
+ public TypeNode $type;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public ?StringValueNode $description = null;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldNode.php
new file mode 100644
index 00000000000..03ce7dec148
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FieldNode.php
@@ -0,0 +1,27 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class FieldNode extends Node implements SelectionNode
+{
+ public string $kind = NodeKind::FIELD;
+
+ public NameNode $name;
+
+ public ?NameNode $alias = null;
+
+ /** @var NodeList<ArgumentNode> */
+ public NodeList $arguments;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public ?SelectionSetNode $selectionSet = null;
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->directives ??= new NodeList([]);
+ $this->arguments ??= new NodeList([]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FloatValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FloatValueNode.php
new file mode 100644
index 00000000000..392956675df
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FloatValueNode.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class FloatValueNode extends Node implements ValueNode
+{
+ public string $kind = NodeKind::FLOAT;
+
+ public string $value;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentDefinitionNode.php
new file mode 100644
index 00000000000..08bc15c79e8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentDefinitionNode.php
@@ -0,0 +1,38 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class FragmentDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
+{
+ public string $kind = NodeKind::FRAGMENT_DEFINITION;
+
+ public NameNode $name;
+
+ /**
+ * Note: fragment variable definitions are experimental and may be changed
+ * or removed in the future.
+ *
+ * Thus, this property is the single exception where this is not always a NodeList but may be null.
+ *
+ * @var NodeList<VariableDefinitionNode>|null
+ */
+ public ?NodeList $variableDefinitions = null;
+
+ public NamedTypeNode $typeCondition;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public SelectionSetNode $selectionSet;
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->directives ??= new NodeList([]);
+ }
+
+ public function getSelectionSet(): SelectionSetNode
+ {
+ return $this->selectionSet;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentSpreadNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentSpreadNode.php
new file mode 100644
index 00000000000..e535c582eff
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/FragmentSpreadNode.php
@@ -0,0 +1,19 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class FragmentSpreadNode extends Node implements SelectionNode
+{
+ public string $kind = NodeKind::FRAGMENT_SPREAD;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->directives ??= new NodeList([]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/HasSelectionSet.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/HasSelectionSet.php
new file mode 100644
index 00000000000..217b24fdfea
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/HasSelectionSet.php
@@ -0,0 +1,12 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type DefinitionNode = OperationDefinitionNode
+ * | FragmentDefinitionNode.
+ */
+interface HasSelectionSet
+{
+ public function getSelectionSet(): SelectionSetNode;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InlineFragmentNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InlineFragmentNode.php
new file mode 100644
index 00000000000..33da8fcf68c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InlineFragmentNode.php
@@ -0,0 +1,21 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class InlineFragmentNode extends Node implements SelectionNode
+{
+ public string $kind = NodeKind::INLINE_FRAGMENT;
+
+ public ?NamedTypeNode $typeCondition = null;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public SelectionSetNode $selectionSet;
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->directives ??= new NodeList([]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeDefinitionNode.php
new file mode 100644
index 00000000000..ff503b486b4
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeDefinitionNode.php
@@ -0,0 +1,29 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class InputObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
+{
+ public string $kind = NodeKind::INPUT_OBJECT_TYPE_DEFINITION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<InputValueDefinitionNode> */
+ public NodeList $fields;
+
+ public ?StringValueNode $description = null;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->directives ??= new NodeList([]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeExtensionNode.php
new file mode 100644
index 00000000000..b8102c24f68
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputObjectTypeExtensionNode.php
@@ -0,0 +1,21 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class InputObjectTypeExtensionNode extends Node implements TypeExtensionNode
+{
+ public string $kind = NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<InputValueDefinitionNode> */
+ public NodeList $fields;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputValueDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputValueDefinitionNode.php
new file mode 100644
index 00000000000..8194f7cac42
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InputValueDefinitionNode.php
@@ -0,0 +1,21 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class InputValueDefinitionNode extends Node
+{
+ public string $kind = NodeKind::INPUT_VALUE_DEFINITION;
+
+ public NameNode $name;
+
+ /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
+ public TypeNode $type;
+
+ /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */
+ public ?ValueNode $defaultValue = null;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public ?StringValueNode $description = null;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/IntValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/IntValueNode.php
new file mode 100644
index 00000000000..c4def4465c1
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/IntValueNode.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class IntValueNode extends Node implements ValueNode
+{
+ public string $kind = NodeKind::INT;
+
+ public string $value;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeDefinitionNode.php
new file mode 100644
index 00000000000..ab54f1073d4
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeDefinitionNode.php
@@ -0,0 +1,26 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class InterfaceTypeDefinitionNode extends Node implements TypeDefinitionNode
+{
+ public string $kind = NodeKind::INTERFACE_TYPE_DEFINITION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<NamedTypeNode> */
+ public NodeList $interfaces;
+
+ /** @var NodeList<FieldDefinitionNode> */
+ public NodeList $fields;
+
+ public ?StringValueNode $description = null;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeExtensionNode.php
new file mode 100644
index 00000000000..c1342bad446
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/InterfaceTypeExtensionNode.php
@@ -0,0 +1,24 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class InterfaceTypeExtensionNode extends Node implements TypeExtensionNode
+{
+ public string $kind = NodeKind::INTERFACE_TYPE_EXTENSION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<NamedTypeNode> */
+ public NodeList $interfaces;
+
+ /** @var NodeList<FieldDefinitionNode> */
+ public NodeList $fields;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListTypeNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListTypeNode.php
new file mode 100644
index 00000000000..63d58931763
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListTypeNode.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class ListTypeNode extends Node implements TypeNode
+{
+ public string $kind = NodeKind::LIST_TYPE;
+
+ /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
+ public TypeNode $type;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListValueNode.php
new file mode 100644
index 00000000000..f52a71bf5d8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ListValueNode.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class ListValueNode extends Node implements ValueNode
+{
+ public string $kind = NodeKind::LST;
+
+ /** @var NodeList<ValueNode&Node> */
+ public NodeList $values;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Location.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Location.php
new file mode 100644
index 00000000000..38ecbba0999
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Location.php
@@ -0,0 +1,63 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Token;
+
+/**
+ * Contains a range of UTF-8 character offsets and token references that
+ * identify the region of the source from which the AST derived.
+ *
+ * @phpstan-type LocationArray array{start: int, end: int}
+ */
+class Location
+{
+ /** The character offset at which this Node begins. */
+ public int $start;
+
+ /** The character offset at which this Node ends. */
+ public int $end;
+
+ /** The Token at which this Node begins. */
+ public ?Token $startToken = null;
+
+ /** The Token at which this Node ends. */
+ public ?Token $endToken = null;
+
+ /** The Source document the AST represents. */
+ public ?Source $source = null;
+
+ public static function create(int $start, int $end): self
+ {
+ $tmp = new static();
+
+ $tmp->start = $start;
+ $tmp->end = $end;
+
+ return $tmp;
+ }
+
+ public function __construct(?Token $startToken = null, ?Token $endToken = null, ?Source $source = null)
+ {
+ $this->startToken = $startToken;
+ $this->endToken = $endToken;
+ $this->source = $source;
+
+ if ($startToken === null || $endToken === null) {
+ return;
+ }
+
+ $this->start = $startToken->start;
+ $this->end = $endToken->end;
+ }
+
+ /** @return LocationArray */
+ public function toArray(): array
+ {
+ return [
+ 'start' => $this->start,
+ 'end' => $this->end,
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NameNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NameNode.php
new file mode 100644
index 00000000000..77e0605a0ae
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NameNode.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class NameNode extends Node implements TypeNode
+{
+ public string $kind = NodeKind::NAME;
+
+ public string $value;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NamedTypeNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NamedTypeNode.php
new file mode 100644
index 00000000000..ce51a50d18b
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NamedTypeNode.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class NamedTypeNode extends Node implements TypeNode
+{
+ public string $kind = NodeKind::NAMED_TYPE;
+
+ public NameNode $name;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Node.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Node.php
new file mode 100644
index 00000000000..bd474c59038
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/Node.php
@@ -0,0 +1,145 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * type Node = NameNode
+ * | DocumentNode
+ * | OperationDefinitionNode
+ * | VariableDefinitionNode
+ * | VariableNode
+ * | SelectionSetNode
+ * | FieldNode
+ * | ArgumentNode
+ * | FragmentSpreadNode
+ * | InlineFragmentNode
+ * | FragmentDefinitionNode
+ * | IntValueNode
+ * | FloatValueNode
+ * | StringValueNode
+ * | BooleanValueNode
+ * | EnumValueNode
+ * | ListValueNode
+ * | ObjectValueNode
+ * | ObjectFieldNode
+ * | DirectiveNode
+ * | ListTypeNode
+ * | NonNullTypeNode.
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\AST\NodeTest
+ */
+abstract class Node implements \JsonSerializable
+{
+ public ?Location $loc = null;
+
+ public string $kind;
+
+ /** @param array<string, mixed> $vars */
+ public function __construct(array $vars)
+ {
+ Utils::assign($this, $vars);
+ }
+
+ /**
+ * Returns a clone of this instance and all its children, except Location $loc.
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ *
+ * @return static
+ */
+ public function cloneDeep(): self
+ {
+ return static::cloneValue($this);
+ }
+
+ /**
+ * @template TNode of Node
+ * @template TCloneable of TNode|NodeList<TNode>|Location|string
+ *
+ * @phpstan-param TCloneable $value
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ *
+ * @phpstan-return TCloneable
+ */
+ protected static function cloneValue($value)
+ {
+ if ($value instanceof self) {
+ $cloned = clone $value;
+ foreach (get_object_vars($cloned) as $prop => $propValue) {
+ $cloned->{$prop} = static::cloneValue($propValue); // @phpstan-ignore argument.templateType
+ }
+
+ return $cloned;
+ }
+
+ if ($value instanceof NodeList) {
+ /**
+ * @phpstan-var TCloneable
+ *
+ * @phpstan-ignore varTag.nativeType (PHPStan is strict about template types and sees NodeList<TNode> as potentially different from TCloneable)
+ */
+ return $value->cloneDeep();
+ }
+
+ return $value;
+ }
+
+ /** @throws \JsonException */
+ public function __toString(): string
+ {
+ return json_encode($this, JSON_THROW_ON_ERROR);
+ }
+
+ /**
+ * Improves upon the default serialization by:
+ * - excluding null values
+ * - excluding large reference values such as @see Location::$source.
+ *
+ * @return array<string, mixed>
+ */
+ public function jsonSerialize(): array
+ {
+ return $this->toArray();
+ }
+
+ /** @return array<string, mixed> */
+ public function toArray(): array
+ {
+ return self::recursiveToArray($this);
+ }
+
+ /** @return array<string, mixed> */
+ private static function recursiveToArray(Node $node): array
+ {
+ $result = [];
+
+ foreach (get_object_vars($node) as $prop => $propValue) {
+ if ($propValue === null) {
+ continue;
+ }
+
+ if ($propValue instanceof NodeList) {
+ $converted = [];
+ foreach ($propValue as $item) {
+ $converted[] = self::recursiveToArray($item);
+ }
+ } elseif ($propValue instanceof Node) {
+ $converted = self::recursiveToArray($propValue);
+ } elseif ($propValue instanceof Location) {
+ $converted = $propValue->toArray();
+ } else {
+ $converted = $propValue;
+ }
+
+ $result[$prop] = $converted;
+ }
+
+ return $result;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeKind.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeKind.php
new file mode 100644
index 00000000000..8cff5b0ecb3
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeKind.php
@@ -0,0 +1,138 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * Holds constants of possible AST nodes.
+ */
+class NodeKind
+{
+ // constants from language/kinds.js:
+
+ public const NAME = 'Name';
+
+ // Document
+ public const DOCUMENT = 'Document';
+ public const OPERATION_DEFINITION = 'OperationDefinition';
+ public const VARIABLE_DEFINITION = 'VariableDefinition';
+ public const VARIABLE = 'Variable';
+ public const SELECTION_SET = 'SelectionSet';
+ public const FIELD = 'Field';
+ public const ARGUMENT = 'Argument';
+
+ // Fragments
+ public const FRAGMENT_SPREAD = 'FragmentSpread';
+ public const INLINE_FRAGMENT = 'InlineFragment';
+ public const FRAGMENT_DEFINITION = 'FragmentDefinition';
+
+ // Values
+ public const INT = 'IntValue';
+ public const FLOAT = 'FloatValue';
+ public const STRING = 'StringValue';
+ public const BOOLEAN = 'BooleanValue';
+ public const ENUM = 'EnumValue';
+ public const NULL = 'NullValue';
+ public const LST = 'ListValue';
+ public const OBJECT = 'ObjectValue';
+ public const OBJECT_FIELD = 'ObjectField';
+
+ // Directives
+ public const DIRECTIVE = 'Directive';
+
+ // Types
+ public const NAMED_TYPE = 'NamedType';
+ public const LIST_TYPE = 'ListType';
+ public const NON_NULL_TYPE = 'NonNullType';
+
+ // Type System Definitions
+ public const SCHEMA_DEFINITION = 'SchemaDefinition';
+ public const OPERATION_TYPE_DEFINITION = 'OperationTypeDefinition';
+
+ // Type Definitions
+ public const SCALAR_TYPE_DEFINITION = 'ScalarTypeDefinition';
+ public const OBJECT_TYPE_DEFINITION = 'ObjectTypeDefinition';
+ public const FIELD_DEFINITION = 'FieldDefinition';
+ public const INPUT_VALUE_DEFINITION = 'InputValueDefinition';
+ public const INTERFACE_TYPE_DEFINITION = 'InterfaceTypeDefinition';
+ public const UNION_TYPE_DEFINITION = 'UnionTypeDefinition';
+ public const ENUM_TYPE_DEFINITION = 'EnumTypeDefinition';
+ public const ENUM_VALUE_DEFINITION = 'EnumValueDefinition';
+ public const INPUT_OBJECT_TYPE_DEFINITION = 'InputObjectTypeDefinition';
+
+ // Type Extensions
+ public const SCALAR_TYPE_EXTENSION = 'ScalarTypeExtension';
+ public const OBJECT_TYPE_EXTENSION = 'ObjectTypeExtension';
+ public const INTERFACE_TYPE_EXTENSION = 'InterfaceTypeExtension';
+ public const UNION_TYPE_EXTENSION = 'UnionTypeExtension';
+ public const ENUM_TYPE_EXTENSION = 'EnumTypeExtension';
+ public const INPUT_OBJECT_TYPE_EXTENSION = 'InputObjectTypeExtension';
+
+ // Directive Definitions
+ public const DIRECTIVE_DEFINITION = 'DirectiveDefinition';
+
+ // Type System Extensions
+ public const SCHEMA_EXTENSION = 'SchemaExtension';
+
+ public const CLASS_MAP = [
+ self::NAME => NameNode::class,
+
+ // Document
+ self::DOCUMENT => DocumentNode::class,
+ self::OPERATION_DEFINITION => OperationDefinitionNode::class,
+ self::VARIABLE_DEFINITION => VariableDefinitionNode::class,
+ self::VARIABLE => VariableNode::class,
+ self::SELECTION_SET => SelectionSetNode::class,
+ self::FIELD => FieldNode::class,
+ self::ARGUMENT => ArgumentNode::class,
+
+ // Fragments
+ self::FRAGMENT_SPREAD => FragmentSpreadNode::class,
+ self::INLINE_FRAGMENT => InlineFragmentNode::class,
+ self::FRAGMENT_DEFINITION => FragmentDefinitionNode::class,
+
+ // Values
+ self::INT => IntValueNode::class,
+ self::FLOAT => FloatValueNode::class,
+ self::STRING => StringValueNode::class,
+ self::BOOLEAN => BooleanValueNode::class,
+ self::ENUM => EnumValueNode::class,
+ self::NULL => NullValueNode::class,
+ self::LST => ListValueNode::class,
+ self::OBJECT => ObjectValueNode::class,
+ self::OBJECT_FIELD => ObjectFieldNode::class,
+
+ // Directives
+ self::DIRECTIVE => DirectiveNode::class,
+
+ // Types
+ self::NAMED_TYPE => NamedTypeNode::class,
+ self::LIST_TYPE => ListTypeNode::class,
+ self::NON_NULL_TYPE => NonNullTypeNode::class,
+
+ // Type System Definitions
+ self::SCHEMA_DEFINITION => SchemaDefinitionNode::class,
+ self::OPERATION_TYPE_DEFINITION => OperationTypeDefinitionNode::class,
+
+ // Type Definitions
+ self::SCALAR_TYPE_DEFINITION => ScalarTypeDefinitionNode::class,
+ self::OBJECT_TYPE_DEFINITION => ObjectTypeDefinitionNode::class,
+ self::FIELD_DEFINITION => FieldDefinitionNode::class,
+ self::INPUT_VALUE_DEFINITION => InputValueDefinitionNode::class,
+ self::INTERFACE_TYPE_DEFINITION => InterfaceTypeDefinitionNode::class,
+ self::UNION_TYPE_DEFINITION => UnionTypeDefinitionNode::class,
+ self::ENUM_TYPE_DEFINITION => EnumTypeDefinitionNode::class,
+ self::ENUM_VALUE_DEFINITION => EnumValueDefinitionNode::class,
+ self::INPUT_OBJECT_TYPE_DEFINITION => InputObjectTypeDefinitionNode::class,
+
+ // Type Extensions
+ self::SCALAR_TYPE_EXTENSION => ScalarTypeExtensionNode::class,
+ self::OBJECT_TYPE_EXTENSION => ObjectTypeExtensionNode::class,
+ self::INTERFACE_TYPE_EXTENSION => InterfaceTypeExtensionNode::class,
+ self::UNION_TYPE_EXTENSION => UnionTypeExtensionNode::class,
+ self::ENUM_TYPE_EXTENSION => EnumTypeExtensionNode::class,
+ self::INPUT_OBJECT_TYPE_EXTENSION => InputObjectTypeExtensionNode::class,
+
+ // Directive Definitions
+ self::DIRECTIVE_DEFINITION => DirectiveDefinitionNode::class,
+ ];
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeList.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeList.php
new file mode 100644
index 00000000000..ade50d309b1
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NodeList.php
@@ -0,0 +1,161 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+
+/**
+ * @template T of Node
+ *
+ * @phpstan-implements \ArrayAccess<array-key, T>
+ * @phpstan-implements \IteratorAggregate<array-key, T>
+ */
+class NodeList implements \ArrayAccess, \IteratorAggregate, \Countable
+{
+ /**
+ * @var array<Node|array>
+ *
+ * @phpstan-var array<T|array<string, mixed>>
+ */
+ private array $nodes;
+
+ /**
+ * @param array<Node|array> $nodes
+ *
+ * @phpstan-param array<T|array<string, mixed>> $nodes
+ */
+ public function __construct(array $nodes)
+ {
+ $this->nodes = $nodes;
+ }
+
+ /** @param int|string $offset */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset): bool
+ {
+ return isset($this->nodes[$offset]);
+ }
+
+ /**
+ * @param int|string $offset
+ *
+ * @phpstan-return T
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset): Node
+ {
+ $item = $this->nodes[$offset];
+
+ if (is_array($item)) {
+ // @phpstan-ignore-next-line not really possible to express the correctness of this in PHP
+ return $this->nodes[$offset] = AST::fromArray($item);
+ }
+
+ return $item;
+ }
+
+ /**
+ * @param int|string|null $offset
+ * @param Node|array<string, mixed> $value
+ *
+ * @phpstan-param T|array<string, mixed> $value
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value): void
+ {
+ if (is_array($value)) {
+ /** @phpstan-var T $value */
+ $value = AST::fromArray($value);
+ }
+
+ // Happens when a Node is pushed via []=
+ if ($offset === null) {
+ $this->nodes[] = $value;
+
+ return;
+ }
+
+ $this->nodes[$offset] = $value;
+ }
+
+ /** @param int|string $offset */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset): void
+ {
+ unset($this->nodes[$offset]);
+ }
+
+ public function getIterator(): \Traversable
+ {
+ foreach ($this->nodes as $key => $_) {
+ yield $key => $this->offsetGet($key);
+ }
+ }
+
+ public function count(): int
+ {
+ return count($this->nodes);
+ }
+
+ /**
+ * Remove a portion of the NodeList and replace it with something else.
+ *
+ * @param T|iterable<T>|null $replacement
+ *
+ * @phpstan-return NodeList<T> the NodeList with the extracted elements
+ */
+ public function splice(int $offset, int $length, $replacement = null): NodeList
+ {
+ if (is_iterable($replacement) && ! is_array($replacement)) {
+ $replacement = iterator_to_array($replacement);
+ }
+
+ return new NodeList(
+ array_splice($this->nodes, $offset, $length, $replacement)
+ );
+ }
+
+ /**
+ * @phpstan-param iterable<array-key, T> $list
+ *
+ * @phpstan-return NodeList<T>
+ */
+ public function merge(iterable $list): NodeList
+ {
+ if (! is_array($list)) {
+ $list = iterator_to_array($list);
+ }
+
+ return new NodeList(array_merge($this->nodes, $list));
+ }
+
+ /** Resets the keys of the stored nodes to contiguous numeric indexes. */
+ public function reindex(): void
+ {
+ $this->nodes = array_values($this->nodes);
+ }
+
+ /**
+ * Returns a clone of this instance and all its children, except Location $loc.
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ *
+ * @return static<T>
+ */
+ public function cloneDeep(): self
+ {
+ /** @var array<T> $empty */
+ $empty = [];
+ $cloned = new static($empty);
+ foreach ($this->getIterator() as $key => $node) {
+ $cloned[$key] = $node->cloneDeep();
+ }
+
+ return $cloned;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NonNullTypeNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NonNullTypeNode.php
new file mode 100644
index 00000000000..5fbd29d5778
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NonNullTypeNode.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class NonNullTypeNode extends Node implements TypeNode
+{
+ public string $kind = NodeKind::NON_NULL_TYPE;
+
+ /** @var NamedTypeNode|ListTypeNode */
+ public TypeNode $type;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NullValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NullValueNode.php
new file mode 100644
index 00000000000..a28d08e1680
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/NullValueNode.php
@@ -0,0 +1,8 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class NullValueNode extends Node implements ValueNode
+{
+ public string $kind = NodeKind::NULL;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectFieldNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectFieldNode.php
new file mode 100644
index 00000000000..cf024840369
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectFieldNode.php
@@ -0,0 +1,13 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class ObjectFieldNode extends Node
+{
+ public string $kind = NodeKind::OBJECT_FIELD;
+
+ public NameNode $name;
+
+ /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode */
+ public ValueNode $value;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeDefinitionNode.php
new file mode 100644
index 00000000000..294bd0056eb
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeDefinitionNode.php
@@ -0,0 +1,26 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class ObjectTypeDefinitionNode extends Node implements TypeDefinitionNode
+{
+ public string $kind = NodeKind::OBJECT_TYPE_DEFINITION;
+
+ public NameNode $name;
+
+ /** @var NodeList<NamedTypeNode> */
+ public NodeList $interfaces;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<FieldDefinitionNode> */
+ public NodeList $fields;
+
+ public ?StringValueNode $description = null;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeExtensionNode.php
new file mode 100644
index 00000000000..b0a46cbb24e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectTypeExtensionNode.php
@@ -0,0 +1,24 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class ObjectTypeExtensionNode extends Node implements TypeExtensionNode
+{
+ public string $kind = NodeKind::OBJECT_TYPE_EXTENSION;
+
+ public NameNode $name;
+
+ /** @var NodeList<NamedTypeNode> */
+ public NodeList $interfaces;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<FieldDefinitionNode> */
+ public NodeList $fields;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectValueNode.php
new file mode 100644
index 00000000000..a2b95d57d45
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ObjectValueNode.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class ObjectValueNode extends Node implements ValueNode
+{
+ public string $kind = NodeKind::OBJECT;
+
+ /** @var NodeList<ObjectFieldNode> */
+ public NodeList $fields;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationDefinitionNode.php
new file mode 100644
index 00000000000..03fe8120b93
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationDefinitionNode.php
@@ -0,0 +1,36 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * @phpstan-type OperationType 'query'|'mutation'|'subscription'
+ */
+class OperationDefinitionNode extends Node implements ExecutableDefinitionNode, HasSelectionSet
+{
+ public string $kind = NodeKind::OPERATION_DEFINITION;
+
+ public ?NameNode $name = null;
+
+ /** @var OperationType */
+ public string $operation;
+
+ /** @var NodeList<VariableDefinitionNode> */
+ public NodeList $variableDefinitions;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public SelectionSetNode $selectionSet;
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->directives ??= new NodeList([]);
+ $this->variableDefinitions ??= new NodeList([]);
+ }
+
+ public function getSelectionSet(): SelectionSetNode
+ {
+ return $this->selectionSet;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationTypeDefinitionNode.php
new file mode 100644
index 00000000000..7e882cfacdd
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/OperationTypeDefinitionNode.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * @phpstan-import-type OperationType from OperationDefinitionNode
+ */
+class OperationTypeDefinitionNode extends Node
+{
+ public string $kind = NodeKind::OPERATION_TYPE_DEFINITION;
+
+ /** @var OperationType */
+ public string $operation;
+
+ public NamedTypeNode $type;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeDefinitionNode.php
new file mode 100644
index 00000000000..1f3b20a8c65
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeDefinitionNode.php
@@ -0,0 +1,20 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class ScalarTypeDefinitionNode extends Node implements TypeDefinitionNode
+{
+ public string $kind = NodeKind::SCALAR_TYPE_DEFINITION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public ?StringValueNode $description = null;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeExtensionNode.php
new file mode 100644
index 00000000000..c65e9e3e44c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ScalarTypeExtensionNode.php
@@ -0,0 +1,18 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class ScalarTypeExtensionNode extends Node implements TypeExtensionNode
+{
+ public string $kind = NodeKind::SCALAR_TYPE_EXTENSION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaDefinitionNode.php
new file mode 100644
index 00000000000..3449e0daca0
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaDefinitionNode.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class SchemaDefinitionNode extends Node implements TypeSystemDefinitionNode
+{
+ public string $kind = NodeKind::SCHEMA_DEFINITION;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<OperationTypeDefinitionNode> */
+ public NodeList $operationTypes;
+
+ public ?StringValueNode $description = null;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaExtensionNode.php
new file mode 100644
index 00000000000..19968f8c10e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SchemaExtensionNode.php
@@ -0,0 +1,14 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class SchemaExtensionNode extends Node implements TypeSystemExtensionNode
+{
+ public string $kind = NodeKind::SCHEMA_EXTENSION;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<OperationTypeDefinitionNode> */
+ public NodeList $operationTypes;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionNode.php
new file mode 100644
index 00000000000..d3b54fc4306
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionNode.php
@@ -0,0 +1,8 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type SelectionNode = FieldNode | FragmentSpreadNode | InlineFragmentNode.
+ */
+interface SelectionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionSetNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionSetNode.php
new file mode 100644
index 00000000000..45e071516d3
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/SelectionSetNode.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class SelectionSetNode extends Node
+{
+ public string $kind = NodeKind::SELECTION_SET;
+
+ /** @var NodeList<SelectionNode&Node> */
+ public NodeList $selections;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/StringValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/StringValueNode.php
new file mode 100644
index 00000000000..73796ed2963
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/StringValueNode.php
@@ -0,0 +1,12 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class StringValueNode extends Node implements ValueNode
+{
+ public string $kind = NodeKind::STRING;
+
+ public string $value;
+
+ public bool $block = false;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeDefinitionNode.php
new file mode 100644
index 00000000000..30905861a2c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeDefinitionNode.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type TypeDefinitionNode = ScalarTypeDefinitionNode
+ * | ObjectTypeDefinitionNode
+ * | InterfaceTypeDefinitionNode
+ * | UnionTypeDefinitionNode
+ * | EnumTypeDefinitionNode
+ * | InputObjectTypeDefinitionNode.
+ */
+interface TypeDefinitionNode extends TypeSystemDefinitionNode
+{
+ public function getName(): NameNode;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeExtensionNode.php
new file mode 100644
index 00000000000..9153e5f23d4
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeExtensionNode.php
@@ -0,0 +1,17 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type TypeExtensionNode =
+ * | ScalarTypeExtensionNode
+ * | ObjectTypeExtensionNode
+ * | InterfaceTypeExtensionNode
+ * | UnionTypeExtensionNode
+ * | EnumTypeExtensionNode
+ * | InputObjectTypeExtensionNode;.
+ */
+interface TypeExtensionNode extends TypeSystemExtensionNode
+{
+ public function getName(): NameNode;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeNode.php
new file mode 100644
index 00000000000..d18ca8ba25b
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeNode.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type TypeNode = NamedTypeNode
+ * | ListTypeNode
+ * | NonNullTypeNode.
+ */
+interface TypeNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemDefinitionNode.php
new file mode 100644
index 00000000000..ca03605780d
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemDefinitionNode.php
@@ -0,0 +1,11 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type TypeSystemDefinitionNode =
+ * | SchemaDefinitionNode
+ * | TypeDefinitionNode
+ * | DirectiveDefinitionNode.
+ */
+interface TypeSystemDefinitionNode extends DefinitionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemExtensionNode.php
new file mode 100644
index 00000000000..bbd57cf5689
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/TypeSystemExtensionNode.php
@@ -0,0 +1,8 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type TypeSystemExtensionNode = SchemaExtensionNode | TypeExtensionNode;.
+ */
+interface TypeSystemExtensionNode extends DefinitionNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeDefinitionNode.php
new file mode 100644
index 00000000000..23c61ed1df0
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeDefinitionNode.php
@@ -0,0 +1,23 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class UnionTypeDefinitionNode extends Node implements TypeDefinitionNode
+{
+ public string $kind = NodeKind::UNION_TYPE_DEFINITION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<NamedTypeNode> */
+ public NodeList $types;
+
+ public ?StringValueNode $description = null;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeExtensionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeExtensionNode.php
new file mode 100644
index 00000000000..21d320c61fe
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/UnionTypeExtensionNode.php
@@ -0,0 +1,21 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class UnionTypeExtensionNode extends Node implements TypeExtensionNode
+{
+ public string $kind = NodeKind::UNION_TYPE_EXTENSION;
+
+ public NameNode $name;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ /** @var NodeList<NamedTypeNode> */
+ public NodeList $types;
+
+ public function getName(): NameNode
+ {
+ return $this->name;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ValueNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ValueNode.php
new file mode 100644
index 00000000000..a49cf77e5e8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/ValueNode.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+/**
+ * export type ValueNode = VariableNode
+ * | NullValueNode
+ * | IntValueNode
+ * | FloatValueNode
+ * | StringValueNode
+ * | BooleanValueNode
+ * | EnumValueNode
+ * | ListValueNode
+ * | ObjectValueNode.
+ */
+interface ValueNode {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableDefinitionNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableDefinitionNode.php
new file mode 100644
index 00000000000..9f3700aefb2
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableDefinitionNode.php
@@ -0,0 +1,25 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class VariableDefinitionNode extends Node implements DefinitionNode
+{
+ public string $kind = NodeKind::VARIABLE_DEFINITION;
+
+ public VariableNode $variable;
+
+ /** @var NamedTypeNode|ListTypeNode|NonNullTypeNode */
+ public TypeNode $type;
+
+ /** @var VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode|null */
+ public ?ValueNode $defaultValue = null;
+
+ /** @var NodeList<DirectiveNode> */
+ public NodeList $directives;
+
+ public function __construct(array $vars)
+ {
+ parent::__construct($vars);
+ $this->directives ??= new NodeList([]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableNode.php
new file mode 100644
index 00000000000..2b7571f353e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/AST/VariableNode.php
@@ -0,0 +1,10 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language\AST;
+
+class VariableNode extends Node implements ValueNode
+{
+ public string $kind = NodeKind::VARIABLE;
+
+ public NameNode $name;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/BlockString.php b/plugins/woocommerce/lib/packages/GraphQL/Language/BlockString.php
new file mode 100644
index 00000000000..16413a52ae0
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/BlockString.php
@@ -0,0 +1,155 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\BlockStringTest
+ */
+class BlockString
+{
+ /**
+ * Produces the value of a block string from its parsed raw value, similar to
+ * CoffeeScript's block string, Python's docstring trim or Ruby's strip_heredoc.
+ *
+ * This implements the Automattic\WooCommerce\Vendor\GraphQL spec's BlockStringValue() static algorithm.
+ */
+ public static function dedentBlockStringLines(string $rawString): string
+ {
+ $lines = Utils::splitLines($rawString);
+
+ // Remove common indentation from all lines but first.
+ $commonIndent = self::getIndentation($rawString);
+ $linesLength = count($lines);
+
+ if ($commonIndent > 0) {
+ for ($i = 1; $i < $linesLength; ++$i) {
+ $lines[$i] = mb_substr($lines[$i], $commonIndent);
+ }
+ }
+
+ // Remove leading and trailing blank lines.
+ $startLine = 0;
+ while ($startLine < $linesLength && self::isBlank($lines[$startLine])) {
+ ++$startLine;
+ }
+
+ $endLine = $linesLength;
+ while ($endLine > $startLine && self::isBlank($lines[$endLine - 1])) {
+ --$endLine;
+ }
+
+ // Return a string of the lines joined with U+000A.
+ return implode("\n", array_slice($lines, $startLine, $endLine - $startLine));
+ }
+
+ private static function isBlank(string $str): bool
+ {
+ $strLength = mb_strlen($str);
+ for ($i = 0; $i < $strLength; ++$i) {
+ if ($str[$i] !== ' ' && $str[$i] !== '\t') {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static function getIndentation(string $value): int
+ {
+ $isFirstLine = true;
+ $isEmptyLine = true;
+ $indent = 0;
+ $commonIndent = null;
+ $valueLength = mb_strlen($value);
+
+ for ($i = 0; $i < $valueLength; ++$i) {
+ switch (Utils::charCodeAt($value, $i)) {
+ case 13: // \r
+ if (Utils::charCodeAt($value, $i + 1) === 10) {
+ ++$i; // skip \r\n as one symbol
+ }
+ // falls through
+ // no break
+ case 10: // \n
+ $isFirstLine = false;
+ $isEmptyLine = true;
+ $indent = 0;
+ break;
+ case 9: // \t
+ case 32: // <space>
+ ++$indent;
+ break;
+ default:
+ if (
+ $isEmptyLine
+ && ! $isFirstLine
+ && ($commonIndent === null || $indent < $commonIndent)
+ ) {
+ $commonIndent = $indent;
+ }
+
+ $isEmptyLine = false;
+ }
+ }
+
+ return $commonIndent ?? 0;
+ }
+
+ /**
+ * Print a block string in the indented block form by adding a leading and
+ * trailing blank line. However, if a block string starts with whitespace and is
+ * a single-line, adding a leading blank line would strip that whitespace.
+ */
+ public static function print(string $value): string
+ {
+ $escapedValue = str_replace('"""', '\\"""', $value);
+
+ // Expand a block string's raw value into independent lines.
+ $lines = Utils::splitLines($escapedValue);
+ $isSingleLine = count($lines) === 1;
+
+ // If common indentation is found we can fix some of those cases by adding leading new line
+ $forceLeadingNewLine = count($lines) > 1;
+ foreach ($lines as $i => $line) {
+ if ($i === 0) {
+ continue;
+ }
+
+ if ($line !== '' && preg_match('/^\s/', $line) !== 1) {
+ $forceLeadingNewLine = false;
+ }
+ }
+
+ // Trailing triple quotes just looks confusing but doesn't force trailing new line
+ $hasTrailingTripleQuotes = preg_match('/\\\\"""$/', $escapedValue) === 1;
+
+ // Trailing quote (single or double) or slash forces trailing new line
+ $hasTrailingQuote = preg_match('/"$/', $value) === 1 && ! $hasTrailingTripleQuotes;
+ $hasTrailingSlash = preg_match('/\\\\$/', $value) === 1;
+ $forceTrailingNewline = $hasTrailingQuote || $hasTrailingSlash;
+
+ // add leading and trailing new lines only if it improves readability
+ $printAsMultipleLines = ! $isSingleLine
+ || mb_strlen($value) > 70
+ || $forceTrailingNewline
+ || $forceLeadingNewLine
+ || $hasTrailingTripleQuotes;
+
+ $result = '';
+
+ // Format a multi-line block quote to account for leading space.
+ $skipLeadingNewLine = $isSingleLine && preg_match('/^\s/', $value) === 1;
+ if (($printAsMultipleLines && ! $skipLeadingNewLine) || $forceLeadingNewLine) {
+ $result .= "\n";
+ }
+
+ $result .= $escapedValue;
+ if ($printAsMultipleLines) {
+ $result .= "\n";
+ }
+
+ return '"""' . $result . '"""';
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/DirectiveLocation.php b/plugins/woocommerce/lib/packages/GraphQL/Language/DirectiveLocation.php
new file mode 100644
index 00000000000..bac355962b8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/DirectiveLocation.php
@@ -0,0 +1,62 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+/**
+ * Enumeration of available directive locations.
+ */
+class DirectiveLocation
+{
+ public const QUERY = 'QUERY';
+ public const MUTATION = 'MUTATION';
+ public const SUBSCRIPTION = 'SUBSCRIPTION';
+ public const FIELD = 'FIELD';
+ public const FRAGMENT_DEFINITION = 'FRAGMENT_DEFINITION';
+ public const FRAGMENT_SPREAD = 'FRAGMENT_SPREAD';
+ public const INLINE_FRAGMENT = 'INLINE_FRAGMENT';
+ public const VARIABLE_DEFINITION = 'VARIABLE_DEFINITION';
+
+ public const EXECUTABLE_LOCATIONS = [
+ self::QUERY => self::QUERY,
+ self::MUTATION => self::MUTATION,
+ self::SUBSCRIPTION => self::SUBSCRIPTION,
+ self::FIELD => self::FIELD,
+ self::FRAGMENT_DEFINITION => self::FRAGMENT_DEFINITION,
+ self::FRAGMENT_SPREAD => self::FRAGMENT_SPREAD,
+ self::INLINE_FRAGMENT => self::INLINE_FRAGMENT,
+ self::VARIABLE_DEFINITION => self::VARIABLE_DEFINITION,
+ ];
+
+ public const SCHEMA = 'SCHEMA';
+ public const SCALAR = 'SCALAR';
+ public const OBJECT = 'OBJECT';
+ public const FIELD_DEFINITION = 'FIELD_DEFINITION';
+ public const ARGUMENT_DEFINITION = 'ARGUMENT_DEFINITION';
+ public const IFACE = 'INTERFACE';
+ public const UNION = 'UNION';
+ public const ENUM = 'ENUM';
+ public const ENUM_VALUE = 'ENUM_VALUE';
+ public const INPUT_OBJECT = 'INPUT_OBJECT';
+ public const INPUT_FIELD_DEFINITION = 'INPUT_FIELD_DEFINITION';
+
+ public const TYPE_SYSTEM_LOCATIONS = [
+ self::SCHEMA => self::SCHEMA,
+ self::SCALAR => self::SCALAR,
+ self::OBJECT => self::OBJECT,
+ self::FIELD_DEFINITION => self::FIELD_DEFINITION,
+ self::ARGUMENT_DEFINITION => self::ARGUMENT_DEFINITION,
+ self::IFACE => self::IFACE,
+ self::UNION => self::UNION,
+ self::ENUM => self::ENUM,
+ self::ENUM_VALUE => self::ENUM_VALUE,
+ self::INPUT_OBJECT => self::INPUT_OBJECT,
+ self::INPUT_FIELD_DEFINITION => self::INPUT_FIELD_DEFINITION,
+ ];
+
+ public const LOCATIONS = self::EXECUTABLE_LOCATIONS + self::TYPE_SYSTEM_LOCATIONS;
+
+ public static function has(string $name): bool
+ {
+ return isset(self::LOCATIONS[$name]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Lexer.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Lexer.php
new file mode 100644
index 00000000000..5430d0634c5
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/Lexer.php
@@ -0,0 +1,743 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * A lexer is a stateful stream generator, it returns the next token in the Source when advanced.
+ * Assuming the source is valid, the final returned token will be EOF,
+ * after which the lexer will repeatedly return the same EOF token whenever called.
+ *
+ * Algorithm is O(N) both on memory and time.
+ *
+ * @phpstan-import-type ParserOptions from Parser
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\LexerTest
+ */
+class Lexer
+{
+ // https://spec.graphql.org/October2021/#sec-Punctuators
+ private const TOKEN_BANG = 33;
+ private const TOKEN_DOLLAR = 36;
+ private const TOKEN_AMP = 38;
+ private const TOKEN_PAREN_L = 40;
+ private const TOKEN_PAREN_R = 41;
+ private const TOKEN_DOT = 46;
+ private const TOKEN_COLON = 58;
+ private const TOKEN_EQUALS = 61;
+ private const TOKEN_AT = 64;
+ private const TOKEN_BRACKET_L = 91;
+ private const TOKEN_BRACKET_R = 93;
+ private const TOKEN_BRACE_L = 123;
+ private const TOKEN_PIPE = 124;
+ private const TOKEN_BRACE_R = 125;
+
+ public Source $source;
+
+ /** @phpstan-var ParserOptions */
+ public array $options;
+
+ /** The previously focused non-ignored token. */
+ public Token $lastToken;
+
+ /** The currently focused non-ignored token. */
+ public Token $token;
+
+ /** The (1-indexed) line containing the current token. */
+ public int $line = 1;
+
+ /** The character offset at which the current line begins. */
+ public int $lineStart = 0;
+
+ /** Current cursor position for UTF8 encoding of the source. */
+ private int $position = 0;
+
+ /** Current cursor position for ASCII representation of the source. */
+ private int $byteStreamPosition = 0;
+
+ /** @phpstan-param ParserOptions $options */
+ public function __construct(Source $source, array $options = [])
+ {
+ $startOfFileToken = new Token(Token::SOF, 0, 0, 0, 0);
+
+ $this->source = $source;
+ $this->options = $options;
+ $this->lastToken = $startOfFileToken;
+ $this->token = $startOfFileToken;
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ public function advance(): Token
+ {
+ $this->lastToken = $this->token;
+
+ return $this->token = $this->lookahead();
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ public function lookahead(): Token
+ {
+ $token = $this->token;
+ if ($token->kind !== Token::EOF) {
+ do {
+ $token = $token->next ?? ($token->next = $this->readToken($token));
+ } while ($token->kind === Token::COMMENT);
+ }
+
+ return $token;
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function readToken(Token $prev): Token
+ {
+ $bodyLength = $this->source->length;
+
+ $this->positionAfterWhitespace();
+ $position = $this->position;
+
+ $line = $this->line;
+ $col = 1 + $position - $this->lineStart;
+
+ if ($position >= $bodyLength) {
+ return new Token(Token::EOF, $bodyLength, $bodyLength, $line, $col, $prev);
+ }
+
+ // Read next char and advance string cursor:
+ [, $code, $bytes] = $this->readChar(true);
+
+ switch ($code) {
+ case self::TOKEN_BANG: // !
+ return new Token(Token::BANG, $position, $position + 1, $line, $col, $prev);
+ case 35: // #
+ $this->moveStringCursor(-1, -1 * $bytes);
+
+ return $this->readComment($line, $col, $prev);
+ case self::TOKEN_DOLLAR: // $
+ return new Token(Token::DOLLAR, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_AMP: // &
+ return new Token(Token::AMP, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_PAREN_L: // (
+ return new Token(Token::PAREN_L, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_PAREN_R: // )
+ return new Token(Token::PAREN_R, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_DOT: // .
+ [, $charCode1] = $this->readChar(true);
+ [, $charCode2] = $this->readChar(true);
+
+ if ($charCode1 === self::TOKEN_DOT && $charCode2 === self::TOKEN_DOT) {
+ return new Token(Token::SPREAD, $position, $position + 3, $line, $col, $prev);
+ }
+
+ break;
+ case self::TOKEN_COLON: // :
+ return new Token(Token::COLON, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_EQUALS: // =
+ return new Token(Token::EQUALS, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_AT: // @
+ return new Token(Token::AT, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_BRACKET_L: // [
+ return new Token(Token::BRACKET_L, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_BRACKET_R: // ]
+ return new Token(Token::BRACKET_R, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_BRACE_L: // {
+ return new Token(Token::BRACE_L, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_PIPE: // |
+ return new Token(Token::PIPE, $position, $position + 1, $line, $col, $prev);
+ case self::TOKEN_BRACE_R: // }
+ return new Token(Token::BRACE_R, $position, $position + 1, $line, $col, $prev);
+ // A-Z
+ case 65:
+ case 66:
+ case 67:
+ case 68:
+ case 69:
+ case 70:
+ case 71:
+ case 72:
+ case 73:
+ case 74:
+ case 75:
+ case 76:
+ case 77:
+ case 78:
+ case 79:
+ case 80:
+ case 81:
+ case 82:
+ case 83:
+ case 84:
+ case 85:
+ case 86:
+ case 87:
+ case 88:
+ case 89:
+ case 90:
+ // _
+ case 95:
+ // a-z
+ case 97:
+ case 98:
+ case 99:
+ case 100:
+ case 101:
+ case 102:
+ case 103:
+ case 104:
+ case 105:
+ case 106:
+ case 107:
+ case 108:
+ case 109:
+ case 110:
+ case 111:
+ case 112:
+ case 113:
+ case 114:
+ case 115:
+ case 116:
+ case 117:
+ case 118:
+ case 119:
+ case 120:
+ case 121:
+ case 122:
+ return $this->moveStringCursor(-1, -1 * $bytes)
+ ->readName($line, $col, $prev);
+ // -
+ case 45:
+ // 0-9
+ case 48:
+ case 49:
+ case 50:
+ case 51:
+ case 52:
+ case 53:
+ case 54:
+ case 55:
+ case 56:
+ case 57:
+ return $this->moveStringCursor(-1, -1 * $bytes)
+ ->readNumber($line, $col, $prev);
+ // "
+ case 34:
+ [, $nextCode] = $this->readChar();
+ [, $nextNextCode] = $this->moveStringCursor(1, 1)
+ ->readChar();
+
+ if ($nextCode === 34 && $nextNextCode === 34) {
+ return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
+ ->readBlockString($line, $col, $prev);
+ }
+
+ return $this->moveStringCursor(-2, (-1 * $bytes) - 1)
+ ->readString($line, $col, $prev);
+ }
+
+ throw new SyntaxError($this->source, $position, $this->unexpectedCharacterMessage($code));
+ }
+
+ /** @throws \JsonException */
+ private function unexpectedCharacterMessage(?int $code): string
+ {
+ // SourceCharacter
+ if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
+ return 'Cannot contain the invalid character ' . Utils::printCharCode($code);
+ }
+
+ if ($code === 39) {
+ return 'Unexpected single quote character (\'), did you mean to use a double quote (")?';
+ }
+
+ return 'Cannot parse the unexpected character ' . Utils::printCharCode($code) . '.';
+ }
+
+ /**
+ * Reads an alphanumeric + underscore name from the source.
+ *
+ * [_A-Za-z][_0-9A-Za-z]*
+ */
+ private function readName(int $line, int $col, Token $prev): Token
+ {
+ $start = $this->position;
+ $body = $this->source->body;
+ $length = strspn($body, 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_', $this->byteStreamPosition);
+ $value = substr($body, $this->byteStreamPosition, $length);
+ $this->moveStringCursor($length, $length);
+
+ return new Token(
+ Token::NAME,
+ $start,
+ $this->position,
+ $line,
+ $col,
+ $prev,
+ $value
+ );
+ }
+
+ /**
+ * Reads a number token from the source file, either a float
+ * or an int depending on whether a decimal point appears.
+ *
+ * Int: -?(0|[1-9][0-9]*)
+ * Float: -?(0|[1-9][0-9]*)(\.[0-9]+)?((E|e)(+|-)?[0-9]+)?
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function readNumber(int $line, int $col, Token $prev): Token
+ {
+ $value = '';
+ $start = $this->position;
+ [$char, $code] = $this->readChar();
+
+ $isFloat = false;
+
+ if ($code === 45) { // -
+ $value .= $char;
+ [$char, $code] = $this->moveStringCursor(1, 1)->readChar();
+ }
+
+ // guard against leading zero's
+ if ($code === 48) { // 0
+ $value .= $char;
+ [$char, $code] = $this->moveStringCursor(1, 1)->readChar();
+
+ if ($code >= 48 && $code <= 57) {
+ throw new SyntaxError($this->source, $this->position, 'Invalid number, unexpected digit after 0: ' . Utils::printCharCode($code));
+ }
+ } else {
+ $value .= $this->readDigits();
+ [$char, $code] = $this->readChar();
+ }
+
+ if ($code === 46) { // .
+ $isFloat = true;
+ $this->moveStringCursor(1, 1);
+
+ $value .= $char;
+ $value .= $this->readDigits();
+ [$char, $code] = $this->readChar();
+ }
+
+ if ($code === 69 || $code === 101) { // E e
+ $isFloat = true;
+ $value .= $char;
+ [$char, $code] = $this->moveStringCursor(1, 1)->readChar();
+
+ if ($code === 43 || $code === 45) { // + -
+ $value .= $char;
+ $this->moveStringCursor(1, 1);
+ }
+
+ $value .= $this->readDigits();
+ }
+
+ return new Token(
+ $isFloat ? Token::FLOAT : Token::INT,
+ $start,
+ $this->position,
+ $line,
+ $col,
+ $prev,
+ $value
+ );
+ }
+
+ /**
+ * Returns string with all digits + changes current string cursor position to point to the first char after digits.
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function readDigits(): string
+ {
+ [$char, $code] = $this->readChar();
+
+ if ($code >= 48 && $code <= 57) { // 0 - 9
+ $value = '';
+
+ do {
+ $value .= $char;
+ [$char, $code] = $this->moveStringCursor(1, 1)->readChar();
+ } while ($code >= 48 && $code <= 57); // 0 - 9
+
+ return $value;
+ }
+
+ if ($this->position > $this->source->length - 1) {
+ $code = null;
+ }
+
+ throw new SyntaxError($this->source, $this->position, 'Invalid number, expected digit but got: ' . Utils::printCharCode($code));
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function readString(int $line, int $col, Token $prev): Token
+ {
+ $start = $this->position;
+
+ // Skip leading quote and read first string char:
+ [$char, $code, $bytes] = $this->moveStringCursor(1, 1)
+ ->readChar();
+
+ $chunk = '';
+ $value = '';
+
+ while (! in_array($code, [null, 10, 13], true)) { // not LineTerminator
+ if ($code === 34) { // Closing Quote (")
+ $value .= $chunk;
+
+ // Skip quote
+ $this->moveStringCursor(1, 1);
+
+ return new Token(
+ Token::STRING,
+ $start,
+ $this->position,
+ $line,
+ $col,
+ $prev,
+ $value
+ );
+ }
+
+ $this->assertValidStringCharacterCode($code, $this->position);
+ $this->moveStringCursor(1, $bytes);
+
+ if ($code === 92) { // \
+ $value .= $chunk;
+ [, $code] = $this->readChar(true);
+
+ switch ($code) {
+ case 34:
+ $value .= '"';
+ break;
+ case 47:
+ $value .= '/';
+ break;
+ case 92:
+ $value .= '\\';
+ break;
+ case 98:
+ $value .= chr(8); // \b (backspace)
+ break;
+ case 102:
+ $value .= "\f";
+ break;
+ case 110:
+ $value .= "\n";
+ break;
+ case 114:
+ $value .= "\r";
+ break;
+ case 116:
+ $value .= "\t";
+ break;
+ case 117:
+ $position = $this->position;
+ [$hex] = $this->readChars(4);
+ if (preg_match('/[0-9a-fA-F]{4}/', $hex) !== 1) {
+ throw new SyntaxError($this->source, $position - 1, "Invalid character escape sequence: \\u{$hex}");
+ }
+
+ $code = hexdec($hex);
+ assert(is_int($code), 'Since only a single char is read');
+
+ // UTF-16 surrogate pair detection and handling.
+ $highOrderByte = $code >> 8;
+ if ($highOrderByte >= 0xD8 && $highOrderByte <= 0xDF) {
+ [$utf16Continuation] = $this->readChars(6);
+ if (preg_match('/^\\\u[0-9a-fA-F]{4}$/', $utf16Continuation) !== 1) {
+ throw new SyntaxError($this->source, $this->position - 5, 'Invalid UTF-16 trailing surrogate: ' . $utf16Continuation);
+ }
+
+ $surrogatePairHex = $hex . substr($utf16Continuation, 2, 4);
+ $value .= mb_convert_encoding(pack('H*', $surrogatePairHex), 'UTF-8', 'UTF-16');
+ break;
+ }
+
+ $this->assertValidStringCharacterCode($code, $position - 2);
+
+ $value .= Utils::chr($code);
+ break;
+ // null means EOF, will delegate to general handling of unterminated strings
+ case null:
+ continue 2;
+ default:
+ $chr = Utils::chr($code);
+ throw new SyntaxError($this->source, $this->position - 1, "Invalid character escape sequence: \\{$chr}");
+ }
+
+ $chunk = '';
+ } else {
+ $chunk .= $char;
+ }
+
+ [$char, $code, $bytes] = $this->readChar();
+ }
+
+ throw new SyntaxError($this->source, $this->position, 'Unterminated string.');
+ }
+
+ /**
+ * Reads a block string token from the source file.
+ *
+ * """("?"?(\\"""|\\(?!=""")|[^"\\]))*"""
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function readBlockString(int $line, int $col, Token $prev): Token
+ {
+ $start = $this->position;
+
+ // Skip leading quotes and read first string char:
+ [$char, $code, $bytes] = $this->moveStringCursor(3, 3)->readChar();
+
+ $chunk = '';
+ $value = '';
+
+ while ($code !== null) {
+ // Closing Triple-Quote (""")
+ if ($code === 34) {
+ // Move 2 quotes
+ [, $nextCode] = $this->moveStringCursor(1, 1)->readChar();
+ [, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar();
+
+ if ($nextCode === 34 && $nextNextCode === 34) {
+ $value .= $chunk;
+
+ $this->moveStringCursor(1, 1);
+
+ return new Token(
+ Token::BLOCK_STRING,
+ $start,
+ $this->position,
+ $line,
+ $col,
+ $prev,
+ BlockString::dedentBlockStringLines($value)
+ );
+ }
+
+ // move cursor back to before the first quote
+ $this->moveStringCursor(-2, -2);
+ }
+
+ $this->assertValidBlockStringCharacterCode($code, $this->position);
+ $this->moveStringCursor(1, $bytes);
+
+ [, $nextCode] = $this->readChar();
+ [, $nextNextCode] = $this->moveStringCursor(1, 1)->readChar();
+ [, $nextNextNextCode] = $this->moveStringCursor(1, 1)->readChar();
+
+ // Escape Triple-Quote (\""")
+ if (
+ $code === 92
+ && $nextCode === 34
+ && $nextNextCode === 34
+ && $nextNextNextCode === 34
+ ) {
+ $this->moveStringCursor(1, 1);
+ $value .= $chunk . '"""';
+ $chunk = '';
+ } else {
+ // move cursor back to before the first quote
+ $this->moveStringCursor(-2, -2);
+
+ if ($code === 10) { // new line
+ ++$this->line;
+ $this->lineStart = $this->position;
+ }
+
+ $chunk .= $char;
+ }
+
+ [$char, $code, $bytes] = $this->readChar();
+ }
+
+ throw new SyntaxError($this->source, $this->position, 'Unterminated string.');
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function assertValidStringCharacterCode(int $code, int $position): void
+ {
+ // SourceCharacter
+ if ($code < 0x0020 && $code !== 0x0009) {
+ $char = Utils::printCharCode($code);
+ throw new SyntaxError($this->source, $position, "Invalid character within String: {$char}");
+ }
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function assertValidBlockStringCharacterCode(int $code, int $position): void
+ {
+ // SourceCharacter
+ if ($code < 0x0020 && $code !== 0x0009 && $code !== 0x000A && $code !== 0x000D) {
+ $char = Utils::printCharCode($code);
+ throw new SyntaxError($this->source, $position, "Invalid character within String: {$char}");
+ }
+ }
+
+ /**
+ * Reads from body starting at startPosition until it finds a non-whitespace
+ * or commented character, then places cursor to the position of that character.
+ */
+ private function positionAfterWhitespace(): void
+ {
+ while ($this->position < $this->source->length) {
+ [, $code, $bytes] = $this->readChar();
+
+ // Skip whitespace
+ // tab | space | comma | BOM
+ if (in_array($code, [9, 32, 44, 0xFEFF], true)) {
+ $this->moveStringCursor(1, $bytes);
+ } elseif ($code === 10) { // new line
+ $this->moveStringCursor(1, $bytes);
+ ++$this->line;
+ $this->lineStart = $this->position;
+ } elseif ($code === 13) { // carriage return
+ [, $nextCode, $nextBytes] = $this->moveStringCursor(1, $bytes)->readChar();
+
+ if ($nextCode === 10) { // lf after cr
+ $this->moveStringCursor(1, $nextBytes);
+ }
+
+ ++$this->line;
+ $this->lineStart = $this->position;
+ } else {
+ break;
+ }
+ }
+ }
+
+ /**
+ * Reads a comment token from the source file.
+ *
+ * #[\u0009\u0020-\uFFFF]*
+ */
+ private function readComment(int $line, int $col, Token $prev): Token
+ {
+ $start = $this->position;
+ $value = '';
+ $bytes = 1;
+
+ do {
+ [$char, $code, $bytes] = $this->moveStringCursor(1, $bytes)->readChar();
+ $value .= $char;
+ } while (
+ $code !== null
+ // SourceCharacter but not LineTerminator
+ && ($code > 0x001F || $code === 0x0009)
+ );
+
+ return new Token(
+ Token::COMMENT,
+ $start,
+ $this->position,
+ $line,
+ $col,
+ $prev,
+ $value
+ );
+ }
+
+ /**
+ * Reads next UTF8Character from the byte stream, starting from $byteStreamPosition.
+ *
+ * @return array{string, int|null, int}
+ */
+ private function readChar(bool $advance = false, ?int $byteStreamPosition = null): array
+ {
+ if ($byteStreamPosition === null) {
+ $byteStreamPosition = $this->byteStreamPosition;
+ }
+
+ $code = null;
+ $utf8char = '';
+ $bytes = 0;
+ $positionOffset = 0;
+
+ if (isset($this->source->body[$byteStreamPosition])) {
+ $ord = ord($this->source->body[$byteStreamPosition]);
+
+ if ($ord < 128) {
+ $bytes = 1;
+ } elseif ($ord < 224) {
+ $bytes = 2;
+ } elseif ($ord < 240) {
+ $bytes = 3;
+ } else {
+ $bytes = 4;
+ }
+
+ for ($pos = $byteStreamPosition; $pos < $byteStreamPosition + $bytes; ++$pos) {
+ $utf8char .= $this->source->body[$pos];
+ }
+
+ $positionOffset = 1;
+ $code = $bytes === 1
+ ? $ord
+ : Utils::ord($utf8char);
+ }
+
+ if ($advance) {
+ $this->moveStringCursor($positionOffset, $bytes);
+ }
+
+ return [$utf8char, $code, $bytes];
+ }
+
+ /**
+ * Reads next $numberOfChars UTF8 characters from the byte stream.
+ *
+ * @return array{string, int}
+ */
+ private function readChars(int $charCount): array
+ {
+ $result = '';
+ $totalBytes = 0;
+ $byteOffset = $this->byteStreamPosition;
+
+ for ($i = 0; $i < $charCount; ++$i) {
+ [$char, $code, $bytes] = $this->readChar(false, $byteOffset);
+ $totalBytes += $bytes;
+ $byteOffset += $bytes;
+ $result .= $char;
+ }
+
+ $this->moveStringCursor($charCount, $totalBytes);
+
+ return [$result, $totalBytes];
+ }
+
+ /** Moves internal string cursor position. */
+ private function moveStringCursor(int $positionOffset, int $byteStreamOffset): self
+ {
+ $this->position += $positionOffset;
+ $this->byteStreamPosition += $byteStreamOffset;
+
+ return $this;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Parser.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Parser.php
new file mode 100644
index 00000000000..8a79c081f56
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/Parser.php
@@ -0,0 +1,1884 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ExecutableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Location;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeSystemDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeSystemExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
+
+/**
+ * Parses string containing Automattic\WooCommerce\Vendor\GraphQL query language or [schema definition language](schema-definition-language.md) to Abstract Syntax Tree.
+ *
+ * @phpstan-type ParserOptions array{
+ * noLocation?: bool,
+ * allowLegacySDLEmptyFields?: bool,
+ * allowLegacySDLImplementsInterfaces?: bool,
+ * experimentalFragmentVariables?: bool
+ * }
+ *
+ * - **noLocation**:
+ * By default, the parser creates AST nodes that know the location in the source.
+ * This configuration flag disables that behavior for performance or testing.
+ *
+ * - **allowLegacySDLEmptyFields**:
+ * If enabled, the parser will parse empty fields sets in the Schema Definition Language.
+ * Otherwise, the parser will follow the current specification.
+ * This option is provided to ease adoption of the final SDL specification and will be removed in a future major release.
+ *
+ * - **allowLegacySDLImplementsInterfaces**:
+ * If enabled, the parser will parse implemented interfaces with no `&` character between each interface.
+ * Otherwise, the parser will follow the current specification.
+ * This option is provided to ease adoption of the final SDL specification and will be removed in a future major release.
+ *
+ * - **experimentalFragmentVariables**:
+ * If enabled, the parser will understand and parse variable definitions contained in a fragment definition.
+ * They'll be represented in the `variableDefinitions` field of the FragmentDefinitionNode.
+ * The syntax is identical to normal, query-defined variables. For example:
+ *
+ * ```graphql
+ * fragment A($var: Boolean = false) on T {
+ * ...
+ * }
+ * ```
+ *
+ * Note: this feature is experimental and may change or be removed in the future.
+ *
+ * Those magic functions allow partial parsing:
+ *
+ * @method static NameNode name(Source|string $source, ParserOptions $options = [])
+ * @method static ExecutableDefinitionNode|TypeSystemDefinitionNode definition(Source|string $source, ParserOptions $options = [])
+ * @method static ExecutableDefinitionNode executableDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static OperationDefinitionNode operationDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static string operationType(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<VariableDefinitionNode> variableDefinitions(Source|string $source, ParserOptions $options = [])
+ * @method static VariableDefinitionNode variableDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static VariableNode variable(Source|string $source, ParserOptions $options = [])
+ * @method static SelectionSetNode selectionSet(Source|string $source, ParserOptions $options = [])
+ * @method static mixed selection(Source|string $source, ParserOptions $options = [])
+ * @method static FieldNode field(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<ArgumentNode> arguments(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<ArgumentNode> constArguments(Source|string $source, ParserOptions $options = [])
+ * @method static ArgumentNode argument(Source|string $source, ParserOptions $options = [])
+ * @method static ArgumentNode constArgument(Source|string $source, ParserOptions $options = [])
+ * @method static FragmentSpreadNode|InlineFragmentNode fragment(Source|string $source, ParserOptions $options = [])
+ * @method static FragmentDefinitionNode fragmentDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static NameNode fragmentName(Source|string $source, ParserOptions $options = [])
+ * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode|VariableNode valueLiteral(Source|string $source, ParserOptions $options = [])
+ * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode constValueLiteral(Source|string $source, ParserOptions $options = [])
+ * @method static StringValueNode stringLiteral(Source|string $source, ParserOptions $options = [])
+ * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode constValue(Source|string $source, ParserOptions $options = [])
+ * @method static BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|ObjectValueNode|StringValueNode|VariableNode variableValue(Source|string $source, ParserOptions $options = [])
+ * @method static ListValueNode array(Source|string $source, ParserOptions $options = [])
+ * @method static ListValueNode constArray(Source|string $source, ParserOptions $options = [])
+ * @method static ObjectValueNode object(Source|string $source, ParserOptions $options = [])
+ * @method static ObjectValueNode constObject(Source|string $source, ParserOptions $options = [])
+ * @method static ObjectFieldNode objectField(Source|string $source, ParserOptions $options = [])
+ * @method static ObjectFieldNode constObjectField(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<DirectiveNode> directives(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<DirectiveNode> constDirectives(Source|string $source, ParserOptions $options = [])
+ * @method static DirectiveNode directive(Source|string $source, ParserOptions $options = [])
+ * @method static DirectiveNode constDirective(Source|string $source, ParserOptions $options = [])
+ * @method static ListTypeNode|NamedTypeNode|NonNullTypeNode typeReference(Source|string $source, ParserOptions $options = [])
+ * @method static NamedTypeNode namedType(Source|string $source, ParserOptions $options = [])
+ * @method static TypeSystemDefinitionNode typeSystemDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static StringValueNode|null description(Source|string $source, ParserOptions $options = [])
+ * @method static SchemaDefinitionNode schemaDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static OperationTypeDefinitionNode operationTypeDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static ScalarTypeDefinitionNode scalarTypeDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static ObjectTypeDefinitionNode objectTypeDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<NamedTypeNode> implementsInterfaces(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<FieldDefinitionNode> fieldsDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static FieldDefinitionNode fieldDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<InputValueDefinitionNode> argumentsDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static InputValueDefinitionNode inputValueDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static InterfaceTypeDefinitionNode interfaceTypeDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static UnionTypeDefinitionNode unionTypeDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<NamedTypeNode> unionMemberTypes(Source|string $source, ParserOptions $options = [])
+ * @method static EnumTypeDefinitionNode enumTypeDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<EnumValueDefinitionNode> enumValuesDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static EnumValueDefinitionNode enumValueDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static InputObjectTypeDefinitionNode inputObjectTypeDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<InputValueDefinitionNode> inputFieldsDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static TypeExtensionNode typeExtension(Source|string $source, ParserOptions $options = [])
+ * @method static SchemaExtensionNode schemaTypeExtension(Source|string $source, ParserOptions $options = [])
+ * @method static ScalarTypeExtensionNode scalarTypeExtension(Source|string $source, ParserOptions $options = [])
+ * @method static ObjectTypeExtensionNode objectTypeExtension(Source|string $source, ParserOptions $options = [])
+ * @method static InterfaceTypeExtensionNode interfaceTypeExtension(Source|string $source, ParserOptions $options = [])
+ * @method static UnionTypeExtensionNode unionTypeExtension(Source|string $source, ParserOptions $options = [])
+ * @method static EnumTypeExtensionNode enumTypeExtension(Source|string $source, ParserOptions $options = [])
+ * @method static InputObjectTypeExtensionNode inputObjectTypeExtension(Source|string $source, ParserOptions $options = [])
+ * @method static DirectiveDefinitionNode directiveDefinition(Source|string $source, ParserOptions $options = [])
+ * @method static NodeList<NameNode> directiveLocations(Source|string $source, ParserOptions $options = [])
+ * @method static NameNode directiveLocation(Source|string $source, ParserOptions $options = [])
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\ParserTest
+ */
+class Parser
+{
+ /**
+ * Given a Automattic\WooCommerce\Vendor\GraphQL source, parses it into a `Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode`.
+ *
+ * Throws `Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError` if a syntax error is encountered.
+ *
+ * @param Source|string $source
+ *
+ * @phpstan-param ParserOptions $options
+ *
+ * @api
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ public static function parse($source, array $options = []): DocumentNode
+ {
+ return (new self($source, $options))->parseDocument();
+ }
+
+ /**
+ * Given a string containing a Automattic\WooCommerce\Vendor\GraphQL value (ex. `[42]`), parse the AST for that value.
+ *
+ * Throws `Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError` if a syntax error is encountered.
+ *
+ * This is useful within tools that operate upon Automattic\WooCommerce\Vendor\GraphQL Values directly and
+ * in isolation of complete Automattic\WooCommerce\Vendor\GraphQL documents.
+ *
+ * Consider providing the results to the utility function: `Automattic\WooCommerce\Vendor\GraphQL\Utils\AST::valueFromAST()`.
+ *
+ * @param Source|string $source
+ *
+ * @phpstan-param ParserOptions $options
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|ListValueNode|NullValueNode|ObjectValueNode|StringValueNode|VariableNode
+ *
+ * @api
+ */
+ public static function parseValue($source, array $options = [])
+ {
+ $parser = new Parser($source, $options);
+ $parser->expect(Token::SOF);
+ $value = $parser->parseValueLiteral(false);
+ $parser->expect(Token::EOF);
+
+ return $value;
+ }
+
+ /**
+ * Given a string containing a Automattic\WooCommerce\Vendor\GraphQL Type (ex. `[Int!]`), parse the AST for that type.
+ *
+ * Throws `Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError` if a syntax error is encountered.
+ *
+ * This is useful within tools that operate upon Automattic\WooCommerce\Vendor\GraphQL Types directly and
+ * in isolation of complete Automattic\WooCommerce\Vendor\GraphQL documents.
+ *
+ * Consider providing the results to the utility function: `Automattic\WooCommerce\Vendor\GraphQL\Utils\AST::typeFromAST()`.
+ *
+ * @param Source|string $source
+ *
+ * @phpstan-param ParserOptions $options
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return ListTypeNode|NamedTypeNode|NonNullTypeNode
+ *
+ * @api
+ */
+ public static function parseType($source, array $options = [])
+ {
+ $parser = new Parser($source, $options);
+ $parser->expect(Token::SOF);
+ $type = $parser->parseTypeReference();
+ $parser->expect(Token::EOF);
+
+ return $type;
+ }
+
+ /**
+ * Parse partial source by delegating calls to the internal parseX methods.
+ *
+ * @phpstan-param array{string, ParserOptions} $arguments
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return Node|NodeList<Node>
+ */
+ public static function __callStatic(string $name, array $arguments)
+ {
+ $parser = new Parser(...$arguments);
+ $parser->expect(Token::SOF);
+
+ switch ($name) {
+ case 'arguments':
+ $parsed = $parser->parseArguments(false);
+ break;
+ case 'valueLiteral':
+ $parsed = $parser->parseValueLiteral(false);
+ break;
+ case 'array':
+ $parsed = $parser->parseArray(false);
+ break;
+ case 'object':
+ $parsed = $parser->parseObject(false);
+ break;
+ case 'objectField':
+ $parsed = $parser->parseObjectField(false);
+ break;
+ case 'directives':
+ $parsed = $parser->parseDirectives(false);
+ break;
+ case 'directive':
+ $parsed = $parser->parseDirective(false);
+ break;
+ case 'constArguments':
+ $parsed = $parser->parseArguments(true);
+ break;
+ case 'constValueLiteral':
+ $parsed = $parser->parseValueLiteral(true);
+ break;
+ case 'constArray':
+ $parsed = $parser->parseArray(true);
+ break;
+ case 'constObject':
+ $parsed = $parser->parseObject(true);
+ break;
+ case 'constObjectField':
+ $parsed = $parser->parseObjectField(true);
+ break;
+ case 'constDirectives':
+ $parsed = $parser->parseDirectives(true);
+ break;
+ case 'constDirective':
+ $parsed = $parser->parseDirective(true);
+ break;
+ default:
+ $parsed = $parser->{'parse' . $name}();
+ }
+
+ $parser->expect(Token::EOF);
+
+ return $parsed;
+ }
+
+ private Lexer $lexer;
+
+ /**
+ * @param Source|string $source
+ *
+ * @phpstan-param ParserOptions $options
+ */
+ public function __construct($source, array $options = [])
+ {
+ $sourceObj = $source instanceof Source
+ ? $source
+ : new Source($source);
+ $this->lexer = new Lexer($sourceObj, $options);
+ }
+
+ /**
+ * Returns a location object, used to identify the place in
+ * the source that created a given parsed object.
+ */
+ private function loc(Token $startToken): ?Location
+ {
+ if (! ($this->lexer->options['noLocation'] ?? false)) {
+ return new Location($startToken, $this->lexer->lastToken, $this->lexer->source);
+ }
+
+ return null;
+ }
+
+ /** Determines if the next token is of a given kind. */
+ private function peek(string $kind): bool
+ {
+ return $this->lexer->token->kind === $kind;
+ }
+
+ /**
+ * If the next token is of the given kind, return true after advancing
+ * the parser. Otherwise, do not change the parser state and return false.
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function skip(string $kind): bool
+ {
+ $match = $this->lexer->token->kind === $kind;
+
+ if ($match) {
+ $this->lexer->advance();
+ }
+
+ return $match;
+ }
+
+ /**
+ * If the next token is of the given kind, return that token after advancing
+ * the parser. Otherwise, do not change the parser state and return false.
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function expect(string $kind): Token
+ {
+ $token = $this->lexer->token;
+
+ if ($token->kind === $kind) {
+ $this->lexer->advance();
+
+ return $token;
+ }
+
+ throw new SyntaxError($this->lexer->source, $token->start, "Expected {$kind}, found {$token->getDescription()}");
+ }
+
+ /**
+ * If the next token is a keyword with the given value, advance the lexer.
+ * Otherwise, throw an error.
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function expectKeyword(string $value): void
+ {
+ $token = $this->lexer->token;
+ if ($token->kind !== Token::NAME || $token->value !== $value) {
+ throw new SyntaxError($this->lexer->source, $token->start, "Expected \"{$value}\", found {$token->getDescription()}");
+ }
+
+ $this->lexer->advance();
+ }
+
+ /**
+ * If the next token is a given keyword, return "true" after advancing
+ * the lexer. Otherwise, do not change the parser state and return "false".
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function expectOptionalKeyword(string $value): bool
+ {
+ $token = $this->lexer->token;
+ if ($token->kind === Token::NAME && $token->value === $value) {
+ $this->lexer->advance();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ private function unexpected(?Token $atToken = null): SyntaxError
+ {
+ $token = $atToken ?? $this->lexer->token;
+
+ return new SyntaxError($this->lexer->source, $token->start, 'Unexpected ' . $token->getDescription());
+ }
+
+ /**
+ * Returns a possibly empty list of parse nodes, determined by
+ * the parseFn. This list begins with a lex token of openKind
+ * and ends with a lex token of closeKind. Advances the parser
+ * to the next lex token after the closing token.
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<Node>
+ */
+ private function any(string $openKind, callable $parseFn, string $closeKind): NodeList
+ {
+ $this->expect($openKind);
+
+ $nodes = [];
+ while (! $this->skip($closeKind)) {
+ $nodes[] = $parseFn($this);
+ }
+
+ return new NodeList($nodes);
+ }
+
+ /**
+ * Returns a non-empty list of parse nodes, determined by
+ * the parseFn. This list begins with a lex token of openKind
+ * and ends with a lex token of closeKind. Advances the parser
+ * to the next lex token after the closing token.
+ *
+ * @template TNode of Node
+ *
+ * @param callable(self): TNode $parseFn
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<TNode>
+ */
+ private function many(string $openKind, callable $parseFn, string $closeKind): NodeList
+ {
+ $this->expect($openKind);
+
+ $nodes = [$parseFn($this)];
+ while (! $this->skip($closeKind)) {
+ $nodes[] = $parseFn($this);
+ }
+
+ return new NodeList($nodes);
+ }
+
+ /**
+ * Converts a name lex token into a name parse node.
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseName(): NameNode
+ {
+ $token = $this->expect(Token::NAME);
+
+ return new NameNode([
+ 'value' => $token->value,
+ 'loc' => $this->loc($token),
+ ]);
+ }
+
+ /**
+ * Implements the parsing rules in the Document section.
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseDocument(): DocumentNode
+ {
+ $start = $this->lexer->token;
+
+ return new DocumentNode([
+ 'definitions' => $this->many(
+ Token::SOF,
+ fn (): DefinitionNode => $this->parseDefinition(),
+ Token::EOF
+ ),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return DefinitionNode&Node
+ */
+ private function parseDefinition(): DefinitionNode
+ {
+ if ($this->peek(Token::NAME)) {
+ switch ($this->lexer->token->value) {
+ case 'query':
+ case 'mutation':
+ case 'subscription':
+ case 'fragment':
+ return $this->parseExecutableDefinition();
+
+ // Note: The schema definition language is an experimental addition.
+ case 'schema':
+ case 'scalar':
+ case 'type':
+ case 'interface':
+ case 'union':
+ case 'enum':
+ case 'input':
+ case 'directive':
+ // Note: The schema definition language is an experimental addition.
+ return $this->parseTypeSystemDefinition();
+
+ case 'extend':
+ return $this->parseTypeSystemExtension();
+ }
+ } elseif ($this->peek(Token::BRACE_L)) {
+ return $this->parseExecutableDefinition();
+ } elseif ($this->peekDescription()) {
+ // Note: The schema definition language is an experimental addition.
+ return $this->parseTypeSystemDefinition();
+ }
+
+ throw $this->unexpected();
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return ExecutableDefinitionNode&Node
+ */
+ private function parseExecutableDefinition(): ExecutableDefinitionNode
+ {
+ if ($this->peek(Token::NAME)) {
+ switch ($this->lexer->token->value) {
+ case 'query':
+ case 'mutation':
+ case 'subscription':
+ return $this->parseOperationDefinition();
+
+ case 'fragment':
+ return $this->parseFragmentDefinition();
+ }
+ } elseif ($this->peek(Token::BRACE_L)) {
+ return $this->parseOperationDefinition();
+ }
+
+ throw $this->unexpected();
+ }
+
+ // Implements the parsing rules in the Operations section.
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseOperationDefinition(): OperationDefinitionNode
+ {
+ $start = $this->lexer->token;
+ if ($this->peek(Token::BRACE_L)) {
+ return new OperationDefinitionNode([
+ 'name' => null,
+ 'operation' => 'query',
+ 'variableDefinitions' => new NodeList([]),
+ 'directives' => new NodeList([]),
+ 'selectionSet' => $this->parseSelectionSet(),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ $operation = $this->parseOperationType();
+
+ $name = null;
+ if ($this->peek(Token::NAME)) {
+ $name = $this->parseName();
+ }
+
+ return new OperationDefinitionNode([
+ 'name' => $name,
+ 'operation' => $operation,
+ 'variableDefinitions' => $this->parseVariableDefinitions(),
+ 'directives' => $this->parseDirectives(false),
+ 'selectionSet' => $this->parseSelectionSet(),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseOperationType(): string
+ {
+ $operationToken = $this->expect(Token::NAME);
+ switch ($operationToken->value) {
+ case 'query':
+ return 'query';
+
+ case 'mutation':
+ return 'mutation';
+
+ case 'subscription':
+ return 'subscription';
+ }
+
+ throw $this->unexpected($operationToken);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<VariableDefinitionNode>
+ */
+ private function parseVariableDefinitions(): NodeList
+ {
+ return $this->peek(Token::PAREN_L)
+ ? $this->many(
+ Token::PAREN_L,
+ fn (): VariableDefinitionNode => $this->parseVariableDefinition(),
+ Token::PAREN_R
+ )
+ : new NodeList([]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseVariableDefinition(): VariableDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $var = $this->parseVariable();
+
+ $this->expect(Token::COLON);
+ $type = $this->parseTypeReference();
+
+ return new VariableDefinitionNode([
+ 'variable' => $var,
+ 'type' => $type,
+ 'defaultValue' => $this->skip(Token::EQUALS)
+ ? $this->parseValueLiteral(true)
+ : null,
+ 'directives' => $this->parseDirectives(true),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseVariable(): VariableNode
+ {
+ $start = $this->lexer->token;
+ $this->expect(Token::DOLLAR);
+
+ return new VariableNode([
+ 'name' => $this->parseName(),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseSelectionSet(): SelectionSetNode
+ {
+ $start = $this->lexer->token;
+
+ return new SelectionSetNode(
+ [
+ 'selections' => $this->many(
+ Token::BRACE_L,
+ fn (): SelectionNode => $this->parseSelection(),
+ Token::BRACE_R
+ ),
+ 'loc' => $this->loc($start),
+ ]
+ );
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return SelectionNode&Node
+ */
+ private function parseSelection(): SelectionNode
+ {
+ return $this->peek(Token::SPREAD)
+ ? $this->parseFragment()
+ : $this->parseField();
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseField(): FieldNode
+ {
+ $start = $this->lexer->token;
+ $nameOrAlias = $this->parseName();
+
+ if ($this->skip(Token::COLON)) {
+ $alias = $nameOrAlias;
+ $name = $this->parseName();
+ } else {
+ $alias = null;
+ $name = $nameOrAlias;
+ }
+
+ return new FieldNode([
+ 'name' => $name,
+ 'alias' => $alias,
+ 'arguments' => $this->parseArguments(false),
+ 'directives' => $this->parseDirectives(false),
+ 'selectionSet' => $this->peek(Token::BRACE_L) ? $this->parseSelectionSet() : null,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<ArgumentNode>
+ */
+ private function parseArguments(bool $isConst): NodeList
+ {
+ $parseFn = $isConst
+ ? fn (): ArgumentNode => $this->parseConstArgument()
+ : fn (): ArgumentNode => $this->parseArgument();
+
+ return $this->peek(Token::PAREN_L)
+ ? $this->many(Token::PAREN_L, $parseFn, Token::PAREN_R)
+ : new NodeList([]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseArgument(): ArgumentNode
+ {
+ $start = $this->lexer->token;
+ $name = $this->parseName();
+
+ $this->expect(Token::COLON);
+ $value = $this->parseValueLiteral(false);
+
+ return new ArgumentNode([
+ 'name' => $name,
+ 'value' => $value,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseConstArgument(): ArgumentNode
+ {
+ $start = $this->lexer->token;
+ $name = $this->parseName();
+
+ $this->expect(Token::COLON);
+ $value = $this->parseConstValue();
+
+ return new ArgumentNode([
+ 'name' => $name,
+ 'value' => $value,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ // Implements the parsing rules in the Fragments section.
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return FragmentSpreadNode|InlineFragmentNode
+ */
+ private function parseFragment(): SelectionNode
+ {
+ $start = $this->lexer->token;
+ $this->expect(Token::SPREAD);
+
+ $hasTypeCondition = $this->expectOptionalKeyword('on');
+ if (! $hasTypeCondition && $this->peek(Token::NAME)) {
+ return new FragmentSpreadNode([
+ 'name' => $this->parseFragmentName(),
+ 'directives' => $this->parseDirectives(false),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ return new InlineFragmentNode([
+ 'typeCondition' => $hasTypeCondition ? $this->parseNamedType() : null,
+ 'directives' => $this->parseDirectives(false),
+ 'selectionSet' => $this->parseSelectionSet(),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseFragmentDefinition(): FragmentDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $this->expectKeyword('fragment');
+
+ $name = $this->parseFragmentName();
+
+ // Experimental support for defining variables within fragments changes
+ // the grammar of FragmentDefinition:
+ // - fragment FragmentName VariableDefinitions? on TypeCondition Directives? SelectionSet
+ $variableDefinitions = isset($this->lexer->options['experimentalFragmentVariables'])
+ ? $this->parseVariableDefinitions()
+ : null;
+
+ $this->expectKeyword('on');
+ $typeCondition = $this->parseNamedType();
+
+ return new FragmentDefinitionNode([
+ 'name' => $name,
+ 'variableDefinitions' => $variableDefinitions,
+ 'typeCondition' => $typeCondition,
+ 'directives' => $this->parseDirectives(false),
+ 'selectionSet' => $this->parseSelectionSet(),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseFragmentName(): NameNode
+ {
+ if ($this->lexer->token->value === 'on') {
+ throw $this->unexpected();
+ }
+
+ return $this->parseName();
+ }
+
+ // Implements the parsing rules in the Values section.
+
+ /**
+ * Value[Const] :
+ * - [~Const] Variable
+ * - IntValue
+ * - FloatValue
+ * - StringValue
+ * - BooleanValue
+ * - NullValue
+ * - EnumValue
+ * - ListValue[?Const]
+ * - ObjectValue[?Const].
+ *
+ * BooleanValue : one of `true` `false`
+ *
+ * NullValue : `null`
+ *
+ * EnumValue : Name but not `true`, `false` or `null`
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return BooleanValueNode|EnumValueNode|FloatValueNode|IntValueNode|StringValueNode|VariableNode|ListValueNode|ObjectValueNode|NullValueNode
+ */
+ private function parseValueLiteral(bool $isConst): ValueNode
+ {
+ $token = $this->lexer->token;
+ switch ($token->kind) {
+ case Token::BRACKET_L:
+ return $this->parseArray($isConst);
+
+ case Token::BRACE_L:
+ return $this->parseObject($isConst);
+
+ case Token::INT:
+ $this->lexer->advance();
+
+ return new IntValueNode([
+ 'value' => $token->value,
+ 'loc' => $this->loc($token),
+ ]);
+
+ case Token::FLOAT:
+ $this->lexer->advance();
+
+ return new FloatValueNode([
+ 'value' => $token->value,
+ 'loc' => $this->loc($token),
+ ]);
+
+ case Token::STRING:
+ case Token::BLOCK_STRING:
+ return $this->parseStringLiteral();
+
+ case Token::NAME:
+ if ($token->value === 'true' || $token->value === 'false') {
+ $this->lexer->advance();
+
+ return new BooleanValueNode([
+ 'value' => $token->value === 'true',
+ 'loc' => $this->loc($token),
+ ]);
+ }
+
+ if ($token->value === 'null') {
+ $this->lexer->advance();
+
+ return new NullValueNode([
+ 'loc' => $this->loc($token),
+ ]);
+ }
+ $this->lexer->advance();
+
+ return new EnumValueNode([
+ 'value' => $token->value,
+ 'loc' => $this->loc($token),
+ ]);
+
+ case Token::DOLLAR:
+ if (! $isConst) {
+ return $this->parseVariable();
+ }
+
+ break;
+ }
+
+ throw $this->unexpected();
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseStringLiteral(): StringValueNode
+ {
+ $token = $this->lexer->token;
+ $this->lexer->advance();
+
+ return new StringValueNode([
+ 'value' => $token->value,
+ 'block' => $token->kind === Token::BLOCK_STRING,
+ 'loc' => $this->loc($token),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseConstValue(): ValueNode
+ {
+ return $this->parseValueLiteral(true);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseVariableValue(): ValueNode
+ {
+ return $this->parseValueLiteral(false);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseArray(bool $isConst): ListValueNode
+ {
+ $start = $this->lexer->token;
+ $parseFn = $isConst
+ ? fn (): ValueNode => $this->parseConstValue()
+ : fn (): ValueNode => $this->parseVariableValue();
+
+ return new ListValueNode([
+ 'values' => $this->any(Token::BRACKET_L, $parseFn, Token::BRACKET_R),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseObject(bool $isConst): ObjectValueNode
+ {
+ $start = $this->lexer->token;
+ $this->expect(Token::BRACE_L);
+ $fields = [];
+ while (! $this->skip(Token::BRACE_R)) {
+ $fields[] = $this->parseObjectField($isConst);
+ }
+
+ return new ObjectValueNode([
+ 'fields' => new NodeList($fields),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseObjectField(bool $isConst): ObjectFieldNode
+ {
+ $start = $this->lexer->token;
+ $name = $this->parseName();
+
+ $this->expect(Token::COLON);
+
+ return new ObjectFieldNode([
+ 'name' => $name,
+ 'value' => $this->parseValueLiteral($isConst),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ // Implements the parsing rules in the Directives section.
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<DirectiveNode>
+ */
+ private function parseDirectives(bool $isConst): NodeList
+ {
+ $directives = [];
+ while ($this->peek(Token::AT)) {
+ $directives[] = $this->parseDirective($isConst);
+ }
+
+ return new NodeList($directives);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseDirective(bool $isConst): DirectiveNode
+ {
+ $start = $this->lexer->token;
+ $this->expect(Token::AT);
+
+ return new DirectiveNode([
+ 'name' => $this->parseName(),
+ 'arguments' => $this->parseArguments($isConst),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ // Implements the parsing rules in the Types section.
+
+ /**
+ * Handles the Type: TypeName, ListType, and NonNullType parsing rules.
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return ListTypeNode|NamedTypeNode|NonNullTypeNode
+ */
+ private function parseTypeReference(): TypeNode
+ {
+ $start = $this->lexer->token;
+
+ if ($this->skip(Token::BRACKET_L)) {
+ $type = $this->parseTypeReference();
+ $this->expect(Token::BRACKET_R);
+ $type = new ListTypeNode([
+ 'type' => $type,
+ 'loc' => $this->loc($start),
+ ]);
+ } else {
+ $type = $this->parseNamedType();
+ }
+
+ if ($this->skip(Token::BANG)) {
+ return new NonNullTypeNode([
+ 'type' => $type,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ return $type;
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseNamedType(): NamedTypeNode
+ {
+ $start = $this->lexer->token;
+
+ return new NamedTypeNode([
+ 'name' => $this->parseName(),
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ // Implements the parsing rules in the Type Definition section.
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return TypeSystemDefinitionNode&Node
+ */
+ private function parseTypeSystemDefinition(): TypeSystemDefinitionNode
+ {
+ // Many definitions begin with a description and require a lookahead.
+ $keywordToken = $this->peekDescription()
+ ? $this->lexer->lookahead()
+ : $this->lexer->token;
+
+ if ($keywordToken->kind === Token::NAME) {
+ switch ($keywordToken->value) {
+ case 'schema':
+ return $this->parseSchemaDefinition();
+
+ case 'scalar':
+ return $this->parseScalarTypeDefinition();
+
+ case 'type':
+ return $this->parseObjectTypeDefinition();
+
+ case 'interface':
+ return $this->parseInterfaceTypeDefinition();
+
+ case 'union':
+ return $this->parseUnionTypeDefinition();
+
+ case 'enum':
+ return $this->parseEnumTypeDefinition();
+
+ case 'input':
+ return $this->parseInputObjectTypeDefinition();
+
+ case 'directive':
+ return $this->parseDirectiveDefinition();
+ }
+ }
+
+ throw $this->unexpected($keywordToken);
+ }
+
+ private function peekDescription(): bool
+ {
+ return $this->peek(Token::STRING) || $this->peek(Token::BLOCK_STRING);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseDescription(): ?StringValueNode
+ {
+ if ($this->peekDescription()) {
+ return $this->parseStringLiteral();
+ }
+
+ return null;
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseSchemaDefinition(): SchemaDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $this->expectKeyword('schema');
+ $directives = $this->parseDirectives(true);
+
+ $operationTypes = $this->many(
+ Token::BRACE_L,
+ fn (): OperationTypeDefinitionNode => $this->parseOperationTypeDefinition(),
+ Token::BRACE_R
+ );
+
+ return new SchemaDefinitionNode([
+ 'directives' => $directives,
+ 'operationTypes' => $operationTypes,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseOperationTypeDefinition(): OperationTypeDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $operation = $this->parseOperationType();
+ $this->expect(Token::COLON);
+ $type = $this->parseNamedType();
+
+ return new OperationTypeDefinitionNode([
+ 'operation' => $operation,
+ 'type' => $type,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseScalarTypeDefinition(): ScalarTypeDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $this->expectKeyword('scalar');
+ $name = $this->parseName();
+ $directives = $this->parseDirectives(true);
+
+ return new ScalarTypeDefinitionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseObjectTypeDefinition(): ObjectTypeDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $this->expectKeyword('type');
+ $name = $this->parseName();
+ $interfaces = $this->parseImplementsInterfaces();
+ $directives = $this->parseDirectives(true);
+ $fields = $this->parseFieldsDefinition();
+
+ return new ObjectTypeDefinitionNode([
+ 'name' => $name,
+ 'interfaces' => $interfaces,
+ 'directives' => $directives,
+ 'fields' => $fields,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<NamedTypeNode>
+ */
+ private function parseImplementsInterfaces(): NodeList
+ {
+ $types = [];
+ if ($this->expectOptionalKeyword('implements')) {
+ // Optional leading ampersand
+ $this->skip(Token::AMP);
+ do {
+ $types[] = $this->parseNamedType();
+ } while (
+ $this->skip(Token::AMP)
+ // Legacy support for the SDL?
+ || (($this->lexer->options['allowLegacySDLImplementsInterfaces'] ?? false) && $this->peek(Token::NAME))
+ );
+ }
+
+ return new NodeList($types);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<FieldDefinitionNode>
+ */
+ private function parseFieldsDefinition(): NodeList
+ {
+ // Legacy support for the SDL?
+ if (
+ ($this->lexer->options['allowLegacySDLEmptyFields'] ?? false)
+ && $this->peek(Token::BRACE_L)
+ && $this->lexer->lookahead()->kind === Token::BRACE_R
+ ) {
+ $this->lexer->advance();
+ $this->lexer->advance();
+
+ /** @phpstan-var NodeList<FieldDefinitionNode> $nodeList */
+ $nodeList = new NodeList([]);
+ } else {
+ /** @phpstan-var NodeList<FieldDefinitionNode> $nodeList */
+ $nodeList = $this->peek(Token::BRACE_L)
+ ? $this->many(
+ Token::BRACE_L,
+ fn (): FieldDefinitionNode => $this->parseFieldDefinition(),
+ Token::BRACE_R
+ )
+ : new NodeList([]);
+ }
+
+ return $nodeList;
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseFieldDefinition(): FieldDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $name = $this->parseName();
+ $args = $this->parseArgumentsDefinition();
+ $this->expect(Token::COLON);
+ $type = $this->parseTypeReference();
+ $directives = $this->parseDirectives(true);
+
+ return new FieldDefinitionNode([
+ 'name' => $name,
+ 'arguments' => $args,
+ 'type' => $type,
+ 'directives' => $directives,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<InputValueDefinitionNode>
+ */
+ private function parseArgumentsDefinition(): NodeList
+ {
+ return $this->peek(Token::PAREN_L)
+ ? $this->many(
+ Token::PAREN_L,
+ fn (): InputValueDefinitionNode => $this->parseInputValueDefinition(),
+ Token::PAREN_R
+ )
+ : new NodeList([]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseInputValueDefinition(): InputValueDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $name = $this->parseName();
+ $this->expect(Token::COLON);
+ $type = $this->parseTypeReference();
+ $defaultValue = null;
+ if ($this->skip(Token::EQUALS)) {
+ $defaultValue = $this->parseConstValue();
+ }
+
+ $directives = $this->parseDirectives(true);
+
+ return new InputValueDefinitionNode([
+ 'name' => $name,
+ 'type' => $type,
+ 'defaultValue' => $defaultValue,
+ 'directives' => $directives,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseInterfaceTypeDefinition(): InterfaceTypeDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $this->expectKeyword('interface');
+ $name = $this->parseName();
+ $interfaces = $this->parseImplementsInterfaces();
+ $directives = $this->parseDirectives(true);
+ $fields = $this->parseFieldsDefinition();
+
+ return new InterfaceTypeDefinitionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'interfaces' => $interfaces,
+ 'fields' => $fields,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * UnionTypeDefinition :
+ * - Description? union Name Directives[Const]? UnionMemberTypes?
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseUnionTypeDefinition(): UnionTypeDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $this->expectKeyword('union');
+ $name = $this->parseName();
+ $directives = $this->parseDirectives(true);
+ $types = $this->parseUnionMemberTypes();
+
+ return new UnionTypeDefinitionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'types' => $types,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<NamedTypeNode>
+ */
+ private function parseUnionMemberTypes(): NodeList
+ {
+ $types = [];
+ if ($this->skip(Token::EQUALS)) {
+ // Optional leading pipe
+ $this->skip(Token::PIPE);
+ do {
+ $types[] = $this->parseNamedType();
+ } while ($this->skip(Token::PIPE));
+ }
+
+ return new NodeList($types);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseEnumTypeDefinition(): EnumTypeDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $this->expectKeyword('enum');
+ $name = $this->parseName();
+ $directives = $this->parseDirectives(true);
+ $values = $this->parseEnumValuesDefinition();
+
+ return new EnumTypeDefinitionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'values' => $values,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<EnumValueDefinitionNode>
+ */
+ private function parseEnumValuesDefinition(): NodeList
+ {
+ return $this->peek(Token::BRACE_L)
+ ? $this->many(
+ Token::BRACE_L,
+ fn (): EnumValueDefinitionNode => $this->parseEnumValueDefinition(),
+ Token::BRACE_R
+ )
+ : new NodeList([]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseEnumValueDefinition(): EnumValueDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $name = $this->parseName();
+ $directives = $this->parseDirectives(true);
+
+ return new EnumValueDefinitionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseInputObjectTypeDefinition(): InputObjectTypeDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $this->expectKeyword('input');
+ $name = $this->parseName();
+ $directives = $this->parseDirectives(true);
+ $fields = $this->parseInputFieldsDefinition();
+
+ return new InputObjectTypeDefinitionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'fields' => $fields,
+ 'loc' => $this->loc($start),
+ 'description' => $description,
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<InputValueDefinitionNode>
+ */
+ private function parseInputFieldsDefinition(): NodeList
+ {
+ return $this->peek(Token::BRACE_L)
+ ? $this->many(
+ Token::BRACE_L,
+ fn (): InputValueDefinitionNode => $this->parseInputValueDefinition(),
+ Token::BRACE_R
+ )
+ : new NodeList([]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return TypeSystemExtensionNode&Node
+ */
+ private function parseTypeSystemExtension(): TypeSystemExtensionNode
+ {
+ $keywordToken = $this->lexer->lookahead();
+
+ if ($keywordToken->kind === Token::NAME) {
+ switch ($keywordToken->value) {
+ case 'schema':
+ return $this->parseSchemaTypeExtension();
+
+ case 'scalar':
+ return $this->parseScalarTypeExtension();
+
+ case 'type':
+ return $this->parseObjectTypeExtension();
+
+ case 'interface':
+ return $this->parseInterfaceTypeExtension();
+
+ case 'union':
+ return $this->parseUnionTypeExtension();
+
+ case 'enum':
+ return $this->parseEnumTypeExtension();
+
+ case 'input':
+ return $this->parseInputObjectTypeExtension();
+ }
+ }
+
+ throw $this->unexpected($keywordToken);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseSchemaTypeExtension(): SchemaExtensionNode
+ {
+ $start = $this->lexer->token;
+ $this->expectKeyword('extend');
+ $this->expectKeyword('schema');
+ $directives = $this->parseDirectives(true);
+
+ $operationTypes = $this->peek(Token::BRACE_L)
+ ? $this->many(
+ Token::BRACE_L,
+ fn (): OperationTypeDefinitionNode => $this->parseOperationTypeDefinition(),
+ Token::BRACE_R
+ )
+ : new NodeList([]);
+ if (count($directives) === 0 && count($operationTypes) === 0) {
+ $this->unexpected();
+ }
+
+ return new SchemaExtensionNode([
+ 'directives' => $directives,
+ 'operationTypes' => $operationTypes,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseScalarTypeExtension(): ScalarTypeExtensionNode
+ {
+ $start = $this->lexer->token;
+ $this->expectKeyword('extend');
+ $this->expectKeyword('scalar');
+ $name = $this->parseName();
+ $directives = $this->parseDirectives(true);
+ if (count($directives) === 0) {
+ throw $this->unexpected();
+ }
+
+ return new ScalarTypeExtensionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseObjectTypeExtension(): ObjectTypeExtensionNode
+ {
+ $start = $this->lexer->token;
+ $this->expectKeyword('extend');
+ $this->expectKeyword('type');
+ $name = $this->parseName();
+ $interfaces = $this->parseImplementsInterfaces();
+ $directives = $this->parseDirectives(true);
+ $fields = $this->parseFieldsDefinition();
+
+ if (
+ count($interfaces) === 0
+ && count($directives) === 0
+ && count($fields) === 0
+ ) {
+ throw $this->unexpected();
+ }
+
+ return new ObjectTypeExtensionNode([
+ 'name' => $name,
+ 'interfaces' => $interfaces,
+ 'directives' => $directives,
+ 'fields' => $fields,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseInterfaceTypeExtension(): InterfaceTypeExtensionNode
+ {
+ $start = $this->lexer->token;
+ $this->expectKeyword('extend');
+ $this->expectKeyword('interface');
+ $name = $this->parseName();
+ $interfaces = $this->parseImplementsInterfaces();
+ $directives = $this->parseDirectives(true);
+ $fields = $this->parseFieldsDefinition();
+ if (
+ count($interfaces) === 0
+ && count($directives) === 0
+ && count($fields) === 0
+ ) {
+ throw $this->unexpected();
+ }
+
+ return new InterfaceTypeExtensionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'interfaces' => $interfaces,
+ 'fields' => $fields,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * UnionTypeExtension :
+ * - extend union Name Directives[Const]? UnionMemberTypes
+ * - extend union Name Directives[Const].
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseUnionTypeExtension(): UnionTypeExtensionNode
+ {
+ $start = $this->lexer->token;
+ $this->expectKeyword('extend');
+ $this->expectKeyword('union');
+ $name = $this->parseName();
+ $directives = $this->parseDirectives(true);
+ $types = $this->parseUnionMemberTypes();
+ if (count($directives) === 0 && count($types) === 0) {
+ throw $this->unexpected();
+ }
+
+ return new UnionTypeExtensionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'types' => $types,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseEnumTypeExtension(): EnumTypeExtensionNode
+ {
+ $start = $this->lexer->token;
+ $this->expectKeyword('extend');
+ $this->expectKeyword('enum');
+ $name = $this->parseName();
+ $directives = $this->parseDirectives(true);
+ $values = $this->parseEnumValuesDefinition();
+ if (
+ count($directives) === 0
+ && count($values) === 0
+ ) {
+ throw $this->unexpected();
+ }
+
+ return new EnumTypeExtensionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'values' => $values,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseInputObjectTypeExtension(): InputObjectTypeExtensionNode
+ {
+ $start = $this->lexer->token;
+ $this->expectKeyword('extend');
+ $this->expectKeyword('input');
+ $name = $this->parseName();
+ $directives = $this->parseDirectives(true);
+ $fields = $this->parseInputFieldsDefinition();
+ if (
+ count($directives) === 0
+ && count($fields) === 0
+ ) {
+ throw $this->unexpected();
+ }
+
+ return new InputObjectTypeExtensionNode([
+ 'name' => $name,
+ 'directives' => $directives,
+ 'fields' => $fields,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * DirectiveDefinition :
+ * - Description? directive @ Name ArgumentsDefinition? `repeatable`? on DirectiveLocations.
+ *
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseDirectiveDefinition(): DirectiveDefinitionNode
+ {
+ $start = $this->lexer->token;
+ $description = $this->parseDescription();
+ $this->expectKeyword('directive');
+ $this->expect(Token::AT);
+ $name = $this->parseName();
+ $args = $this->parseArgumentsDefinition();
+ $repeatable = $this->expectOptionalKeyword('repeatable');
+ $this->expectKeyword('on');
+ $locations = $this->parseDirectiveLocations();
+
+ return new DirectiveDefinitionNode([
+ 'name' => $name,
+ 'description' => $description,
+ 'arguments' => $args,
+ 'repeatable' => $repeatable,
+ 'locations' => $locations,
+ 'loc' => $this->loc($start),
+ ]);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ *
+ * @return NodeList<NameNode>
+ */
+ private function parseDirectiveLocations(): NodeList
+ {
+ // Optional leading pipe
+ $this->skip(Token::PIPE);
+ $locations = [];
+ do {
+ $locations[] = $this->parseDirectiveLocation();
+ } while ($this->skip(Token::PIPE));
+
+ return new NodeList($locations);
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws SyntaxError
+ */
+ private function parseDirectiveLocation(): NameNode
+ {
+ $start = $this->lexer->token;
+ $name = $this->parseName();
+ if (DirectiveLocation::has($name->value)) {
+ return $name;
+ }
+
+ throw $this->unexpected($start);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Printer.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Printer.php
new file mode 100644
index 00000000000..fe1b912a173
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/Printer.php
@@ -0,0 +1,525 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
+
+/**
+ * Prints AST to string. Capable of printing Automattic\WooCommerce\Vendor\GraphQL queries and Type definition language.
+ * Useful for pretty-printing queries or printing back AST for logging, documentation, etc.
+ *
+ * Usage example:
+ *
+ * ```php
+ * $query = 'query myQuery {someField}';
+ * $ast = Automattic\WooCommerce\Vendor\GraphQL\Language\Parser::parse($query);
+ * $printed = Automattic\WooCommerce\Vendor\GraphQL\Language\Printer::doPrint($ast);
+ * ```
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\PrinterTest
+ */
+class Printer
+{
+ /**
+ * Converts the AST of a Automattic\WooCommerce\Vendor\GraphQL node to a string.
+ *
+ * Handles both executable definitions and schema definitions.
+ *
+ * @throws \JsonException
+ *
+ * @api
+ */
+ public static function doPrint(Node $ast): string
+ {
+ return static::p($ast);
+ }
+
+ /** @throws \JsonException */
+ protected static function p(?Node $node): string
+ {
+ if ($node === null) {
+ return '';
+ }
+
+ switch (true) {
+ case $node instanceof ArgumentNode:
+ case $node instanceof ObjectFieldNode:
+ return static::p($node->name) . ': ' . static::p($node->value);
+
+ case $node instanceof BooleanValueNode:
+ return $node->value
+ ? 'true'
+ : 'false';
+
+ case $node instanceof DirectiveDefinitionNode:
+ $argStrings = [];
+ foreach ($node->arguments as $arg) {
+ $argStrings[] = static::p($arg);
+ }
+
+ $noIndent = true;
+ foreach ($argStrings as $argString) {
+ if (strpos($argString, "\n") !== false) {
+ $noIndent = false;
+ break;
+ }
+ }
+
+ return static::addDescription($node->description, 'directive @'
+ . static::p($node->name)
+ . ($noIndent
+ ? static::wrap('(', static::join($argStrings, ', '), ')')
+ : static::wrap("(\n", static::indent(static::join($argStrings, "\n")), "\n"))
+ . ($node->repeatable
+ ? ' repeatable'
+ : '')
+ . ' on ' . static::printList($node->locations, ' | '));
+
+ case $node instanceof DirectiveNode:
+ return '@' . static::p($node->name) . static::wrap('(', static::printList($node->arguments, ', '), ')');
+
+ case $node instanceof DocumentNode:
+ return static::printList($node->definitions, "\n\n") . "\n";
+
+ case $node instanceof EnumTypeDefinitionNode:
+ return static::addDescription($node->description, static::join(
+ [
+ 'enum',
+ static::p($node->name),
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->values),
+ ],
+ ' '
+ ));
+
+ case $node instanceof EnumTypeExtensionNode:
+ return static::join(
+ [
+ 'extend enum',
+ static::p($node->name),
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->values),
+ ],
+ ' '
+ );
+
+ case $node instanceof EnumValueDefinitionNode:
+ return static::addDescription(
+ $node->description,
+ static::join([static::p($node->name), static::printList($node->directives, ' ')], ' ')
+ );
+
+ case $node instanceof EnumValueNode:
+ case $node instanceof FloatValueNode:
+ case $node instanceof IntValueNode:
+ case $node instanceof NameNode:
+ return $node->value;
+
+ case $node instanceof FieldDefinitionNode:
+ $argStrings = [];
+ foreach ($node->arguments as $item) {
+ $argStrings[] = static::p($item);
+ }
+
+ $noIndent = true;
+ foreach ($argStrings as $argString) {
+ if (strpos($argString, "\n") !== false) {
+ $noIndent = false;
+ break;
+ }
+ }
+
+ return static::addDescription(
+ $node->description,
+ static::p($node->name)
+ . ($noIndent
+ ? static::wrap('(', static::join($argStrings, ', '), ')')
+ : static::wrap("(\n", static::indent(static::join($argStrings, "\n")), "\n)"))
+ . ': ' . static::p($node->type)
+ . static::wrap(' ', static::printList($node->directives, ' '))
+ );
+
+ case $node instanceof FieldNode:
+ $prefix = static::wrap('', $node->alias->value ?? null, ': ') . static::p($node->name);
+
+ $argsLine = $prefix . static::wrap(
+ '(',
+ static::printList($node->arguments, ', '),
+ ')'
+ );
+ if (strlen($argsLine) > 80) {
+ $argsLine = $prefix . static::wrap(
+ "(\n",
+ static::indent(
+ static::printList($node->arguments, "\n")
+ ),
+ "\n)"
+ );
+ }
+
+ return static::join(
+ [
+ $argsLine,
+ static::printList($node->directives, ' '),
+ static::p($node->selectionSet),
+ ],
+ ' '
+ );
+
+ case $node instanceof FragmentDefinitionNode:
+ // Note: fragment variable definitions are experimental and may be changed or removed in the future.
+ return 'fragment ' . static::p($node->name)
+ . static::wrap(
+ '(',
+ static::printList($node->variableDefinitions ?? new NodeList([]), ', '),
+ ')'
+ )
+ . ' on ' . static::p($node->typeCondition->name) . ' '
+ . static::wrap(
+ '',
+ static::printList($node->directives, ' '),
+ ' '
+ )
+ . static::p($node->selectionSet);
+
+ case $node instanceof FragmentSpreadNode:
+ return '...'
+ . static::p($node->name)
+ . static::wrap(' ', static::printList($node->directives, ' '));
+
+ case $node instanceof InlineFragmentNode:
+ return static::join(
+ [
+ '...',
+ static::wrap('on ', static::p($node->typeCondition->name ?? null)),
+ static::printList($node->directives, ' '),
+ static::p($node->selectionSet),
+ ],
+ ' '
+ );
+
+ case $node instanceof InputObjectTypeDefinitionNode:
+ return static::addDescription($node->description, static::join(
+ [
+ 'input',
+ static::p($node->name),
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->fields),
+ ],
+ ' '
+ ));
+
+ case $node instanceof InputObjectTypeExtensionNode:
+ return static::join(
+ [
+ 'extend input',
+ static::p($node->name),
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->fields),
+ ],
+ ' '
+ );
+
+ case $node instanceof InputValueDefinitionNode:
+ return static::addDescription($node->description, static::join(
+ [
+ static::p($node->name) . ': ' . static::p($node->type),
+ static::wrap('= ', static::p($node->defaultValue)),
+ static::printList($node->directives, ' '),
+ ],
+ ' '
+ ));
+
+ case $node instanceof InterfaceTypeDefinitionNode:
+ return static::addDescription($node->description, static::join(
+ [
+ 'interface',
+ static::p($node->name),
+ static::wrap('implements ', static::printList($node->interfaces, ' & ')),
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->fields),
+ ],
+ ' '
+ ));
+
+ case $node instanceof InterfaceTypeExtensionNode:
+ return static::join(
+ [
+ 'extend interface',
+ static::p($node->name),
+ static::wrap('implements ', static::printList($node->interfaces, ' & ')),
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->fields),
+ ],
+ ' '
+ );
+
+ case $node instanceof ListTypeNode:
+ return '[' . static::p($node->type) . ']';
+
+ case $node instanceof ListValueNode:
+ return '[' . static::printList($node->values, ', ') . ']';
+
+ case $node instanceof NamedTypeNode:
+ return static::p($node->name);
+
+ case $node instanceof NonNullTypeNode:
+ return static::p($node->type) . '!';
+
+ case $node instanceof NullValueNode:
+ return 'null';
+
+ case $node instanceof ObjectTypeDefinitionNode:
+ return static::addDescription($node->description, static::join(
+ [
+ 'type',
+ static::p($node->name),
+ static::wrap('implements ', static::printList($node->interfaces, ' & ')),
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->fields),
+ ],
+ ' '
+ ));
+
+ case $node instanceof ObjectTypeExtensionNode:
+ return static::join(
+ [
+ 'extend type',
+ static::p($node->name),
+ static::wrap('implements ', static::printList($node->interfaces, ' & ')),
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->fields),
+ ],
+ ' '
+ );
+
+ case $node instanceof ObjectValueNode:
+ return '{ '
+ . static::printList($node->fields, ', ')
+ . ' }';
+
+ case $node instanceof OperationDefinitionNode:
+ $op = $node->operation;
+ $name = static::p($node->name);
+ $varDefs = static::wrap('(', static::printList($node->variableDefinitions, ', '), ')');
+ $directives = static::printList($node->directives, ' ');
+ $selectionSet = static::p($node->selectionSet);
+
+ // Anonymous queries with no directives or variable definitions can use
+ // the query short form.
+ return $name === '' && $directives === '' && $varDefs === '' && $op === 'query'
+ ? $selectionSet
+ : static::join([$op, static::join([$name, $varDefs]), $directives, $selectionSet], ' ');
+
+ case $node instanceof OperationTypeDefinitionNode:
+ return $node->operation . ': ' . static::p($node->type);
+
+ case $node instanceof ScalarTypeDefinitionNode:
+ return static::addDescription($node->description, static::join([
+ 'scalar',
+ static::p($node->name),
+ static::printList($node->directives, ' '),
+ ], ' '));
+
+ case $node instanceof ScalarTypeExtensionNode:
+ return static::join(
+ [
+ 'extend scalar',
+ static::p($node->name),
+ static::printList($node->directives, ' '),
+ ],
+ ' '
+ );
+
+ case $node instanceof SchemaDefinitionNode:
+ return static::addDescription($node->description, static::join(
+ [
+ 'schema',
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->operationTypes),
+ ],
+ ' '
+ ));
+
+ case $node instanceof SchemaExtensionNode:
+ return static::join(
+ [
+ 'extend schema',
+ static::printList($node->directives, ' '),
+ static::printListBlock($node->operationTypes),
+ ],
+ ' '
+ );
+
+ case $node instanceof SelectionSetNode:
+ return static::printListBlock($node->selections);
+
+ case $node instanceof StringValueNode:
+ if ($node->block) {
+ return BlockString::print($node->value);
+ }
+
+ return json_encode($node->value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
+
+ case $node instanceof UnionTypeDefinitionNode:
+ $typesStr = static::printList($node->types, ' | ');
+
+ return static::addDescription($node->description, static::join(
+ [
+ 'union',
+ static::p($node->name),
+ static::printList($node->directives, ' '),
+ $typesStr !== ''
+ ? "= {$typesStr}"
+ : '',
+ ],
+ ' '
+ ));
+
+ case $node instanceof UnionTypeExtensionNode:
+ $typesStr = static::printList($node->types, ' | ');
+
+ return static::join(
+ [
+ 'extend union',
+ static::p($node->name),
+ static::printList($node->directives, ' '),
+ $typesStr !== ''
+ ? "= {$typesStr}"
+ : '',
+ ],
+ ' '
+ );
+
+ case $node instanceof VariableDefinitionNode:
+ return '$' . static::p($node->variable->name)
+ . ': '
+ . static::p($node->type)
+ . static::wrap(' = ', static::p($node->defaultValue))
+ . static::wrap(' ', static::printList($node->directives, ' '));
+
+ case $node instanceof VariableNode:
+ return '$' . static::p($node->name);
+ }
+
+ return '';
+ }
+
+ /**
+ * @template TNode of Node
+ *
+ * @param NodeList<TNode> $list
+ *
+ * @throws \JsonException
+ */
+ protected static function printList(NodeList $list, string $separator = ''): string
+ {
+ $parts = [];
+ foreach ($list as $item) {
+ $parts[] = static::p($item);
+ }
+
+ return static::join($parts, $separator);
+ }
+
+ /**
+ * Print each item on its own line, wrapped in an indented "{ }" block.
+ *
+ * @template TNode of Node
+ *
+ * @param NodeList<TNode> $list
+ *
+ * @throws \JsonException
+ */
+ protected static function printListBlock(NodeList $list): string
+ {
+ if (count($list) === 0) {
+ return '';
+ }
+
+ $parts = [];
+ foreach ($list as $item) {
+ $parts[] = static::p($item);
+ }
+
+ return "{\n" . static::indent(static::join($parts, "\n")) . "\n}";
+ }
+
+ /** @throws \JsonException */
+ protected static function addDescription(?StringValueNode $description, string $body): string
+ {
+ return static::join([static::p($description), $body], "\n");
+ }
+
+ /**
+ * If maybeString is not null or empty, then wrap with start and end, otherwise
+ * print an empty string.
+ */
+ protected static function wrap(string $start, ?string $maybeString, string $end = ''): string
+ {
+ if ($maybeString === null || $maybeString === '') {
+ return '';
+ }
+
+ return $start . $maybeString . $end;
+ }
+
+ protected static function indent(string $string): string
+ {
+ if ($string === '') {
+ return '';
+ }
+
+ return ' ' . str_replace("\n", "\n ", $string);
+ }
+
+ /** @param array<string|null> $parts */
+ protected static function join(array $parts, string $separator = ''): string
+ {
+ return implode($separator, array_filter($parts, static fn (?string $part) => $part !== '' && $part !== null));
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Source.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Source.php
new file mode 100644
index 00000000000..fd9a662090d
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/Source.php
@@ -0,0 +1,52 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+class Source
+{
+ public string $body;
+
+ public int $length;
+
+ public string $name;
+
+ public SourceLocation $locationOffset;
+
+ /**
+ * A representation of source input to GraphQL.
+ *
+ * `name` and `locationOffset` are optional. They are useful for clients who
+ * store Automattic\WooCommerce\Vendor\GraphQL documents in source files; for example, if the Automattic\WooCommerce\Vendor\GraphQL input
+ * starts at line 40 in a file named Foo.graphql, it might be useful for name to
+ * be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
+ * line and column in locationOffset are 1-indexed
+ */
+ public function __construct(string $body, ?string $name = null, ?SourceLocation $location = null)
+ {
+ $this->body = $body;
+ $this->length = mb_strlen($body, 'UTF-8');
+ $this->name = $name === '' || $name === null
+ ? 'Automattic\WooCommerce\Vendor\GraphQL request'
+ : $name;
+ $this->locationOffset = $location ?? new SourceLocation(1, 1);
+ }
+
+ public function getLocation(int $position): SourceLocation
+ {
+ $line = 1;
+ $column = $position + 1;
+
+ $utfChars = json_decode('"\u2028\u2029"');
+ $lineRegexp = '/\r\n|[\n\r' . $utfChars . ']/su';
+ $matches = [];
+ preg_match_all($lineRegexp, mb_substr($this->body, 0, $position, 'UTF-8'), $matches, \PREG_OFFSET_CAPTURE);
+
+ foreach ($matches[0] as $match) {
+ ++$line;
+
+ $column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8'));
+ }
+
+ return new SourceLocation($line, $column);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/SourceLocation.php b/plugins/woocommerce/lib/packages/GraphQL/Language/SourceLocation.php
new file mode 100644
index 00000000000..40bc74dc762
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/SourceLocation.php
@@ -0,0 +1,38 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+class SourceLocation implements \JsonSerializable
+{
+ public int $line;
+
+ public int $column;
+
+ public function __construct(int $line, int $col)
+ {
+ $this->line = $line;
+ $this->column = $col;
+ }
+
+ /** @return array{line: int, column: int} */
+ public function toArray(): array
+ {
+ return [
+ 'line' => $this->line,
+ 'column' => $this->column,
+ ];
+ }
+
+ /** @return array{line: int, column: int} */
+ public function toSerializableArray(): array
+ {
+ return $this->toArray();
+ }
+
+ /** @return array{line: int, column: int} */
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize(): array
+ {
+ return $this->toArray();
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Token.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Token.php
new file mode 100644
index 00000000000..1f6edb0ecac
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/Token.php
@@ -0,0 +1,99 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+/**
+ * Represents a range of characters represented by a lexical token
+ * within a Source.
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\TokenTest
+ */
+class Token
+{
+ // Each kind of token.
+ public const SOF = '<SOF>';
+ public const EOF = '<EOF>';
+ public const BANG = '!';
+ public const DOLLAR = '$';
+ public const AMP = '&';
+ public const PAREN_L = '(';
+ public const PAREN_R = ')';
+ public const SPREAD = '...';
+ public const COLON = ':';
+ public const EQUALS = '=';
+ public const AT = '@';
+ public const BRACKET_L = '[';
+ public const BRACKET_R = ']';
+ public const BRACE_L = '{';
+ public const PIPE = '|';
+ public const BRACE_R = '}';
+ public const NAME = 'Name';
+ public const INT = 'Int';
+ public const FLOAT = 'Float';
+ public const STRING = 'String';
+ public const BLOCK_STRING = 'BlockString';
+ public const COMMENT = 'Comment';
+
+ /** The kind of Token (see one of constants above). */
+ public string $kind;
+
+ /** The character offset at which this Node begins. */
+ public int $start;
+
+ /** The character offset at which this Node ends. */
+ public int $end;
+
+ /** The 1-indexed line number on which this Token appears. */
+ public int $line;
+
+ /** The 1-indexed column number at which this Token begins. */
+ public int $column;
+
+ public ?string $value;
+
+ /**
+ * Tokens exist as nodes in a double-linked-list amongst all tokens
+ * including ignored tokens. <SOF> is always the first node and <EOF>
+ * the last.
+ */
+ public ?Token $prev;
+
+ public ?Token $next = null;
+
+ public function __construct(string $kind, int $start, int $end, int $line, int $column, ?Token $previous = null, ?string $value = null)
+ {
+ $this->kind = $kind;
+ $this->start = $start;
+ $this->end = $end;
+ $this->line = $line;
+ $this->column = $column;
+ $this->prev = $previous;
+ $this->value = $value;
+ }
+
+ public function getDescription(): string
+ {
+ return $this->kind
+ . ($this->value === null
+ ? ''
+ : " \"{$this->value}\"");
+ }
+
+ /**
+ * @return array{
+ * kind: string,
+ * value: string|null,
+ * line: int,
+ * column: int,
+ * }
+ */
+ public function toArray(): array
+ {
+ return [
+ 'kind' => $this->kind,
+ 'value' => $this->value,
+ 'line' => $this->line,
+ 'column' => $this->column,
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/Visitor.php b/plugins/woocommerce/lib/packages/GraphQL/Language/Visitor.php
new file mode 100644
index 00000000000..2dbf722b5e5
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/Visitor.php
@@ -0,0 +1,526 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * Utility for efficient AST traversal and modification.
+ *
+ * `visit()` will walk through an AST using a depth first traversal, calling
+ * the visitor's enter function at each node in the traversal, and calling the
+ * leave function after visiting that node and all of its child nodes.
+ *
+ * By returning different values from the `enter` and `leave` functions, the behavior of the visitor can be altered.
+ *
+ * - no return (`void`) or return `null`: no action
+ * - `Visitor::skipNode()`: skips over the subtree at the current node of the AST
+ * - `Visitor::stop()`: stop the Visitor completely
+ * - `Visitor::removeNode()`: remove the current node
+ * - return any other value: replace this node with the returned value
+ *
+ * When using `visit()` to edit an AST, the original AST will not be modified, and
+ * a new version of the AST with the changes applied will be returned from the
+ * visit function.
+ *
+ * ```php
+ * $editedAST = Visitor::visit($ast, [
+ * 'enter' => function (Node $node, $key, $parent, array $path, array $ancestors) {
+ * // ...
+ * },
+ * 'leave' => function (Node $node, $key, $parent, array $path, array $ancestors) {
+ * // ...
+ * }
+ * ]);
+ * ```
+ *
+ * Alternatively to providing `enter` and `leave` functions, a visitor can
+ * instead provide functions named the same as the [kinds of AST nodes](class-reference.md#graphqllanguageastnodekind),
+ * or enter/leave visitors at a named key, leading to four permutations of
+ * visitor API:
+ *
+ * 1. Named visitors triggered when entering a node a specific kind.
+ *
+ * ```php
+ * Visitor::visit($ast, [
+ * NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) {
+ * // enter the "ObjectTypeDefinition" node
+ * }
+ * ]);
+ * ```
+ *
+ * 2. Named visitors that trigger upon entering and leaving a node of
+ * a specific kind.
+ *
+ * ```php
+ * Visitor::visit($ast, [
+ * NodeKind::OBJECT_TYPE_DEFINITION => [
+ * 'enter' => function (ObjectTypeDefinitionNode $node) {
+ * // enter the "ObjectTypeDefinition" node
+ * },
+ * 'leave' => function (ObjectTypeDefinitionNode $node) {
+ * // leave the "ObjectTypeDefinition" node
+ * }
+ * ]
+ * ]);
+ * ```
+ *
+ * 3. Generic visitors that trigger upon entering and leaving any node.
+ *
+ * ```php
+ * Visitor::visit($ast, [
+ * 'enter' => function (Node $node) {
+ * // enter any node
+ * },
+ * 'leave' => function (Node $node) {
+ * // leave any node
+ * }
+ * ]);
+ * ```
+ *
+ * 4. Parallel visitors for entering and leaving nodes of a specific kind.
+ *
+ * ```php
+ * Visitor::visit($ast, [
+ * 'enter' => [
+ * NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) {
+ * // enter the "ObjectTypeDefinition" node
+ * }
+ * ],
+ * 'leave' => [
+ * NodeKind::OBJECT_TYPE_DEFINITION => function (ObjectTypeDefinitionNode $node) {
+ * // leave the "ObjectTypeDefinition" node
+ * }
+ * ]
+ * ]);
+ * ```
+ *
+ * @phpstan-type NodeVisitor callable(Node): (VisitorOperation|Node|NodeList<Node>|null|false|void)
+ * @phpstan-type VisitorArray array<string, NodeVisitor>|array<string, array<string, NodeVisitor>>
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Language\VisitorTest
+ */
+class Visitor
+{
+ public const VISITOR_KEYS = [
+ NodeKind::NAME => [],
+ NodeKind::DOCUMENT => ['definitions'],
+ NodeKind::OPERATION_DEFINITION => ['name', 'variableDefinitions', 'directives', 'selectionSet'],
+ NodeKind::VARIABLE_DEFINITION => ['variable', 'type', 'defaultValue', 'directives'],
+ NodeKind::VARIABLE => ['name'],
+ NodeKind::SELECTION_SET => ['selections'],
+ NodeKind::FIELD => ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
+ NodeKind::ARGUMENT => ['name', 'value'],
+ NodeKind::FRAGMENT_SPREAD => ['name', 'directives'],
+ NodeKind::INLINE_FRAGMENT => ['typeCondition', 'directives', 'selectionSet'],
+ NodeKind::FRAGMENT_DEFINITION => [
+ 'name',
+ // Note: fragment variable definitions are experimental and may be changed
+ // or removed in the future.
+ 'variableDefinitions',
+ 'typeCondition',
+ 'directives',
+ 'selectionSet',
+ ],
+
+ NodeKind::INT => [],
+ NodeKind::FLOAT => [],
+ NodeKind::STRING => [],
+ NodeKind::BOOLEAN => [],
+ NodeKind::NULL => [],
+ NodeKind::ENUM => [],
+ NodeKind::LST => ['values'],
+ NodeKind::OBJECT => ['fields'],
+ NodeKind::OBJECT_FIELD => ['name', 'value'],
+ NodeKind::DIRECTIVE => ['name', 'arguments'],
+ NodeKind::NAMED_TYPE => ['name'],
+ NodeKind::LIST_TYPE => ['type'],
+ NodeKind::NON_NULL_TYPE => ['type'],
+
+ NodeKind::SCHEMA_DEFINITION => ['description', 'directives', 'operationTypes'],
+ NodeKind::OPERATION_TYPE_DEFINITION => ['type'],
+ NodeKind::SCALAR_TYPE_DEFINITION => ['description', 'name', 'directives'],
+ NodeKind::OBJECT_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'],
+ NodeKind::FIELD_DEFINITION => ['description', 'name', 'arguments', 'type', 'directives'],
+ NodeKind::INPUT_VALUE_DEFINITION => ['description', 'name', 'type', 'defaultValue', 'directives'],
+ NodeKind::INTERFACE_TYPE_DEFINITION => ['description', 'name', 'interfaces', 'directives', 'fields'],
+ NodeKind::UNION_TYPE_DEFINITION => ['description', 'name', 'directives', 'types'],
+ NodeKind::ENUM_TYPE_DEFINITION => ['description', 'name', 'directives', 'values'],
+ NodeKind::ENUM_VALUE_DEFINITION => ['description', 'name', 'directives'],
+ NodeKind::INPUT_OBJECT_TYPE_DEFINITION => ['description', 'name', 'directives', 'fields'],
+
+ NodeKind::SCALAR_TYPE_EXTENSION => ['name', 'directives'],
+ NodeKind::OBJECT_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'],
+ NodeKind::INTERFACE_TYPE_EXTENSION => ['name', 'interfaces', 'directives', 'fields'],
+ NodeKind::UNION_TYPE_EXTENSION => ['name', 'directives', 'types'],
+ NodeKind::ENUM_TYPE_EXTENSION => ['name', 'directives', 'values'],
+ NodeKind::INPUT_OBJECT_TYPE_EXTENSION => ['name', 'directives', 'fields'],
+
+ NodeKind::DIRECTIVE_DEFINITION => ['description', 'name', 'arguments', 'locations'],
+
+ NodeKind::SCHEMA_EXTENSION => ['directives', 'operationTypes'],
+ ];
+
+ /**
+ * Visit the AST (see class description for details).
+ *
+ * @param NodeList<Node>|Node $root
+ * @param VisitorArray $visitor
+ * @param array<string, mixed>|null $keyMap
+ *
+ * @throws \Exception
+ *
+ * @return mixed
+ *
+ * @api
+ */
+ public static function visit(object $root, array $visitor, ?array $keyMap = null)
+ {
+ $visitorKeys = $keyMap ?? self::VISITOR_KEYS;
+
+ /**
+ * @var list<array{
+ * inList: bool,
+ * index: int,
+ * keys: Node|NodeList|mixed,
+ * edits: array<int, array{mixed, mixed}>,
+ * }> $stack */
+ $stack = [];
+ $inList = $root instanceof NodeList;
+ $keys = [$root];
+ $index = -1;
+ $edits = [];
+ $parent = null;
+ $path = [];
+ $ancestors = [];
+
+ do {
+ ++$index;
+ $isLeaving = $index === count($keys);
+ $key = null;
+ $node = null;
+ $isEdited = $isLeaving && $edits !== [];
+
+ if ($isLeaving) {
+ $key = $ancestors === []
+ ? null
+ : $path[count($path) - 1];
+ $node = $parent;
+ $parent = array_pop($ancestors);
+ if ($isEdited) {
+ if ($node instanceof Node || $node instanceof NodeList) {
+ $node = $node->cloneDeep();
+ }
+
+ $editOffset = 0;
+ foreach ($edits as [$editKey, $editValue]) {
+ if ($inList) {
+ $editKey -= $editOffset;
+ }
+
+ if ($inList && $editValue === null) {
+ assert($node instanceof NodeList, 'Follows from $inList');
+ $node->splice($editKey, 1);
+ ++$editOffset;
+ } elseif ($node instanceof NodeList) {
+ if ($editValue instanceof NodeList) {
+ $node->splice($editKey, 1, $editValue);
+ $editOffset -= count($editValue) - 1;
+ } elseif ($editValue instanceof Node) {
+ $node[$editKey] = $editValue;
+ } else {
+ $notNodeOrNodeList = Utils::printSafe($editValue);
+ throw new \Exception("Can only add Node or NodeList to NodeList, got: {$notNodeOrNodeList}.");
+ }
+ } else {
+ $node->{$editKey} = $editValue;
+ }
+ }
+ }
+ // @phpstan-ignore-next-line the stack is guaranteed to be non-empty at this point
+ [
+ 'index' => $index,
+ 'keys' => $keys,
+ 'edits' => $edits,
+ 'inList' => $inList,
+ ] = array_pop($stack);
+ } elseif ($parent === null) {
+ $node = $root;
+ } else {
+ $key = $inList
+ ? $index
+ : $keys[$index];
+ $node = $parent instanceof NodeList
+ ? $parent[$key]
+ : $parent->{$key};
+ if ($node === null) {
+ continue;
+ }
+ $path[] = $key;
+ }
+
+ $result = null;
+ if (! $node instanceof NodeList) {
+ if (! $node instanceof Node) {
+ $notNode = Utils::printSafe($node);
+ throw new \Exception("Invalid AST Node: {$notNode}.");
+ }
+
+ $visitFn = self::extractVisitFn($visitor, $node->kind, $isLeaving);
+
+ if ($visitFn !== null) {
+ $result = $visitFn($node, $key, $parent, $path, $ancestors);
+
+ if ($result !== null) {
+ if ($result instanceof VisitorStop) {
+ break;
+ }
+
+ if ($result instanceof VisitorSkipNode) {
+ if (! $isLeaving) {
+ array_pop($path);
+ }
+ continue;
+ }
+
+ $editValue = $result instanceof VisitorRemoveNode
+ ? null
+ : $result;
+
+ $edits[] = [$key, $editValue];
+ if (! $isLeaving) {
+ if (! $editValue instanceof Node) {
+ array_pop($path);
+ continue;
+ }
+
+ $node = $editValue;
+ }
+ }
+ }
+ }
+
+ if ($result === null && $isEdited) {
+ $edits[] = [$key, $node];
+ }
+
+ if ($isLeaving) {
+ array_pop($path);
+ } else {
+ $stack[] = [
+ 'inList' => $inList,
+ 'index' => $index,
+ 'keys' => $keys,
+ 'edits' => $edits,
+ ];
+ $inList = $node instanceof NodeList;
+
+ $keys = ($inList ? $node : $visitorKeys[$node->kind]) ?? [];
+ $index = -1;
+ $edits = [];
+ if ($parent !== null) {
+ $ancestors[] = $parent;
+ }
+
+ $parent = $node;
+ }
+ } while ($stack !== []);
+
+ return $edits === []
+ ? $root
+ : $edits[0][1];
+ }
+
+ /**
+ * Returns marker for stopping.
+ *
+ * @api
+ */
+ public static function stop(): VisitorStop
+ {
+ static $stop;
+
+ return $stop ??= new VisitorStop();
+ }
+
+ /**
+ * Returns marker for skipping the subtree at the current node.
+ *
+ * @api
+ */
+ public static function skipNode(): VisitorSkipNode
+ {
+ static $skipNode;
+
+ return $skipNode ??= new VisitorSkipNode();
+ }
+
+ /**
+ * Returns marker for removing the current node.
+ *
+ * @api
+ */
+ public static function removeNode(): VisitorRemoveNode
+ {
+ static $removeNode;
+
+ return $removeNode ??= new VisitorRemoveNode();
+ }
+
+ /**
+ * Combines the given visitors to run in parallel.
+ *
+ * @phpstan-param array<int, VisitorArray> $visitors
+ *
+ * @return VisitorArray
+ */
+ public static function visitInParallel(array $visitors): array
+ {
+ $visitorsCount = count($visitors);
+ $skipping = new \SplFixedArray($visitorsCount);
+
+ return [
+ 'enter' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
+ for ($i = 0; $i < $visitorsCount; ++$i) {
+ if ($skipping[$i] !== null) {
+ continue;
+ }
+
+ $fn = self::extractVisitFn(
+ $visitors[$i],
+ $node->kind,
+ false
+ );
+
+ if ($fn === null) {
+ continue;
+ }
+
+ $result = $fn(...func_get_args());
+
+ if ($result === null) {
+ continue;
+ }
+ if ($result instanceof VisitorSkipNode) {
+ $skipping[$i] = $node;
+ } elseif ($result instanceof VisitorStop) {
+ $skipping[$i] = $result;
+ } else {
+ return $result;
+ }
+ }
+
+ return null;
+ },
+ 'leave' => static function (Node $node) use ($visitors, $skipping, $visitorsCount) {
+ for ($i = 0; $i < $visitorsCount; ++$i) {
+ if ($skipping[$i] === null) {
+ $fn = self::extractVisitFn(
+ $visitors[$i],
+ $node->kind,
+ true
+ );
+
+ if ($fn !== null) {
+ $result = $fn(...func_get_args());
+
+ if ($result === null) {
+ continue;
+ }
+ if ($result instanceof VisitorStop) {
+ $skipping[$i] = $result;
+ } elseif ($result instanceof VisitorRemoveNode) {
+ return $result;
+ } else {
+ return $result;
+ }
+ }
+ } elseif ($skipping[$i] === $node) {
+ $skipping[$i] = null;
+ }
+ }
+
+ return null;
+ },
+ ];
+ }
+
+ /**
+ * Creates a new visitor that updates TypeInfo and delegates to the given visitor.
+ *
+ * @phpstan-param VisitorArray $visitor
+ *
+ * @phpstan-return VisitorArray
+ */
+ public static function visitWithTypeInfo(TypeInfo $typeInfo, array $visitor): array
+ {
+ return [
+ 'enter' => static function (Node $node) use ($typeInfo, $visitor) {
+ $typeInfo->enter($node);
+ $fn = self::extractVisitFn($visitor, $node->kind, false);
+
+ if ($fn === null) {
+ return null;
+ }
+
+ $result = $fn(...func_get_args());
+ if ($result === null) {
+ return null;
+ }
+
+ $typeInfo->leave($node);
+ if ($result instanceof Node) {
+ $typeInfo->enter($result);
+ }
+
+ return $result;
+ },
+ 'leave' => static function (Node $node) use ($typeInfo, $visitor) {
+ $fn = self::extractVisitFn($visitor, $node->kind, true);
+ $result = $fn !== null
+ ? $fn(...func_get_args())
+ : null;
+
+ $typeInfo->leave($node);
+
+ return $result;
+ },
+ ];
+ }
+
+ /**
+ * @phpstan-param VisitorArray $visitor
+ *
+ * @return (callable(Node $node, string|int|null $key, Node|NodeList<Node>|null $parent, array<int, int|string> $path, array<int, Node|NodeList<Node>> $ancestors): (VisitorOperation|Node|null))|(callable(Node): (VisitorOperation|Node|NodeList<Node>|void|false|null))|null
+ */
+ protected static function extractVisitFn(array $visitor, string $kind, bool $isLeaving): ?callable
+ {
+ $kindVisitor = $visitor[$kind] ?? null;
+
+ if ($kindVisitor !== null) {
+ if (is_array($kindVisitor)) {
+ return $isLeaving
+ ? $kindVisitor['leave'] ?? null
+ : $kindVisitor['enter'] ?? null;
+ }
+
+ if (! $isLeaving) {
+ return $kindVisitor;
+ }
+ }
+
+ $specificVisitor = $isLeaving
+ ? $visitor['leave'] ?? null
+ : $visitor['enter'] ?? null;
+
+ if ($specificVisitor !== null && is_array($specificVisitor)) {
+ return $specificVisitor[$kind] ?? null;
+ }
+
+ return $specificVisitor;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorOperation.php b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorOperation.php
new file mode 100644
index 00000000000..8bf68bae570
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorOperation.php
@@ -0,0 +1,5 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+abstract class VisitorOperation {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorRemoveNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorRemoveNode.php
new file mode 100644
index 00000000000..cb129188eae
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorRemoveNode.php
@@ -0,0 +1,5 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+final class VisitorRemoveNode extends VisitorOperation {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorSkipNode.php b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorSkipNode.php
new file mode 100644
index 00000000000..134cdd1d7c5
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorSkipNode.php
@@ -0,0 +1,5 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+final class VisitorSkipNode extends VisitorOperation {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorStop.php b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorStop.php
new file mode 100644
index 00000000000..4457992ed6f
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Language/VisitorStop.php
@@ -0,0 +1,5 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Language;
+
+final class VisitorStop extends VisitorOperation {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/BatchedQueriesAreNotSupported.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/BatchedQueriesAreNotSupported.php
new file mode 100644
index 00000000000..fd74f68f153
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/BatchedQueriesAreNotSupported.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class BatchedQueriesAreNotSupported extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseJsonBody.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseJsonBody.php
new file mode 100644
index 00000000000..50726a3bcc0
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseJsonBody.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class CannotParseJsonBody extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseVariables.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseVariables.php
new file mode 100644
index 00000000000..98684060fdd
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotParseVariables.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class CannotParseVariables extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotReadBody.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotReadBody.php
new file mode 100644
index 00000000000..c434a557b46
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/CannotReadBody.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class CannotReadBody extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/FailedToDetermineOperationType.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/FailedToDetermineOperationType.php
new file mode 100644
index 00000000000..135c144cb8e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/FailedToDetermineOperationType.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class FailedToDetermineOperationType extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/GetMethodSupportsOnlyQueryOperation.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/GetMethodSupportsOnlyQueryOperation.php
new file mode 100644
index 00000000000..1143b988246
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/GetMethodSupportsOnlyQueryOperation.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class GetMethodSupportsOnlyQueryOperation extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/HttpMethodNotSupported.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/HttpMethodNotSupported.php
new file mode 100644
index 00000000000..72dec119415
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/HttpMethodNotSupported.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class HttpMethodNotSupported extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidOperationParameter.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidOperationParameter.php
new file mode 100644
index 00000000000..5f31366704a
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidOperationParameter.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class InvalidOperationParameter extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryIdParameter.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryIdParameter.php
new file mode 100644
index 00000000000..826a8807c6e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryIdParameter.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class InvalidQueryIdParameter extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryParameter.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryParameter.php
new file mode 100644
index 00000000000..ba3e9a8a1ae
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/InvalidQueryParameter.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class InvalidQueryParameter extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingContentTypeHeader.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingContentTypeHeader.php
new file mode 100644
index 00000000000..939a811b2cc
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingContentTypeHeader.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class MissingContentTypeHeader extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingQueryOrQueryIdParameter.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingQueryOrQueryIdParameter.php
new file mode 100644
index 00000000000..68dea3fd3ea
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/MissingQueryOrQueryIdParameter.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class MissingQueryOrQueryIdParameter extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/PersistedQueriesAreNotSupported.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/PersistedQueriesAreNotSupported.php
new file mode 100644
index 00000000000..eefc8aa7c1c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/PersistedQueriesAreNotSupported.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class PersistedQueriesAreNotSupported extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/UnexpectedContentType.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/UnexpectedContentType.php
new file mode 100644
index 00000000000..934335c9485
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Exception/UnexpectedContentType.php
@@ -0,0 +1,7 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server\Exception;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Server\RequestError;
+
+class UnexpectedContentType extends RequestError {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/Helper.php b/plugins/woocommerce/lib/packages/GraphQL/Server/Helper.php
new file mode 100644
index 00000000000..c1eae7a18a5
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/Helper.php
@@ -0,0 +1,573 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\FormattedError;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Executor;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Adapter\SyncPromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\GraphQL;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\BatchedQueriesAreNotSupported;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\CannotParseJsonBody;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\CannotParseVariables;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\CannotReadBody;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\FailedToDetermineOperationType;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\GetMethodSupportsOnlyQueryOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\HttpMethodNotSupported;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\InvalidOperationParameter;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\InvalidQueryIdParameter;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\InvalidQueryParameter;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\MissingContentTypeHeader;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\MissingQueryOrQueryIdParameter;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\PersistedQueriesAreNotSupported;
+use Automattic\WooCommerce\Vendor\GraphQL\Server\Exception\UnexpectedContentType;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\ServerRequestInterface;
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * Contains functionality that could be re-used by various server implementations.
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Server\HelperTest
+ */
+class Helper
+{
+ /**
+ * Parses HTTP request using PHP globals and returns Automattic\WooCommerce\Vendor\GraphQL OperationParams
+ * contained in this request. For batched requests it returns an array of OperationParams.
+ *
+ * This function does not check validity of these params
+ * (validation is performed separately in validateOperationParams() method).
+ *
+ * If $readRawBodyFn argument is not provided - will attempt to read raw request body
+ * from `php://input` stream.
+ *
+ * Internally it normalizes input to $method, $bodyParams and $queryParams and
+ * calls `parseRequestParams()` to produce actual return value.
+ *
+ * For PSR-7 request parsing use `parsePsrRequest()` instead.
+ *
+ * @throws RequestError
+ *
+ * @return OperationParams|array<int, OperationParams>
+ *
+ * @api
+ */
+ public function parseHttpRequest(?callable $readRawBodyFn = null)
+ {
+ $method = $_SERVER['REQUEST_METHOD'] ?? null;
+ $bodyParams = [];
+ $urlParams = $_GET;
+
+ if ($method === 'POST') {
+ $contentType = $_SERVER['CONTENT_TYPE'] ?? null;
+
+ if ($contentType === null) {
+ throw new MissingContentTypeHeader('Missing "Content-Type" header');
+ }
+
+ if (stripos($contentType, 'application/graphql') !== false) {
+ $rawBody = $readRawBodyFn === null
+ ? $this->readRawBody()
+ : $readRawBodyFn();
+ $bodyParams = ['query' => $rawBody];
+ } elseif (stripos($contentType, 'application/json') !== false) {
+ $rawBody = $readRawBodyFn === null
+ ? $this->readRawBody()
+ : $readRawBodyFn();
+ $bodyParams = $this->decodeJson($rawBody);
+
+ $this->assertJsonObjectOrArray($bodyParams);
+ } elseif (stripos($contentType, 'application/x-www-form-urlencoded') !== false) {
+ $bodyParams = $_POST;
+ } elseif (stripos($contentType, 'multipart/form-data') !== false) {
+ $bodyParams = $_POST;
+ } else {
+ throw new UnexpectedContentType('Unexpected content type: ' . Utils::printSafeJson($contentType));
+ }
+ }
+
+ return $this->parseRequestParams($method, $bodyParams, $urlParams);
+ }
+
+ /**
+ * Parses normalized request params and returns instance of OperationParams
+ * or array of OperationParams in case of batch operation.
+ *
+ * Returned value is a suitable input for `executeOperation` or `executeBatch` (if array)
+ *
+ * @param array<mixed> $bodyParams
+ * @param array<mixed> $queryParams
+ *
+ * @throws RequestError
+ *
+ * @return OperationParams|array<int, OperationParams>
+ *
+ * @api
+ */
+ public function parseRequestParams(string $method, array $bodyParams, array $queryParams)
+ {
+ if ($method === 'GET') {
+ return OperationParams::create($queryParams, true);
+ }
+
+ if ($method === 'POST') {
+ if (isset($bodyParams[0])) {
+ $operations = [];
+ foreach ($bodyParams as $entry) {
+ $operations[] = OperationParams::create($entry);
+ }
+
+ return $operations;
+ }
+
+ return OperationParams::create($bodyParams);
+ }
+
+ throw new HttpMethodNotSupported("HTTP Method \"{$method}\" is not supported");
+ }
+
+ /**
+ * Checks validity of OperationParams extracted from HTTP request and returns an array of errors
+ * if params are invalid (or empty array when params are valid).
+ *
+ * @return list<RequestError>
+ *
+ * @api
+ */
+ public function validateOperationParams(OperationParams $params): array
+ {
+ $errors = [];
+ $query = $params->query ?? '';
+ $queryId = $params->queryId ?? '';
+ if ($query === '' && $queryId === '') {
+ $errors[] = new MissingQueryOrQueryIdParameter('Automattic\WooCommerce\Vendor\GraphQL Request must include at least one of those two parameters: "query" or "queryId"');
+ }
+
+ if (! is_string($query)) {
+ $errors[] = new InvalidQueryParameter(
+ 'Automattic\WooCommerce\Vendor\GraphQL Request parameter "query" must be string, but got '
+ . Utils::printSafeJson($params->query)
+ );
+ }
+
+ if (! is_string($queryId)) {
+ $errors[] = new InvalidQueryIdParameter(
+ 'Automattic\WooCommerce\Vendor\GraphQL Request parameter "queryId" must be string, but got '
+ . Utils::printSafeJson($params->queryId)
+ );
+ }
+
+ if ($params->operation !== null && ! is_string($params->operation)) {
+ $errors[] = new InvalidOperationParameter(
+ 'Automattic\WooCommerce\Vendor\GraphQL Request parameter "operation" must be string, but got '
+ . Utils::printSafeJson($params->operation)
+ );
+ }
+
+ if ($params->variables !== null && (! is_array($params->variables) || isset($params->variables[0]))) {
+ $errors[] = new CannotParseVariables(
+ 'Automattic\WooCommerce\Vendor\GraphQL Request parameter "variables" must be object or JSON string parsed to object, but got '
+ . Utils::printSafeJson($params->originalInput['variables'])
+ );
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Executes Automattic\WooCommerce\Vendor\GraphQL operation with given server configuration and returns execution result
+ * (or promise when promise adapter is different from SyncPromiseAdapter).
+ *
+ * @throws \Exception
+ * @throws InvariantViolation
+ *
+ * @return ExecutionResult|Promise
+ *
+ * @api
+ */
+ public function executeOperation(ServerConfig $config, OperationParams $op)
+ {
+ $promiseAdapter = $config->getPromiseAdapter() ?? Executor::getDefaultPromiseAdapter();
+ $result = $this->promiseToExecuteOperation($promiseAdapter, $config, $op);
+
+ if ($promiseAdapter instanceof SyncPromiseAdapter) {
+ $result = $promiseAdapter->wait($result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Executes batched Automattic\WooCommerce\Vendor\GraphQL operations with shared promise queue
+ * (thus, effectively batching deferreds|promises of all queries at once).
+ *
+ * @param array<OperationParams> $operations
+ *
+ * @throws \Exception
+ * @throws InvariantViolation
+ *
+ * @return array<int, ExecutionResult>|Promise
+ *
+ * @api
+ */
+ public function executeBatch(ServerConfig $config, array $operations)
+ {
+ $promiseAdapter = $config->getPromiseAdapter() ?? Executor::getDefaultPromiseAdapter();
+
+ $result = [];
+ foreach ($operations as $operation) {
+ $result[] = $this->promiseToExecuteOperation($promiseAdapter, $config, $operation, true);
+ }
+
+ $result = $promiseAdapter->all($result);
+
+ // Wait for promised results when using sync promises
+ if ($promiseAdapter instanceof SyncPromiseAdapter) {
+ $result = $promiseAdapter->wait($result);
+ }
+
+ return $result;
+ }
+
+ /**
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ protected function promiseToExecuteOperation(
+ PromiseAdapter $promiseAdapter,
+ ServerConfig $config,
+ OperationParams $op,
+ bool $isBatch = false
+ ): Promise {
+ try {
+ if ($config->getSchema() === null) {
+ throw new InvariantViolation('Schema is required for the server');
+ }
+
+ if ($isBatch && ! $config->getQueryBatching()) {
+ throw new BatchedQueriesAreNotSupported('Batched queries are not supported by this server');
+ }
+
+ $errors = $this->validateOperationParams($op);
+
+ if ($errors !== []) {
+ $locatedErrors = array_map(
+ [Error::class, 'createLocatedError'],
+ $errors
+ );
+
+ return $promiseAdapter->createFulfilled(
+ new ExecutionResult(null, $locatedErrors)
+ );
+ }
+
+ $doc = $op->queryId !== null
+ ? $this->loadPersistedQuery($config, $op)
+ : $op->query;
+
+ if (! $doc instanceof DocumentNode) {
+ $doc = Parser::parse($doc);
+ }
+
+ $operationAST = AST::getOperationAST($doc, $op->operation);
+
+ if ($operationAST === null) {
+ throw new FailedToDetermineOperationType('Failed to determine operation type');
+ }
+
+ $operationType = $operationAST->operation;
+ if ($operationType !== 'query' && $op->readOnly) {
+ throw new GetMethodSupportsOnlyQueryOperation('GET supports only query operation');
+ }
+
+ $result = GraphQL::promiseToExecute(
+ $promiseAdapter,
+ $config->getSchema(),
+ $doc,
+ $this->resolveRootValue($config, $op, $doc, $operationType),
+ $this->resolveContextValue($config, $op, $doc, $operationType),
+ $op->variables,
+ $op->operation,
+ $config->getFieldResolver(),
+ $this->resolveValidationRules($config, $op, $doc, $operationType)
+ );
+ } catch (RequestError $e) {
+ $result = $promiseAdapter->createFulfilled(
+ new ExecutionResult(null, [Error::createLocatedError($e)])
+ );
+ } catch (Error $e) {
+ $result = $promiseAdapter->createFulfilled(
+ new ExecutionResult(null, [$e])
+ );
+ }
+
+ $applyErrorHandling = static function (ExecutionResult $result) use ($config): ExecutionResult {
+ $result->setErrorsHandler($config->getErrorsHandler());
+
+ $result->setErrorFormatter(
+ FormattedError::prepareFormatter(
+ $config->getErrorFormatter(),
+ $config->getDebugFlag()
+ )
+ );
+
+ return $result;
+ };
+
+ return $result->then($applyErrorHandling);
+ }
+
+ /**
+ * @throws RequestError
+ *
+ * @return mixed
+ */
+ protected function loadPersistedQuery(ServerConfig $config, OperationParams $operationParams)
+ {
+ $loader = $config->getPersistedQueryLoader();
+
+ if ($loader === null) {
+ throw new PersistedQueriesAreNotSupported('Persisted queries are not supported by this server');
+ }
+
+ $source = $loader($operationParams->queryId, $operationParams);
+
+ // @phpstan-ignore-next-line Necessary until PHP gains function types
+ if (! is_string($source) && ! $source instanceof DocumentNode) {
+ $documentNode = DocumentNode::class;
+ $safeSource = Utils::printSafe($source);
+ throw new InvariantViolation("Persisted query loader must return query string or instance of {$documentNode} but got: {$safeSource}");
+ }
+
+ return $source;
+ }
+
+ /** @return array<mixed>|null */
+ protected function resolveValidationRules(
+ ServerConfig $config,
+ OperationParams $params,
+ DocumentNode $doc,
+ string $operationType
+ ): ?array {
+ $validationRules = $config->getValidationRules();
+
+ if (is_callable($validationRules)) {
+ $validationRules = $validationRules($params, $doc, $operationType);
+ }
+
+ // @phpstan-ignore-next-line unless PHP gains function types, we have to check this at runtime
+ if ($validationRules !== null && ! is_array($validationRules)) {
+ $safeValidationRules = Utils::printSafe($validationRules);
+ throw new InvariantViolation("Expecting validation rules to be array or callable returning array, but got: {$safeValidationRules}");
+ }
+
+ return $validationRules;
+ }
+
+ /** @return mixed */
+ protected function resolveRootValue(
+ ServerConfig $config,
+ OperationParams $params,
+ DocumentNode $doc,
+ string $operationType
+ ) {
+ $rootValue = $config->getRootValue();
+
+ if (is_callable($rootValue)) {
+ $rootValue = $rootValue($params, $doc, $operationType);
+ }
+
+ return $rootValue;
+ }
+
+ /** @return mixed user defined */
+ protected function resolveContextValue(
+ ServerConfig $config,
+ OperationParams $params,
+ DocumentNode $doc,
+ string $operationType
+ ) {
+ $context = $config->getContext();
+
+ if (is_callable($context)) {
+ $context = $context($params, $doc, $operationType);
+ }
+
+ return $context;
+ }
+
+ /**
+ * Send response using standard PHP `header()` and `echo`.
+ *
+ * @param Promise|ExecutionResult|array<ExecutionResult> $result
+ *
+ * @api
+ *
+ * @throws \JsonException
+ */
+ public function sendResponse($result): void
+ {
+ if ($result instanceof Promise) {
+ $result->then(function ($actualResult): void {
+ $this->emitResponse($actualResult);
+ });
+ } else {
+ $this->emitResponse($result);
+ }
+ }
+
+ /**
+ * @param array<mixed>|\JsonSerializable $jsonSerializable
+ *
+ * @throws \JsonException
+ */
+ protected function emitResponse($jsonSerializable): void
+ {
+ header('Content-Type: application/json;charset=utf-8');
+ echo json_encode($jsonSerializable, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
+ }
+
+ /** @throws RequestError */
+ protected function readRawBody(): string
+ {
+ $body = file_get_contents('php://input');
+ if ($body === false) {
+ throw new CannotReadBody('Cannot not read body.');
+ }
+
+ return $body;
+ }
+
+ /**
+ * Converts PSR-7 request to OperationParams or an array thereof.
+ *
+ * @throws RequestError
+ *
+ * @return OperationParams|array<OperationParams>
+ *
+ * @api
+ */
+ public function parsePsrRequest(RequestInterface $request)
+ {
+ if ($request->getMethod() === 'GET') {
+ $bodyParams = [];
+ } else {
+ $contentType = $request->getHeader('content-type');
+
+ if (! isset($contentType[0])) {
+ throw new MissingContentTypeHeader('Missing "Content-Type" header');
+ }
+
+ if (stripos($contentType[0], 'application/graphql') !== false) {
+ $bodyParams = ['query' => (string) $request->getBody()];
+ } elseif (stripos($contentType[0], 'application/json') !== false) {
+ $bodyParams = $request instanceof ServerRequestInterface
+ ? $request->getParsedBody()
+ : $this->decodeJson((string) $request->getBody());
+
+ $this->assertJsonObjectOrArray($bodyParams);
+ } else {
+ if ($request instanceof ServerRequestInterface) {
+ $bodyParams = $request->getParsedBody();
+ }
+
+ $bodyParams ??= $this->decodeContent((string) $request->getBody());
+ }
+ }
+
+ parse_str(html_entity_decode($request->getUri()->getQuery()), $queryParams);
+
+ return $this->parseRequestParams(
+ $request->getMethod(),
+ $bodyParams,
+ $queryParams
+ );
+ }
+
+ /**
+ * @throws RequestError
+ *
+ * @return mixed
+ */
+ protected function decodeJson(string $rawBody)
+ {
+ $bodyParams = json_decode($rawBody, true);
+
+ if (json_last_error() !== \JSON_ERROR_NONE) {
+ throw new CannotParseJsonBody('Expected JSON object or array for "application/json" request, but failed to parse because: ' . json_last_error_msg());
+ }
+
+ return $bodyParams;
+ }
+
+ /** @return array<mixed> */
+ protected function decodeContent(string $rawBody): array
+ {
+ parse_str($rawBody, $bodyParams);
+
+ return $bodyParams;
+ }
+
+ /**
+ * @param mixed $bodyParams
+ *
+ * @throws RequestError
+ */
+ protected function assertJsonObjectOrArray($bodyParams): void
+ {
+ if (! is_array($bodyParams)) {
+ $notArray = Utils::printSafeJson($bodyParams);
+ throw new CannotParseJsonBody("Expected JSON object or array for \"application/json\" request, got: {$notArray}");
+ }
+ }
+
+ /**
+ * Converts query execution result to PSR-7 response.
+ *
+ * @param Promise|ExecutionResult|array<ExecutionResult> $result
+ *
+ * @throws \InvalidArgumentException
+ * @throws \JsonException
+ * @throws \RuntimeException
+ *
+ * @return Promise|ResponseInterface
+ *
+ * @api
+ */
+ public function toPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream)
+ {
+ if ($result instanceof Promise) {
+ return $result->then(
+ fn ($actualResult): ResponseInterface => $this->doConvertToPsrResponse($actualResult, $response, $writableBodyStream)
+ );
+ }
+
+ return $this->doConvertToPsrResponse($result, $response, $writableBodyStream);
+ }
+
+ /**
+ * @param ExecutionResult|array<ExecutionResult> $result
+ *
+ * @throws \InvalidArgumentException
+ * @throws \JsonException
+ * @throws \RuntimeException
+ */
+ protected function doConvertToPsrResponse($result, ResponseInterface $response, StreamInterface $writableBodyStream): ResponseInterface
+ {
+ $writableBodyStream->write(json_encode($result, JSON_THROW_ON_ERROR));
+
+ return $response
+ ->withHeader('Content-Type', 'application/json')
+ ->withBody($writableBodyStream);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/OperationParams.php b/plugins/woocommerce/lib/packages/GraphQL/Server/OperationParams.php
new file mode 100644
index 00000000000..d16685f9dd3
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/OperationParams.php
@@ -0,0 +1,148 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
+
+/**
+ * Structure representing parsed HTTP parameters for Automattic\WooCommerce\Vendor\GraphQL operation.
+ *
+ * The properties in this class are not strictly typed, as this class
+ * is only meant to serve as an intermediary representation which is
+ * not yet validated.
+ */
+class OperationParams
+{
+ /**
+ * Id of the query (when using persisted queries).
+ *
+ * Valid aliases (case-insensitive):
+ * - id
+ * - queryId
+ * - documentId
+ *
+ * @api
+ *
+ * @var mixed should be string|null
+ */
+ public $queryId;
+
+ /**
+ * A document containing Automattic\WooCommerce\Vendor\GraphQL operations and fragments to execute.
+ *
+ * @api
+ *
+ * @var mixed should be string|null
+ */
+ public $query;
+
+ /**
+ * The name of the operation in the document to execute.
+ *
+ * @api
+ *
+ * @var mixed should be string|null
+ */
+ public $operation;
+
+ /**
+ * Values for any variables defined by the operation.
+ *
+ * @api
+ *
+ * @var mixed should be array<string, mixed>
+ */
+ public $variables;
+
+ /**
+ * Reserved for implementors to extend the protocol however they see fit.
+ *
+ * @api
+ *
+ * @var mixed should be array<string, mixed>
+ */
+ public $extensions;
+
+ /**
+ * Executed in read-only context (e.g. via HTTP GET request)?
+ *
+ * @api
+ */
+ public bool $readOnly;
+
+ /**
+ * The raw params used to construct this instance.
+ *
+ * @api
+ *
+ * @var array<string, mixed>
+ */
+ public array $originalInput;
+
+ /**
+ * Creates an instance from given array.
+ *
+ * @param array<string, mixed> $params
+ *
+ * @api
+ */
+ public static function create(array $params, bool $readonly = false): OperationParams
+ {
+ $instance = new static();
+
+ $params = array_change_key_case($params, \CASE_LOWER);
+ $instance->originalInput = $params;
+
+ $params += [
+ 'query' => null,
+ 'queryid' => null,
+ 'documentid' => null, // alias to queryid
+ 'id' => null, // alias to queryid
+ 'operationname' => null,
+ 'variables' => null,
+ 'extensions' => null,
+ ];
+
+ foreach ($params as &$value) {
+ if ($value === '') {
+ $value = null;
+ }
+ }
+
+ $instance->query = $params['query'];
+ $instance->queryId = $params['queryid'] ?? $params['documentid'] ?? $params['id'];
+ $instance->operation = $params['operationname'];
+ $instance->variables = static::decodeIfJSON($params['variables']);
+ $instance->extensions = static::decodeIfJSON($params['extensions']);
+ $instance->readOnly = $readonly;
+
+ // Apollo server/client compatibility
+ if (
+ isset($instance->extensions['persistedQuery']['sha256Hash'])
+ && $instance->queryId === null
+ ) {
+ $instance->queryId = $instance->extensions['persistedQuery']['sha256Hash'];
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Decodes the value if it is JSON, otherwise returns it unchanged.
+ *
+ * @param mixed $value
+ *
+ * @return mixed
+ */
+ protected static function decodeIfJSON($value)
+ {
+ if (! is_string($value)) {
+ return $value;
+ }
+
+ $decoded = json_decode($value, true);
+ if (json_last_error() === \JSON_ERROR_NONE) {
+ return $decoded;
+ }
+
+ return $value;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/RequestError.php b/plugins/woocommerce/lib/packages/GraphQL/Server/RequestError.php
new file mode 100644
index 00000000000..829115cb022
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/RequestError.php
@@ -0,0 +1,13 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\ClientAware;
+
+class RequestError extends \Exception implements ClientAware
+{
+ public function isClientSafe(): bool
+ {
+ return true;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/ServerConfig.php b/plugins/woocommerce/lib/packages/GraphQL/Server/ServerConfig.php
new file mode 100644
index 00000000000..9317e61c25a
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/ServerConfig.php
@@ -0,0 +1,347 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\PromiseAdapter;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValidationRule;
+
+/**
+ * Server configuration class.
+ * Could be passed directly to server constructor. List of options accepted by **create** method is
+ * [described in docs](executing-queries.md#server-configuration-options).
+ *
+ * Usage example:
+ *
+ * $config = Automattic\WooCommerce\Vendor\GraphQL\Server\ServerConfig::create()
+ * ->setSchema($mySchema)
+ * ->setContext($myContext);
+ *
+ * $server = new Automattic\WooCommerce\Vendor\GraphQL\Server\StandardServer($config);
+ *
+ * @see ExecutionResult
+ *
+ * @phpstan-type PersistedQueryLoader callable(string $queryId, OperationParams $operation): (string|DocumentNode)
+ * @phpstan-type RootValueResolver callable(OperationParams $operation, DocumentNode $doc, string $operationType): mixed
+ * @phpstan-type ValidationRulesOption array<ValidationRule>|null|callable(OperationParams $operation, DocumentNode $doc, string $operationType): array<ValidationRule>
+ *
+ * @phpstan-import-type ErrorsHandler from ExecutionResult
+ * @phpstan-import-type ErrorFormatter from ExecutionResult
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Server\ServerConfigTest
+ */
+class ServerConfig
+{
+ /**
+ * Converts an array of options to instance of ServerConfig
+ * (or just returns empty config when array is not passed).
+ *
+ * @param array<string, mixed> $config
+ *
+ * @api
+ *
+ * @throws InvariantViolation
+ */
+ public static function create(array $config = []): self
+ {
+ $instance = new static();
+ foreach ($config as $key => $value) {
+ switch ($key) {
+ case 'schema':
+ $instance->setSchema($value);
+ break;
+ case 'rootValue':
+ $instance->setRootValue($value);
+ break;
+ case 'context':
+ $instance->setContext($value);
+ break;
+ case 'fieldResolver':
+ $instance->setFieldResolver($value);
+ break;
+ case 'validationRules':
+ $instance->setValidationRules($value);
+ break;
+ case 'queryBatching':
+ $instance->setQueryBatching($value);
+ break;
+ case 'debugFlag':
+ $instance->setDebugFlag($value);
+ break;
+ case 'persistedQueryLoader':
+ $instance->setPersistedQueryLoader($value);
+ break;
+ case 'errorFormatter':
+ $instance->setErrorFormatter($value);
+ break;
+ case 'errorsHandler':
+ $instance->setErrorsHandler($value);
+ break;
+ case 'promiseAdapter':
+ $instance->setPromiseAdapter($value);
+ break;
+ default:
+ throw new InvariantViolation("Unknown server config option: {$key}");
+ }
+ }
+
+ return $instance;
+ }
+
+ private ?Schema $schema = null;
+
+ /** @var mixed|callable(self, OperationParams, DocumentNode): mixed|null */
+ private $context;
+
+ /**
+ * @var mixed|callable
+ *
+ * @phpstan-var mixed|RootValueResolver
+ */
+ private $rootValue;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var ErrorFormatter|null
+ */
+ private $errorFormatter;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var ErrorsHandler|null
+ */
+ private $errorsHandler;
+
+ private int $debugFlag = DebugFlag::NONE;
+
+ private bool $queryBatching = false;
+
+ /**
+ * @var array<ValidationRule>|callable|null
+ *
+ * @phpstan-var ValidationRulesOption
+ */
+ private $validationRules;
+
+ /** @var callable|null */
+ private $fieldResolver;
+
+ private ?PromiseAdapter $promiseAdapter = null;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var PersistedQueryLoader|null
+ */
+ private $persistedQueryLoader;
+
+ /** @api */
+ public function setSchema(Schema $schema): self
+ {
+ $this->schema = $schema;
+
+ return $this;
+ }
+
+ /**
+ * @param mixed|callable $context
+ *
+ * @api
+ */
+ public function setContext($context): self
+ {
+ $this->context = $context;
+
+ return $this;
+ }
+
+ /**
+ * @param mixed|callable $rootValue
+ *
+ * @phpstan-param mixed|RootValueResolver $rootValue
+ *
+ * @api
+ */
+ public function setRootValue($rootValue): self
+ {
+ $this->rootValue = $rootValue;
+
+ return $this;
+ }
+
+ /**
+ * @phpstan-param ErrorFormatter $errorFormatter
+ *
+ * @api
+ */
+ public function setErrorFormatter(callable $errorFormatter): self
+ {
+ $this->errorFormatter = $errorFormatter;
+
+ return $this;
+ }
+
+ /**
+ * @phpstan-param ErrorsHandler $handler
+ *
+ * @api
+ */
+ public function setErrorsHandler(callable $handler): self
+ {
+ $this->errorsHandler = $handler;
+
+ return $this;
+ }
+
+ /**
+ * Set validation rules for this server.
+ *
+ * @param array<ValidationRule>|callable|null $validationRules
+ *
+ * @phpstan-param ValidationRulesOption $validationRules
+ *
+ * @api
+ */
+ public function setValidationRules($validationRules): self
+ {
+ // @phpstan-ignore-next-line necessary until we can use proper union types
+ if (! is_array($validationRules) && ! is_callable($validationRules) && $validationRules !== null) {
+ $invalidValidationRules = Utils::printSafe($validationRules);
+ throw new InvariantViolation("Server config expects array of validation rules or callable returning such array, but got {$invalidValidationRules}");
+ }
+
+ $this->validationRules = $validationRules;
+
+ return $this;
+ }
+
+ /** @api */
+ public function setFieldResolver(callable $fieldResolver): self
+ {
+ $this->fieldResolver = $fieldResolver;
+
+ return $this;
+ }
+
+ /**
+ * @phpstan-param PersistedQueryLoader|null $persistedQueryLoader
+ *
+ * @api
+ */
+ public function setPersistedQueryLoader(?callable $persistedQueryLoader): self
+ {
+ $this->persistedQueryLoader = $persistedQueryLoader;
+
+ return $this;
+ }
+
+ /**
+ * Set response debug flags.
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag class for a list of all available flags
+ *
+ * @api
+ */
+ public function setDebugFlag(int $debugFlag = DebugFlag::INCLUDE_DEBUG_MESSAGE): self
+ {
+ $this->debugFlag = $debugFlag;
+
+ return $this;
+ }
+
+ /**
+ * Allow batching queries (disabled by default).
+ *
+ * @api
+ */
+ public function setQueryBatching(bool $enableBatching): self
+ {
+ $this->queryBatching = $enableBatching;
+
+ return $this;
+ }
+
+ /** @api */
+ public function setPromiseAdapter(PromiseAdapter $promiseAdapter): self
+ {
+ $this->promiseAdapter = $promiseAdapter;
+
+ return $this;
+ }
+
+ /** @return mixed|callable */
+ public function getContext()
+ {
+ return $this->context;
+ }
+
+ /**
+ * @return mixed|callable
+ *
+ * @phpstan-return mixed|RootValueResolver
+ */
+ public function getRootValue()
+ {
+ return $this->rootValue;
+ }
+
+ public function getSchema(): ?Schema
+ {
+ return $this->schema;
+ }
+
+ /** @phpstan-return ErrorFormatter|null */
+ public function getErrorFormatter(): ?callable
+ {
+ return $this->errorFormatter;
+ }
+
+ /** @phpstan-return ErrorsHandler|null */
+ public function getErrorsHandler(): ?callable
+ {
+ return $this->errorsHandler;
+ }
+
+ public function getPromiseAdapter(): ?PromiseAdapter
+ {
+ return $this->promiseAdapter;
+ }
+
+ /**
+ * @return array<ValidationRule>|callable|null
+ *
+ * @phpstan-return ValidationRulesOption
+ */
+ public function getValidationRules()
+ {
+ return $this->validationRules;
+ }
+
+ public function getFieldResolver(): ?callable
+ {
+ return $this->fieldResolver;
+ }
+
+ /** @phpstan-return PersistedQueryLoader|null */
+ public function getPersistedQueryLoader(): ?callable
+ {
+ return $this->persistedQueryLoader;
+ }
+
+ public function getDebugFlag(): int
+ {
+ return $this->debugFlag;
+ }
+
+ public function getQueryBatching(): bool
+ {
+ return $this->queryBatching;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Server/StandardServer.php b/plugins/woocommerce/lib/packages/GraphQL/Server/StandardServer.php
new file mode 100644
index 00000000000..bbec3fbda8d
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Server/StandardServer.php
@@ -0,0 +1,168 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Server;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\ExecutionResult;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Promise\Promise;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Psr\Http\Message\RequestInterface;
+use Psr\Http\Message\ResponseInterface;
+use Psr\Http\Message\StreamInterface;
+
+/**
+ * Automattic\WooCommerce\Vendor\GraphQL server compatible with both: [express-graphql](https://github.com/graphql/express-graphql)
+ * and [Apollo Server](https://github.com/apollographql/graphql-server).
+ * Usage Example:.
+ *
+ * $server = new StandardServer([
+ * 'schema' => $mySchema
+ * ]);
+ * $server->handleRequest();
+ *
+ * Or using [ServerConfig](class-reference.md#graphqlserverserverconfig) instance:
+ *
+ * $config = Automattic\WooCommerce\Vendor\GraphQL\Server\ServerConfig::create()
+ * ->setSchema($mySchema)
+ * ->setContext($myContext);
+ *
+ * $server = new Automattic\WooCommerce\Vendor\GraphQL\Server\StandardServer($config);
+ * $server->handleRequest();
+ *
+ * See [dedicated section in docs](executing-queries.md#using-server) for details.
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Server\StandardServerTest
+ */
+class StandardServer
+{
+ protected ServerConfig $config;
+
+ protected Helper $helper;
+
+ /**
+ * @param ServerConfig|array<string, mixed> $config
+ *
+ * @api
+ *
+ * @throws InvariantViolation
+ */
+ public function __construct($config)
+ {
+ if (is_array($config)) {
+ $config = ServerConfig::create($config);
+ }
+
+ // @phpstan-ignore-next-line necessary until we can use proper union types
+ if (! $config instanceof ServerConfig) {
+ $safeConfig = Utils::printSafe($config);
+ throw new InvariantViolation("Expecting valid server config, but got {$safeConfig}");
+ }
+
+ $this->config = $config;
+ $this->helper = new Helper();
+ }
+
+ /**
+ * Parses HTTP request, executes and emits response (using standard PHP `header` function and `echo`).
+ *
+ * When $parsedBody is not set, it uses PHP globals to parse a request.
+ * It is possible to implement request parsing elsewhere (e.g. using framework Request instance)
+ * and then pass it to the server.
+ *
+ * See `executeRequest()` if you prefer to emit the response yourself
+ * (e.g. using the Response object of some framework).
+ *
+ * @param OperationParams|array<OperationParams> $parsedBody
+ *
+ * @api
+ *
+ * @throws \Exception
+ * @throws InvariantViolation
+ * @throws RequestError
+ */
+ public function handleRequest($parsedBody = null): void
+ {
+ $result = $this->executeRequest($parsedBody);
+ $this->helper->sendResponse($result);
+ }
+
+ /**
+ * Executes a Automattic\WooCommerce\Vendor\GraphQL operation and returns an execution result
+ * (or promise when promise adapter is different from SyncPromiseAdapter).
+ *
+ * When $parsedBody is not set, it uses PHP globals to parse a request.
+ * It is possible to implement request parsing elsewhere (e.g. using framework Request instance)
+ * and then pass it to the server.
+ *
+ * PSR-7 compatible method executePsrRequest() does exactly this.
+ *
+ * @param OperationParams|array<OperationParams> $parsedBody
+ *
+ * @throws \Exception
+ * @throws InvariantViolation
+ * @throws RequestError
+ *
+ * @return ExecutionResult|array<int, ExecutionResult>|Promise
+ *
+ * @api
+ */
+ public function executeRequest($parsedBody = null)
+ {
+ if ($parsedBody === null) {
+ $parsedBody = $this->helper->parseHttpRequest();
+ }
+
+ if (is_array($parsedBody)) {
+ return $this->helper->executeBatch($this->config, $parsedBody);
+ }
+
+ return $this->helper->executeOperation($this->config, $parsedBody);
+ }
+
+ /**
+ * Executes PSR-7 request and fulfills PSR-7 response.
+ *
+ * See `executePsrRequest()` if you prefer to create response yourself
+ * (e.g. using specific JsonResponse instance of some framework).
+ *
+ * @throws \Exception
+ * @throws \InvalidArgumentException
+ * @throws \JsonException
+ * @throws \RuntimeException
+ * @throws InvariantViolation
+ * @throws RequestError
+ *
+ * @return ResponseInterface|Promise
+ *
+ * @api
+ */
+ public function processPsrRequest(
+ RequestInterface $request,
+ ResponseInterface $response,
+ StreamInterface $writableBodyStream
+ ) {
+ $result = $this->executePsrRequest($request);
+
+ return $this->helper->toPsrResponse($result, $response, $writableBodyStream);
+ }
+
+ /**
+ * Executes Automattic\WooCommerce\Vendor\GraphQL operation and returns execution result
+ * (or promise when promise adapter is different from SyncPromiseAdapter).
+ *
+ * @throws \Exception
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws RequestError
+ *
+ * @return ExecutionResult|array<int, ExecutionResult>|Promise
+ *
+ * @api
+ */
+ public function executePsrRequest(RequestInterface $request)
+ {
+ $parsedBody = $this->helper->parsePsrRequest($request);
+
+ return $this->executeRequest($parsedBody);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/AbstractType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/AbstractType.php
new file mode 100644
index 00000000000..083a6e4d608
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/AbstractType.php
@@ -0,0 +1,39 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Deferred;
+
+/**
+ * @phpstan-type ResolveTypeReturn ObjectType|string|callable(): (ObjectType|string|null)|Deferred|null
+ * @phpstan-type ResolveType callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): ResolveTypeReturn
+ * @phpstan-type ResolveValue callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): mixed
+ */
+interface AbstractType
+{
+ /**
+ * Receives the original resolved value and transforms it if necessary.
+ *
+ * This will be called before `resolveType`.
+ *
+ * @param mixed $objectValue The resolved value for the object type
+ * @param mixed $context The context that was passed to GraphQL::execute()
+ *
+ * @return mixed The possibly transformed value
+ */
+ public function resolveValue($objectValue, $context, ResolveInfo $info);
+
+ /**
+ * Resolves the concrete ObjectType for the given value.
+ *
+ * This will be called after `resolveValue`.
+ *
+ * @param mixed $objectValue The resolved value for the object type
+ * @param mixed $context The context that was passed to GraphQL::execute()
+ *
+ * @return ObjectType|string|callable|Deferred|null
+ *
+ * @phpstan-return ResolveTypeReturn
+ */
+ public function resolveType($objectValue, $context, ResolveInfo $info);
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Argument.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Argument.php
new file mode 100644
index 00000000000..68a1ae9e0d7
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Argument.php
@@ -0,0 +1,134 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-type ArgumentType (Type&InputType)|callable(): (Type&InputType)
+ * @phpstan-type UnnamedArgumentConfig array{
+ * name?: string,
+ * type: ArgumentType,
+ * defaultValue?: mixed,
+ * description?: string|null,
+ * deprecationReason?: string|null,
+ * astNode?: InputValueDefinitionNode|null
+ * }
+ * @phpstan-type ArgumentConfig array{
+ * name: string,
+ * type: ArgumentType,
+ * defaultValue?: mixed,
+ * description?: string|null,
+ * deprecationReason?: string|null,
+ * astNode?: InputValueDefinitionNode|null
+ * }
+ * @phpstan-type ArgumentListConfig iterable<ArgumentConfig|ArgumentType>|iterable<UnnamedArgumentConfig>
+ */
+class Argument
+{
+ public string $name;
+
+ /** @var mixed */
+ public $defaultValue;
+
+ public ?string $description;
+
+ public ?string $deprecationReason;
+
+ /** @var Type&InputType */
+ private Type $type;
+
+ public ?InputValueDefinitionNode $astNode;
+
+ /** @phpstan-var ArgumentConfig */
+ public array $config;
+
+ /** @phpstan-param ArgumentConfig $config */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'];
+ $this->defaultValue = $config['defaultValue'] ?? null;
+ $this->description = $config['description'] ?? null;
+ $this->deprecationReason = $config['deprecationReason'] ?? null;
+ // Do nothing for type, it is lazy loaded in getType()
+ $this->astNode = $config['astNode'] ?? null;
+
+ $this->config = $config;
+ }
+
+ /**
+ * @phpstan-param ArgumentListConfig $config
+ *
+ * @return array<int, self>
+ */
+ public static function listFromConfig(iterable $config): array
+ {
+ $list = [];
+
+ foreach ($config as $name => $argConfig) {
+ if (! is_array($argConfig)) {
+ $argConfig = ['type' => $argConfig];
+ }
+
+ /** @phpstan-var ArgumentConfig $argConfigWithName */
+ $argConfigWithName = $argConfig + ['name' => $name];
+
+ $list[] = new self($argConfigWithName);
+ }
+
+ return $list;
+ }
+
+ /** @return Type&InputType */
+ public function getType(): Type
+ {
+ if (! isset($this->type)) {
+ $this->type = Schema::resolveType($this->config['type']);
+ }
+
+ return $this->type;
+ }
+
+ public function defaultValueExists(): bool
+ {
+ return array_key_exists('defaultValue', $this->config);
+ }
+
+ public function isRequired(): bool
+ {
+ return $this->getType() instanceof NonNull
+ && ! $this->defaultValueExists();
+ }
+
+ public function isDeprecated(): bool
+ {
+ return (bool) $this->deprecationReason;
+ }
+
+ /**
+ * @param Type&NamedType $parentType
+ *
+ * @throws InvariantViolation
+ */
+ public function assertValid(FieldDefinition $parentField, Type $parentType): void
+ {
+ $error = Utils::isValidNameError($this->name);
+ if ($error !== null) {
+ throw new InvariantViolation("{$parentType->name}.{$parentField->name}({$this->name}:) {$error->getMessage()}");
+ }
+
+ $type = Type::getNamedType($this->getType());
+
+ if (! $type instanceof InputType) {
+ $notInputType = Utils::printSafe($this->type);
+ throw new InvariantViolation("{$parentType->name}.{$parentField->name}({$this->name}): argument type must be Input Type but got: {$notInputType}");
+ }
+
+ if ($this->isRequired() && $this->isDeprecated()) {
+ throw new InvariantViolation("Required argument {$parentType->name}.{$parentField->name}({$this->name}:) cannot be deprecated.");
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/BooleanType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/BooleanType.php
new file mode 100644
index 00000000000..4f7e8b1ccd6
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/BooleanType.php
@@ -0,0 +1,52 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+class BooleanType extends ScalarType
+{
+ public string $name = Type::BOOLEAN;
+
+ public ?string $description = 'The `Boolean` scalar type represents `true` or `false`.';
+
+ /**
+ * Serialize the given value to a Boolean.
+ *
+ * The Automattic\WooCommerce\Vendor\GraphQL spec leaves this up to the implementations, so we just do what
+ * PHP does natively to make this intuitive for developers.
+ */
+ public function serialize($value): bool
+ {
+ return (bool) $value;
+ }
+
+ /** @throws Error */
+ public function parseValue($value): bool
+ {
+ if (is_bool($value)) {
+ return $value;
+ }
+
+ $notBoolean = Utils::printSafeJson($value);
+ throw new Error("Boolean cannot represent a non boolean value: {$notBoolean}");
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws Error
+ */
+ public function parseLiteral(Node $valueNode, ?array $variables = null): bool
+ {
+ if ($valueNode instanceof BooleanValueNode) {
+ return $valueNode->value;
+ }
+
+ $notBoolean = Printer::doPrint($valueNode);
+ throw new Error("Boolean cannot represent a non boolean value: {$notBoolean}", $valueNode);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CompositeType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CompositeType.php
new file mode 100644
index 00000000000..1167bb5e09a
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CompositeType.php
@@ -0,0 +1,12 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+/*
+export type GraphQLCompositeType =
+GraphQLObjectType |
+GraphQLInterfaceType |
+GraphQLUnionType;
+*/
+
+interface CompositeType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CustomScalarType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CustomScalarType.php
new file mode 100644
index 00000000000..f6845b0a507
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/CustomScalarType.php
@@ -0,0 +1,122 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-type InputCustomScalarConfig array{
+ * name?: string|null,
+ * description?: string|null,
+ * serialize?: callable(mixed): mixed,
+ * parseValue: callable(mixed): mixed,
+ * parseLiteral: callable(ValueNode&Node, array<string, mixed>|null): mixed,
+ * astNode?: ScalarTypeDefinitionNode|null,
+ * extensionASTNodes?: array<ScalarTypeExtensionNode>|null
+ * }
+ * @phpstan-type OutputCustomScalarConfig array{
+ * name?: string|null,
+ * description?: string|null,
+ * serialize: callable(mixed): mixed,
+ * parseValue?: callable(mixed): mixed,
+ * parseLiteral?: callable(ValueNode&Node, array<string, mixed>|null): mixed,
+ * astNode?: ScalarTypeDefinitionNode|null,
+ * extensionASTNodes?: array<ScalarTypeExtensionNode>|null
+ * }
+ * @phpstan-type CustomScalarConfig InputCustomScalarConfig|OutputCustomScalarConfig
+ */
+class CustomScalarType extends ScalarType
+{
+ /** @phpstan-var CustomScalarConfig */
+ // @phpstan-ignore-next-line specialize type
+ public array $config;
+
+ /**
+ * @param array<string, mixed> $config
+ *
+ * @phpstan-param CustomScalarConfig $config
+ */
+ public function __construct(array $config)
+ {
+ parent::__construct($config);
+ }
+
+ public function serialize($value)
+ {
+ if (isset($this->config['serialize'])) {
+ return $this->config['serialize']($value);
+ }
+
+ return $value;
+ }
+
+ public function parseValue($value)
+ {
+ if (isset($this->config['parseValue'])) {
+ return $this->config['parseValue']($value);
+ }
+
+ return $value;
+ }
+
+ /** @throws \Exception */
+ public function parseLiteral(Node $valueNode, ?array $variables = null)
+ {
+ if (isset($this->config['parseLiteral'])) {
+ return $this->config['parseLiteral']($valueNode, $variables);
+ }
+
+ return AST::valueFromASTUntyped($valueNode, $variables);
+ }
+
+ /**
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function assertValid(): void
+ {
+ parent::assertValid();
+
+ $serialize = $this->config['serialize'] ?? null;
+ $parseValue = $this->config['parseValue'] ?? null;
+ $parseLiteral = $this->config['parseLiteral'] ?? null;
+
+ $hasSerialize = $serialize !== null;
+ $hasParseValue = $parseValue !== null;
+ $hasParseLiteral = $parseLiteral !== null;
+ $hasParse = $hasParseValue && $hasParseLiteral;
+
+ if ($hasParseValue !== $hasParseLiteral) {
+ throw new InvariantViolation("{$this->name} must provide both \"parseValue\" and \"parseLiteral\" functions to work as an input type.");
+ }
+
+ if (! $hasSerialize && ! $hasParse) {
+ throw new InvariantViolation("{$this->name} must provide \"parseValue\" and \"parseLiteral\" functions, \"serialize\" function, or both.");
+ }
+
+ // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
+ if ($hasSerialize && ! is_callable($serialize)) {
+ $notCallable = Utils::printSafe($serialize);
+ throw new InvariantViolation("{$this->name} must provide \"serialize\" as a callable if given, but got: {$notCallable}.");
+ }
+
+ // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
+ if ($hasParseValue && ! is_callable($parseValue)) {
+ $notCallable = Utils::printSafe($parseValue);
+ throw new InvariantViolation("{$this->name} must provide \"parseValue\" as a callable if given, but got: {$notCallable}.");
+ }
+
+ // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
+ if ($hasParseLiteral && ! is_callable($parseLiteral)) {
+ $notCallable = Utils::printSafe($parseLiteral);
+ throw new InvariantViolation("{$this->name} must provide \"parseLiteral\" as a callable if given, but got: {$notCallable}.");
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Deprecated.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Deprecated.php
new file mode 100644
index 00000000000..e9798bcd06c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Deprecated.php
@@ -0,0 +1,14 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+#[\Attribute(\Attribute::TARGET_ALL)]
+class Deprecated
+{
+ public string $reason;
+
+ public function __construct(string $reason = Directive::DEFAULT_DEPRECATION_REASON)
+ {
+ $this->reason = $reason;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Description.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Description.php
new file mode 100644
index 00000000000..96d59c7c3f8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Description.php
@@ -0,0 +1,14 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+#[\Attribute(\Attribute::TARGET_ALL)]
+class Description
+{
+ public string $description;
+
+ public function __construct(string $description)
+ {
+ $this->description = $description;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Directive.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Directive.php
new file mode 100644
index 00000000000..0395ab3a8c4
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Directive.php
@@ -0,0 +1,185 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\DirectiveLocation;
+
+/**
+ * @phpstan-import-type ArgumentListConfig from Argument
+ *
+ * @phpstan-type DirectiveConfig array{
+ * name: string,
+ * description?: string|null,
+ * args?: ArgumentListConfig|null,
+ * locations: array<string>,
+ * isRepeatable?: bool|null,
+ * astNode?: DirectiveDefinitionNode|null
+ * }
+ */
+class Directive
+{
+ public const DEFAULT_DEPRECATION_REASON = 'No longer supported';
+
+ public const INCLUDE_NAME = 'include';
+ public const IF_ARGUMENT_NAME = 'if';
+ public const SKIP_NAME = 'skip';
+ public const DEPRECATED_NAME = 'deprecated';
+ public const REASON_ARGUMENT_NAME = 'reason';
+ public const ONE_OF_NAME = 'oneOf';
+
+ /**
+ * Lazily initialized.
+ *
+ * @var array<string, Directive>|null
+ */
+ protected static ?array $internalDirectives = null;
+
+ public string $name;
+
+ public ?string $description;
+
+ /** @var array<int, Argument> */
+ public array $args;
+
+ public bool $isRepeatable;
+
+ /** @var array<string> */
+ public array $locations;
+
+ public ?DirectiveDefinitionNode $astNode;
+
+ /**
+ * @var array<string, mixed>
+ *
+ * @phpstan-var DirectiveConfig
+ */
+ public array $config;
+
+ /**
+ * @param array<string, mixed> $config
+ *
+ * @phpstan-param DirectiveConfig $config
+ */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'];
+ $this->description = $config['description'] ?? null;
+ $this->args = isset($config['args'])
+ ? Argument::listFromConfig($config['args'])
+ : [];
+ $this->isRepeatable = $config['isRepeatable'] ?? false;
+ $this->locations = $config['locations'];
+ $this->astNode = $config['astNode'] ?? null;
+
+ $this->config = $config;
+ }
+
+ /** @return array<string, Directive> */
+ public static function builtInDirectives(): array
+ {
+ return [
+ self::INCLUDE_NAME => self::includeDirective(),
+ self::SKIP_NAME => self::skipDirective(),
+ self::DEPRECATED_NAME => self::deprecatedDirective(),
+ self::ONE_OF_NAME => self::oneOfDirective(),
+ ];
+ }
+
+ /**
+ * @deprecated use {@see Directive::builtInDirectives()}
+ *
+ * @return array<string, Directive>
+ */
+ public static function getInternalDirectives(): array
+ {
+ return self::builtInDirectives();
+ }
+
+ public static function includeDirective(): Directive
+ {
+ return self::$internalDirectives[self::INCLUDE_NAME] ??= new self([
+ 'name' => self::INCLUDE_NAME,
+ 'description' => 'Directs the executor to include this field or fragment only when the `if` argument is true.',
+ 'locations' => [
+ DirectiveLocation::FIELD,
+ DirectiveLocation::FRAGMENT_SPREAD,
+ DirectiveLocation::INLINE_FRAGMENT,
+ ],
+ 'args' => [
+ self::IF_ARGUMENT_NAME => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'description' => 'Included when true.',
+ ],
+ ],
+ ]);
+ }
+
+ public static function skipDirective(): Directive
+ {
+ return self::$internalDirectives[self::SKIP_NAME] ??= new self([
+ 'name' => self::SKIP_NAME,
+ 'description' => 'Directs the executor to skip this field or fragment when the `if` argument is true.',
+ 'locations' => [
+ DirectiveLocation::FIELD,
+ DirectiveLocation::FRAGMENT_SPREAD,
+ DirectiveLocation::INLINE_FRAGMENT,
+ ],
+ 'args' => [
+ self::IF_ARGUMENT_NAME => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'description' => 'Skipped when true.',
+ ],
+ ],
+ ]);
+ }
+
+ public static function deprecatedDirective(): Directive
+ {
+ return self::$internalDirectives[self::DEPRECATED_NAME] ??= new self([
+ 'name' => self::DEPRECATED_NAME,
+ 'description' => 'Marks an element of a Automattic\WooCommerce\Vendor\GraphQL schema as no longer supported.',
+ 'locations' => [
+ DirectiveLocation::FIELD_DEFINITION,
+ DirectiveLocation::ENUM_VALUE,
+ DirectiveLocation::ARGUMENT_DEFINITION,
+ DirectiveLocation::INPUT_FIELD_DEFINITION,
+ ],
+ 'args' => [
+ self::REASON_ARGUMENT_NAME => [
+ 'type' => Type::string(),
+ 'description' => 'Explains why this element was deprecated, usually also including a suggestion for how to access supported similar data. Formatted using the Markdown syntax, as specified by [CommonMark](https://commonmark.org/).',
+ 'defaultValue' => self::DEFAULT_DEPRECATION_REASON,
+ ],
+ ],
+ ]);
+ }
+
+ public static function oneOfDirective(): Directive
+ {
+ return self::$internalDirectives[self::ONE_OF_NAME] ??= new self([
+ 'name' => self::ONE_OF_NAME,
+ 'description' => 'Indicates that an Input Object is a OneOf Input Object (and thus requires exactly one of its fields be provided).',
+ 'locations' => [
+ DirectiveLocation::INPUT_OBJECT,
+ ],
+ 'args' => [],
+ ]);
+ }
+
+ public static function isBuiltInDirective(self $directive): bool
+ {
+ return array_key_exists($directive->name, self::builtInDirectives());
+ }
+
+ /** @deprecated use {@see Directive::isBuiltInDirective()} */
+ public static function isSpecifiedDirective(Directive $directive): bool
+ {
+ return self::isBuiltInDirective($directive);
+ }
+
+ public static function resetCachedInstances(): void
+ {
+ self::$internalDirectives = null;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumType.php
new file mode 100644
index 00000000000..b10e881d034
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumType.php
@@ -0,0 +1,270 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\MixedStore;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @see EnumValueDefinitionNode
+ *
+ * @phpstan-type PartialEnumValueConfig array{
+ * name?: string,
+ * value?: mixed,
+ * deprecationReason?: string|null,
+ * description?: string|null,
+ * astNode?: EnumValueDefinitionNode|null
+ * }
+ * @phpstan-type EnumValues iterable<string, PartialEnumValueConfig>|iterable<string, mixed>|iterable<int, string>
+ * @phpstan-type EnumTypeConfig array{
+ * name?: string|null,
+ * description?: string|null,
+ * values: EnumValues|callable(): EnumValues,
+ * astNode?: EnumTypeDefinitionNode|null,
+ * extensionASTNodes?: array<EnumTypeExtensionNode>|null
+ * }
+ */
+class EnumType extends Type implements InputType, OutputType, LeafType, NullableType, NamedType
+{
+ use NamedTypeImplementation;
+
+ public ?EnumTypeDefinitionNode $astNode;
+
+ /** @var array<EnumTypeExtensionNode> */
+ public array $extensionASTNodes;
+
+ /** @phpstan-var EnumTypeConfig */
+ public array $config;
+
+ /**
+ * Lazily initialized.
+ *
+ * @var array<int, EnumValueDefinition>
+ */
+ private array $values;
+
+ /**
+ * Lazily initialized.
+ *
+ * @var MixedStore<EnumValueDefinition>
+ */
+ private MixedStore $valueLookup;
+
+ /** @var array<string, EnumValueDefinition> */
+ private array $nameLookup;
+
+ /**
+ * @phpstan-param EnumTypeConfig $config
+ *
+ * @throws InvariantViolation
+ */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'] ?? $this->inferName();
+ $this->description = $config['description'] ?? null;
+ $this->astNode = $config['astNode'] ?? null;
+ $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
+
+ $this->config = $config;
+ }
+
+ /** @throws InvariantViolation */
+ public function getValue(string $name): ?EnumValueDefinition
+ {
+ if (! isset($this->nameLookup)) {
+ $this->initializeNameLookup();
+ }
+
+ return $this->nameLookup[$name] ?? null;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<int, EnumValueDefinition>
+ */
+ public function getValues(): array
+ {
+ if (! isset($this->values)) {
+ $this->values = [];
+
+ $values = $this->config['values'];
+ if (is_callable($values)) {
+ $values = $values();
+ }
+
+ // We are just assuming the config option is set correctly here, validation happens in assertValid()
+ foreach ($values as $name => $value) {
+ if (is_string($name)) {
+ if (is_array($value)) {
+ $value += ['name' => $name, 'value' => $name];
+ } else {
+ $value = ['name' => $name, 'value' => $value];
+ }
+ } elseif (is_string($value)) {
+ $value = ['name' => $value, 'value' => $value];
+ } else {
+ throw new InvariantViolation("{$this->name} values must be an array with value names as keys or values.");
+ }
+
+ // @phpstan-ignore-next-line assume the config matches
+ $this->values[] = new EnumValueDefinition($value);
+ }
+ }
+
+ return $this->values;
+ }
+
+ /**
+ * @throws \InvalidArgumentException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ public function serialize($value)
+ {
+ $lookup = $this->getValueLookup();
+ if (isset($lookup[$value])) {
+ return $lookup[$value]->name;
+ }
+
+ if ($value instanceof \BackedEnum) {
+ return $value->name;
+ }
+
+ if ($value instanceof \UnitEnum) {
+ return $value->name;
+ }
+
+ $safeValue = Utils::printSafe($value);
+ throw new SerializationError("Cannot serialize value as enum: {$safeValue}");
+ }
+
+ /**
+ * @throws \InvalidArgumentException
+ * @throws InvariantViolation
+ *
+ * @return MixedStore<EnumValueDefinition>
+ */
+ private function getValueLookup(): MixedStore
+ {
+ if (! isset($this->valueLookup)) {
+ $this->valueLookup = new MixedStore();
+
+ foreach ($this->getValues() as $value) {
+ $this->valueLookup->offsetSet($value->value, $value);
+ }
+ }
+
+ return $this->valueLookup;
+ }
+
+ /**
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function parseValue($value)
+ {
+ if (! is_string($value)) {
+ $safeValue = Utils::printSafeJson($value);
+ throw new Error("Enum \"{$this->name}\" cannot represent non-string value: {$safeValue}.{$this->didYouMean($safeValue)}");
+ }
+
+ if (! isset($this->nameLookup)) {
+ $this->initializeNameLookup();
+ }
+
+ if (! isset($this->nameLookup[$value])) {
+ throw new Error("Value \"{$value}\" does not exist in \"{$this->name}\" enum.{$this->didYouMean($value)}");
+ }
+
+ return $this->nameLookup[$value]->value;
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function parseLiteral(Node $valueNode, ?array $variables = null)
+ {
+ if (! $valueNode instanceof EnumValueNode) {
+ $valueStr = Printer::doPrint($valueNode);
+ throw new Error("Enum \"{$this->name}\" cannot represent non-enum value: {$valueStr}.{$this->didYouMean($valueStr)}", $valueNode);
+ }
+
+ $name = $valueNode->value;
+
+ if (! isset($this->nameLookup)) {
+ $this->initializeNameLookup();
+ }
+
+ if (isset($this->nameLookup[$name])) {
+ return $this->nameLookup[$name]->value;
+ }
+
+ $valueStr = Printer::doPrint($valueNode);
+ throw new Error("Value \"{$valueStr}\" does not exist in \"{$this->name}\" enum.{$this->didYouMean($valueStr)}", $valueNode);
+ }
+
+ /**
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function assertValid(): void
+ {
+ Utils::assertValidName($this->name);
+
+ $values = $this->config['values'] ?? null; // @phpstan-ignore nullCoalesce.initializedProperty (unnecessary according to types, but can happen during runtime)
+ if (! is_iterable($values) && ! is_callable($values)) {
+ $notIterable = Utils::printSafe($values);
+ throw new InvariantViolation("{$this->name} values must be an iterable or callable, got: {$notIterable}");
+ }
+
+ $this->getValues();
+ }
+
+ /** @throws InvariantViolation */
+ private function initializeNameLookup(): void
+ {
+ $this->nameLookup = [];
+ foreach ($this->getValues() as $value) {
+ $this->nameLookup[$value->name] = $value;
+ }
+ }
+
+ /** @throws InvariantViolation */
+ protected function didYouMean(string $unknownValue): ?string
+ {
+ $suggestions = Utils::suggestionList(
+ $unknownValue,
+ array_map(
+ static fn (EnumValueDefinition $value): string => $value->name,
+ $this->getValues()
+ )
+ );
+
+ return $suggestions === []
+ ? null
+ : ' Did you mean the enum value ' . Utils::quotedOrList($suggestions) . '?';
+ }
+
+ public function astNode(): ?EnumTypeDefinitionNode
+ {
+ return $this->astNode;
+ }
+
+ /** @return array<EnumTypeExtensionNode> */
+ public function extensionASTNodes(): array
+ {
+ return $this->extensionASTNodes;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumValueDefinition.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumValueDefinition.php
new file mode 100644
index 00000000000..83516a3f127
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/EnumValueDefinition.php
@@ -0,0 +1,48 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
+
+/**
+ * @phpstan-type EnumValueConfig array{
+ * name: string,
+ * value?: mixed,
+ * deprecationReason?: string|null,
+ * description?: string|null,
+ * astNode?: EnumValueDefinitionNode|null
+ * }
+ */
+class EnumValueDefinition
+{
+ public string $name;
+
+ /** @var mixed */
+ public $value;
+
+ public ?string $deprecationReason;
+
+ public ?string $description;
+
+ public ?EnumValueDefinitionNode $astNode;
+
+ /** @phpstan-var EnumValueConfig */
+ public array $config;
+
+ /** @phpstan-param EnumValueConfig $config */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'];
+ $this->value = $config['value'] ?? null;
+ $this->deprecationReason = $config['deprecationReason'] ?? null;
+ $this->description = $config['description'] ?? null;
+ $this->astNode = $config['astNode'] ?? null;
+
+ $this->config = $config;
+ }
+
+ public function isDeprecated(): bool
+ {
+ return (bool) $this->deprecationReason;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FieldDefinition.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FieldDefinition.php
new file mode 100644
index 00000000000..d3170e107c2
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FieldDefinition.php
@@ -0,0 +1,251 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Executor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @see Executor
+ *
+ * @phpstan-import-type FieldResolver from Executor
+ * @phpstan-import-type ArgsMapper from Executor
+ * @phpstan-import-type ArgumentListConfig from Argument
+ *
+ * @phpstan-type FieldType (Type&OutputType)|callable(): (Type&OutputType)
+ * @phpstan-type ComplexityFn callable(int, array<string, mixed>): int
+ * @phpstan-type VisibilityFn callable(): bool
+ * @phpstan-type FieldDefinitionConfig array{
+ * name: string,
+ * type: FieldType,
+ * resolve?: FieldResolver|null,
+ * args?: ArgumentListConfig|null,
+ * argsMapper?: ArgsMapper|null,
+ * description?: string|null,
+ * visible?: VisibilityFn|bool,
+ * deprecationReason?: string|null,
+ * astNode?: FieldDefinitionNode|null,
+ * complexity?: ComplexityFn|null
+ * }
+ * @phpstan-type UnnamedFieldDefinitionConfig array{
+ * type: FieldType,
+ * resolve?: FieldResolver|null,
+ * args?: ArgumentListConfig|null,
+ * argsMapper?: ArgsMapper|null,
+ * description?: string|null,
+ * visible?: VisibilityFn|bool,
+ * deprecationReason?: string|null,
+ * astNode?: FieldDefinitionNode|null,
+ * complexity?: ComplexityFn|null
+ * }
+ * @phpstan-type FieldsConfig iterable<mixed>|callable(): iterable<mixed>
+ */
+/*
+ * TODO check if newer versions of PHPStan can handle the full definition, it currently crashes when it is used
+ * @phpstan-type EagerListEntry FieldDefinitionConfig|(Type&OutputType)
+ * @phpstan-type EagerMapEntry UnnamedFieldDefinitionConfig|FieldDefinition
+ * @phpstan-type FieldsList iterable<EagerListEntry|(callable(): EagerListEntry)>
+ * @phpstan-type FieldsMap iterable<string, EagerMapEntry|(callable(): EagerMapEntry)>
+ * @phpstan-type FieldsIterable FieldsList|FieldsMap
+ * @phpstan-type FieldsConfig FieldsIterable|(callable(): FieldsIterable)
+ */
+class FieldDefinition
+{
+ public string $name;
+
+ /** @var array<int, Argument> */
+ public array $args;
+
+ /**
+ * Callback to transform args to value object.
+ *
+ * @var callable|null
+ *
+ * @phpstan-var ArgsMapper|null
+ */
+ public $argsMapper;
+
+ /**
+ * Callback for resolving field value given parent value.
+ *
+ * @var callable|null
+ *
+ * @phpstan-var FieldResolver|null
+ */
+ public $resolveFn;
+
+ public ?string $description;
+
+ /**
+ * @var callable|bool
+ *
+ * @phpstan-var VisibilityFn|bool
+ */
+ public $visible;
+
+ public ?string $deprecationReason;
+
+ public ?FieldDefinitionNode $astNode;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var ComplexityFn|null
+ */
+ public $complexityFn;
+
+ /**
+ * Original field definition config.
+ *
+ * @phpstan-var FieldDefinitionConfig
+ */
+ public array $config;
+
+ /** @var Type&OutputType */
+ private Type $type;
+
+ /** @param FieldDefinitionConfig $config */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'];
+ $this->resolveFn = $config['resolve'] ?? null;
+ $this->args = isset($config['args'])
+ ? Argument::listFromConfig($config['args'])
+ : [];
+ $this->argsMapper = $config['argsMapper'] ?? null;
+ $this->description = $config['description'] ?? null;
+ $this->visible = $config['visible'] ?? true;
+ $this->deprecationReason = $config['deprecationReason'] ?? null;
+ $this->astNode = $config['astNode'] ?? null;
+ $this->complexityFn = $config['complexity'] ?? null;
+
+ $this->config = $config;
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $parentType
+ * @param callable|iterable $fields
+ *
+ * @phpstan-param FieldsConfig $fields
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<string, self|UnresolvedFieldDefinition>
+ */
+ public static function defineFieldMap(Type $parentType, $fields): array
+ {
+ if (is_callable($fields)) {
+ $fields = $fields();
+ }
+
+ if (! is_iterable($fields)) {
+ throw new InvariantViolation("{$parentType->name} fields must be an iterable or a callable which returns such an iterable.");
+ }
+
+ $map = [];
+ foreach ($fields as $maybeName => $field) {
+ if (is_array($field)) {
+ if (! isset($field['name'])) {
+ if (! is_string($maybeName)) {
+ throw new InvariantViolation("{$parentType->name} fields must be an associative array with field names as keys or a function which returns such an array.");
+ }
+
+ $field['name'] = $maybeName;
+ }
+
+ // @phpstan-ignore-next-line PHPStan won't let us define the whole type
+ $fieldDef = new self($field);
+ } elseif ($field instanceof self) {
+ $fieldDef = $field;
+ } elseif (is_callable($field)) {
+ if (! is_string($maybeName)) {
+ throw new InvariantViolation("{$parentType->name} lazy fields must be an associative array with field names as keys.");
+ }
+
+ $fieldDef = new UnresolvedFieldDefinition($maybeName, $field);
+ } elseif ($field instanceof Type) {
+ // @phpstan-ignore-next-line PHPStan won't let us define the whole type
+ $fieldDef = new self([
+ 'name' => $maybeName,
+ 'type' => $field,
+ ]);
+ } else {
+ $invalidFieldConfig = Utils::printSafe($field);
+ throw new InvariantViolation("{$parentType->name}.{$maybeName} field config must be an array, but got: {$invalidFieldConfig}");
+ }
+
+ $map[$fieldDef->getName()] = $fieldDef;
+ }
+
+ return $map;
+ }
+
+ public function getArg(string $name): ?Argument
+ {
+ foreach ($this->args as $arg) {
+ if ($arg->name === $name) {
+ return $arg;
+ }
+ }
+
+ return null;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ /** @return Type&OutputType */
+ public function getType(): Type
+ {
+ return $this->type ??= Schema::resolveType($this->config['type']);
+ }
+
+ public function isVisible(): bool
+ {
+ if (is_bool($this->visible)) {
+ return $this->visible;
+ }
+
+ return $this->visible = ($this->visible)();
+ }
+
+ public function isDeprecated(): bool
+ {
+ return (bool) $this->deprecationReason;
+ }
+
+ /**
+ * @param Type&NamedType $parentType
+ *
+ * @throws InvariantViolation
+ */
+ public function assertValid(Type $parentType): void
+ {
+ $error = Utils::isValidNameError($this->name);
+ if ($error !== null) {
+ throw new InvariantViolation("{$parentType->name}.{$this->name}: {$error->getMessage()}");
+ }
+
+ $type = Type::getNamedType($this->getType());
+
+ if (! $type instanceof OutputType) {
+ $safeType = Utils::printSafe($this->type);
+ throw new InvariantViolation("{$parentType->name}.{$this->name} field type must be Output Type but got: {$safeType}.");
+ }
+
+ // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
+ if ($this->resolveFn !== null && ! is_callable($this->resolveFn)) {
+ $safeResolveFn = Utils::printSafe($this->resolveFn);
+ throw new InvariantViolation("{$parentType->name}.{$this->name} field resolver must be a function if provided, but got: {$safeResolveFn}.");
+ }
+
+ foreach ($this->args as $fieldArgument) {
+ $fieldArgument->assertValid($this, $type);
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FloatType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FloatType.php
new file mode 100644
index 00000000000..4dd9cac874a
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/FloatType.php
@@ -0,0 +1,65 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+class FloatType extends ScalarType
+{
+ public string $name = Type::FLOAT;
+
+ public ?string $description
+ = 'The `Float` scalar type represents signed double-precision fractional
+values as specified by
+[IEEE 754](http://en.wikipedia.org/wiki/IEEE_floating_point). ';
+
+ /** @throws SerializationError */
+ public function serialize($value): float
+ {
+ $float = is_numeric($value) || is_bool($value)
+ ? (float) $value
+ : null;
+
+ if ($float === null || ! is_finite($float)) {
+ $notFloat = Utils::printSafe($value);
+ throw new SerializationError("Float cannot represent non numeric value: {$notFloat}");
+ }
+
+ return $float;
+ }
+
+ /** @throws Error */
+ public function parseValue($value): float
+ {
+ $float = is_float($value) || is_int($value)
+ ? (float) $value
+ : null;
+
+ if ($float === null || ! is_finite($float)) {
+ $notFloat = Utils::printSafeJson($value);
+ throw new Error("Float cannot represent non numeric value: {$notFloat}");
+ }
+
+ return $float;
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws Error
+ */
+ public function parseLiteral(Node $valueNode, ?array $variables = null)
+ {
+ if ($valueNode instanceof FloatValueNode || $valueNode instanceof IntValueNode) {
+ return (float) $valueNode->value;
+ }
+
+ $notFloat = Printer::doPrint($valueNode);
+ throw new Error("Float cannot represent non numeric value: {$notFloat}", $valueNode);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsType.php
new file mode 100644
index 00000000000..eaba00481a3
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsType.php
@@ -0,0 +1,38 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+
+interface HasFieldsType
+{
+ /** @throws InvariantViolation */
+ public function getField(string $name): FieldDefinition;
+
+ public function hasField(string $name): bool;
+
+ public function findField(string $name): ?FieldDefinition;
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<string, FieldDefinition>
+ */
+ public function getFields(): array;
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<string, FieldDefinition>
+ */
+ public function getVisibleFields(): array;
+
+ /**
+ * Get all field names, including only visible fields.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, string>
+ */
+ public function getFieldNames(): array;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsTypeImplementation.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsTypeImplementation.php
new file mode 100644
index 00000000000..516e568124e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/HasFieldsTypeImplementation.php
@@ -0,0 +1,106 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+
+/**
+ * @see HasFieldsType
+ */
+trait HasFieldsTypeImplementation
+{
+ /**
+ * Lazily initialized.
+ *
+ * @var array<string, FieldDefinition|UnresolvedFieldDefinition>
+ */
+ private array $fields;
+
+ /** @throws InvariantViolation */
+ private function initializeFields(): void
+ {
+ if (isset($this->fields)) {
+ return;
+ }
+
+ $this->fields = FieldDefinition::defineFieldMap($this, $this->config['fields']);
+ }
+
+ /** @throws InvariantViolation */
+ public function getField(string $name): FieldDefinition
+ {
+ $field = $this->findField($name);
+
+ if ($field === null) {
+ throw new InvariantViolation("Field \"{$name}\" is not defined for type \"{$this->name}\"");
+ }
+
+ return $field;
+ }
+
+ /** @throws InvariantViolation */
+ public function findField(string $name): ?FieldDefinition
+ {
+ $this->initializeFields();
+
+ if (! isset($this->fields[$name])) {
+ return null;
+ }
+
+ $field = $this->fields[$name];
+ if ($field instanceof UnresolvedFieldDefinition) {
+ return $this->fields[$name] = $field->resolve();
+ }
+
+ return $field;
+ }
+
+ /** @throws InvariantViolation */
+ public function hasField(string $name): bool
+ {
+ $this->initializeFields();
+
+ return isset($this->fields[$name]);
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<string, FieldDefinition>
+ */
+ public function getFields(): array
+ {
+ $this->initializeFields();
+
+ foreach ($this->fields as $name => $field) {
+ if ($field instanceof UnresolvedFieldDefinition) {
+ $this->fields[$name] = $field->resolve();
+ }
+ }
+
+ // @phpstan-ignore-next-line all field definitions are now resolved
+ return $this->fields;
+ }
+
+ /** @return array<string, FieldDefinition> */
+ public function getVisibleFields(): array
+ {
+ return array_filter(
+ $this->getFields(),
+ fn (FieldDefinition $fieldDefinition): bool => $fieldDefinition->isVisible()
+ );
+ }
+
+ /** @throws InvariantViolation */
+ public function getFieldNames(): array
+ {
+ $this->initializeFields();
+
+ $visibleFieldNames = array_map(
+ fn (FieldDefinition $fieldDefinition): string => $fieldDefinition->getName(),
+ $this->getVisibleFields()
+ );
+
+ return array_values($visibleFieldNames);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IDType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IDType.php
new file mode 100644
index 00000000000..1d923ad305c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IDType.php
@@ -0,0 +1,63 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+class IDType extends ScalarType
+{
+ public string $name = 'ID';
+
+ public ?string $description
+ = 'The `ID` scalar type represents a unique identifier, often used to
+refetch an object or as key for a cache. The ID type appears in a JSON
+response as a String; however, it is not intended to be human-readable.
+When expected as an input type, any string (such as `"4"`) or integer
+(such as `4`) input value will be accepted as an ID.';
+
+ /** @throws SerializationError */
+ public function serialize($value): string
+ {
+ $canCast = is_string($value)
+ || is_int($value)
+ || (is_object($value) && method_exists($value, '__toString'));
+
+ if (! $canCast) {
+ $notID = Utils::printSafe($value);
+ throw new SerializationError("ID cannot represent a non-string and non-integer value: {$notID}");
+ }
+
+ return (string) $value;
+ }
+
+ /** @throws Error */
+ public function parseValue($value): string
+ {
+ if (is_string($value) || is_int($value)) {
+ return (string) $value;
+ }
+
+ $notID = Utils::printSafeJson($value);
+ throw new Error("ID cannot represent a non-string and non-integer value: {$notID}");
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws Error
+ */
+ public function parseLiteral(Node $valueNode, ?array $variables = null): string
+ {
+ if ($valueNode instanceof StringValueNode || $valueNode instanceof IntValueNode) {
+ return $valueNode->value;
+ }
+
+ $notID = Printer::doPrint($valueNode);
+ throw new Error("ID cannot represent a non-string and non-integer value: {$notID}", $valueNode);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingType.php
new file mode 100644
index 00000000000..f93f9885fc1
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingType.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+/**
+ * export type GraphQLImplementingType =
+ * GraphQLObjectType |
+ * GraphQLInterfaceType;.
+ */
+interface ImplementingType
+{
+ public function implementsInterface(InterfaceType $interfaceType): bool;
+
+ /** @return array<int, InterfaceType> */
+ public function getInterfaces(): array;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingTypeImplementation.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingTypeImplementation.php
new file mode 100644
index 00000000000..387ffac926b
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ImplementingTypeImplementation.php
@@ -0,0 +1,80 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * @see ImplementingType
+ */
+trait ImplementingTypeImplementation
+{
+ /**
+ * Lazily initialized.
+ *
+ * @var array<int, InterfaceType>
+ */
+ private array $interfaces;
+
+ public function implementsInterface(InterfaceType $interfaceType): bool
+ {
+ if (! isset($this->interfaces)) {
+ $this->initializeInterfaces();
+ }
+
+ foreach ($this->interfaces as $interface) {
+ if ($interfaceType->name === $interface->name) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /** @return array<int, InterfaceType> */
+ public function getInterfaces(): array
+ {
+ if (! isset($this->interfaces)) {
+ $this->initializeInterfaces();
+ }
+
+ return $this->interfaces;
+ }
+
+ private function initializeInterfaces(): void
+ {
+ $this->interfaces = [];
+
+ if (! isset($this->config['interfaces'])) {
+ return;
+ }
+
+ $interfaces = $this->config['interfaces'];
+ if (is_callable($interfaces)) {
+ $interfaces = $interfaces();
+ }
+
+ foreach ($interfaces as $interface) {
+ $this->interfaces[] = Schema::resolveType($interface); // @phpstan-ignore argument.templateType
+ }
+ }
+
+ /** @throws InvariantViolation */
+ protected function assertValidInterfaces(): void
+ {
+ if (! isset($this->config['interfaces'])) {
+ return;
+ }
+
+ $interfaces = $this->config['interfaces'];
+ if (is_callable($interfaces)) {
+ $interfaces = $interfaces();
+ }
+
+ // @phpstan-ignore-next-line should not happen if used correctly
+ if (! is_iterable($interfaces)) {
+ throw new InvariantViolation("{$this->name} interfaces must be an iterable or a callable which returns an iterable.");
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectField.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectField.php
new file mode 100644
index 00000000000..516f80b05ef
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectField.php
@@ -0,0 +1,115 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-type ArgumentType (Type&InputType)|callable(): (Type&InputType)
+ * @phpstan-type InputObjectFieldConfig array{
+ * name: string,
+ * type: ArgumentType,
+ * defaultValue?: mixed,
+ * description?: string|null,
+ * deprecationReason?: string|null,
+ * astNode?: InputValueDefinitionNode|null
+ * }
+ * @phpstan-type UnnamedInputObjectFieldConfig array{
+ * name?: string,
+ * type: ArgumentType,
+ * defaultValue?: mixed,
+ * description?: string|null,
+ * deprecationReason?: string|null,
+ * astNode?: InputValueDefinitionNode|null
+ * }
+ */
+class InputObjectField
+{
+ public string $name;
+
+ /** @var mixed */
+ public $defaultValue;
+
+ public ?string $description;
+
+ public ?string $deprecationReason;
+
+ /** @var Type&InputType */
+ private Type $type;
+
+ public ?InputValueDefinitionNode $astNode;
+
+ /** @phpstan-var InputObjectFieldConfig */
+ public array $config;
+
+ /** @phpstan-param InputObjectFieldConfig $config */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'];
+ $this->defaultValue = $config['defaultValue'] ?? null;
+ $this->description = $config['description'] ?? null;
+ $this->deprecationReason = $config['deprecationReason'] ?? null;
+ // Do nothing for type, it is lazy loaded in getType()
+ $this->astNode = $config['astNode'] ?? null;
+
+ $this->config = $config;
+ }
+
+ /** @return Type&InputType */
+ public function getType(): Type
+ {
+ if (! isset($this->type)) {
+ $this->type = Schema::resolveType($this->config['type']);
+ }
+
+ return $this->type;
+ }
+
+ public function defaultValueExists(): bool
+ {
+ return array_key_exists('defaultValue', $this->config);
+ }
+
+ public function isRequired(): bool
+ {
+ return $this->getType() instanceof NonNull
+ && ! $this->defaultValueExists();
+ }
+
+ public function isDeprecated(): bool
+ {
+ return (bool) $this->deprecationReason;
+ }
+
+ /**
+ * @param Type&NamedType $parentType
+ *
+ * @throws InvariantViolation
+ */
+ public function assertValid(Type $parentType): void
+ {
+ $error = Utils::isValidNameError($this->name);
+ if ($error !== null) {
+ throw new InvariantViolation("{$parentType->name}.{$this->name}: {$error->getMessage()}");
+ }
+
+ $type = Type::getNamedType($this->getType());
+
+ if (! $type instanceof InputType) {
+ $notInputType = Utils::printSafe($this->type);
+ throw new InvariantViolation("{$parentType->name}.{$this->name} field type must be Input Type but got: {$notInputType}");
+ }
+
+ // @phpstan-ignore-next-line should not happen if used properly
+ if (array_key_exists('resolve', $this->config)) {
+ throw new InvariantViolation("{$parentType->name}.{$this->name} field has a resolve property, but Input Types cannot define resolvers.");
+ }
+
+ if ($this->isRequired() && $this->isDeprecated()) {
+ throw new InvariantViolation("Required input field {$parentType->name}.{$this->name} cannot be deprecated.");
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectType.php
new file mode 100644
index 00000000000..af8b70f7f7f
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputObjectType.php
@@ -0,0 +1,259 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField
+ *
+ * @phpstan-type EagerFieldConfig InputObjectField|(Type&InputType)|UnnamedInputObjectFieldConfig
+ * @phpstan-type LazyFieldConfig callable(): EagerFieldConfig
+ * @phpstan-type FieldConfig EagerFieldConfig|LazyFieldConfig
+ * @phpstan-type ParseValueFn callable(array<string, mixed>): mixed
+ * @phpstan-type InputObjectConfig array{
+ * name?: string|null,
+ * description?: string|null,
+ * isOneOf?: bool|null,
+ * fields: iterable<FieldConfig>|callable(): iterable<FieldConfig>,
+ * parseValue?: ParseValueFn|null,
+ * astNode?: InputObjectTypeDefinitionNode|null,
+ * extensionASTNodes?: array<InputObjectTypeExtensionNode>|null
+ * }
+ */
+class InputObjectType extends Type implements InputType, NullableType, NamedType
+{
+ use NamedTypeImplementation;
+
+ public bool $isOneOf;
+
+ /**
+ * Lazily initialized.
+ *
+ * @var array<string, InputObjectField>
+ */
+ private array $fields;
+
+ /** @var ParseValueFn|null */
+ private $parseValue;
+
+ public ?InputObjectTypeDefinitionNode $astNode;
+
+ /** @var array<InputObjectTypeExtensionNode> */
+ public array $extensionASTNodes;
+
+ /** @phpstan-var InputObjectConfig */
+ public array $config;
+
+ /**
+ * @phpstan-param InputObjectConfig $config
+ *
+ * @throws InvariantViolation
+ * @throws InvariantViolation
+ */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'] ?? $this->inferName();
+ $this->description = $config['description'] ?? null;
+ $this->isOneOf = $config['isOneOf'] ?? false;
+ // $this->fields is initialized lazily
+ $this->parseValue = $config['parseValue'] ?? null;
+ $this->astNode = $config['astNode'] ?? null;
+ $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
+
+ $this->config = $config;
+ }
+
+ /** @throws InvariantViolation */
+ public function getField(string $name): InputObjectField
+ {
+ $field = $this->findField($name);
+
+ if ($field === null) {
+ throw new InvariantViolation("Field \"{$name}\" is not defined for type \"{$this->name}\"");
+ }
+
+ return $field;
+ }
+
+ /** @throws InvariantViolation */
+ public function findField(string $name): ?InputObjectField
+ {
+ if (! isset($this->fields)) {
+ $this->initializeFields();
+ }
+
+ return $this->fields[$name] ?? null;
+ }
+
+ /** @throws InvariantViolation */
+ public function hasField(string $name): bool
+ {
+ if (! isset($this->fields)) {
+ $this->initializeFields();
+ }
+
+ return isset($this->fields[$name]);
+ }
+
+ /** Returns true if this is a oneOf input object type. */
+ public function isOneOf(): bool
+ {
+ return $this->isOneOf;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<string, InputObjectField>
+ */
+ public function getFields(): array
+ {
+ if (! isset($this->fields)) {
+ $this->initializeFields();
+ }
+
+ return $this->fields;
+ }
+
+ /** @throws InvariantViolation */
+ protected function initializeFields(): void
+ {
+ $fields = $this->config['fields'];
+ if (is_callable($fields)) {
+ $fields = $fields();
+ }
+
+ $this->fields = [];
+ foreach ($fields as $nameOrIndex => $field) {
+ $this->initializeField($nameOrIndex, $field);
+ }
+ }
+
+ /**
+ * @param string|int $nameOrIndex
+ *
+ * @phpstan-param FieldConfig $field
+ *
+ * @throws InvariantViolation
+ */
+ protected function initializeField($nameOrIndex, $field): void
+ {
+ if (is_callable($field)) {
+ $field = $field();
+ }
+ assert($field instanceof Type || is_array($field) || $field instanceof InputObjectField);
+
+ if ($field instanceof Type) {
+ $field = ['type' => $field];
+ }
+ assert(is_array($field) || $field instanceof InputObjectField); // @phpstan-ignore-line TODO remove when using actual union types
+
+ if (is_array($field)) {
+ $field['name'] ??= $nameOrIndex;
+
+ if (! is_string($field['name'])) {
+ throw new InvariantViolation("{$this->name} fields must be an associative array with field names as keys, an array of arrays with a name attribute, or a callable which returns one of those.");
+ }
+
+ $field = new InputObjectField($field); // @phpstan-ignore-line array type is wrongly inferred
+ }
+ assert($field instanceof InputObjectField); // @phpstan-ignore-line TODO remove when using actual union types
+
+ $this->fields[$field->name] = $field;
+ }
+
+ /**
+ * Parses an externally provided value (query variable) to use as an input.
+ *
+ * Should throw an exception with a client-friendly message on invalid values, @see ClientAware.
+ *
+ * @param array<string, mixed> $value
+ *
+ * @return mixed
+ */
+ public function parseValue(array $value)
+ {
+ if (isset($this->parseValue)) {
+ return ($this->parseValue)($value);
+ }
+
+ return $value;
+ }
+
+ /**
+ * Validates type config and throws if one of the type options is invalid.
+ * Note: this method is shallow, it won't validate object fields and their arguments.
+ *
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function assertValid(): void
+ {
+ Utils::assertValidName($this->name);
+
+ $fields = $this->config['fields'] ?? null; // @phpstan-ignore nullCoalesce.initializedProperty (unnecessary according to types, but can happen during runtime)
+ if (is_callable($fields)) {
+ $fields = $fields();
+ }
+
+ if (! is_iterable($fields)) {
+ $invalidFields = Utils::printSafe($fields);
+ throw new InvariantViolation("{$this->name} fields must be an iterable or a callable which returns an iterable, got: {$invalidFields}.");
+ }
+
+ $resolvedFields = $this->getFields();
+
+ foreach ($resolvedFields as $field) {
+ $field->assertValid($this);
+ }
+
+ // Additional validation for oneOf input objects
+ if ($this->isOneOf()) {
+ $this->validateOneOfConstraints($resolvedFields);
+ }
+ }
+
+ /**
+ * Validates that oneOf input object constraints are met.
+ *
+ * @param array<string, InputObjectField> $fields
+ *
+ * @throws InvariantViolation
+ */
+ private function validateOneOfConstraints(array $fields): void
+ {
+ if (count($fields) === 0) {
+ throw new InvariantViolation("OneOf input object type {$this->name} must define one or more fields.");
+ }
+
+ foreach ($fields as $fieldName => $field) {
+ $fieldType = $field->getType();
+
+ // OneOf fields must be nullable (not wrapped in NonNull)
+ if ($fieldType instanceof NonNull) {
+ throw new InvariantViolation("OneOf input object type {$this->name} field {$fieldName} must be nullable.");
+ }
+
+ // OneOf fields cannot have default values
+ if ($field->defaultValueExists()) {
+ throw new InvariantViolation("OneOf input object type {$this->name} field {$fieldName} cannot have a default value.");
+ }
+ }
+ }
+
+ public function astNode(): ?InputObjectTypeDefinitionNode
+ {
+ return $this->astNode;
+ }
+
+ /** @return array<InputObjectTypeExtensionNode> */
+ public function extensionASTNodes(): array
+ {
+ return $this->extensionASTNodes;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputType.php
new file mode 100644
index 00000000000..09d87941cef
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InputType.php
@@ -0,0 +1,18 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+/**
+ * export type InputType =
+ * | ScalarType
+ * | EnumType
+ * | InputObjectType
+ * | ListOfType<InputType>
+ * | NonNull<
+ * | ScalarType
+ * | EnumType
+ * | InputObjectType
+ * | ListOfType<InputType>,
+ * >;.
+ */
+interface InputType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IntType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IntType.php
new file mode 100644
index 00000000000..4fdd037b8a5
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/IntType.php
@@ -0,0 +1,88 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+class IntType extends ScalarType
+{
+ // As per the Automattic\WooCommerce\Vendor\GraphQL Spec, Integers are only treated as valid when a valid
+ // 32-bit signed integer, providing the broadest support across platforms.
+ //
+ // n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because
+ // they are internally represented as IEEE 754 doubles.
+ public const MAX_INT = 2147483647;
+ public const MIN_INT = -2147483648;
+
+ public string $name = Type::INT;
+
+ public ?string $description
+ = 'The `Int` scalar type represents non-fractional signed whole numeric
+values. Int can represent values between -(2^31) and 2^31 - 1. ';
+
+ /** @throws SerializationError */
+ public function serialize($value): int
+ {
+ // Fast path for 90+% of cases:
+ if (is_int($value) && $value <= self::MAX_INT && $value >= self::MIN_INT) {
+ return $value;
+ }
+
+ $float = is_numeric($value) || is_bool($value)
+ ? (float) $value
+ : null;
+
+ if ($float === null || floor($float) !== $float) {
+ $notInt = Utils::printSafe($value);
+ throw new SerializationError("Int cannot represent non-integer value: {$notInt}");
+ }
+
+ if ($float > self::MAX_INT || $float < self::MIN_INT) {
+ $outOfRangeInt = Utils::printSafe($value);
+ throw new SerializationError("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}");
+ }
+
+ return (int) $float;
+ }
+
+ /** @throws Error */
+ public function parseValue($value): int
+ {
+ $isInt = is_int($value)
+ || (is_float($value) && floor($value) === $value);
+
+ if (! $isInt) {
+ $notInt = Utils::printSafeJson($value);
+ throw new Error("Int cannot represent non-integer value: {$notInt}");
+ }
+
+ if ($value > self::MAX_INT || $value < self::MIN_INT) {
+ $outOfRangeInt = Utils::printSafeJson($value);
+ throw new Error("Int cannot represent non 32-bit signed integer value: {$outOfRangeInt}");
+ }
+
+ return (int) $value;
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws Error
+ */
+ public function parseLiteral(Node $valueNode, ?array $variables = null): int
+ {
+ if ($valueNode instanceof IntValueNode) {
+ $val = (int) $valueNode->value;
+ if ($valueNode->value === (string) $val && $val >= self::MIN_INT && $val <= self::MAX_INT) {
+ return $val;
+ }
+ }
+
+ $notInt = Printer::doPrint($valueNode);
+ throw new Error("Int cannot represent non-integer value: {$notInt}", $valueNode);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InterfaceType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InterfaceType.php
new file mode 100644
index 00000000000..b3263f43e99
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/InterfaceType.php
@@ -0,0 +1,118 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-import-type ResolveType from AbstractType
+ * @phpstan-import-type ResolveValue from AbstractType
+ * @phpstan-import-type FieldsConfig from FieldDefinition
+ *
+ * @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType
+ * @phpstan-type InterfaceConfig array{
+ * name?: string|null,
+ * description?: string|null,
+ * fields: FieldsConfig,
+ * interfaces?: iterable<InterfaceTypeReference>|callable(): iterable<InterfaceTypeReference>,
+ * resolveType?: ResolveType|null,
+ * resolveValue?: ResolveValue|null,
+ * astNode?: InterfaceTypeDefinitionNode|null,
+ * extensionASTNodes?: array<InterfaceTypeExtensionNode>|null
+ * }
+ */
+class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, HasFieldsType, NamedType, ImplementingType
+{
+ use HasFieldsTypeImplementation;
+ use NamedTypeImplementation;
+ use ImplementingTypeImplementation;
+
+ public ?InterfaceTypeDefinitionNode $astNode;
+
+ /** @var array<InterfaceTypeExtensionNode> */
+ public array $extensionASTNodes;
+
+ /** @phpstan-var InterfaceConfig */
+ public array $config;
+
+ /**
+ * @phpstan-param InterfaceConfig $config
+ *
+ * @throws InvariantViolation
+ */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'] ?? $this->inferName();
+ $this->description = $config['description'] ?? null;
+ $this->astNode = $config['astNode'] ?? null;
+ $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
+
+ $this->config = $config;
+ }
+
+ /**
+ * @param mixed $type
+ *
+ * @throws InvariantViolation
+ */
+ public static function assertInterfaceType($type): self
+ {
+ if (! $type instanceof self) {
+ $notInterfaceType = Utils::printSafe($type);
+ throw new InvariantViolation("Expected {$notInterfaceType} to be a Automattic\WooCommerce\Vendor\GraphQL Interface type.");
+ }
+
+ return $type;
+ }
+
+ public function resolveValue($objectValue, $context, ResolveInfo $info)
+ {
+ if (isset($this->config['resolveValue'])) {
+ return ($this->config['resolveValue'])($objectValue, $context, $info);
+ }
+
+ return $objectValue;
+ }
+
+ public function resolveType($objectValue, $context, ResolveInfo $info)
+ {
+ if (isset($this->config['resolveType'])) {
+ return ($this->config['resolveType'])($objectValue, $context, $info);
+ }
+
+ return null;
+ }
+
+ /**
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function assertValid(): void
+ {
+ Utils::assertValidName($this->name);
+
+ $resolveType = $this->config['resolveType'] ?? null;
+ // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
+ if ($resolveType !== null && ! is_callable($resolveType)) {
+ $notCallable = Utils::printSafe($resolveType);
+ throw new InvariantViolation("{$this->name} must provide \"resolveType\" as null or a callable, but got: {$notCallable}.");
+ }
+
+ $this->assertValidInterfaces();
+ }
+
+ public function astNode(): ?InterfaceTypeDefinitionNode
+ {
+ return $this->astNode;
+ }
+
+ /** @return array<InterfaceTypeExtensionNode> */
+ public function extensionASTNodes(): array
+ {
+ return $this->extensionASTNodes;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/LeafType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/LeafType.php
new file mode 100644
index 00000000000..2a897b29beb
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/LeafType.php
@@ -0,0 +1,57 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
+
+/*
+export type GraphQLLeafType =
+GraphQLScalarType |
+GraphQLEnumType;
+*/
+
+interface LeafType
+{
+ /**
+ * Serializes an internal value to include in a response.
+ *
+ * Should throw an exception on invalid values.
+ *
+ * @param mixed $value
+ *
+ * @throws SerializationError
+ *
+ * @return mixed
+ */
+ public function serialize($value);
+
+ /**
+ * Parses an externally provided value (query variable) to use as an input.
+ *
+ * Should throw an exception with a client-friendly message on invalid values, @see ClientAware.
+ *
+ * @param mixed $value
+ *
+ * @throws Error
+ *
+ * @return mixed
+ */
+ public function parseValue($value);
+
+ /**
+ * Parses an externally provided literal value (hardcoded in Automattic\WooCommerce\Vendor\GraphQL query) to use as an input.
+ *
+ * Should throw an exception with a client-friendly message on invalid value nodes, @see ClientAware.
+ *
+ * @param ValueNode&Node $valueNode
+ * @param array<string, mixed>|null $variables
+ *
+ * @throws Error
+ *
+ * @return mixed
+ */
+ public function parseLiteral(Node $valueNode, ?array $variables = null);
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ListOfType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ListOfType.php
new file mode 100644
index 00000000000..95d49d76055
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ListOfType.php
@@ -0,0 +1,51 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * @template-covariant OfType of Type
+ */
+class ListOfType extends Type implements WrappingType, OutputType, NullableType, InputType
+{
+ /**
+ * @var Type|callable
+ *
+ * @phpstan-var OfType|callable(): OfType
+ */
+ private $wrappedType;
+
+ /**
+ * @param Type|callable $type
+ *
+ * @phpstan-param OfType|callable(): OfType $type
+ */
+ public function __construct($type)
+ {
+ $this->wrappedType = $type;
+ }
+
+ public function toString(): string
+ {
+ return '[' . $this->getWrappedType()->toString() . ']';
+ }
+
+ /** @phpstan-return OfType */
+ public function getWrappedType(): Type
+ {
+ return Schema::resolveType($this->wrappedType);
+ }
+
+ public function getInnermostType(): NamedType
+ {
+ $type = $this->getWrappedType();
+ while ($type instanceof WrappingType) {
+ $type = $type->getWrappedType();
+ }
+
+ assert($type instanceof NamedType, 'known because we unwrapped all the way down');
+
+ return $type;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedType.php
new file mode 100644
index 00000000000..85c9b1cd8a8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedType.php
@@ -0,0 +1,41 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
+
+/**
+ * export type NamedType =
+ * | ScalarType
+ * | ObjectType
+ * | InterfaceType
+ * | UnionType
+ * | EnumType
+ * | InputObjectType;.
+ *
+ * @property string $name
+ * @property string|null $description
+ * @property (Node&TypeDefinitionNode)|null $astNode
+ * @property array<Node&TypeExtensionNode> $extensionASTNodes
+ */
+interface NamedType
+{
+ /** @throws Error */
+ public function assertValid(): void;
+
+ /** Is this type a built-in type? */
+ public function isBuiltInType(): bool;
+
+ public function name(): string;
+
+ public function description(): ?string;
+
+ /** @return (Node&TypeDefinitionNode)|null */
+ public function astNode(): ?Node;
+
+ /** @return array<Node&TypeExtensionNode> */
+ public function extensionASTNodes(): array;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedTypeImplementation.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedTypeImplementation.php
new file mode 100644
index 00000000000..55f0005e8e3
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NamedTypeImplementation.php
@@ -0,0 +1,58 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+
+/**
+ * @see NamedType
+ */
+trait NamedTypeImplementation
+{
+ public string $name;
+
+ public ?string $description;
+
+ public function toString(): string
+ {
+ return $this->name;
+ }
+
+ /** @throws InvariantViolation */
+ protected function inferName(): string
+ {
+ if (isset($this->name)) { // @phpstan-ignore-line property might be uninitialized
+ return $this->name;
+ }
+
+ // If class is extended - infer name from className
+ // QueryType -> Type
+ // SomeOtherType -> SomeOther
+ $reflection = new \ReflectionClass($this);
+ $name = $reflection->getShortName();
+
+ if ($reflection->getNamespaceName() !== __NAMESPACE__) {
+ $withoutPrefixType = preg_replace('~Type$~', '', $name);
+ assert(is_string($withoutPrefixType), 'regex is statically known to be correct');
+
+ return $withoutPrefixType;
+ }
+
+ throw new InvariantViolation('Must provide name for Type.');
+ }
+
+ public function isBuiltInType(): bool
+ {
+ return in_array($this->name, Type::BUILT_IN_TYPE_NAMES, true);
+ }
+
+ public function name(): string
+ {
+ return $this->name;
+ }
+
+ public function description(): ?string
+ {
+ return $this->description;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NonNull.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NonNull.php
new file mode 100644
index 00000000000..a2bffdcc931
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NonNull.php
@@ -0,0 +1,51 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * @phpstan-type WrappedType (NullableType&Type)|callable():(NullableType&Type)
+ */
+class NonNull extends Type implements WrappingType, OutputType, InputType
+{
+ /**
+ * @var Type|callable
+ *
+ * @phpstan-var WrappedType
+ */
+ private $wrappedType;
+
+ /**
+ * @param Type|callable $type
+ *
+ * @phpstan-param WrappedType $type
+ */
+ public function __construct($type)
+ {
+ $this->wrappedType = $type;
+ }
+
+ public function toString(): string
+ {
+ return $this->getWrappedType()->toString() . '!';
+ }
+
+ /** @return NullableType&Type */
+ public function getWrappedType(): Type
+ {
+ return Schema::resolveType($this->wrappedType);
+ }
+
+ public function getInnermostType(): NamedType
+ {
+ $type = $this->getWrappedType();
+ while ($type instanceof WrappingType) {
+ $type = $type->getWrappedType();
+ }
+
+ assert($type instanceof NamedType, 'known because we unwrapped all the way down');
+
+ return $type;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NullableType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NullableType.php
new file mode 100644
index 00000000000..241667b2ef8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/NullableType.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+/*
+export type GraphQLNullableType =
+ | GraphQLScalarType
+ | GraphQLObjectType
+ | GraphQLInterfaceType
+ | GraphQLUnionType
+ | GraphQLEnumType
+ | GraphQLInputObjectType
+ | GraphQLList<any>;
+ */
+
+interface NullableType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ObjectType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ObjectType.php
new file mode 100644
index 00000000000..796e4cf53fc
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ObjectType.php
@@ -0,0 +1,176 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Deferred;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Executor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * Object Type Definition.
+ *
+ * Most Automattic\WooCommerce\Vendor\GraphQL types you define will be object types.
+ * Object types have a name, but most importantly describe their fields.
+ *
+ * Example:
+ *
+ * $AddressType = new ObjectType([
+ * 'name' => 'Address',
+ * 'fields' => [
+ * 'street' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type::string(),
+ * 'number' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type::int(),
+ * 'formatted' => [
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type::string(),
+ * 'resolve' => fn (AddressModel $address): string => "{$address->number} {$address->street}",
+ * ],
+ * ],
+ * ]);
+ *
+ * When two types need to refer to each other, or a type needs to refer to
+ * itself in a field, you can use a function expression (aka a closure or a
+ * thunk) to supply the fields lazily.
+ *
+ * Example:
+ *
+ * $PersonType = null;
+ * $PersonType = new ObjectType([
+ * 'name' => 'Person',
+ * 'fields' => fn (): array => [
+ * 'name' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type::string(),
+ * 'bestFriend' => $PersonType,
+ * ],
+ * ]);
+ *
+ * @phpstan-import-type FieldResolver from Executor
+ * @phpstan-import-type ArgsMapper from Executor
+ *
+ * @phpstan-type InterfaceTypeReference InterfaceType|callable(): InterfaceType
+ * @phpstan-type ObjectConfig array{
+ * name?: string|null,
+ * description?: string|null,
+ * resolveField?: FieldResolver|null,
+ * argsMapper?: ArgsMapper|null,
+ * fields: (callable(): iterable<mixed>)|iterable<mixed>,
+ * interfaces?: iterable<InterfaceTypeReference>|callable(): iterable<InterfaceTypeReference>,
+ * isTypeOf?: (callable(mixed $objectValue, mixed $context, ResolveInfo $resolveInfo): (bool|Deferred|null))|null,
+ * astNode?: ObjectTypeDefinitionNode|null,
+ * extensionASTNodes?: array<ObjectTypeExtensionNode>|null
+ * }
+ */
+class ObjectType extends Type implements OutputType, CompositeType, NullableType, HasFieldsType, NamedType, ImplementingType
+{
+ use HasFieldsTypeImplementation;
+ use NamedTypeImplementation;
+ use ImplementingTypeImplementation;
+
+ public ?ObjectTypeDefinitionNode $astNode;
+
+ /** @var array<ObjectTypeExtensionNode> */
+ public array $extensionASTNodes;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var FieldResolver|null
+ */
+ public $resolveFieldFn;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var ArgsMapper|null
+ */
+ public $argsMapper;
+
+ /** @phpstan-var ObjectConfig */
+ public array $config;
+
+ /**
+ * @phpstan-param ObjectConfig $config
+ *
+ * @throws InvariantViolation
+ */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'] ?? $this->inferName();
+ $this->description = $config['description'] ?? null;
+ $this->resolveFieldFn = $config['resolveField'] ?? null;
+ $this->argsMapper = $config['argsMapper'] ?? null;
+ $this->astNode = $config['astNode'] ?? null;
+ $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
+
+ $this->config = $config;
+ }
+
+ /**
+ * @param mixed $type
+ *
+ * @throws InvariantViolation
+ */
+ public static function assertObjectType($type): self
+ {
+ if (! $type instanceof self) {
+ $notObjectType = Utils::printSafe($type);
+ throw new InvariantViolation("Expected {$notObjectType} to be a Automattic\WooCommerce\Vendor\GraphQL Object type.");
+ }
+
+ return $type;
+ }
+
+ /**
+ * @param mixed $objectValue The resolved value for the object type
+ * @param mixed $context The context that was passed to GraphQL::execute()
+ *
+ * @return bool|Deferred|null
+ */
+ public function isTypeOf($objectValue, $context, ResolveInfo $info)
+ {
+ return isset($this->config['isTypeOf'])
+ ? $this->config['isTypeOf'](
+ $objectValue,
+ $context,
+ $info
+ )
+ : null;
+ }
+
+ /**
+ * Validates type config and throws if one of the type options is invalid.
+ * Note: this method is shallow, it won't validate object fields and their arguments.
+ *
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function assertValid(): void
+ {
+ Utils::assertValidName($this->name);
+
+ $isTypeOf = $this->config['isTypeOf'] ?? null;
+ // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
+ if (isset($isTypeOf) && ! is_callable($isTypeOf)) {
+ $notCallable = Utils::printSafe($isTypeOf);
+ throw new InvariantViolation("{$this->name} must provide \"isTypeOf\" as null or a callable, but got: {$notCallable}.");
+ }
+
+ foreach ($this->getFields() as $field) {
+ $field->assertValid($this);
+ }
+
+ $this->assertValidInterfaces();
+ }
+
+ public function astNode(): ?ObjectTypeDefinitionNode
+ {
+ return $this->astNode;
+ }
+
+ /** @return array<ObjectTypeExtensionNode> */
+ public function extensionASTNodes(): array
+ {
+ return $this->extensionASTNodes;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/OutputType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/OutputType.php
new file mode 100644
index 00000000000..6664d5e39ff
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/OutputType.php
@@ -0,0 +1,15 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+/*
+GraphQLScalarType |
+GraphQLObjectType |
+GraphQLInterfaceType |
+GraphQLUnionType |
+GraphQLEnumType |
+GraphQLList |
+GraphQLNonNull;
+*/
+
+interface OutputType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/PhpEnumType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/PhpEnumType.php
new file mode 100644
index 00000000000..9249ad3d317
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/PhpEnumType.php
@@ -0,0 +1,139 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\PhpDoc;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-import-type PartialEnumValueConfig from EnumType
+ */
+class PhpEnumType extends EnumType
+{
+ public const MULTIPLE_DESCRIPTIONS_DISALLOWED = 'Using more than 1 Description attribute is not supported.';
+ public const MULTIPLE_DEPRECATIONS_DISALLOWED = 'Using more than 1 Deprecated attribute is not supported.';
+
+ /** @var class-string<\UnitEnum> */
+ protected string $enumClass;
+
+ /**
+ * @param class-string<\UnitEnum> $enumClass The fully qualified class name of a native PHP enum
+ * @param string|null $name The name the enum will have in the schema, defaults to the basename of the given class
+ * @param string|null $description The description the enum will have in the schema, defaults to PHPDoc of the given class
+ * @param array<EnumTypeExtensionNode>|null $extensionASTNodes
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ */
+ public function __construct(
+ string $enumClass,
+ ?string $name = null,
+ ?string $description = null,
+ ?EnumTypeDefinitionNode $astNode = null,
+ ?array $extensionASTNodes = null
+ ) {
+ $this->enumClass = $enumClass;
+ $reflection = new \ReflectionEnum($enumClass);
+
+ /**
+ * @var array<string, PartialEnumValueConfig> $enumDefinitions
+ */
+ $enumDefinitions = [];
+ foreach ($reflection->getCases() as $case) {
+ $enumDefinitions[$case->name] = [
+ 'value' => $case->getValue(),
+ 'description' => $this->extractDescription($case),
+ 'deprecationReason' => $this->deprecationReason($case),
+ ];
+ }
+
+ parent::__construct([
+ 'name' => $name ?? $this->baseName($enumClass),
+ 'values' => $enumDefinitions,
+ 'description' => $description ?? $this->extractDescription($reflection),
+ 'astNode' => $astNode,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ public function serialize($value): string
+ {
+ if ($value instanceof $this->enumClass) {
+ return $value->name;
+ }
+
+ if (is_a($this->enumClass, \BackedEnum::class, true)) {
+ try {
+ $instance = $this->enumClass::from($value);
+ } catch (\ValueError|\TypeError $error) {
+ $notEnumInstanceOrValue = Utils::printSafe($value);
+ throw new SerializationError("Cannot serialize value as enum: {$notEnumInstanceOrValue}, expected instance or valid value of {$this->enumClass}.", $error->getCode(), $error);
+ }
+
+ return $instance->name;
+ }
+
+ $notEnum = Utils::printSafe($value);
+ throw new SerializationError("Cannot serialize value as enum: {$notEnum}, expected instance of {$this->enumClass}.");
+ }
+
+ public function parseValue($value)
+ {
+ // Can happen when variable values undergo a serialization cycle before execution
+ if ($value instanceof $this->enumClass) {
+ return $value;
+ }
+
+ return parent::parseValue($value);
+ }
+
+ /** @param class-string $class */
+ protected function baseName(string $class): string
+ {
+ $parts = explode('\\', $class);
+
+ return end($parts);
+ }
+
+ /**
+ * @param \ReflectionClassConstant|\ReflectionClass<\UnitEnum> $reflection
+ *
+ * @throws \Exception
+ */
+ protected function extractDescription(\ReflectionClassConstant|\ReflectionClass $reflection): ?string
+ {
+ $attributes = $reflection->getAttributes(Description::class);
+
+ if (count($attributes) === 1) {
+ return $attributes[0]->newInstance()->description;
+ }
+
+ if (count($attributes) > 1) {
+ throw new \Exception(self::MULTIPLE_DESCRIPTIONS_DISALLOWED);
+ }
+
+ $comment = $reflection->getDocComment();
+ $unpadded = PhpDoc::unpad($comment);
+
+ return PhpDoc::unwrap($unpadded);
+ }
+
+ /** @throws \Exception */
+ protected function deprecationReason(\ReflectionClassConstant $reflection): ?string
+ {
+ $attributes = $reflection->getAttributes(Deprecated::class);
+
+ if (count($attributes) === 1) {
+ return $attributes[0]->newInstance()->reason;
+ }
+
+ if (count($attributes) > 1) {
+ throw new \Exception(self::MULTIPLE_DEPRECATIONS_DISALLOWED);
+ }
+
+ return null;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/QueryPlan.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/QueryPlan.php
new file mode 100644
index 00000000000..0083348811c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/QueryPlan.php
@@ -0,0 +1,307 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Values;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * @phpstan-type QueryPlanOptions array{
+ * groupImplementorFields?: bool,
+ * }
+ */
+class QueryPlan
+{
+ /**
+ * Map from type names to a list of fields referenced of that type.
+ *
+ * @var array<string, array<string, true>>
+ */
+ private array $typeToFields = [];
+
+ private Schema $schema;
+
+ /** @var array<string, mixed> */
+ private array $queryPlan = [];
+
+ /** @var array<string, mixed> */
+ private array $variableValues;
+
+ /** @var array<string, FragmentDefinitionNode> */
+ private array $fragments;
+
+ private bool $groupImplementorFields;
+
+ /**
+ * @param iterable<FieldNode> $fieldNodes
+ * @param array<string, mixed> $variableValues
+ * @param array<string, FragmentDefinitionNode> $fragments
+ * @param QueryPlanOptions $options
+ *
+ * @throws \Exception
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function __construct(ObjectType $parentType, Schema $schema, iterable $fieldNodes, array $variableValues, array $fragments, array $options = [])
+ {
+ $this->schema = $schema;
+ $this->variableValues = $variableValues;
+ $this->fragments = $fragments;
+ $this->groupImplementorFields = $options['groupImplementorFields'] ?? false;
+ $this->analyzeQueryPlan($parentType, $fieldNodes);
+ }
+
+ /** @return array<string, mixed> */
+ public function queryPlan(): array
+ {
+ return $this->queryPlan;
+ }
+
+ /** @return array<int, string> */
+ public function getReferencedTypes(): array
+ {
+ return array_keys($this->typeToFields);
+ }
+
+ public function hasType(string $type): bool
+ {
+ return isset($this->typeToFields[$type]);
+ }
+
+ /**
+ * TODO return array<string, true>.
+ *
+ * @return array<int, string>
+ */
+ public function getReferencedFields(): array
+ {
+ $allFields = [];
+ foreach ($this->typeToFields as $fields) {
+ foreach ($fields as $field => $_) {
+ $allFields[$field] = true;
+ }
+ }
+
+ return array_keys($allFields);
+ }
+
+ public function hasField(string $field): bool
+ {
+ foreach ($this->typeToFields as $fields) {
+ if (array_key_exists($field, $fields)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * TODO return array<string, true>.
+ *
+ * @return array<int, string>
+ */
+ public function subFields(string $typename): array
+ {
+ return array_keys($this->typeToFields[$typename] ?? []);
+ }
+
+ /**
+ * @param iterable<FieldNode> $fieldNodes
+ *
+ * @throws \Exception
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ private function analyzeQueryPlan(ObjectType $parentType, iterable $fieldNodes): void
+ {
+ $queryPlan = [];
+ $implementors = [];
+ foreach ($fieldNodes as $fieldNode) {
+ if ($fieldNode->selectionSet === null) {
+ continue;
+ }
+
+ $type = Type::getNamedType(
+ $parentType->getField($fieldNode->name->value)->getType()
+ );
+
+ $subfields = $this->analyzeSelectionSet($fieldNode->selectionSet, $type, $implementors);
+ $queryPlan = $this->arrayMergeDeep($queryPlan, $subfields);
+ }
+
+ if ($this->groupImplementorFields) {
+ $this->queryPlan = ['fields' => $queryPlan];
+
+ if ($implementors !== []) {
+ $this->queryPlan['implementors'] = $implementors;
+ }
+ } else {
+ $this->queryPlan = $queryPlan;
+ }
+ }
+
+ /**
+ * @param Type&NamedType $parentType
+ * @param array<string, mixed> $implementors
+ *
+ * @throws \Exception
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return array<mixed>
+ */
+ private function analyzeSelectionSet(SelectionSetNode $selectionSet, Type $parentType, array &$implementors): array
+ {
+ $fields = [];
+ $implementors = [];
+ foreach ($selectionSet->selections as $selection) {
+ if ($selection instanceof FieldNode) {
+ $fieldName = $selection->name->value;
+
+ if ($fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
+ continue;
+ }
+
+ assert($parentType instanceof HasFieldsType, 'ensured by query validation');
+
+ $type = $parentType->getField($fieldName);
+ $selectionType = $type->getType();
+
+ $subImplementors = [];
+ $nestedSelectionSet = $selection->selectionSet;
+ $subfields = $nestedSelectionSet === null
+ ? []
+ : $this->analyzeSubFields($selectionType, $nestedSelectionSet, $subImplementors);
+
+ $fields[$fieldName] = [
+ 'type' => $selectionType,
+ 'fields' => $subfields,
+ 'args' => Values::getArgumentValues($type, $selection, $this->variableValues),
+ ];
+ if ($this->groupImplementorFields && $subImplementors !== []) {
+ $fields[$fieldName]['implementors'] = $subImplementors;
+ }
+ } elseif ($selection instanceof FragmentSpreadNode) {
+ $spreadName = $selection->name->value;
+ $fragment = $this->fragments[$spreadName] ?? null;
+ if ($fragment === null) {
+ continue;
+ }
+
+ $type = $this->schema->getType($fragment->typeCondition->name->value);
+ assert($type instanceof Type, 'ensured by query validation');
+
+ $subfields = $this->analyzeSubFields($type, $fragment->selectionSet);
+ $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors);
+ } elseif ($selection instanceof InlineFragmentNode) {
+ $typeCondition = $selection->typeCondition;
+ $type = $typeCondition === null
+ ? $parentType
+ : $this->schema->getType($typeCondition->name->value);
+ assert($type instanceof Type, 'ensured by query validation');
+
+ $subfields = $this->analyzeSubFields($type, $selection->selectionSet);
+ $fields = $this->mergeFields($parentType, $type, $fields, $subfields, $implementors);
+ }
+ }
+
+ $parentTypeName = $parentType->name();
+
+ // TODO evaluate if this line is really necessary.
+ // It causes abstract types to appear in getReferencedTypes() even if they do not have any fields directly referencing them.
+ $this->typeToFields[$parentTypeName] ??= [];
+ foreach ($fields as $fieldName => $_) {
+ $this->typeToFields[$parentTypeName][$fieldName] = true;
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @param array<string, mixed> $implementors
+ *
+ * @throws \Exception
+ * @throws Error
+ *
+ * @return array<mixed>
+ */
+ private function analyzeSubFields(Type $type, SelectionSetNode $selectionSet, array &$implementors = []): array
+ {
+ $type = Type::getNamedType($type);
+
+ return $type instanceof ObjectType || $type instanceof AbstractType
+ ? $this->analyzeSelectionSet($selectionSet, $type, $implementors)
+ : [];
+ }
+
+ /**
+ * @param Type&NamedType $parentType
+ * @param Type&NamedType $type
+ * @param array<mixed> $fields
+ * @param array<mixed> $subfields
+ * @param array<string, mixed> $implementors
+ *
+ * @return array<mixed>
+ */
+ private function mergeFields(Type $parentType, Type $type, array $fields, array $subfields, array &$implementors): array
+ {
+ if ($this->groupImplementorFields && $parentType instanceof AbstractType && ! $type instanceof AbstractType) {
+ $name = $type->name;
+ assert(is_string($name));
+
+ $implementors[$name] = [
+ 'type' => $type,
+ 'fields' => $this->arrayMergeDeep(
+ $implementors[$name]['fields'] ?? [],
+ array_diff_key($subfields, $fields)
+ ),
+ ];
+
+ $fields = $this->arrayMergeDeep(
+ $fields,
+ array_intersect_key($subfields, $fields)
+ );
+ } else {
+ $fields = $this->arrayMergeDeep($subfields, $fields);
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Merges nested arrays, but handles non array values differently from array_merge_recursive.
+ * While array_merge_recursive tries to merge non-array values, in this implementation they will be overwritten.
+ *
+ * @see https://stackoverflow.com/a/25712428
+ *
+ * @param array<mixed> $array1
+ * @param array<mixed> $array2
+ *
+ * @return array<mixed>
+ */
+ private function arrayMergeDeep(array $array1, array $array2): array
+ {
+ foreach ($array2 as $key => &$value) {
+ if (is_numeric($key)) {
+ if (! in_array($value, $array1, true)) {
+ $array1[] = $value;
+ }
+ } elseif (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) {
+ $array1[$key] = $this->arrayMergeDeep($array1[$key], $value);
+ } else {
+ $array1[$key] = $value;
+ }
+ }
+
+ return $array1;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ResolveInfo.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ResolveInfo.php
new file mode 100644
index 00000000000..4591b6b26db
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ResolveInfo.php
@@ -0,0 +1,512 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Values;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * Structure containing information useful for field resolution process.
+ *
+ * Passed as 4th argument to every field resolver. See [docs on field resolving (data fetching)](data-fetching.md).
+ *
+ * @phpstan-import-type QueryPlanOptions from QueryPlan
+ *
+ * @phpstan-type Path list<string|int>
+ */
+class ResolveInfo
+{
+ /**
+ * The definition of the field being resolved.
+ *
+ * @api
+ */
+ public FieldDefinition $fieldDefinition;
+
+ /**
+ * The name of the field being resolved.
+ *
+ * @api
+ */
+ public string $fieldName;
+
+ /**
+ * Expected return type of the field being resolved.
+ *
+ * @api
+ */
+ public Type $returnType;
+
+ /**
+ * AST of all nodes referencing this field in the query.
+ *
+ * @api
+ *
+ * @var \ArrayObject<int, FieldNode>
+ */
+ public \ArrayObject $fieldNodes;
+
+ /**
+ * Parent type of the field being resolved.
+ *
+ * @api
+ */
+ public ObjectType $parentType;
+
+ /**
+ * Path to this field from the very root value. When fields are aliased, the path includes aliases.
+ *
+ * @api
+ *
+ * @var list<string|int>
+ *
+ * @phpstan-var Path
+ */
+ public array $path;
+
+ /**
+ * Path to this field from the very root value. This will never include aliases.
+ *
+ * @api
+ *
+ * @var list<string|int>
+ *
+ * @phpstan-var Path
+ */
+ public array $unaliasedPath;
+
+ /**
+ * Instance of a schema used for execution.
+ *
+ * @api
+ */
+ public Schema $schema;
+
+ /**
+ * AST of all fragments defined in query.
+ *
+ * @api
+ *
+ * @var array<string, FragmentDefinitionNode>
+ */
+ public array $fragments = [];
+
+ /**
+ * Root value passed to query execution.
+ *
+ * @api
+ *
+ * @var mixed
+ */
+ public $rootValue;
+
+ /**
+ * AST of operation definition node (query, mutation).
+ *
+ * @api
+ */
+ public OperationDefinitionNode $operation;
+
+ /**
+ * Array of variables passed to query execution.
+ *
+ * @api
+ *
+ * @var array<string, mixed>
+ */
+ public array $variableValues = [];
+
+ /**
+ * @param \ArrayObject<int, FieldNode> $fieldNodes
+ * @param list<string|int> $path
+ * @param array<string, FragmentDefinitionNode> $fragments
+ * @param mixed|null $rootValue
+ * @param array<string, mixed> $variableValues
+ * @param list<string|int> $unaliasedPath
+ *
+ * @phpstan-param Path $path
+ * @phpstan-param Path $unaliasedPath
+ */
+ public function __construct(
+ FieldDefinition $fieldDefinition,
+ \ArrayObject $fieldNodes,
+ ObjectType $parentType,
+ array $path,
+ Schema $schema,
+ array $fragments,
+ $rootValue,
+ OperationDefinitionNode $operation,
+ array $variableValues,
+ array $unaliasedPath = []
+ ) {
+ $this->fieldDefinition = $fieldDefinition;
+ $this->fieldName = $fieldDefinition->name;
+ $this->returnType = $fieldDefinition->getType();
+ $this->fieldNodes = $fieldNodes;
+ $this->parentType = $parentType;
+ $this->path = $path;
+ $this->unaliasedPath = $unaliasedPath;
+ $this->schema = $schema;
+ $this->fragments = $fragments;
+ $this->rootValue = $rootValue;
+ $this->operation = $operation;
+ $this->variableValues = $variableValues;
+ }
+
+ /**
+ * Returns names of all fields selected in query for `$this->fieldName` up to `$depth` levels.
+ *
+ * Example:
+ * {
+ * root {
+ * id
+ * nested {
+ * nested1
+ * nested2 {
+ * nested3
+ * }
+ * }
+ * }
+ * }
+ *
+ * Given this ResolveInfo instance is a part of root field resolution, and $depth === 1,
+ * this method will return:
+ * [
+ * 'id' => true,
+ * 'nested' => [
+ * 'nested1' => true,
+ * 'nested2' => true,
+ * ],
+ * ]
+ *
+ * This method does not consider conditional typed fragments.
+ * Use it with care for fields of interface and union types.
+ *
+ * @param int $depth How many levels to include in the output beyond the first
+ *
+ * @return array<string, mixed>
+ *
+ * @api
+ */
+ public function getFieldSelection(int $depth = 0): array
+ {
+ $fields = [];
+
+ foreach ($this->fieldNodes as $fieldNode) {
+ $selectionSet = $fieldNode->selectionSet;
+ if ($selectionSet !== null) {
+ $fields = array_merge_recursive(
+ $fields,
+ $this->foldSelectionSet($selectionSet, $depth)
+ );
+ }
+ }
+
+ return $fields;
+ }
+
+ /**
+ * Returns names and args of all fields selected in query for `$this->fieldName` up to `$depth` levels, including aliases.
+ *
+ * The result maps original field names to a map of selections for that field, including aliases.
+ * For each of those selections, you can find the following keys:
+ * - "args" contains the passed arguments for this field/alias (not on an union inline fragment)
+ * - "type" contains the related Type instance found (will be the same for all aliases of a field)
+ * - "selectionSet" contains potential nested fields of this field/alias (only on ObjectType). The structure is recursive from here.
+ * - "unions" contains potential object types contained in an UnionType (only on UnionType). The structure is recursive from here and will go through the selectionSet of the object types.
+ *
+ * Example:
+ * {
+ * root {
+ * id
+ * nested {
+ * nested1(myArg: 1)
+ * nested1Bis: nested1
+ * }
+ * alias1: nested {
+ * nested1(myArg: 2, mySecondAg: "test")
+ * }
+ * myUnion(myArg: 3) {
+ * ...on Nested {
+ * nested1(myArg: 4)
+ * }
+ * ...on MyCustomObject {
+ * nested3
+ * }
+ * }
+ * }
+ * }
+ *
+ * Given this ResolveInfo instance is a part of root field resolution,
+ * $depth === 1,
+ * and fields "nested" represents an ObjectType named "Nested",
+ * this method will return:
+ * [
+ * 'id' => [
+ * 'id' => [
+ * 'args' => [],
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\IntType Object ( ... )),
+ * ],
+ * ],
+ * 'nested' => [
+ * 'nested' => [
+ * 'args' => [],
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType Object ( ... )),
+ * 'selectionSet' => [
+ * 'nested1' => [
+ * 'nested1' => [
+ * 'args' => [
+ * 'myArg' => 1,
+ * ],
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
+ * ],
+ * 'nested1Bis' => [
+ * 'args' => [],
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
+ * ],
+ * ],
+ * ],
+ * ],
+ * ],
+ * 'alias1' => [
+ * 'alias1' => [
+ * 'args' => [],
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType Object ( ... )),
+ * 'selectionSet' => [
+ * 'nested1' => [
+ * 'nested1' => [
+ * 'args' => [
+ * 'myArg' => 2,
+ * 'mySecondAg' => "test",
+ * ],
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
+ * ],
+ * ],
+ * ],
+ * ],
+ * ],
+ * 'myUnion' => [
+ * 'myUnion' => [
+ * 'args' => [
+ * 'myArg' => 3,
+ * ],
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType Object ( ... )),
+ * 'unions' => [
+ * 'Nested' => [
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType Object ( ... )),
+ * 'selectionSet' => [
+ * 'nested1' => [
+ * 'nested1' => [
+ * 'args' => [
+ * 'myArg' => 4,
+ * ],
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
+ * ],
+ * ],
+ * ],
+ * ],
+ * 'MyCustomObject' => [
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Tests\Type\TestClasses\MyCustomType Object ( ... )),
+ * 'selectionSet' => [
+ * 'nested3' => [
+ * 'nested3' => [
+ * 'args' => [],
+ * 'type' => Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\StringType Object ( ... )),
+ * ],
+ * ],
+ * ],
+ * ],
+ * ],
+ * ],
+ * ],
+ * ]
+ *
+ * @param int $depth How many levels to include in the output beyond the first
+ *
+ * @throws \Exception
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return array<string, mixed>
+ *
+ * @api
+ */
+ public function getFieldSelectionWithAliases(int $depth = 0): array
+ {
+ $fields = [];
+
+ foreach ($this->fieldNodes as $fieldNode) {
+ $selectionSet = $fieldNode->selectionSet;
+ if ($selectionSet !== null) {
+ $field = $this->parentType->getField($fieldNode->name->value);
+ $fieldType = $field->getType();
+
+ $fields = array_merge_recursive(
+ $fields,
+ $this->foldSelectionWithAlias($selectionSet, $depth, $fieldType)
+ );
+ }
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @param QueryPlanOptions $options
+ *
+ * @throws \Exception
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function lookAhead(array $options = []): QueryPlan
+ {
+ return new QueryPlan(
+ $this->parentType,
+ $this->schema,
+ $this->fieldNodes,
+ $this->variableValues,
+ $this->fragments,
+ $options
+ );
+ }
+
+ /** @return array<string, bool> */
+ private function foldSelectionSet(SelectionSetNode $selectionSet, int $descend): array
+ {
+ /** @var array<string, bool> $fields */
+ $fields = [];
+
+ foreach ($selectionSet->selections as $selection) {
+ if ($selection instanceof FieldNode) {
+ $fields[$selection->name->value] = $descend > 0 && $selection->selectionSet !== null
+ ? array_merge_recursive(
+ $fields[$selection->name->value] ?? [],
+ $this->foldSelectionSet($selection->selectionSet, $descend - 1)
+ )
+ : true;
+ } elseif ($selection instanceof FragmentSpreadNode) {
+ $spreadName = $selection->name->value;
+ $fragment = $this->fragments[$spreadName] ?? null;
+ if ($fragment === null) {
+ continue;
+ }
+
+ $fields = array_merge_recursive(
+ $this->foldSelectionSet($fragment->selectionSet, $descend),
+ $fields
+ );
+ } elseif ($selection instanceof InlineFragmentNode) {
+ $fields = array_merge_recursive(
+ $this->foldSelectionSet($selection->selectionSet, $descend),
+ $fields
+ );
+ }
+ }
+
+ return $fields;
+ }
+
+ /**
+ * @throws \Exception
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return array<string>
+ */
+ private function foldSelectionWithAlias(SelectionSetNode $selectionSet, int $descend, Type $parentType): array
+ {
+ /** @var array<string, bool> $fields */
+ $fields = [];
+
+ if ($parentType instanceof WrappingType) {
+ $parentType = $parentType->getInnermostType();
+ }
+
+ foreach ($selectionSet->selections as $selection) {
+ if ($selection instanceof FieldNode) {
+ $fieldName = $selection->name->value;
+ $aliasName = $selection->alias->value ?? $fieldName;
+
+ if ($fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
+ continue;
+ }
+ assert($parentType instanceof HasFieldsType, 'ensured by query validation');
+
+ $aliasInfo = &$fields[$fieldName][$aliasName];
+
+ $fieldDef = $parentType->getField($fieldName);
+
+ $aliasInfo['args'] = Values::getArgumentValues($fieldDef, $selection, $this->variableValues);
+
+ $fieldType = $fieldDef->getType();
+
+ $namedFieldType = $fieldType;
+ if ($namedFieldType instanceof WrappingType) {
+ $namedFieldType = $namedFieldType->getInnermostType();
+ }
+
+ $aliasInfo['type'] = $namedFieldType;
+
+ if ($descend <= 0) {
+ continue;
+ }
+
+ $nestedSelectionSet = $selection->selectionSet;
+ if ($nestedSelectionSet === null) {
+ continue;
+ }
+
+ if ($namedFieldType instanceof UnionType) {
+ $aliasInfo['unions'] = $this->foldSelectionWithAlias($nestedSelectionSet, $descend, $fieldType);
+ continue;
+ }
+
+ $aliasInfo['selectionSet'] = $this->foldSelectionWithAlias($nestedSelectionSet, $descend - 1, $fieldType);
+ } elseif ($selection instanceof FragmentSpreadNode) {
+ $spreadName = $selection->name->value;
+ $fragment = $this->fragments[$spreadName] ?? null;
+ if ($fragment === null) {
+ continue;
+ }
+
+ $fieldType = $this->schema->getType($fragment->typeCondition->name->value);
+ assert($fieldType instanceof Type, 'ensured by query validation');
+
+ $fields = array_merge_recursive(
+ $this->foldSelectionWithAlias($fragment->selectionSet, $descend, $fieldType),
+ $fields
+ );
+ } elseif ($selection instanceof InlineFragmentNode) {
+ $typeCondition = $selection->typeCondition;
+ $fieldType = $typeCondition === null
+ ? $parentType
+ : $this->schema->getType($typeCondition->name->value);
+ assert($fieldType instanceof Type, 'ensured by query validation');
+
+ if ($parentType instanceof UnionType) {
+ assert($fieldType instanceof NamedType, 'ensured by query validation');
+ $fieldTypeInfo = &$fields[$fieldType->name()];
+ $fieldTypeInfo['type'] = $fieldType;
+ $fieldTypeInfo['selectionSet'] = $this->foldSelectionWithAlias($selection->selectionSet, $descend, $fieldType);
+ continue;
+ }
+
+ $fields = array_merge_recursive(
+ $this->foldSelectionWithAlias($selection->selectionSet, $descend, $fieldType),
+ $fields
+ );
+ }
+ }
+
+ return $fields;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ScalarType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ScalarType.php
new file mode 100644
index 00000000000..ac40a26b1b7
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/ScalarType.php
@@ -0,0 +1,77 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * Scalar Type Definition.
+ *
+ * The leaf values of any request and input values to arguments are
+ * Scalars (or Enums) and are defined with a name and a series of coercion
+ * functions used to ensure validity.
+ *
+ * Example:
+ *
+ * class OddType extends ScalarType
+ * {
+ * public $name = 'Odd',
+ * public function serialize($value)
+ * {
+ * return $value % 2 === 1 ? $value : null;
+ * }
+ * }
+ *
+ * @phpstan-type ScalarConfig array{
+ * name?: string|null,
+ * description?: string|null,
+ * astNode?: ScalarTypeDefinitionNode|null,
+ * extensionASTNodes?: array<ScalarTypeExtensionNode>|null
+ * }
+ */
+abstract class ScalarType extends Type implements OutputType, InputType, LeafType, NullableType, NamedType
+{
+ use NamedTypeImplementation;
+
+ public ?ScalarTypeDefinitionNode $astNode;
+
+ /** @var array<ScalarTypeExtensionNode> */
+ public array $extensionASTNodes;
+
+ /** @phpstan-var ScalarConfig */
+ public array $config;
+
+ /**
+ * @phpstan-param ScalarConfig $config
+ *
+ * @throws InvariantViolation
+ */
+ public function __construct(array $config = [])
+ {
+ $this->name = $config['name'] ?? $this->inferName();
+ $this->description = $config['description'] ?? $this->description ?? null;
+ $this->astNode = $config['astNode'] ?? null;
+ $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
+
+ $this->config = $config;
+ }
+
+ public function assertValid(): void
+ {
+ Utils::assertValidName($this->name);
+ }
+
+ public function astNode(): ?ScalarTypeDefinitionNode
+ {
+ return $this->astNode;
+ }
+
+ /** @return array<ScalarTypeExtensionNode> */
+ public function extensionASTNodes(): array
+ {
+ return $this->extensionASTNodes;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/StringType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/StringType.php
new file mode 100644
index 00000000000..22c9835a5b3
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/StringType.php
@@ -0,0 +1,60 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+class StringType extends ScalarType
+{
+ public string $name = Type::STRING;
+
+ public ?string $description
+ = 'The `String` scalar type represents textual data, represented as UTF-8
+character sequences. The String type is most often used by Automattic\WooCommerce\Vendor\GraphQL to
+represent free-form human-readable text.';
+
+ /** @throws SerializationError */
+ public function serialize($value): string
+ {
+ $canCast = is_scalar($value)
+ || (is_object($value) && method_exists($value, '__toString'))
+ || $value === null;
+
+ if (! $canCast) {
+ $notStringable = Utils::printSafe($value);
+ throw new SerializationError("String cannot represent value: {$notStringable}");
+ }
+
+ return (string) $value;
+ }
+
+ /** @throws Error */
+ public function parseValue($value): string
+ {
+ if (! is_string($value)) {
+ $notString = Utils::printSafeJson($value);
+ throw new Error("String cannot represent a non string value: {$notString}");
+ }
+
+ return $value;
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws Error
+ */
+ public function parseLiteral(Node $valueNode, ?array $variables = null): string
+ {
+ if ($valueNode instanceof StringValueNode) {
+ return $valueNode->value;
+ }
+
+ $notString = Printer::doPrint($valueNode);
+ throw new Error("String cannot represent a non string value: {$notString}", $valueNode);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Type.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Type.php
new file mode 100644
index 00000000000..d4892631266
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/Type.php
@@ -0,0 +1,350 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * Registry of built-in Automattic\WooCommerce\Vendor\GraphQL types and base class for all other types.
+ */
+abstract class Type implements \JsonSerializable
+{
+ public const INT = 'Int';
+ public const FLOAT = 'Float';
+ public const STRING = 'String';
+ public const BOOLEAN = 'Boolean';
+ public const ID = 'ID';
+
+ /** @var list<string> */
+ public const BUILT_IN_SCALAR_NAMES = [
+ self::INT,
+ self::FLOAT,
+ self::STRING,
+ self::BOOLEAN,
+ self::ID,
+ ];
+
+ /**
+ * @deprecated use {@see Type::BUILT_IN_SCALAR_NAMES}
+ *
+ * @var list<string>
+ */
+ public const STANDARD_TYPE_NAMES = self::BUILT_IN_SCALAR_NAMES;
+
+ /**
+ * Names of all built-in types: built-in scalars and introspection types.
+ *
+ * @see Type::BUILT_IN_SCALAR_NAMES for just the built-in scalar names.
+ *
+ * @var list<string>
+ */
+ public const BUILT_IN_TYPE_NAMES = [
+ ...self::BUILT_IN_SCALAR_NAMES,
+ ...Introspection::TYPE_NAMES,
+ ];
+
+ /** @var array<string, ScalarType>|null */
+ protected static ?array $builtInScalars;
+
+ /** @var array<string, Type&NamedType>|null */
+ protected static ?array $builtInTypes;
+
+ /**
+ * Returns the built-in Int scalar type.
+ *
+ * @api
+ */
+ public static function int(): ScalarType
+ {
+ return static::$builtInScalars[self::INT] ??= new IntType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ }
+
+ /**
+ * Returns the built-in Float scalar type.
+ *
+ * @api
+ */
+ public static function float(): ScalarType
+ {
+ return static::$builtInScalars[self::FLOAT] ??= new FloatType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ }
+
+ /**
+ * Returns the built-in String scalar type.
+ *
+ * @api
+ */
+ public static function string(): ScalarType
+ {
+ return static::$builtInScalars[self::STRING] ??= new StringType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ }
+
+ /**
+ * Returns the built-in Boolean scalar type.
+ *
+ * @api
+ */
+ public static function boolean(): ScalarType
+ {
+ return static::$builtInScalars[self::BOOLEAN] ??= new BooleanType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ }
+
+ /**
+ * Returns the built-in ID scalar type.
+ *
+ * @api
+ */
+ public static function id(): ScalarType
+ {
+ return static::$builtInScalars[self::ID] ??= new IDType(); // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ }
+
+ /**
+ * Wraps the given type in a list type.
+ *
+ * @template T of Type
+ *
+ * @param T|callable():T $type
+ *
+ * @return ListOfType<T>
+ *
+ * @api
+ */
+ public static function listOf($type): ListOfType
+ {
+ return new ListOfType($type);
+ }
+
+ /**
+ * Wraps the given type in a non-null type.
+ *
+ * @param NonNull|(NullableType&Type)|callable():(NullableType&Type) $type
+ *
+ * @api
+ */
+ public static function nonNull($type): NonNull
+ {
+ if ($type instanceof NonNull) {
+ return $type;
+ }
+
+ return new NonNull($type);
+ }
+
+ /**
+ * Returns all built-in types: built-in scalars and introspection types.
+ *
+ * @api
+ *
+ * @return array<string, Type&NamedType>
+ */
+ public static function builtInTypes(): array
+ {
+ return self::$builtInTypes ??= array_merge(
+ Introspection::getTypes(),
+ self::builtInScalars()
+ );
+ }
+
+ /**
+ * Returns all built-in scalar types.
+ *
+ * @api
+ *
+ * @return array<string, ScalarType>
+ */
+ public static function builtInScalars(): array
+ {
+ return [
+ self::INT => static::int(),
+ self::FLOAT => static::float(),
+ self::STRING => static::string(),
+ self::BOOLEAN => static::boolean(),
+ self::ID => static::id(),
+ ];
+ }
+
+ /**
+ * Returns all built-in scalar types.
+ *
+ * @deprecated use {@see Type::builtInScalars()}
+ *
+ * @return array<string, ScalarType>
+ */
+ public static function getStandardTypes(): array
+ {
+ return self::builtInScalars();
+ }
+
+ /**
+ * Allows partially or completely overriding the standard types globally.
+ *
+ * @deprecated prefer per-schema scalar overrides via {@see SchemaConfig::$types} or {@see SchemaConfig::$typeLoader}
+ *
+ * @param array<ScalarType> $types
+ *
+ * @throws InvariantViolation
+ */
+ public static function overrideStandardTypes(array $types): void
+ {
+ // Reset caches that might contain instances of built-in scalars
+ static::$builtInTypes = null;
+ Introspection::resetCachedInstances();
+ Directive::resetCachedInstances();
+
+ foreach ($types as $type) {
+ // @phpstan-ignore-next-line generic type is not enforced by PHP
+ if (! $type instanceof ScalarType) {
+ $typeClass = ScalarType::class;
+ $notType = Utils::printSafe($type);
+ throw new InvariantViolation("Expecting instance of {$typeClass}, got {$notType}");
+ }
+
+ if (! self::isBuiltInScalarName($type->name)) {
+ $standardTypeNames = implode(', ', self::BUILT_IN_SCALAR_NAMES);
+ $notStandardTypeName = Utils::printSafe($type->name);
+ throw new InvariantViolation("Expecting one of the following names for a standard type: {$standardTypeNames}; got {$notStandardTypeName}");
+ }
+
+ static::$builtInScalars[$type->name] = $type;
+ }
+ }
+
+ /**
+ * Determines if the given type is a built-in scalar (Int, Float, String, Boolean, ID).
+ *
+ * Does not unwrap NonNull/List wrappers — checks the type instance directly.
+ * ScalarType is a NamedType, so {@see Type::getNamedType()} is unnecessary.
+ *
+ * @param mixed $type
+ *
+ * @phpstan-assert-if-true ScalarType $type
+ *
+ * @api
+ */
+ public static function isBuiltInScalar($type): bool
+ {
+ return $type instanceof ScalarType
+ && self::isBuiltInScalarName($type->name);
+ }
+
+ /** Checks if the given name is one of the built-in scalar type names (ID, String, Int, Float, Boolean). */
+ public static function isBuiltInScalarName(string $name): bool
+ {
+ return in_array($name, self::BUILT_IN_SCALAR_NAMES, true);
+ }
+
+ /**
+ * Determines if the given type is an input type.
+ *
+ * @param mixed $type
+ *
+ * @api
+ */
+ public static function isInputType($type): bool
+ {
+ return self::getNamedType($type) instanceof InputType;
+ }
+
+ /**
+ * Returns the underlying named type of the given type.
+ *
+ * @return (Type&NamedType)|null
+ *
+ * @phpstan-return ($type is null ? null : Type&NamedType)
+ *
+ * @api
+ */
+ public static function getNamedType(?Type $type): ?Type
+ {
+ if ($type instanceof WrappingType) {
+ return $type->getInnermostType();
+ }
+
+ assert($type === null || $type instanceof NamedType, 'only other option');
+
+ return $type;
+ }
+
+ /**
+ * Determines if the given type is an output type.
+ *
+ * @param mixed $type
+ *
+ * @api
+ */
+ public static function isOutputType($type): bool
+ {
+ return self::getNamedType($type) instanceof OutputType;
+ }
+
+ /**
+ * Determines if the given type is a leaf type.
+ *
+ * @param mixed $type
+ *
+ * @api
+ */
+ public static function isLeafType($type): bool
+ {
+ return $type instanceof LeafType;
+ }
+
+ /**
+ * Determines if the given type is a composite type.
+ *
+ * @param mixed $type
+ *
+ * @api
+ */
+ public static function isCompositeType($type): bool
+ {
+ return $type instanceof CompositeType;
+ }
+
+ /**
+ * Determines if the given type is an abstract type.
+ *
+ * @param mixed $type
+ *
+ * @api
+ */
+ public static function isAbstractType($type): bool
+ {
+ return $type instanceof AbstractType;
+ }
+
+ /**
+ * Unwraps a potentially non-null type to return the underlying nullable type.
+ *
+ * @return Type&NullableType
+ *
+ * @api
+ */
+ public static function getNullableType(Type $type): Type
+ {
+ if ($type instanceof NonNull) {
+ return $type->getWrappedType();
+ }
+
+ assert($type instanceof NullableType, 'only other option');
+
+ return $type;
+ }
+
+ abstract public function toString(): string;
+
+ public function __toString(): string
+ {
+ return $this->toString();
+ }
+
+ #[\ReturnTypeWillChange]
+ public function jsonSerialize(): string
+ {
+ return $this->toString();
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnionType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnionType.php
new file mode 100644
index 00000000000..76838906692
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnionType.php
@@ -0,0 +1,151 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-import-type ResolveType from AbstractType
+ * @phpstan-import-type ResolveValue from AbstractType
+ *
+ * @phpstan-type ObjectTypeReference ObjectType|callable(): ObjectType
+ * @phpstan-type UnionConfig array{
+ * name?: string|null,
+ * description?: string|null,
+ * types: iterable<ObjectTypeReference>|callable(): iterable<ObjectTypeReference>,
+ * resolveType?: ResolveType|null,
+ * resolveValue?: ResolveValue|null,
+ * astNode?: UnionTypeDefinitionNode|null,
+ * extensionASTNodes?: array<UnionTypeExtensionNode>|null
+ * }
+ */
+class UnionType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType
+{
+ use NamedTypeImplementation;
+
+ public ?UnionTypeDefinitionNode $astNode;
+
+ /** @var array<UnionTypeExtensionNode> */
+ public array $extensionASTNodes;
+
+ /** @phpstan-var UnionConfig */
+ public array $config;
+
+ /**
+ * Lazily initialized.
+ *
+ * @var array<int, ObjectType>
+ */
+ private array $types;
+
+ /**
+ * Lazily initialized.
+ *
+ * @var array<string, bool>
+ */
+ private array $possibleTypeNames;
+
+ /**
+ * @phpstan-param UnionConfig $config
+ *
+ * @throws InvariantViolation
+ */
+ public function __construct(array $config)
+ {
+ $this->name = $config['name'] ?? $this->inferName();
+ $this->description = $config['description'] ?? $this->description ?? null;
+ $this->astNode = $config['astNode'] ?? null;
+ $this->extensionASTNodes = $config['extensionASTNodes'] ?? [];
+
+ $this->config = $config;
+ }
+
+ /** @throws InvariantViolation */
+ public function isPossibleType(Type $type): bool
+ {
+ if (! $type instanceof ObjectType) {
+ return false;
+ }
+
+ if (! isset($this->possibleTypeNames)) {
+ $this->possibleTypeNames = [];
+ foreach ($this->getTypes() as $possibleType) {
+ $this->possibleTypeNames[$possibleType->name] = true;
+ }
+ }
+
+ return isset($this->possibleTypeNames[$type->name]);
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<int, ObjectType>
+ */
+ public function getTypes(): array
+ {
+ if (! isset($this->types)) {
+ $this->types = [];
+
+ $types = $this->config['types'] ?? null; // @phpstan-ignore nullCoalesce.initializedProperty (unnecessary according to types, but can happen during runtime)
+ if (is_callable($types)) {
+ $types = $types();
+ }
+
+ if (! is_iterable($types)) {
+ throw new InvariantViolation("Must provide iterable of types or a callable which returns such an iterable for Union {$this->name}.");
+ }
+
+ foreach ($types as $type) {
+ $this->types[] = Schema::resolveType($type); // @phpstan-ignore argument.templateType
+ }
+ }
+
+ return $this->types;
+ }
+
+ public function resolveValue($objectValue, $context, ResolveInfo $info)
+ {
+ if (isset($this->config['resolveValue'])) {
+ return ($this->config['resolveValue'])($objectValue, $context, $info);
+ }
+
+ return $objectValue;
+ }
+
+ public function resolveType($objectValue, $context, ResolveInfo $info)
+ {
+ if (isset($this->config['resolveType'])) {
+ return ($this->config['resolveType'])($objectValue, $context, $info);
+ }
+
+ return null;
+ }
+
+ public function assertValid(): void
+ {
+ Utils::assertValidName($this->name);
+
+ $resolveType = $this->config['resolveType'] ?? null;
+ // @phpstan-ignore-next-line unnecessary according to types, but can happen during runtime
+ if (isset($resolveType) && ! is_callable($resolveType)) {
+ $notCallable = Utils::printSafe($resolveType);
+ throw new InvariantViolation("{$this->name} must provide \"resolveType\" as null or a callable, but got: {$notCallable}.");
+ }
+ }
+
+ public function astNode(): ?UnionTypeDefinitionNode
+ {
+ return $this->astNode;
+ }
+
+ /** @return array<UnionTypeExtensionNode> */
+ public function extensionASTNodes(): array
+ {
+ return $this->extensionASTNodes;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnmodifiedType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnmodifiedType.php
new file mode 100644
index 00000000000..ae503758000
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnmodifiedType.php
@@ -0,0 +1,15 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+/*
+export type GraphQLUnmodifiedType =
+GraphQLScalarType |
+GraphQLObjectType |
+GraphQLInterfaceType |
+GraphQLUnionType |
+GraphQLEnumType |
+GraphQLInputObjectType;
+*/
+
+interface UnmodifiedType {}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnresolvedFieldDefinition.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnresolvedFieldDefinition.php
new file mode 100644
index 00000000000..bcaa0dcc4cc
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/UnresolvedFieldDefinition.php
@@ -0,0 +1,50 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+/**
+ * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition
+ *
+ * @phpstan-type DefinitionResolver callable(): (FieldDefinition|(Type&OutputType)|UnnamedFieldDefinitionConfig)
+ */
+class UnresolvedFieldDefinition
+{
+ private string $name;
+
+ /**
+ * @var callable
+ *
+ * @phpstan-var DefinitionResolver
+ */
+ private $definitionResolver;
+
+ /** @param DefinitionResolver $definitionResolver */
+ public function __construct(string $name, callable $definitionResolver)
+ {
+ $this->name = $name;
+ $this->definitionResolver = $definitionResolver;
+ }
+
+ public function getName(): string
+ {
+ return $this->name;
+ }
+
+ public function resolve(): FieldDefinition
+ {
+ $field = ($this->definitionResolver)();
+
+ if ($field instanceof FieldDefinition) {
+ return $field;
+ }
+
+ if ($field instanceof Type) {
+ return new FieldDefinition([
+ 'name' => $this->name,
+ 'type' => $field,
+ ]);
+ }
+
+ return new FieldDefinition($field + ['name' => $this->name]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/WrappingType.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/WrappingType.php
new file mode 100644
index 00000000000..da0b8cec42d
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Definition/WrappingType.php
@@ -0,0 +1,16 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Definition;
+
+interface WrappingType
+{
+ /** Return the wrapped type, which may itself be a wrapping type. */
+ public function getWrappedType(): Type;
+
+ /**
+ * Return the innermost wrapped type, which is guaranteed to be a named type.
+ *
+ * @return Type&NamedType
+ */
+ public function getInnermostType(): NamedType;
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Introspection.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Introspection.php
new file mode 100644
index 00000000000..7d1c05cd59a
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Introspection.php
@@ -0,0 +1,834 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\GraphQL;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\DirectiveLocation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumValueDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\WrappingType;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * @phpstan-type IntrospectionOptions array{
+ * descriptions?: bool,
+ * directiveIsRepeatable?: bool,
+ * schemaDescription?: bool,
+ * typeIsOneOf?: bool,
+ * }
+ *
+ * Available options:
+ * - descriptions
+ * Include descriptions in the introspection result?
+ * Default: true
+ * - directiveIsRepeatable
+ * Include field `isRepeatable` for directives?
+ * Default: false
+ * - typeIsOneOf
+ * Include field `isOneOf` for types?
+ * Default: false
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Type\IntrospectionTest
+ */
+class Introspection
+{
+ public const SCHEMA_FIELD_NAME = '__schema';
+ public const TYPE_FIELD_NAME = '__type';
+ public const TYPE_NAME_FIELD_NAME = '__typename';
+
+ public const SCHEMA_OBJECT_NAME = '__Schema';
+ public const TYPE_OBJECT_NAME = '__Type';
+ public const DIRECTIVE_OBJECT_NAME = '__Directive';
+ public const FIELD_OBJECT_NAME = '__Field';
+ public const INPUT_VALUE_OBJECT_NAME = '__InputValue';
+ public const ENUM_VALUE_OBJECT_NAME = '__EnumValue';
+ public const TYPE_KIND_ENUM_NAME = '__TypeKind';
+ public const DIRECTIVE_LOCATION_ENUM_NAME = '__DirectiveLocation';
+
+ public const TYPE_NAMES = [
+ self::SCHEMA_OBJECT_NAME,
+ self::TYPE_OBJECT_NAME,
+ self::DIRECTIVE_OBJECT_NAME,
+ self::FIELD_OBJECT_NAME,
+ self::INPUT_VALUE_OBJECT_NAME,
+ self::ENUM_VALUE_OBJECT_NAME,
+ self::TYPE_KIND_ENUM_NAME,
+ self::DIRECTIVE_LOCATION_ENUM_NAME,
+ ];
+
+ /** @var array<string, mixed>|null */
+ protected static ?array $cachedInstances;
+
+ /**
+ * @param IntrospectionOptions $options
+ *
+ * @api
+ */
+ public static function getIntrospectionQuery(array $options = []): string
+ {
+ $optionsWithDefaults = array_merge([
+ 'descriptions' => true,
+ 'directiveIsRepeatable' => false,
+ 'schemaDescription' => false,
+ 'typeIsOneOf' => false,
+ ], $options);
+
+ $descriptions = $optionsWithDefaults['descriptions']
+ ? 'description'
+ : '';
+ $directiveIsRepeatable = $optionsWithDefaults['directiveIsRepeatable']
+ ? 'isRepeatable'
+ : '';
+ $schemaDescription = $optionsWithDefaults['schemaDescription']
+ ? $descriptions
+ : '';
+ $typeIsOneOf = $optionsWithDefaults['typeIsOneOf']
+ ? 'isOneOf'
+ : '';
+
+ return <<<GRAPHQL
+ query IntrospectionQuery {
+ __schema {
+ {$schemaDescription}
+ queryType { name }
+ mutationType { name }
+ subscriptionType { name }
+ types {
+ ...FullType
+ }
+ directives {
+ name
+ {$descriptions}
+ args(includeDeprecated: true) {
+ ...InputValue
+ }
+ {$directiveIsRepeatable}
+ locations
+ }
+ }
+ }
+
+ fragment FullType on __Type {
+ kind
+ name
+ {$descriptions}
+ {$typeIsOneOf}
+ fields(includeDeprecated: true) {
+ name
+ {$descriptions}
+ args(includeDeprecated: true) {
+ ...InputValue
+ }
+ type {
+ ...TypeRef
+ }
+ isDeprecated
+ deprecationReason
+ }
+ inputFields(includeDeprecated: true) {
+ ...InputValue
+ }
+ interfaces {
+ ...TypeRef
+ }
+ enumValues(includeDeprecated: true) {
+ name
+ {$descriptions}
+ isDeprecated
+ deprecationReason
+ }
+ possibleTypes {
+ ...TypeRef
+ }
+ }
+
+ fragment InputValue on __InputValue {
+ name
+ {$descriptions}
+ type { ...TypeRef }
+ defaultValue
+ isDeprecated
+ deprecationReason
+ }
+
+ fragment TypeRef on __Type {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ ofType {
+ kind
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+GRAPHQL;
+ }
+
+ /**
+ * Build an introspection query from a Schema.
+ *
+ * Introspection is useful for utilities that care about type and field
+ * relationships, but do not need to traverse through those relationships.
+ *
+ * This is the inverse of BuildClientSchema::build(). The primary use case is
+ * outside the server context, for instance when doing schema comparisons.
+ *
+ * @param IntrospectionOptions $options
+ *
+ * @throws \Exception
+ * @throws \JsonException
+ * @throws InvariantViolation
+ *
+ * @return array<string, array<mixed>>
+ *
+ * @api
+ */
+ public static function fromSchema(Schema $schema, array $options = []): array
+ {
+ $optionsWithDefaults = array_merge([
+ 'directiveIsRepeatable' => true,
+ 'schemaDescription' => true,
+ 'typeIsOneOf' => true,
+ ], $options);
+
+ $result = GraphQL::executeQuery(
+ $schema,
+ self::getIntrospectionQuery($optionsWithDefaults)
+ );
+
+ $data = $result->data;
+ if ($data === null) {
+ $noDataResult = Utils::printSafeJson($result);
+ throw new InvariantViolation("Introspection query returned no data: {$noDataResult}.");
+ }
+
+ return $data;
+ }
+
+ /** @param Type&NamedType $type */
+ public static function isIntrospectionType(NamedType $type): bool
+ {
+ return in_array($type->name, self::TYPE_NAMES, true);
+ }
+
+ /** @return array<string, Type&NamedType> */
+ public static function getTypes(): array
+ {
+ return [
+ self::SCHEMA_OBJECT_NAME => self::_schema(),
+ self::TYPE_OBJECT_NAME => self::_type(),
+ self::DIRECTIVE_OBJECT_NAME => self::_directive(),
+ self::FIELD_OBJECT_NAME => self::_field(),
+ self::INPUT_VALUE_OBJECT_NAME => self::_inputValue(),
+ self::ENUM_VALUE_OBJECT_NAME => self::_enumValue(),
+ self::TYPE_KIND_ENUM_NAME => self::_typeKind(),
+ self::DIRECTIVE_LOCATION_ENUM_NAME => self::_directiveLocation(),
+ ];
+ }
+
+ public static function _schema(): ObjectType
+ {
+ return self::$cachedInstances[self::SCHEMA_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ 'name' => self::SCHEMA_OBJECT_NAME,
+ 'isIntrospection' => true,
+ 'description' => 'A Automattic\WooCommerce\Vendor\GraphQL Schema defines the capabilities of a Automattic\WooCommerce\Vendor\GraphQL '
+ . 'server. It exposes all available types and directives on '
+ . 'the server, as well as the entry points for query, mutation, and '
+ . 'subscription operations.',
+ 'fields' => [
+ 'description' => [
+ 'type' => Type::string(),
+ 'resolve' => static fn (Schema $schema): ?string => $schema->description,
+ ],
+ 'types' => [
+ 'description' => 'A list of all types supported by this server.',
+ 'type' => new NonNull(new ListOfType(new NonNull(self::_type()))),
+ 'resolve' => static fn (Schema $schema): array => $schema->getTypeMap(),
+ ],
+ 'queryType' => [
+ 'description' => 'The type that query operations will be rooted at.',
+ 'type' => new NonNull(self::_type()),
+ 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getQueryType(),
+ ],
+ 'mutationType' => [
+ 'description' => 'If this server supports mutation, the type that mutation operations will be rooted at.',
+ 'type' => self::_type(),
+ 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getMutationType(),
+ ],
+ 'subscriptionType' => [
+ 'description' => 'If this server support subscription, the type that subscription operations will be rooted at.',
+ 'type' => self::_type(),
+ 'resolve' => static fn (Schema $schema): ?ObjectType => $schema->getSubscriptionType(),
+ ],
+ 'directives' => [
+ 'description' => 'A list of all directives supported by this server.',
+ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_directive()))),
+ 'resolve' => static fn (Schema $schema): array => $schema->getDirectives(),
+ ],
+ ],
+ ]);
+ }
+
+ public static function _type(): ObjectType
+ {
+ return self::$cachedInstances[self::TYPE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ 'name' => self::TYPE_OBJECT_NAME,
+ 'isIntrospection' => true,
+ 'description' => 'The fundamental unit of any Automattic\WooCommerce\Vendor\GraphQL Schema is the type. There are '
+ . 'many kinds of types in Automattic\WooCommerce\Vendor\GraphQL as represented by the `__TypeKind` enum.'
+ . "\n\n"
+ . 'Depending on the kind of a type, certain fields describe '
+ . 'information about that type. Scalar types provide no information '
+ . 'beyond a name and description, while Enum types provide their values. '
+ . 'Object and Interface types provide the fields they describe. Abstract '
+ . 'types, Union and Interface, provide the Object types possible '
+ . 'at runtime. List and NonNull types compose other types.',
+ 'fields' => static fn (): array => [
+ 'kind' => [
+ 'type' => Type::nonNull(self::_typeKind()),
+ 'resolve' => static function (Type $type): string {
+ switch (true) {
+ case $type instanceof ListOfType:
+ return TypeKind::LIST;
+ case $type instanceof NonNull:
+ return TypeKind::NON_NULL;
+ case $type instanceof ScalarType:
+ return TypeKind::SCALAR;
+ case $type instanceof ObjectType:
+ return TypeKind::OBJECT;
+ case $type instanceof EnumType:
+ return TypeKind::ENUM;
+ case $type instanceof InputObjectType:
+ return TypeKind::INPUT_OBJECT;
+ case $type instanceof InterfaceType:
+ return TypeKind::INTERFACE;
+ case $type instanceof UnionType:
+ return TypeKind::UNION;
+ default:
+ $safeType = Utils::printSafe($type);
+ throw new \Exception("Unknown kind of type: {$safeType}");
+ }
+ },
+ ],
+ 'name' => [
+ 'type' => Type::string(),
+ 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType
+ ? $type->name
+ : null,
+ ],
+ 'description' => [
+ 'type' => Type::string(),
+ 'resolve' => static fn (Type $type): ?string => $type instanceof NamedType
+ ? $type->description
+ : null,
+ ],
+ 'fields' => [
+ 'type' => Type::listOf(Type::nonNull(self::_field())),
+ 'args' => [
+ 'includeDeprecated' => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'defaultValue' => false,
+ ],
+ ],
+ 'resolve' => static function (Type $type, $args): ?array {
+ if ($type instanceof ObjectType || $type instanceof InterfaceType) {
+ $fields = $type->getVisibleFields();
+
+ if (! $args['includeDeprecated']) {
+ return array_filter(
+ $fields,
+ static fn (FieldDefinition $field): bool => ! $field->isDeprecated()
+ );
+ }
+
+ return $fields;
+ }
+
+ return null;
+ },
+ ],
+ 'interfaces' => [
+ 'type' => Type::listOf(Type::nonNull(self::_type())),
+ 'resolve' => static fn ($type): ?array => $type instanceof ObjectType || $type instanceof InterfaceType
+ ? $type->getInterfaces()
+ : null,
+ ],
+ 'possibleTypes' => [
+ 'type' => Type::listOf(Type::nonNull(self::_type())),
+ 'resolve' => static fn ($type, $args, $context, ResolveInfo $info): ?array => $type instanceof InterfaceType || $type instanceof UnionType
+ ? $info->schema->getPossibleTypes($type)
+ : null,
+ ],
+ 'enumValues' => [
+ 'type' => Type::listOf(Type::nonNull(self::_enumValue())),
+ 'args' => [
+ 'includeDeprecated' => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'defaultValue' => false,
+ ],
+ ],
+ 'resolve' => static function ($type, $args): ?array {
+ if ($type instanceof EnumType) {
+ $values = $type->getValues();
+
+ if (! $args['includeDeprecated']) {
+ return array_filter(
+ $values,
+ static fn (EnumValueDefinition $value): bool => ! $value->isDeprecated()
+ );
+ }
+
+ return $values;
+ }
+
+ return null;
+ },
+ ],
+ 'inputFields' => [
+ 'type' => Type::listOf(Type::nonNull(self::_inputValue())),
+ 'args' => [
+ 'includeDeprecated' => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'defaultValue' => false,
+ ],
+ ],
+ 'resolve' => static function ($type, $args): ?array {
+ if ($type instanceof InputObjectType) {
+ $fields = $type->getFields();
+
+ if (! $args['includeDeprecated']) {
+ return array_filter(
+ $fields,
+ static fn (InputObjectField $field): bool => ! $field->isDeprecated(),
+ );
+ }
+
+ return $fields;
+ }
+
+ return null;
+ },
+ ],
+ 'ofType' => [
+ 'type' => self::_type(),
+ 'resolve' => static fn ($type): ?Type => $type instanceof WrappingType
+ ? $type->getWrappedType()
+ : null,
+ ],
+ 'isOneOf' => [
+ 'type' => Type::boolean(),
+ 'resolve' => static fn ($type): ?bool => $type instanceof InputObjectType
+ ? $type->isOneOf()
+ : null,
+ ],
+ ],
+ ]);
+ }
+
+ public static function _typeKind(): EnumType
+ {
+ return self::$cachedInstances[self::TYPE_KIND_ENUM_NAME] ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ 'name' => self::TYPE_KIND_ENUM_NAME,
+ 'isIntrospection' => true,
+ 'description' => 'An enum describing what kind of type a given `__Type` is.',
+ 'values' => [
+ 'SCALAR' => [
+ 'value' => TypeKind::SCALAR,
+ 'description' => 'Indicates this type is a scalar.',
+ ],
+ 'OBJECT' => [
+ 'value' => TypeKind::OBJECT,
+ 'description' => 'Indicates this type is an object. `fields` and `interfaces` are valid fields.',
+ ],
+ 'INTERFACE' => [
+ 'value' => TypeKind::INTERFACE,
+ 'description' => 'Indicates this type is an interface. `fields`, `interfaces`, and `possibleTypes` are valid fields.',
+ ],
+ 'UNION' => [
+ 'value' => TypeKind::UNION,
+ 'description' => 'Indicates this type is a union. `possibleTypes` is a valid field.',
+ ],
+ 'ENUM' => [
+ 'value' => TypeKind::ENUM,
+ 'description' => 'Indicates this type is an enum. `enumValues` is a valid field.',
+ ],
+ 'INPUT_OBJECT' => [
+ 'value' => TypeKind::INPUT_OBJECT,
+ 'description' => 'Indicates this type is an input object. `inputFields` is a valid field.',
+ ],
+ 'LIST' => [
+ 'value' => TypeKind::LIST,
+ 'description' => 'Indicates this type is a list. `ofType` is a valid field.',
+ ],
+ 'NON_NULL' => [
+ 'value' => TypeKind::NON_NULL,
+ 'description' => 'Indicates this type is a non-null. `ofType` is a valid field.',
+ ],
+ ],
+ ]);
+ }
+
+ public static function _field(): ObjectType
+ {
+ return self::$cachedInstances[self::FIELD_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ 'name' => self::FIELD_OBJECT_NAME,
+ 'isIntrospection' => true,
+ 'description' => 'Object and Interface types are described by a list of Fields, each of '
+ . 'which has a name, potentially a list of arguments, and a return type.',
+ 'fields' => static fn (): array => [
+ 'name' => [
+ 'type' => Type::nonNull(Type::string()),
+ 'resolve' => static fn (FieldDefinition $field): string => $field->name,
+ ],
+ 'description' => [
+ 'type' => Type::string(),
+ 'resolve' => static fn (FieldDefinition $field): ?string => $field->description,
+ ],
+ 'args' => [
+ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
+ 'args' => [
+ 'includeDeprecated' => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'defaultValue' => false,
+ ],
+ ],
+ 'resolve' => static function (FieldDefinition $field, $args): array {
+ $values = $field->args;
+
+ if (! $args['includeDeprecated']) {
+ return array_filter(
+ $values,
+ static fn (Argument $value): bool => ! $value->isDeprecated(),
+ );
+ }
+
+ return $values;
+ },
+ ],
+ 'type' => [
+ 'type' => Type::nonNull(self::_type()),
+ 'resolve' => static fn (FieldDefinition $field): Type => $field->getType(),
+ ],
+ 'isDeprecated' => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'resolve' => static fn (FieldDefinition $field): bool => $field->isDeprecated(),
+ ],
+ 'deprecationReason' => [
+ 'type' => Type::string(),
+ 'resolve' => static fn (FieldDefinition $field): ?string => $field->deprecationReason,
+ ],
+ ],
+ ]);
+ }
+
+ public static function _inputValue(): ObjectType
+ {
+ return self::$cachedInstances[self::INPUT_VALUE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ 'name' => self::INPUT_VALUE_OBJECT_NAME,
+ 'isIntrospection' => true,
+ 'description' => 'Arguments provided to Fields or Directives and the input fields of an '
+ . 'InputObject are represented as Input Values which describe their type '
+ . 'and optionally a default value.',
+ 'fields' => static fn (): array => [
+ 'name' => [
+ 'type' => Type::nonNull(Type::string()),
+ /** @param Argument|InputObjectField $inputValue */
+ 'resolve' => static fn ($inputValue): string => $inputValue->name,
+ ],
+ 'description' => [
+ 'type' => Type::string(),
+ /** @param Argument|InputObjectField $inputValue */
+ 'resolve' => static fn ($inputValue): ?string => $inputValue->description,
+ ],
+ 'type' => [
+ 'type' => Type::nonNull(self::_type()),
+ /** @param Argument|InputObjectField $inputValue */
+ 'resolve' => static fn ($inputValue): Type => $inputValue->getType(),
+ ],
+ 'defaultValue' => [
+ 'type' => Type::string(),
+ 'description' => 'A GraphQL-formatted string representing the default value for this input value.',
+ /** @param Argument|InputObjectField $inputValue */
+ 'resolve' => static function ($inputValue): ?string {
+ if ($inputValue->defaultValueExists()) {
+ $defaultValueAST = AST::astFromValue($inputValue->defaultValue, $inputValue->getType());
+
+ if ($defaultValueAST === null) {
+ $inconvertibleDefaultValue = Utils::printSafe($inputValue->defaultValue);
+ throw new InvariantViolation("Unable to convert defaultValue of argument {$inputValue->name} into AST: {$inconvertibleDefaultValue}.");
+ }
+
+ return Printer::doPrint($defaultValueAST);
+ }
+
+ return null;
+ },
+ ],
+ 'isDeprecated' => [
+ 'type' => Type::nonNull(Type::boolean()),
+ /** @param Argument|InputObjectField $inputValue */
+ 'resolve' => static fn ($inputValue): bool => $inputValue->isDeprecated(),
+ ],
+ 'deprecationReason' => [
+ 'type' => Type::string(),
+ /** @param Argument|InputObjectField $inputValue */
+ 'resolve' => static fn ($inputValue): ?string => $inputValue->deprecationReason,
+ ],
+ ],
+ ]);
+ }
+
+ public static function _enumValue(): ObjectType
+ {
+ return self::$cachedInstances[self::ENUM_VALUE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ 'name' => self::ENUM_VALUE_OBJECT_NAME,
+ 'isIntrospection' => true,
+ 'description' => 'One possible value for a given Enum. Enum values are unique values, not '
+ . 'a placeholder for a string or numeric value. However an Enum value is '
+ . 'returned in a JSON response as a string.',
+ 'fields' => [
+ 'name' => [
+ 'type' => Type::nonNull(Type::string()),
+ 'resolve' => static fn (EnumValueDefinition $enumValue): string => $enumValue->name,
+ ],
+ 'description' => [
+ 'type' => Type::string(),
+ 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->description,
+ ],
+ 'isDeprecated' => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'resolve' => static fn (EnumValueDefinition $enumValue): bool => $enumValue->isDeprecated(),
+ ],
+ 'deprecationReason' => [
+ 'type' => Type::string(),
+ 'resolve' => static fn (EnumValueDefinition $enumValue): ?string => $enumValue->deprecationReason,
+ ],
+ ],
+ ]);
+ }
+
+ public static function _directive(): ObjectType
+ {
+ return self::$cachedInstances[self::DIRECTIVE_OBJECT_NAME] ??= new ObjectType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ 'name' => self::DIRECTIVE_OBJECT_NAME,
+ 'isIntrospection' => true,
+ 'description' => 'A Directive provides a way to describe alternate runtime execution and '
+ . 'type validation behavior in a Automattic\WooCommerce\Vendor\GraphQL document.'
+ . "\n\nIn some cases, you need to provide options to alter GraphQL's "
+ . 'execution behavior in ways field arguments will not suffice, such as '
+ . 'conditionally including or skipping a field. Directives provide this by '
+ . 'describing additional information to the executor.',
+ 'fields' => [
+ 'name' => [
+ 'type' => Type::nonNull(Type::string()),
+ 'resolve' => static fn (Directive $directive): string => $directive->name,
+ ],
+ 'description' => [
+ 'type' => Type::string(),
+ 'resolve' => static fn (Directive $directive): ?string => $directive->description,
+ ],
+ 'isRepeatable' => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'resolve' => static fn (Directive $directive): bool => $directive->isRepeatable,
+ ],
+ 'locations' => [
+ 'type' => Type::nonNull(Type::listOf(Type::nonNull(
+ self::_directiveLocation()
+ ))),
+ 'resolve' => static fn (Directive $directive): array => $directive->locations,
+ ],
+ 'args' => [
+ 'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
+ 'args' => [
+ 'includeDeprecated' => [
+ 'type' => Type::nonNull(Type::boolean()),
+ 'defaultValue' => false,
+ ],
+ ],
+ 'resolve' => static function (Directive $directive, $args): array {
+ $values = $directive->args;
+
+ if (! $args['includeDeprecated']) {
+ return array_filter(
+ $values,
+ static fn (Argument $value): bool => ! $value->isDeprecated(),
+ );
+ }
+
+ return $values;
+ },
+ ],
+ ],
+ ]);
+ }
+
+ public static function _directiveLocation(): EnumType
+ {
+ return self::$cachedInstances[self::DIRECTIVE_LOCATION_ENUM_NAME] ??= new EnumType([ // @phpstan-ignore missingType.checkedException (static configuration is known to be correct)
+ 'name' => self::DIRECTIVE_LOCATION_ENUM_NAME,
+ 'isIntrospection' => true,
+ 'description' => 'A Directive can be adjacent to many parts of the Automattic\WooCommerce\Vendor\GraphQL language, a '
+ . '__DirectiveLocation describes one such possible adjacencies.',
+ 'values' => [
+ 'QUERY' => [
+ 'value' => DirectiveLocation::QUERY,
+ 'description' => 'Location adjacent to a query operation.',
+ ],
+ 'MUTATION' => [
+ 'value' => DirectiveLocation::MUTATION,
+ 'description' => 'Location adjacent to a mutation operation.',
+ ],
+ 'SUBSCRIPTION' => [
+ 'value' => DirectiveLocation::SUBSCRIPTION,
+ 'description' => 'Location adjacent to a subscription operation.',
+ ],
+ 'FIELD' => [
+ 'value' => DirectiveLocation::FIELD,
+ 'description' => 'Location adjacent to a field.',
+ ],
+ 'FRAGMENT_DEFINITION' => [
+ 'value' => DirectiveLocation::FRAGMENT_DEFINITION,
+ 'description' => 'Location adjacent to a fragment definition.',
+ ],
+ 'FRAGMENT_SPREAD' => [
+ 'value' => DirectiveLocation::FRAGMENT_SPREAD,
+ 'description' => 'Location adjacent to a fragment spread.',
+ ],
+ 'INLINE_FRAGMENT' => [
+ 'value' => DirectiveLocation::INLINE_FRAGMENT,
+ 'description' => 'Location adjacent to an inline fragment.',
+ ],
+ 'VARIABLE_DEFINITION' => [
+ 'value' => DirectiveLocation::VARIABLE_DEFINITION,
+ 'description' => 'Location adjacent to a variable definition.',
+ ],
+ 'SCHEMA' => [
+ 'value' => DirectiveLocation::SCHEMA,
+ 'description' => 'Location adjacent to a schema definition.',
+ ],
+ 'SCALAR' => [
+ 'value' => DirectiveLocation::SCALAR,
+ 'description' => 'Location adjacent to a scalar definition.',
+ ],
+ 'OBJECT' => [
+ 'value' => DirectiveLocation::OBJECT,
+ 'description' => 'Location adjacent to an object type definition.',
+ ],
+ 'FIELD_DEFINITION' => [
+ 'value' => DirectiveLocation::FIELD_DEFINITION,
+ 'description' => 'Location adjacent to a field definition.',
+ ],
+ 'ARGUMENT_DEFINITION' => [
+ 'value' => DirectiveLocation::ARGUMENT_DEFINITION,
+ 'description' => 'Location adjacent to an argument definition.',
+ ],
+ 'INTERFACE' => [
+ 'value' => DirectiveLocation::IFACE,
+ 'description' => 'Location adjacent to an interface definition.',
+ ],
+ 'UNION' => [
+ 'value' => DirectiveLocation::UNION,
+ 'description' => 'Location adjacent to a union definition.',
+ ],
+ 'ENUM' => [
+ 'value' => DirectiveLocation::ENUM,
+ 'description' => 'Location adjacent to an enum definition.',
+ ],
+ 'ENUM_VALUE' => [
+ 'value' => DirectiveLocation::ENUM_VALUE,
+ 'description' => 'Location adjacent to an enum value definition.',
+ ],
+ 'INPUT_OBJECT' => [
+ 'value' => DirectiveLocation::INPUT_OBJECT,
+ 'description' => 'Location adjacent to an input object type definition.',
+ ],
+ 'INPUT_FIELD_DEFINITION' => [
+ 'value' => DirectiveLocation::INPUT_FIELD_DEFINITION,
+ 'description' => 'Location adjacent to an input object field definition.',
+ ],
+ ],
+ ]);
+ }
+
+ public static function schemaMetaFieldDef(): FieldDefinition
+ {
+ return self::$cachedInstances[self::SCHEMA_FIELD_NAME] ??= new FieldDefinition([
+ 'name' => self::SCHEMA_FIELD_NAME,
+ 'type' => Type::nonNull(self::_schema()),
+ 'description' => 'Access the current type schema of this server.',
+ 'args' => [],
+ 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): Schema => $info->schema,
+ ]);
+ }
+
+ public static function typeMetaFieldDef(): FieldDefinition
+ {
+ return self::$cachedInstances[self::TYPE_FIELD_NAME] ??= new FieldDefinition([
+ 'name' => self::TYPE_FIELD_NAME,
+ 'type' => self::_type(),
+ 'description' => 'Request the type information of a single type.',
+ 'args' => [
+ [
+ 'name' => 'name',
+ 'type' => Type::nonNull(Type::string()),
+ ],
+ ],
+ 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): ?Type => $info->schema->getType($args['name']),
+ ]);
+ }
+
+ public static function typeNameMetaFieldDef(): FieldDefinition
+ {
+ return self::$cachedInstances[self::TYPE_NAME_FIELD_NAME] ??= new FieldDefinition([
+ 'name' => self::TYPE_NAME_FIELD_NAME,
+ 'type' => Type::nonNull(Type::string()),
+ 'description' => 'The name of the current Object type at runtime.',
+ 'args' => [],
+ 'resolve' => static fn ($source, array $args, $context, ResolveInfo $info): string => $info->parentType->name,
+ ]);
+ }
+
+ public static function resetCachedInstances(): void
+ {
+ self::$cachedInstances = null;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Schema.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Schema.php
new file mode 100644
index 00000000000..30f6f62deb5
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Schema.php
@@ -0,0 +1,625 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\GraphQL;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\AbstractType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\InterfaceImplementations;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+/**
+ * Schema Definition (see [schema definition docs](schema-definition.md)).
+ *
+ * A Schema is created by supplying the root types of each type of operation:
+ * query, mutation (optional) and subscription (optional). A schema definition is
+ * then supplied to the validator and executor. Usage Example:
+ *
+ * $schema = new Automattic\WooCommerce\Vendor\GraphQL\Type\Schema([
+ * 'query' => $MyAppQueryRootType,
+ * 'mutation' => $MyAppMutationRootType,
+ * ]);
+ *
+ * Or using Schema Config instance:
+ *
+ * $config = Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig::create()
+ * ->setQuery($MyAppQueryRootType)
+ * ->setMutation($MyAppMutationRootType);
+ *
+ * $schema = new Automattic\WooCommerce\Vendor\GraphQL\Type\Schema($config);
+ *
+ * @phpstan-import-type SchemaConfigOptions from SchemaConfig
+ * @phpstan-import-type OperationType from OperationDefinitionNode
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Type\SchemaTest
+ */
+class Schema
+{
+ private SchemaConfig $config;
+
+ /**
+ * Contains currently resolved schema types.
+ *
+ * @var array<string, Type&NamedType>
+ */
+ private array $resolvedTypes = [];
+
+ /**
+ * Lazily initialised.
+ *
+ * @var array<string, InterfaceImplementations>
+ */
+ private array $implementationsMap;
+
+ /** True when $resolvedTypes contains all possible schema types. */
+ private bool $fullyLoaded = false;
+
+ /** @var array<string, ScalarType>|null Lazily initialised by getScalarOverrides(). */
+ private ?array $scalarOverrides = null;
+
+ /** @var array<int, Error> */
+ private array $validationErrors;
+
+ public ?string $description;
+
+ public ?SchemaDefinitionNode $astNode;
+
+ /** @var array<SchemaExtensionNode> */
+ public array $extensionASTNodes = [];
+
+ /**
+ * @param SchemaConfig|array<string, mixed> $config
+ *
+ * @phpstan-param SchemaConfig|SchemaConfigOptions $config
+ *
+ * @throws InvariantViolation
+ *
+ * @api
+ */
+ public function __construct($config)
+ {
+ if (is_array($config)) {
+ $config = SchemaConfig::create($config);
+ }
+
+ // If this schema was built from a source known to be valid, then it may be
+ // marked with assumeValid to avoid an additional type system validation.
+ if ($config->getAssumeValid()) {
+ $this->validationErrors = [];
+ }
+
+ $this->description = $config->description;
+ $this->astNode = $config->astNode;
+ $this->extensionASTNodes = $config->extensionASTNodes;
+
+ $this->config = $config;
+ }
+
+ /**
+ * Returns all types in this schema.
+ *
+ * This operation requires a full schema scan. Do not use in production environment.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<string, Type&NamedType> Keys represent type names, values are instances of corresponding type definitions
+ *
+ * @api
+ */
+ public function getTypeMap(): array
+ {
+ if (! $this->fullyLoaded) {
+ // Reset order of user provided types, since calls to getType() may have loaded them
+ $this->resolvedTypes = [];
+
+ $scalarOverrides = $this->getScalarOverrides();
+
+ foreach ($this->materializeTypes() as $typeOrLazyType) {
+ /** @var Type|callable(): Type $typeOrLazyType */
+ $type = self::resolveType($typeOrLazyType);
+ assert($type instanceof NamedType);
+
+ /** @var string $typeName Necessary assertion for PHPStan + PHP 8.2 */
+ $typeName = $type->name;
+
+ if (isset($scalarOverrides[$typeName])) {
+ continue;
+ }
+
+ assert(
+ ! isset($this->resolvedTypes[$typeName]) || $type === $this->resolvedTypes[$typeName],
+ "Schema must contain unique named types but contains multiple types named \"{$type}\" (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).",
+ );
+
+ $this->resolvedTypes[$typeName] = $type;
+ }
+
+ // To preserve order of user-provided types, we add first to add them to
+ // the set of "collected" types, so `collectReferencedTypes` ignore them.
+ /** @var array<string, Type&NamedType> $allReferencedTypes */
+ $allReferencedTypes = [];
+ foreach ($this->resolvedTypes as $type) {
+ // When we ready to process this type, we remove it from "collected" types
+ // and then add it together with all dependent types in the correct position.
+ unset($allReferencedTypes[$type->name]);
+ TypeInfo::extractTypes($type, $allReferencedTypes);
+ }
+
+ foreach ([$this->getQueryType(), $this->getMutationType(), $this->getSubscriptionType()] as $rootType) {
+ if ($rootType instanceof ObjectType) {
+ TypeInfo::extractTypes($rootType, $allReferencedTypes);
+ }
+ }
+
+ foreach ($this->getDirectives() as $directive) {
+ // @phpstan-ignore-next-line generics are not strictly enforceable, error will be caught during schema validation
+ if ($directive instanceof Directive) {
+ TypeInfo::extractTypesFromDirectives($directive, $allReferencedTypes);
+ }
+ }
+ TypeInfo::extractTypes(Introspection::_schema(), $allReferencedTypes);
+
+ // Apply scalar overrides after all extractions, replacing the
+ // global singletons with user-provided instances.
+ foreach ($scalarOverrides as $name => $override) {
+ $allReferencedTypes[$name] = $override;
+ }
+
+ $this->resolvedTypes = $allReferencedTypes;
+ $this->fullyLoaded = true;
+ }
+
+ return $this->resolvedTypes;
+ }
+
+ /**
+ * Returns a list of directives supported by this schema.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<Directive>
+ *
+ * @api
+ */
+ public function getDirectives(): array
+ {
+ return $this->config->directives ?? GraphQL::getStandardDirectives();
+ }
+
+ /** @param mixed $typeLoaderReturn could be anything */
+ public static function typeLoaderNotType($typeLoaderReturn): string
+ {
+ $typeClass = Type::class;
+ $notType = Utils::printSafe($typeLoaderReturn);
+
+ return "Type loader is expected to return an instanceof {$typeClass}, but it returned {$notType}";
+ }
+
+ public static function typeLoaderWrongTypeName(string $expectedTypeName, string $actualTypeName): string
+ {
+ return "Type loader is expected to return type {$expectedTypeName}, but it returned type {$actualTypeName}.";
+ }
+
+ /** Returns root type by operation name. */
+ public function getOperationType(string $operation): ?ObjectType
+ {
+ switch ($operation) {
+ case 'query': return $this->getQueryType();
+ case 'mutation': return $this->getMutationType();
+ case 'subscription': return $this->getSubscriptionType();
+ default: return null;
+ }
+ }
+
+ /**
+ * Returns root query type.
+ *
+ * @api
+ */
+ public function getQueryType(): ?ObjectType
+ {
+ $query = $this->config->query;
+
+ if ($query === null) {
+ return null;
+ }
+
+ if (is_callable($query)) {
+ return $this->config->query = $query();
+ }
+
+ return $query;
+ }
+
+ /**
+ * Returns root mutation type.
+ *
+ * @api
+ */
+ public function getMutationType(): ?ObjectType
+ {
+ $mutation = $this->config->mutation;
+
+ if ($mutation === null) {
+ return null;
+ }
+
+ if (is_callable($mutation)) {
+ return $this->config->mutation = $mutation();
+ }
+
+ return $mutation;
+ }
+
+ /**
+ * Returns schema subscription.
+ *
+ * @api
+ */
+ public function getSubscriptionType(): ?ObjectType
+ {
+ $subscription = $this->config->subscription;
+
+ if ($subscription === null) {
+ return null;
+ }
+
+ if (is_callable($subscription)) {
+ return $this->config->subscription = $subscription();
+ }
+
+ return $subscription;
+ }
+
+ /** @api */
+ public function getConfig(): SchemaConfig
+ {
+ return $this->config;
+ }
+
+ /**
+ * Returns a type by name.
+ *
+ * @throws InvariantViolation
+ *
+ * @return (Type&NamedType)|null
+ *
+ * @api
+ */
+ public function getType(string $name): ?Type
+ {
+ if (isset($this->resolvedTypes[$name])) {
+ return $this->resolvedTypes[$name];
+ }
+
+ $introspectionTypes = Introspection::getTypes();
+ if (isset($introspectionTypes[$name])) {
+ return $introspectionTypes[$name];
+ }
+
+ $type = $this->loadType($name);
+ if ($type !== null) {
+ return $this->resolvedTypes[$name] = self::resolveType($type);
+ }
+
+ $scalarOverrides = $this->getScalarOverrides();
+ if (isset($scalarOverrides[$name])) {
+ return $this->resolvedTypes[$name] = $scalarOverrides[$name];
+ }
+
+ $builtInScalars = Type::builtInScalars();
+ if (isset($builtInScalars[$name])) {
+ return $this->resolvedTypes[$name] = $builtInScalars[$name];
+ }
+
+ return null;
+ }
+
+ /** @throws InvariantViolation */
+ public function hasType(string $name): bool
+ {
+ return $this->getType($name) !== null;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return (Type&NamedType)|null
+ */
+ private function loadType(string $typeName): ?Type
+ {
+ $typeLoader = $this->config->typeLoader;
+
+ if (! isset($typeLoader)) {
+ return $this->getTypeMap()[$typeName] ?? null;
+ }
+
+ // TODO https://github.com/webonyx/graphql-php/issues/1874 - reconsider supporting typeLoader-based scalar overrides in the next major version
+ if (Type::isBuiltInScalarName($typeName)) {
+ return null;
+ }
+
+ $type = $typeLoader($typeName);
+ if ($type === null) {
+ return null;
+ }
+
+ // @phpstan-ignore-next-line not strictly enforceable unless PHP gets function types
+ if (! $type instanceof Type) {
+ throw new InvariantViolation(self::typeLoaderNotType($type));
+ }
+
+ if ($typeName !== $type->name) {
+ throw new InvariantViolation(self::typeLoaderWrongTypeName($typeName, $type->name));
+ }
+
+ return $type;
+ }
+
+ /** @return array<string, ScalarType> */
+ private function getScalarOverrides(): array
+ {
+ if ($this->scalarOverrides === null) {
+ $this->scalarOverrides = [];
+
+ $builtInScalars = Type::builtInScalars();
+ foreach ($this->materializeTypes() as $typeOrLazyType) {
+ /** @var Type|callable(): Type $typeOrLazyType */
+ $type = self::resolveType($typeOrLazyType);
+ if ($type instanceof ScalarType
+ && isset($builtInScalars[$type->name])
+ && $type !== $builtInScalars[$type->name]
+ ) {
+ $this->scalarOverrides[$type->name] = $type;
+ }
+ }
+ }
+
+ return $this->scalarOverrides;
+ }
+
+ /**
+ * Resolve config->types to an array, materializing callables and generators.
+ *
+ * @return array<Type|callable(): Type>
+ */
+ private function materializeTypes(): array
+ {
+ $types = $this->config->types;
+ if (is_callable($types)) {
+ $types = $types();
+ }
+
+ if (! is_array($types)) {
+ $types = iterator_to_array($types);
+ $this->config->types = $types;
+ }
+
+ return $types;
+ }
+
+ /**
+ * @template T of Type
+ *
+ * @param Type|callable $type
+ *
+ * @phpstan-param T|callable():T $type
+ *
+ * @phpstan-return T
+ */
+ public static function resolveType($type): Type
+ {
+ if ($type instanceof Type) {
+ return $type;
+ }
+
+ return $type();
+ }
+
+ /**
+ * Returns all possible concrete types for given abstract type
+ * (implementations for interfaces and members of union type for unions).
+ *
+ * This operation requires full schema scan. Do not use in production environment.
+ *
+ * @param AbstractType&Type $abstractType
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<ObjectType>
+ *
+ * @api
+ */
+ public function getPossibleTypes(AbstractType $abstractType): array
+ {
+ if ($abstractType instanceof UnionType) {
+ return $abstractType->getTypes();
+ }
+
+ assert($abstractType instanceof InterfaceType, 'only other option');
+
+ return $this->getImplementations($abstractType)->objects();
+ }
+
+ /**
+ * Returns all types that implement a given interface type.
+ *
+ * This operation requires full schema scan. Do not use in production environment.
+ *
+ * @api
+ *
+ * @throws InvariantViolation
+ */
+ public function getImplementations(InterfaceType $abstractType): InterfaceImplementations
+ {
+ return $this->collectImplementations()[$abstractType->name];
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<string, InterfaceImplementations>
+ */
+ private function collectImplementations(): array
+ {
+ if (! isset($this->implementationsMap)) {
+ $this->implementationsMap = [];
+
+ /**
+ * @var array<
+ * string,
+ * array{
+ * objects: array<int, ObjectType>,
+ * interfaces: array<int, InterfaceType>,
+ * }
+ * > $foundImplementations
+ */
+ $foundImplementations = [];
+ foreach ($this->getTypeMap() as $type) {
+ if ($type instanceof InterfaceType) {
+ if (! isset($foundImplementations[$type->name])) {
+ $foundImplementations[$type->name] = ['objects' => [], 'interfaces' => []];
+ }
+
+ foreach ($type->getInterfaces() as $iface) {
+ if (! isset($foundImplementations[$iface->name])) {
+ $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
+ }
+
+ $foundImplementations[$iface->name]['interfaces'][] = $type;
+ }
+ } elseif ($type instanceof ObjectType) {
+ foreach ($type->getInterfaces() as $iface) {
+ if (! isset($foundImplementations[$iface->name])) {
+ $foundImplementations[$iface->name] = ['objects' => [], 'interfaces' => []];
+ }
+
+ $foundImplementations[$iface->name]['objects'][] = $type;
+ }
+ }
+ }
+
+ foreach ($foundImplementations as $name => $implementations) {
+ $this->implementationsMap[$name] = new InterfaceImplementations($implementations['objects'], $implementations['interfaces']);
+ }
+ }
+
+ return $this->implementationsMap;
+ }
+
+ /**
+ * Returns true if the given type is a sub type of the given abstract type.
+ *
+ * @param AbstractType&Type $abstractType
+ * @param ImplementingType&Type $maybeSubType
+ *
+ * @api
+ *
+ * @throws InvariantViolation
+ */
+ public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType): bool
+ {
+ if ($abstractType instanceof InterfaceType) {
+ return $maybeSubType->implementsInterface($abstractType);
+ }
+
+ assert($abstractType instanceof UnionType, 'only other option');
+
+ return $abstractType->isPossibleType($maybeSubType);
+ }
+
+ /**
+ * Returns instance of directive by name.
+ *
+ * @api
+ *
+ * @throws InvariantViolation
+ */
+ public function getDirective(string $name): ?Directive
+ {
+ foreach ($this->getDirectives() as $directive) {
+ if ($directive->name === $name) {
+ return $directive;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Throws if the schema is not valid.
+ *
+ * This operation requires a full schema scan. Do not use in production environment.
+ *
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @api
+ */
+ public function assertValid(): void
+ {
+ $errors = $this->validate();
+
+ if ($errors !== []) {
+ throw new InvariantViolation(implode("\n\n", $this->validationErrors));
+ }
+
+ $internalTypes = Type::builtInScalars() + Introspection::getTypes();
+ foreach ($this->getTypeMap() as $name => $type) {
+ if (isset($internalTypes[$name])) {
+ continue;
+ }
+
+ $type->assertValid();
+
+ // Make sure type loader returns the same instance as registered in other places of schema
+ if (isset($this->config->typeLoader) && $this->loadType($name) !== $type) {
+ throw new InvariantViolation("Type loader returns different instance for {$name} than field/argument definitions. Make sure you always return the same instance for the same type name.");
+ }
+ }
+ }
+
+ /**
+ * Validate the schema and return any errors.
+ *
+ * This operation requires a full schema scan. Do not use in production environment.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, Error>
+ *
+ * @api
+ */
+ public function validate(): array
+ {
+ // If this Schema has already been validated, return the previous results.
+ if (isset($this->validationErrors)) {
+ return $this->validationErrors;
+ }
+
+ // Validate the schema, producing a list of errors.
+ $context = new SchemaValidationContext($this);
+ $context->validateRootTypes();
+ $context->validateDirectives();
+ $context->validateTypes();
+
+ // Persist the results of validation before returning to ensure validation
+ // does not run multiple times for this schema.
+ $this->validationErrors = $context->getErrors();
+
+ return $this->validationErrors;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaConfig.php b/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaConfig.php
new file mode 100644
index 00000000000..b8c12e25960
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaConfig.php
@@ -0,0 +1,356 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+
+/**
+ * Configuration options for schema construction.
+ *
+ * The options accepted by the **create** method are described
+ * in the [schema definition docs](schema-definition.md#configuration-options).
+ *
+ * Usage example:
+ *
+ * $config = SchemaConfig::create()
+ * ->setQuery($myQueryType)
+ * ->setTypeLoader($myTypeLoader);
+ *
+ * $schema = new Schema($config);
+ *
+ * @see Type, NamedType
+ *
+ * @phpstan-type MaybeLazyObjectType ObjectType|(callable(): (ObjectType|null))|null
+ * @phpstan-type TypeLoader callable(string $typeName): ((Type&NamedType)|null)
+ * @phpstan-type Types iterable<Type&NamedType>|(callable(): iterable<Type&NamedType>)|iterable<(callable(): Type&NamedType)>|(callable(): iterable<(callable(): Type&NamedType)>)
+ * @phpstan-type SchemaConfigOptions array{
+ * description?: string|null,
+ * query?: MaybeLazyObjectType,
+ * mutation?: MaybeLazyObjectType,
+ * subscription?: MaybeLazyObjectType,
+ * types?: Types|null,
+ * directives?: array<Directive>|null,
+ * typeLoader?: TypeLoader|null,
+ * assumeValid?: bool|null,
+ * astNode?: SchemaDefinitionNode|null,
+ * extensionASTNodes?: array<SchemaExtensionNode>|null,
+ * }
+ */
+class SchemaConfig
+{
+ public ?string $description = null;
+
+ /** @var MaybeLazyObjectType */
+ public $query;
+
+ /** @var MaybeLazyObjectType */
+ public $mutation;
+
+ /** @var MaybeLazyObjectType */
+ public $subscription;
+
+ /**
+ * @var iterable|callable
+ *
+ * @phpstan-var Types
+ */
+ public $types = [];
+
+ /** @var array<Directive>|null */
+ public ?array $directives = null;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var TypeLoader|null
+ */
+ public $typeLoader;
+
+ public bool $assumeValid = false;
+
+ public ?SchemaDefinitionNode $astNode = null;
+
+ /** @var array<SchemaExtensionNode> */
+ public array $extensionASTNodes = [];
+
+ /**
+ * Converts an array of options to instance of SchemaConfig
+ * (or just returns empty config when array is not passed).
+ *
+ * @phpstan-param SchemaConfigOptions $options
+ *
+ * @throws InvariantViolation
+ *
+ * @api
+ */
+ public static function create(array $options = []): self
+ {
+ $config = new static();
+
+ if ($options !== []) {
+ if (isset($options['description'])) {
+ $config->setDescription($options['description']);
+ }
+ if (isset($options['query'])) {
+ $config->setQuery($options['query']);
+ }
+
+ if (isset($options['mutation'])) {
+ $config->setMutation($options['mutation']);
+ }
+
+ if (isset($options['subscription'])) {
+ $config->setSubscription($options['subscription']);
+ }
+
+ if (isset($options['types'])) {
+ $config->setTypes($options['types']);
+ }
+
+ if (isset($options['directives'])) {
+ $config->setDirectives($options['directives']);
+ }
+
+ if (isset($options['typeLoader'])) {
+ $config->setTypeLoader($options['typeLoader']);
+ }
+
+ if (isset($options['assumeValid'])) {
+ $config->setAssumeValid($options['assumeValid']);
+ }
+
+ if (isset($options['astNode'])) {
+ $config->setAstNode($options['astNode']);
+ }
+
+ if (isset($options['extensionASTNodes'])) {
+ $config->setExtensionASTNodes($options['extensionASTNodes']);
+ }
+ }
+
+ return $config;
+ }
+
+ /** @api */
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ /** @api */
+ public function setDescription(?string $description): self
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ /**
+ * @return MaybeLazyObjectType
+ *
+ * @api
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * @param MaybeLazyObjectType $query
+ *
+ * @throws InvariantViolation
+ *
+ * @api
+ */
+ public function setQuery($query): self
+ {
+ $this->assertMaybeLazyObjectType($query);
+ $this->query = $query;
+
+ return $this;
+ }
+
+ /**
+ * @return MaybeLazyObjectType
+ *
+ * @api
+ */
+ public function getMutation()
+ {
+ return $this->mutation;
+ }
+
+ /**
+ * @param MaybeLazyObjectType $mutation
+ *
+ * @throws InvariantViolation
+ *
+ * @api
+ */
+ public function setMutation($mutation): self
+ {
+ $this->assertMaybeLazyObjectType($mutation);
+ $this->mutation = $mutation;
+
+ return $this;
+ }
+
+ /**
+ * @return MaybeLazyObjectType
+ *
+ * @api
+ */
+ public function getSubscription()
+ {
+ return $this->subscription;
+ }
+
+ /**
+ * @param MaybeLazyObjectType $subscription
+ *
+ * @throws InvariantViolation
+ *
+ * @api
+ */
+ public function setSubscription($subscription): self
+ {
+ $this->assertMaybeLazyObjectType($subscription);
+ $this->subscription = $subscription;
+
+ return $this;
+ }
+
+ /**
+ * @return array|callable
+ *
+ * @phpstan-return Types
+ *
+ * @api
+ */
+ public function getTypes()
+ {
+ return $this->types;
+ }
+
+ /**
+ * @param array|callable $types
+ *
+ * @phpstan-param Types $types
+ *
+ * @api
+ */
+ public function setTypes($types): self
+ {
+ $this->types = $types;
+
+ return $this;
+ }
+
+ /**
+ * @return array<Directive>|null
+ *
+ * @api
+ */
+ public function getDirectives(): ?array
+ {
+ return $this->directives;
+ }
+
+ /**
+ * @param array<Directive>|null $directives
+ *
+ * @api
+ */
+ public function setDirectives(?array $directives): self
+ {
+ $this->directives = $directives;
+
+ return $this;
+ }
+
+ /**
+ * @return callable|null $typeLoader
+ *
+ * @phpstan-return TypeLoader|null $typeLoader
+ *
+ * @api
+ */
+ public function getTypeLoader(): ?callable
+ {
+ return $this->typeLoader;
+ }
+
+ /**
+ * @phpstan-param TypeLoader|null $typeLoader
+ *
+ * @api
+ */
+ public function setTypeLoader(?callable $typeLoader): self
+ {
+ $this->typeLoader = $typeLoader;
+
+ return $this;
+ }
+
+ public function getAssumeValid(): bool
+ {
+ return $this->assumeValid;
+ }
+
+ public function setAssumeValid(bool $assumeValid): self
+ {
+ $this->assumeValid = $assumeValid;
+
+ return $this;
+ }
+
+ public function getAstNode(): ?SchemaDefinitionNode
+ {
+ return $this->astNode;
+ }
+
+ public function setAstNode(?SchemaDefinitionNode $astNode): self
+ {
+ $this->astNode = $astNode;
+
+ return $this;
+ }
+
+ /** @return array<SchemaExtensionNode> */
+ public function getExtensionASTNodes(): array
+ {
+ return $this->extensionASTNodes;
+ }
+
+ /** @param array<SchemaExtensionNode> $extensionASTNodes */
+ public function setExtensionASTNodes(array $extensionASTNodes): self
+ {
+ $this->extensionASTNodes = $extensionASTNodes;
+
+ return $this;
+ }
+
+ /**
+ * @param mixed $maybeLazyObjectType Should be MaybeLazyObjectType
+ *
+ * @throws InvariantViolation
+ */
+ protected function assertMaybeLazyObjectType($maybeLazyObjectType): void
+ {
+ if ($maybeLazyObjectType instanceof ObjectType || is_callable($maybeLazyObjectType) || is_null($maybeLazyObjectType)) {
+ return;
+ }
+
+ $notMaybeLazyObjectType = is_object($maybeLazyObjectType)
+ ? get_class($maybeLazyObjectType)
+ : gettype($maybeLazyObjectType);
+ $objectTypeClass = ObjectType::class;
+ throw new InvariantViolation("Expected instanceof {$objectTypeClass}, a callable that returns such an instance, or null, got: {$notMaybeLazyObjectType}.");
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaValidationContext.php b/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaValidationContext.php
new file mode 100644
index 00000000000..c1625b781d4
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/SchemaValidationContext.php
@@ -0,0 +1,856 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\DirectiveLocation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumValueDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Validation\InputObjectCircularRefs;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeComparators;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+
+class SchemaValidationContext
+{
+ /** @var list<Error> */
+ private array $errors = [];
+
+ private Schema $schema;
+
+ private InputObjectCircularRefs $inputObjectCircularRefs;
+
+ public function __construct(Schema $schema)
+ {
+ $this->schema = $schema;
+ $this->inputObjectCircularRefs = new InputObjectCircularRefs($this);
+ }
+
+ /** @return list<Error> */
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+
+ public function validateRootTypes(): void
+ {
+ if ($this->schema->getQueryType() === null) {
+ $this->reportError('Query root type must be provided.', $this->schema->astNode);
+ }
+
+ // Triggers a type error if wrong
+ $this->schema->getMutationType();
+ $this->schema->getSubscriptionType();
+ }
+
+ /** @param array<Node|null>|Node|null $nodes */
+ public function reportError(string $message, $nodes = null): void
+ {
+ $nodes = array_filter(is_array($nodes) ? $nodes : [$nodes]);
+ $this->addError(new Error($message, $nodes));
+ }
+
+ private function addError(Error $error): void
+ {
+ $this->errors[] = $error;
+ }
+
+ /** @throws InvariantViolation */
+ public function validateDirectives(): void
+ {
+ $this->validateDirectiveDefinitions();
+
+ // Validate directives that are used on the schema
+ $this->validateDirectivesAtLocation(
+ $this->getDirectives($this->schema),
+ DirectiveLocation::SCHEMA
+ );
+ }
+
+ /** @throws InvariantViolation */
+ public function validateDirectiveDefinitions(): void
+ {
+ $directiveDefinitions = [];
+
+ $directives = $this->schema->getDirectives();
+ foreach ($directives as $directive) {
+ // Ensure all directives are in fact Automattic\WooCommerce\Vendor\GraphQL directives.
+ // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
+ if (! $directive instanceof Directive) {
+ $notDirective = Utils::printSafe($directive);
+ // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
+ $nodes = is_object($directive) && property_exists($directive, 'astNode')
+ ? $directive->astNode
+ : null;
+
+ $this->reportError(
+ "Expected directive but got: {$notDirective}.",
+ $nodes
+ );
+ continue;
+ }
+
+ $existingDefinitions = $directiveDefinitions[$directive->name] ?? [];
+ $existingDefinitions[] = $directive;
+ $directiveDefinitions[$directive->name] = $existingDefinitions;
+
+ // Ensure they are named correctly.
+ $this->validateName($directive);
+
+ // TODO: Ensure proper locations.
+
+ $argNames = [];
+ foreach ($directive->args as $arg) {
+ // Ensure they are named correctly.
+ $this->validateName($arg);
+
+ $argName = $arg->name;
+
+ if (isset($argNames[$argName])) {
+ $this->reportError(
+ "Argument @{$directive->name}({$argName}:) can only be defined once.",
+ $this->getAllDirectiveArgNodes($directive, $argName)
+ );
+ continue;
+ }
+
+ $argNames[$argName] = true;
+
+ // Ensure the type is an input type.
+ // @phpstan-ignore-next-line necessary until PHP supports union types
+ if (! Type::isInputType($arg->getType())) {
+ $type = Utils::printSafe($arg->getType());
+ $this->reportError(
+ "The type of @{$directive->name}({$argName}:) must be Input Type but got: {$type}.",
+ $this->getDirectiveArgTypeNode($directive, $argName)
+ );
+ }
+ }
+ }
+
+ foreach ($directiveDefinitions as $directiveName => $directiveList) {
+ if (count($directiveList) > 1) {
+ $nodes = [];
+ foreach ($directiveList as $dir) {
+ if (isset($dir->astNode)) {
+ $nodes[] = $dir->astNode;
+ }
+ }
+
+ $this->reportError(
+ "Directive @{$directiveName} defined multiple times.",
+ $nodes
+ );
+ }
+ }
+ }
+
+ /** @param (Type&NamedType)|Directive|FieldDefinition|EnumValueDefinition|InputObjectField|Argument $object */
+ private function validateName(object $object): void
+ {
+ // Ensure names are valid, however introspection types opt out.
+ $error = Utils::isValidNameError($object->name, $object->astNode);
+ if (
+ $error === null
+ || ($object instanceof Type && Introspection::isIntrospectionType($object))
+ ) {
+ return;
+ }
+
+ $this->addError($error);
+ }
+
+ /** @return array<int, InputValueDefinitionNode> */
+ private function getAllDirectiveArgNodes(Directive $directive, string $argName): array
+ {
+ $astNode = $directive->astNode;
+ if ($astNode === null) {
+ return [];
+ }
+
+ $matchingSubnodes = [];
+ foreach ($astNode->arguments as $subNode) {
+ if ($subNode->name->value === $argName) {
+ $matchingSubnodes[] = $subNode;
+ }
+ }
+
+ return $matchingSubnodes;
+ }
+
+ /** @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null */
+ private function getDirectiveArgTypeNode(Directive $directive, string $argName): ?TypeNode
+ {
+ $argNode = $this->getAllDirectiveArgNodes($directive, $argName)[0] ?? null;
+
+ return $argNode === null
+ ? null
+ : $argNode->type;
+ }
+
+ /** @throws InvariantViolation */
+ public function validateTypes(): void
+ {
+ $typeMap = $this->schema->getTypeMap();
+ foreach ($typeMap as $type) {
+ // Ensure all provided types are in fact Automattic\WooCommerce\Vendor\GraphQL type.
+ // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
+ if (! $type instanceof NamedType) {
+ $notNamedType = Utils::printSafe($type);
+ // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
+ $node = $type instanceof Type
+ ? $type->astNode
+ : null;
+
+ $this->reportError("Expected Automattic\WooCommerce\Vendor\GraphQL named type but got: {$notNamedType}.", $node);
+ continue;
+ }
+
+ $this->validateName($type);
+
+ if ($type instanceof ObjectType) {
+ $this->validateFields($type);
+ $this->validateInterfaces($type);
+ $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::OBJECT);
+ } elseif ($type instanceof InterfaceType) {
+ $this->validateFields($type);
+ $this->validateInterfaces($type);
+ $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::IFACE);
+ } elseif ($type instanceof UnionType) {
+ $this->validateUnionMembers($type);
+ $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::UNION);
+ } elseif ($type instanceof EnumType) {
+ $this->validateEnumValues($type);
+ $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::ENUM);
+ } elseif ($type instanceof InputObjectType) {
+ $this->validateInputFields($type);
+ $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::INPUT_OBJECT);
+ $this->inputObjectCircularRefs->validate($type);
+ } else {
+ assert($type instanceof ScalarType, 'only remaining option');
+ $this->validateDirectivesAtLocation($this->getDirectives($type), DirectiveLocation::SCALAR);
+ }
+ }
+ }
+
+ /**
+ * @param NodeList<DirectiveNode> $directives
+ *
+ * @throws InvariantViolation
+ */
+ private function validateDirectivesAtLocation(NodeList $directives, string $location): void
+ {
+ /** @var array<string, array<int, DirectiveNode>> $potentiallyDuplicateDirectives */
+ $potentiallyDuplicateDirectives = [];
+ $schema = $this->schema;
+ foreach ($directives as $directiveNode) {
+ $directiveName = $directiveNode->name->value;
+
+ // Ensure directive used is also defined
+ $schemaDirective = $schema->getDirective($directiveName);
+ if ($schemaDirective === null) {
+ $this->reportError("No directive @{$directiveName} defined.", $directiveNode);
+ continue;
+ }
+
+ if (! in_array($location, $schemaDirective->locations, true)) {
+ $this->reportError(
+ "Directive @{$directiveName} not allowed at {$location} location.",
+ array_filter([$directiveNode, $schemaDirective->astNode])
+ );
+ }
+
+ if (! $schemaDirective->isRepeatable) {
+ $potentiallyDuplicateDirectives[$directiveName][] = $directiveNode;
+ }
+ }
+
+ foreach ($potentiallyDuplicateDirectives as $directiveName => $directiveList) {
+ if (count($directiveList) > 1) {
+ $this->reportError("Non-repeatable directive @{$directiveName} used more than once at the same location.", $directiveList);
+ }
+ }
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ *
+ * @throws InvariantViolation
+ */
+ private function validateFields(Type $type): void
+ {
+ $fieldMap = $type->getFields();
+
+ if ($fieldMap === []) {
+ $this->reportError(
+ "Type {$type->name} must define one or more fields.",
+ $this->getAllNodes($type)
+ );
+ }
+
+ foreach ($fieldMap as $fieldName => $field) {
+ $this->validateName($field);
+
+ $fieldNodes = $this->getAllFieldNodes($type, $fieldName);
+ if (count($fieldNodes) > 1) {
+ $this->reportError("Field {$type->name}.{$fieldName} can only be defined once.", $fieldNodes);
+ continue;
+ }
+
+ $fieldType = $field->getType();
+ // @phpstan-ignore-next-line not statically provable until we can use union types
+ if (! Type::isOutputType($fieldType)) {
+ $safeFieldType = Utils::printSafe($fieldType);
+ $this->reportError(
+ "The type of {$type->name}.{$fieldName} must be Output Type but got: {$safeFieldType}.",
+ $this->getFieldTypeNode($type, $fieldName)
+ );
+ }
+
+ $this->validateTypeIsSingleton($fieldType, "{$type->name}.{$fieldName}");
+
+ $argNames = [];
+ foreach ($field->args as $arg) {
+ $argName = $arg->name;
+ $argPath = "{$type->name}.{$fieldName}({$argName}:)";
+
+ $this->validateName($arg);
+
+ if (isset($argNames[$argName])) {
+ $this->reportError(
+ "Field argument {$argPath} can only be defined once.",
+ $this->getAllFieldArgNodes($type, $fieldName, $argName)
+ );
+ }
+
+ $argNames[$argName] = true;
+
+ $argType = $arg->getType();
+
+ // @phpstan-ignore-next-line the type of $arg->getType() says it is an input type, but it might not always be true
+ if (! Type::isInputType($argType)) {
+ $safeType = Utils::printSafe($argType);
+ $this->reportError(
+ "The type of {$argPath} must be Input Type but got: {$safeType}.",
+ $this->getFieldArgTypeNode($type, $fieldName, $argName)
+ );
+ }
+
+ $this->validateTypeIsSingleton($argType, $argPath);
+
+ if (isset($arg->astNode->directives)) {
+ $this->validateDirectivesAtLocation($arg->astNode->directives, DirectiveLocation::ARGUMENT_DEFINITION);
+ }
+ }
+
+ if (isset($field->astNode->directives)) {
+ $this->validateDirectivesAtLocation($field->astNode->directives, DirectiveLocation::FIELD_DEFINITION);
+ }
+ }
+ }
+
+ /**
+ * @param Schema|ObjectType|InterfaceType|UnionType|EnumType|InputObjectType|Directive $obj
+ *
+ * @return list<SchemaDefinitionNode|SchemaExtensionNode>|list<ObjectTypeDefinitionNode|ObjectTypeExtensionNode>|list<InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode>|list<UnionTypeDefinitionNode|UnionTypeExtensionNode>|list< EnumTypeDefinitionNode|EnumTypeExtensionNode>|list<InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode>|list<DirectiveDefinitionNode>
+ */
+ private function getAllNodes(object $obj): array
+ {
+ $astNode = $obj->astNode;
+
+ if ($obj instanceof Schema) {
+ $extensionNodes = $obj->extensionASTNodes;
+ } elseif ($obj instanceof Directive) {
+ $extensionNodes = [];
+ } else {
+ $extensionNodes = $obj->extensionASTNodes;
+ }
+
+ $allNodes = $astNode === null
+ ? []
+ : [$astNode];
+ foreach ($extensionNodes as $extensionNode) {
+ $allNodes[] = $extensionNode;
+ }
+
+ return $allNodes;
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ *
+ * @return list<FieldDefinitionNode>
+ */
+ private function getAllFieldNodes(Type $type, string $fieldName): array
+ {
+ $allNodes = array_filter([$type->astNode, ...$type->extensionASTNodes]);
+
+ $matchingFieldNodes = [];
+
+ foreach ($allNodes as $node) {
+ foreach ($node->fields as $field) {
+ if ($field->name->value === $fieldName) {
+ $matchingFieldNodes[] = $field;
+ }
+ }
+ }
+
+ return $matchingFieldNodes;
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ *
+ * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
+ */
+ private function getFieldTypeNode(Type $type, string $fieldName): ?TypeNode
+ {
+ $fieldNode = $this->getFieldNode($type, $fieldName);
+
+ return $fieldNode === null
+ ? null
+ : $fieldNode->type;
+ }
+
+ /** @param ObjectType|InterfaceType $type */
+ private function getFieldNode(Type $type, string $fieldName): ?FieldDefinitionNode
+ {
+ $nodes = $this->getAllFieldNodes($type, $fieldName);
+
+ return $nodes[0] ?? null;
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ *
+ * @return array<int, InputValueDefinitionNode>
+ */
+ private function getAllFieldArgNodes(Type $type, string $fieldName, string $argName): array
+ {
+ $argNodes = [];
+ $fieldNode = $this->getFieldNode($type, $fieldName);
+ if ($fieldNode !== null) {
+ foreach ($fieldNode->arguments as $node) {
+ if ($node->name->value === $argName) {
+ $argNodes[] = $node;
+ }
+ }
+ }
+
+ return $argNodes;
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ *
+ * @return NamedTypeNode|ListTypeNode|NonNullTypeNode|null
+ */
+ private function getFieldArgTypeNode(Type $type, string $fieldName, string $argName): ?TypeNode
+ {
+ $fieldArgNode = $this->getFieldArgNode($type, $fieldName, $argName);
+
+ return $fieldArgNode === null
+ ? null
+ : $fieldArgNode->type;
+ }
+
+ /** @param ObjectType|InterfaceType $type */
+ private function getFieldArgNode(Type $type, string $fieldName, string $argName): ?InputValueDefinitionNode
+ {
+ $nodes = $this->getAllFieldArgNodes($type, $fieldName, $argName);
+
+ return $nodes[0] ?? null;
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ *
+ * @throws InvariantViolation
+ */
+ private function validateInterfaces(ImplementingType $type): void
+ {
+ $ifaceTypeNames = [];
+ foreach ($type->getInterfaces() as $interface) {
+ // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
+ if (! $interface instanceof InterfaceType) {
+ $notInterface = Utils::printSafe($interface);
+ $this->reportError(
+ "Type {$type->name} must only implement Interface types, it cannot implement {$notInterface}.",
+ $this->getImplementsInterfaceNode($type, $interface)
+ );
+ continue;
+ }
+
+ if ($type === $interface) {
+ $this->reportError(
+ "Type {$type->name} cannot implement itself because it would create a circular reference.",
+ $this->getImplementsInterfaceNode($type, $interface)
+ );
+ continue;
+ }
+
+ if (isset($ifaceTypeNames[$interface->name])) {
+ $this->reportError(
+ "Type {$type->name} can only implement {$interface->name} once.",
+ $this->getAllImplementsInterfaceNodes($type, $interface)
+ );
+ continue;
+ }
+
+ $ifaceTypeNames[$interface->name] = true;
+
+ $this->validateTypeImplementsAncestors($type, $interface);
+ $this->validateTypeImplementsInterface($type, $interface);
+ }
+ }
+
+ /**
+ * @param Schema|(Type&NamedType) $object
+ *
+ * @return NodeList<DirectiveNode>
+ */
+ private function getDirectives(object $object): NodeList
+ {
+ $directives = [];
+ /**
+ * Excluding directiveNode, since $object is not Directive.
+ *
+ * @var SchemaDefinitionNode|SchemaExtensionNode|ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode|UnionTypeDefinitionNode|UnionTypeExtensionNode|EnumTypeDefinitionNode|EnumTypeExtensionNode|InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode $node
+ */
+ // @phpstan-ignore-next-line union types are not pervasive
+ foreach ($this->getAllNodes($object) as $node) {
+ foreach ($node->directives as $directive) {
+ $directives[] = $directive;
+ }
+ }
+
+ return new NodeList($directives);
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ * @param Type&NamedType $shouldBeInterface
+ */
+ private function getImplementsInterfaceNode(ImplementingType $type, NamedType $shouldBeInterface): ?NamedTypeNode
+ {
+ $nodes = $this->getAllImplementsInterfaceNodes($type, $shouldBeInterface);
+
+ return $nodes[0] ?? null;
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ * @param Type&NamedType $shouldBeInterface
+ *
+ * @return list<NamedTypeNode>
+ */
+ private function getAllImplementsInterfaceNodes(ImplementingType $type, NamedType $shouldBeInterface): array
+ {
+ $allNodes = array_filter([$type->astNode, ...$type->extensionASTNodes]);
+
+ $shouldBeInterfaceName = $shouldBeInterface->name;
+ $matchingInterfaceNodes = [];
+
+ foreach ($allNodes as $node) {
+ foreach ($node->interfaces as $interface) {
+ if ($interface->name->value === $shouldBeInterfaceName) {
+ $matchingInterfaceNodes[] = $interface;
+ }
+ }
+ }
+
+ return $matchingInterfaceNodes;
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ *
+ * @throws InvariantViolation
+ */
+ private function validateTypeImplementsInterface(ImplementingType $type, InterfaceType $iface): void
+ {
+ $typeFieldMap = $type->getFields();
+ $ifaceFieldMap = $iface->getFields();
+
+ foreach ($ifaceFieldMap as $fieldName => $ifaceField) {
+ $typeField = $typeFieldMap[$fieldName] ?? null;
+
+ if ($typeField === null) {
+ $this->reportError(
+ "Interface field {$iface->name}.{$fieldName} expected but {$type->name} does not provide it.",
+ array_merge(
+ [$this->getFieldNode($iface, $fieldName)],
+ $this->getAllNodes($type)
+ )
+ );
+ continue;
+ }
+
+ $typeFieldType = $typeField->getType();
+ $ifaceFieldType = $ifaceField->getType();
+ if (! TypeComparators::isTypeSubTypeOf($this->schema, $typeFieldType, $ifaceFieldType)) {
+ $this->reportError(
+ "Interface field {$iface->name}.{$fieldName} expects type {$ifaceFieldType} but {$type->name}.{$fieldName} is type {$typeFieldType}.",
+ [
+ $this->getFieldTypeNode($iface, $fieldName),
+ $this->getFieldTypeNode($type, $fieldName),
+ ]
+ );
+ }
+
+ foreach ($ifaceField->args as $ifaceArg) {
+ $argName = $ifaceArg->name;
+ $typeArg = $typeField->getArg($argName);
+
+ if ($typeArg === null) {
+ $this->reportError(
+ "Interface field argument {$iface->name}.{$fieldName}({$argName}:) expected but {$type->name}.{$fieldName} does not provide it.",
+ [
+ $this->getFieldArgNode($iface, $fieldName, $argName),
+ $this->getFieldNode($type, $fieldName),
+ ]
+ );
+ continue;
+ }
+
+ $ifaceArgType = $ifaceArg->getType();
+ $typeArgType = $typeArg->getType();
+ if (! TypeComparators::isEqualType($ifaceArgType, $typeArgType)) {
+ $this->reportError(
+ "Interface field argument {$iface->name}.{$fieldName}({$argName}:) expects type {$ifaceArgType} but {$type->name}.{$fieldName}({$argName}:) is type {$typeArgType}.",
+ [
+ $this->getFieldArgTypeNode($iface, $fieldName, $argName),
+ $this->getFieldArgTypeNode($type, $fieldName, $argName),
+ ]
+ );
+ }
+
+ // TODO: validate default values?
+ }
+
+ foreach ($typeField->args as $typeArg) {
+ $argName = $typeArg->name;
+ $ifaceArg = $ifaceField->getArg($argName);
+
+ if ($typeArg->isRequired() && $ifaceArg === null) {
+ $this->reportError(
+ "Object field {$type->name}.{$fieldName} includes required argument {$argName} that is missing from the Interface field {$iface->name}.{$fieldName}.",
+ [
+ $this->getFieldArgNode($type, $fieldName, $argName),
+ $this->getFieldNode($iface, $fieldName),
+ ]
+ );
+ }
+ }
+ }
+ }
+
+ /** @param ObjectType|InterfaceType $type */
+ private function validateTypeImplementsAncestors(ImplementingType $type, InterfaceType $iface): void
+ {
+ $typeInterfaces = $type->getInterfaces();
+ foreach ($iface->getInterfaces() as $transitive) {
+ if (! in_array($transitive, $typeInterfaces, true)) {
+ $this->reportError(
+ $transitive === $type
+ ? "Type {$type->name} cannot implement {$iface->name} because it would create a circular reference."
+ : "Type {$type->name} must implement {$transitive->name} because it is implemented by {$iface->name}.",
+ array_merge(
+ $this->getAllImplementsInterfaceNodes($iface, $transitive),
+ $this->getAllImplementsInterfaceNodes($type, $iface)
+ )
+ );
+ }
+ }
+ }
+
+ /** @throws InvariantViolation */
+ private function validateUnionMembers(UnionType $union): void
+ {
+ $memberTypes = $union->getTypes();
+
+ if ($memberTypes === []) {
+ $this->reportError(
+ "Union type {$union->name} must define one or more member types.",
+ $this->getAllNodes($union)
+ );
+ }
+
+ $includedTypeNames = [];
+
+ foreach ($memberTypes as $memberType) {
+ // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
+ if (! $memberType instanceof ObjectType) {
+ $notObjectType = Utils::printSafe($memberType);
+ $this->reportError(
+ "Union type {$union->name} can only include Object types, it cannot include {$notObjectType}.",
+ $this->getUnionMemberTypeNodes($union, $notObjectType)
+ );
+ continue;
+ }
+
+ if (isset($includedTypeNames[$memberType->name])) {
+ $this->reportError(
+ "Union type {$union->name} can only include type {$memberType->name} once.",
+ $this->getUnionMemberTypeNodes($union, $memberType->name)
+ );
+ continue;
+ }
+
+ $includedTypeNames[$memberType->name] = true;
+ }
+ }
+
+ /** @return list<NamedTypeNode> */
+ private function getUnionMemberTypeNodes(UnionType $union, string $typeName): array
+ {
+ $allNodes = array_filter([$union->astNode, ...$union->extensionASTNodes]);
+
+ $types = [];
+ foreach ($allNodes as $node) {
+ foreach ($node->types as $type) {
+ if ($type->name->value === $typeName) {
+ $types[] = $type;
+ }
+ }
+ }
+
+ return $types;
+ }
+
+ /** @throws InvariantViolation */
+ private function validateEnumValues(EnumType $enumType): void
+ {
+ $enumValues = $enumType->getValues();
+
+ if ($enumValues === []) {
+ $this->reportError(
+ "Enum type {$enumType->name} must define one or more values.",
+ $this->getAllNodes($enumType)
+ );
+ }
+
+ foreach ($enumValues as $enumValue) {
+ $valueName = $enumValue->name;
+
+ // Ensure valid name.
+ $this->validateName($enumValue);
+ if (in_array($valueName, ['true', 'false', 'null'], true)) {
+ $this->reportError(
+ "Enum type {$enumType->name} cannot include value: {$valueName}.",
+ $enumValue->astNode
+ );
+ }
+
+ // Ensure valid directives
+ if (isset($enumValue->astNode, $enumValue->astNode->directives)) {
+ $this->validateDirectivesAtLocation(
+ $enumValue->astNode->directives,
+ DirectiveLocation::ENUM_VALUE
+ );
+ }
+ }
+ }
+
+ /** @throws InvariantViolation */
+ private function validateInputFields(InputObjectType $inputObj): void
+ {
+ $fieldMap = $inputObj->getFields();
+
+ if ($fieldMap === []) {
+ $this->reportError(
+ "Input Object type {$inputObj->name} must define one or more fields.",
+ $this->getAllNodes($inputObj)
+ );
+ }
+
+ // Ensure the arguments are valid
+ foreach ($fieldMap as $fieldName => $field) {
+ // Ensure they are named correctly.
+ $this->validateName($field);
+
+ // TODO: Ensure they are unique per field.
+
+ // Ensure the type is an input type.
+ $type = $field->getType();
+ // @phpstan-ignore-next-line The generic type says this should not happen, but a user may use it wrong nonetheless
+ if (! Type::isInputType($type)) {
+ $notInputType = Utils::printSafe($type);
+ $this->reportError(
+ "The type of {$inputObj->name}.{$fieldName} must be Input Type but got: {$notInputType}.",
+ $field->astNode->type ?? null
+ );
+ }
+
+ // Ensure valid directives
+ if (isset($field->astNode, $field->astNode->directives)) {
+ $this->validateDirectivesAtLocation(
+ $field->astNode->directives,
+ DirectiveLocation::INPUT_FIELD_DEFINITION
+ );
+ }
+ }
+ }
+
+ /** @throws InvariantViolation */
+ private function validateTypeIsSingleton(Type $type, string $path): void
+ {
+ $schemaConfig = $this->schema->getConfig();
+ if (! isset($schemaConfig->typeLoader)) {
+ return;
+ }
+
+ $namedType = Type::getNamedType($type);
+ if ($namedType->isBuiltInType()) {
+ return;
+ }
+
+ $name = $namedType->name;
+ if ($namedType !== ($schemaConfig->typeLoader)($name)) {
+ throw new InvariantViolation(static::duplicateType($this->schema, $path, $name));
+ }
+ }
+
+ public static function duplicateType(Schema $schema, string $path, string $name): string
+ {
+ $hint = isset($schema->getConfig()->typeLoader)
+ ? 'Ensure the type loader returns the same instance. '
+ : '';
+
+ return "Found duplicate type in schema at {$path}: {$name}. {$hint}See https://webonyx.github.io/graphql-php/type-definitions/#type-registry.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/TypeKind.php b/plugins/woocommerce/lib/packages/GraphQL/Type/TypeKind.php
new file mode 100644
index 00000000000..cbe234c7edd
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/TypeKind.php
@@ -0,0 +1,15 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type;
+
+class TypeKind
+{
+ public const SCALAR = 'SCALAR';
+ public const OBJECT = 'OBJECT';
+ public const INTERFACE = 'INTERFACE';
+ public const UNION = 'UNION';
+ public const ENUM = 'ENUM';
+ public const INPUT_OBJECT = 'INPUT_OBJECT';
+ public const LIST = 'LIST';
+ public const NON_NULL = 'NON_NULL';
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Type/Validation/InputObjectCircularRefs.php b/plugins/woocommerce/lib/packages/GraphQL/Type/Validation/InputObjectCircularRefs.php
new file mode 100644
index 00000000000..84992b19799
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Type/Validation/InputObjectCircularRefs.php
@@ -0,0 +1,97 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Type\Validation;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaValidationContext;
+
+class InputObjectCircularRefs
+{
+ private SchemaValidationContext $schemaValidationContext;
+
+ /**
+ * Tracks already visited types to maintain O(N) and to ensure that cycles
+ * are not redundantly reported.
+ *
+ * @var array<string, bool>
+ */
+ private array $visitedTypes = [];
+
+ /** @var array<int, InputObjectField> */
+ private array $fieldPath = [];
+
+ /**
+ * Position in the type path.
+ *
+ * @var array<string, int>
+ */
+ private array $fieldPathIndexByTypeName = [];
+
+ public function __construct(SchemaValidationContext $schemaValidationContext)
+ {
+ $this->schemaValidationContext = $schemaValidationContext;
+ }
+
+ /**
+ * This does a straight-forward DFS to find cycles.
+ * It does not terminate when a cycle was found but continues to explore
+ * the graph to find all possible cycles.
+ *
+ * @throws InvariantViolation
+ */
+ public function validate(InputObjectType $inputObj): void
+ {
+ if (isset($this->visitedTypes[$inputObj->name])) {
+ return;
+ }
+
+ $this->visitedTypes[$inputObj->name] = true;
+ $this->fieldPathIndexByTypeName[$inputObj->name] = count($this->fieldPath);
+
+ $fieldMap = $inputObj->getFields();
+ foreach ($fieldMap as $field) {
+ $type = $field->getType();
+
+ if ($type instanceof NonNull) {
+ $fieldType = $type->getWrappedType();
+
+ // If the type of the field is anything else then a non-nullable input object,
+ // there is no chance of an unbreakable cycle
+ if ($fieldType instanceof InputObjectType) {
+ $this->fieldPath[] = $field;
+
+ if (! isset($this->fieldPathIndexByTypeName[$fieldType->name])) {
+ $this->validate($fieldType);
+ } else {
+ $cycleIndex = $this->fieldPathIndexByTypeName[$fieldType->name];
+ $cyclePath = array_slice($this->fieldPath, $cycleIndex);
+ $fieldNames = implode(
+ '.',
+ array_map(
+ static fn (InputObjectField $field): string => $field->name,
+ $cyclePath
+ )
+ );
+ $fieldNodes = array_map(
+ static fn (InputObjectField $field): ?InputValueDefinitionNode => $field->astNode,
+ $cyclePath
+ );
+
+ $this->schemaValidationContext->reportError(
+ "Cannot reference Input Object \"{$fieldType->name}\" within itself through a series of non-null fields: \"{$fieldNames}\".",
+ $fieldNodes
+ );
+ }
+ }
+ }
+
+ array_pop($this->fieldPath);
+ }
+
+ unset($this->fieldPathIndexByTypeName[$inputObj->name]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/AST.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/AST.php
new file mode 100644
index 00000000000..defad21dc1d
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/AST.php
@@ -0,0 +1,631 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Location;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\IDType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\LeafType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NullableType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * Various utilities dealing with AST.
+ */
+class AST
+{
+ /**
+ * Convert representation of AST as an associative array to instance of Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node.
+ *
+ * For example:
+ *
+ * ```php
+ * AST::fromArray([
+ * 'kind' => 'ListValue',
+ * 'values' => [
+ * ['kind' => 'StringValue', 'value' => 'my str'],
+ * ['kind' => 'StringValue', 'value' => 'my other str']
+ * ],
+ * 'loc' => ['start' => 21, 'end' => 25]
+ * ]);
+ * ```
+ *
+ * Will produce instance of `ListValueNode` where `values` prop is a lazily-evaluated `NodeList`
+ * returning instances of `StringValueNode` on access.
+ *
+ * This is a reverse operation for AST::toArray($node)
+ *
+ * @param array<string, mixed> $node
+ *
+ * @api
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ */
+ public static function fromArray(array $node): Node
+ {
+ $kind = $node['kind'] ?? null;
+ if ($kind === null) {
+ $safeNode = Utils::printSafeJson($node);
+ throw new InvariantViolation("Node is missing kind: {$safeNode}");
+ }
+
+ $class = NodeKind::CLASS_MAP[$kind] ?? null;
+ if ($class === null) {
+ $safeNode = Utils::printSafeJson($node);
+ throw new InvariantViolation("Node has unexpected kind: {$safeNode}");
+ }
+
+ $instance = new $class([]);
+
+ if (isset($node['loc']['start'], $node['loc']['end'])) {
+ $instance->loc = Location::create($node['loc']['start'], $node['loc']['end']);
+ }
+
+ foreach ($node as $key => $value) {
+ if ($key === 'loc' || $key === 'kind') {
+ continue;
+ }
+
+ if (is_array($value)) {
+ $value = isset($value[0]) || $value === []
+ ? new NodeList($value)
+ : self::fromArray($value);
+ }
+
+ $instance->{$key} = $value;
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Convert AST node to serializable array.
+ *
+ * @return array<string, mixed>
+ *
+ * @api
+ */
+ public static function toArray(Node $node): array
+ {
+ return $node->toArray();
+ }
+
+ /**
+ * Produces a Automattic\WooCommerce\Vendor\GraphQL Value AST given a PHP value.
+ *
+ * Optionally, a Automattic\WooCommerce\Vendor\GraphQL type may be provided, which will be used to
+ * disambiguate between value primitives.
+ *
+ * | PHP Value | Automattic\WooCommerce\Vendor\GraphQL Value |
+ * | ------------- | -------------------- |
+ * | Object | Input Object |
+ * | Assoc Array | Input Object |
+ * | Array | List |
+ * | Boolean | Boolean |
+ * | String | String / Enum Value |
+ * | Int | Int |
+ * | Float | Int / Float |
+ * | Mixed | Enum Value |
+ * | null | NullValue |
+ *
+ * @param mixed $value
+ * @param InputType&Type $type
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ *
+ * @return (ValueNode&Node)|null
+ *
+ * @api
+ */
+ public static function astFromValue($value, InputType $type): ?ValueNode
+ {
+ if ($type instanceof NonNull) {
+ $wrappedType = $type->getWrappedType();
+ assert($wrappedType instanceof InputType);
+
+ $astValue = self::astFromValue($value, $wrappedType);
+
+ return $astValue instanceof NullValueNode
+ ? null
+ : $astValue;
+ }
+
+ if ($value === null) {
+ return new NullValueNode([]);
+ }
+
+ // Convert PHP iterables to Automattic\WooCommerce\Vendor\GraphQL list. If the GraphQLType is a list, but
+ // the value is not an array, convert the value using the list's item type.
+ if ($type instanceof ListOfType) {
+ $itemType = $type->getWrappedType();
+ assert($itemType instanceof InputType, 'proven by schema validation');
+
+ if (is_iterable($value)) {
+ $valuesNodes = [];
+ foreach ($value as $item) {
+ $itemNode = self::astFromValue($item, $itemType);
+ if ($itemNode !== null) {
+ $valuesNodes[] = $itemNode;
+ }
+ }
+
+ return new ListValueNode(['values' => new NodeList($valuesNodes)]);
+ }
+
+ return self::astFromValue($value, $itemType);
+ }
+
+ // Populate the fields of the input object by creating ASTs from each value
+ // in the PHP object according to the fields in the input type.
+ if ($type instanceof InputObjectType) {
+ $isArray = is_array($value);
+ $isArrayLike = $isArray || $value instanceof \ArrayAccess;
+ if (! $isArrayLike && ! is_object($value)) {
+ return null;
+ }
+
+ $fields = $type->getFields();
+ $fieldNodes = [];
+ foreach ($fields as $fieldName => $field) {
+ $fieldValue = $isArrayLike
+ ? $value[$fieldName] ?? null
+ : $value->{$fieldName} ?? null;
+
+ // Have to check additionally if key exists, since we differentiate between
+ // "no key" and "value is null":
+ if ($fieldValue !== null) {
+ $fieldExists = true;
+ } elseif ($isArray) {
+ $fieldExists = array_key_exists($fieldName, $value);
+ } elseif ($isArrayLike) {
+ $fieldExists = $value->offsetExists($fieldName);
+ } else {
+ $fieldExists = property_exists($value, $fieldName);
+ }
+
+ if (! $fieldExists) {
+ continue;
+ }
+
+ $fieldNode = self::astFromValue($fieldValue, $field->getType());
+
+ if ($fieldNode === null) {
+ continue;
+ }
+
+ $fieldNodes[] = new ObjectFieldNode([
+ 'name' => new NameNode(['value' => $fieldName]),
+ 'value' => $fieldNode,
+ ]);
+ }
+
+ return new ObjectValueNode(['fields' => new NodeList($fieldNodes)]);
+ }
+
+ assert($type instanceof LeafType, 'other options were exhausted');
+
+ // Since value is an internally represented value, it must be serialized
+ // to an externally represented value before converting into an AST.
+ $serialized = $type->serialize($value);
+
+ // Others serialize based on their corresponding PHP scalar types.
+ if (is_bool($serialized)) {
+ return new BooleanValueNode(['value' => $serialized]);
+ }
+
+ if (is_int($serialized)) {
+ return new IntValueNode(['value' => (string) $serialized]);
+ }
+
+ if (is_float($serialized)) {
+ /** @phpstan-ignore equal.notAllowed (int cast with == used for performance reasons) */
+ if ((int) $serialized == $serialized) {
+ return new IntValueNode(['value' => (string) $serialized]);
+ }
+
+ return new FloatValueNode(['value' => (string) $serialized]);
+ }
+
+ if (is_string($serialized)) {
+ // Enum types use Enum literals.
+ if ($type instanceof EnumType) {
+ return new EnumValueNode(['value' => $serialized]);
+ }
+
+ // ID types can use Int literals.
+ $asInt = (int) $serialized;
+ if ($type instanceof IDType && (string) $asInt === $serialized) {
+ return new IntValueNode(['value' => $serialized]);
+ }
+
+ // Use json_encode, which uses the same string encoding as GraphQL,
+ // then remove the quotes.
+ return new StringValueNode(['value' => $serialized]);
+ }
+
+ $notConvertible = Utils::printSafe($serialized);
+ throw new InvariantViolation("Cannot convert value to AST: {$notConvertible}");
+ }
+
+ /**
+ * Produces a PHP value given a Automattic\WooCommerce\Vendor\GraphQL Value AST.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL type must be provided, which will be used to interpret different
+ * Automattic\WooCommerce\Vendor\GraphQL Value literals.
+ *
+ * Returns `null` when the value could not be validly coerced according to
+ * the provided type.
+ *
+ * | Automattic\WooCommerce\Vendor\GraphQL Value | PHP Value |
+ * | -------------------- | ------------- |
+ * | Input Object | Assoc Array |
+ * | List | Array |
+ * | Boolean | Boolean |
+ * | String | String |
+ * | Int / Float | Int / Float |
+ * | Enum Value | Mixed |
+ * | Null Value | null |
+ *
+ * @param (ValueNode&Node)|null $valueNode
+ * @param array<string, mixed>|null $variables
+ *
+ * @throws \Exception
+ *
+ * @return mixed
+ *
+ * @api
+ */
+ public static function valueFromAST(?ValueNode $valueNode, Type $type, ?array $variables = null, ?Schema $schema = null)
+ {
+ $undefined = Utils::undefined();
+
+ if ($valueNode === null) {
+ // When there is no AST, then there is also no value.
+ // Importantly, this is different from returning the Automattic\WooCommerce\Vendor\GraphQL null value.
+ return $undefined;
+ }
+
+ if ($type instanceof NonNull) {
+ if ($valueNode instanceof NullValueNode) {
+ // Invalid: intentionally return no value.
+ return $undefined;
+ }
+
+ return self::valueFromAST($valueNode, $type->getWrappedType(), $variables, $schema);
+ }
+
+ if ($valueNode instanceof NullValueNode) {
+ // This is explicitly returning the value null.
+ return null;
+ }
+
+ if ($valueNode instanceof VariableNode) {
+ $variableName = $valueNode->name->value;
+
+ if ($variables === null || ! array_key_exists($variableName, $variables)) {
+ // No valid return value.
+ return $undefined;
+ }
+
+ // Note: This does no further checking that this variable is correct.
+ // This assumes that this query has been validated and the variable
+ // usage here is of the correct type.
+ return $variables[$variableName];
+ }
+
+ if ($type instanceof ListOfType) {
+ $itemType = $type->getWrappedType();
+
+ if ($valueNode instanceof ListValueNode) {
+ $coercedValues = [];
+ $itemNodes = $valueNode->values;
+ foreach ($itemNodes as $itemNode) {
+ if (self::isMissingVariable($itemNode, $variables)) {
+ // If an array contains a missing variable, it is either coerced to
+ // null or if the item type is non-null, it considered invalid.
+ if ($itemType instanceof NonNull) {
+ // Invalid: intentionally return no value.
+ return $undefined;
+ }
+
+ $coercedValues[] = null;
+ } else {
+ $itemValue = self::valueFromAST($itemNode, $itemType, $variables, $schema);
+ if ($undefined === $itemValue) {
+ // Invalid: intentionally return no value.
+ return $undefined;
+ }
+
+ $coercedValues[] = $itemValue;
+ }
+ }
+
+ return $coercedValues;
+ }
+
+ $coercedValue = self::valueFromAST($valueNode, $itemType, $variables, $schema);
+ if ($undefined === $coercedValue) {
+ // Invalid: intentionally return no value.
+ return $undefined;
+ }
+
+ return [$coercedValue];
+ }
+
+ if ($type instanceof InputObjectType) {
+ if (! $valueNode instanceof ObjectValueNode) {
+ // Invalid: intentionally return no value.
+ return $undefined;
+ }
+
+ $coercedObj = [];
+ $fields = $type->getFields();
+
+ $fieldNodes = [];
+ foreach ($valueNode->fields as $field) {
+ $fieldNodes[$field->name->value] = $field;
+ }
+
+ foreach ($fields as $field) {
+ $fieldName = $field->name;
+ $fieldNode = $fieldNodes[$fieldName] ?? null;
+
+ if ($fieldNode === null || self::isMissingVariable($fieldNode->value, $variables)) {
+ if ($field->defaultValueExists()) {
+ $coercedObj[$fieldName] = $field->defaultValue;
+ } elseif ($field->getType() instanceof NonNull) {
+ // Invalid: intentionally return no value.
+ return $undefined;
+ }
+
+ continue;
+ }
+
+ $fieldValue = self::valueFromAST(
+ $fieldNode->value,
+ $field->getType(),
+ $variables,
+ $schema,
+ );
+
+ if ($undefined === $fieldValue) {
+ // Invalid: intentionally return no value.
+ return $undefined;
+ }
+
+ $coercedObj[$fieldName] = $fieldValue;
+ }
+
+ return $type->parseValue($coercedObj);
+ }
+
+ if ($type instanceof EnumType) {
+ try {
+ return $type->parseLiteral($valueNode, $variables);
+ } catch (\Throwable $error) {
+ return $undefined;
+ }
+ }
+
+ assert($type instanceof ScalarType, 'only remaining option');
+ $typeName = $type->name;
+
+ // Account for type loader returning a different scalar instance than
+ // the built-in singleton used in field definitions. Resolve the actual
+ // type from the schema to ensure the correct parseLiteral() is called.
+ if ($schema !== null && Type::isBuiltInScalarName($typeName)) {
+ $schemaType = $schema->getType($typeName);
+ assert($schemaType instanceof ScalarType, "Schema must provide a ScalarType for built-in scalar \"{$typeName}\".");
+ $type = $schemaType;
+ }
+
+ // Scalars fulfill parsing a literal value via parseLiteral().
+ // Invalid values represent a failure to parse correctly, in which case
+ // no value is returned.
+ try {
+ return $type->parseLiteral($valueNode, $variables);
+ } catch (\Throwable $error) {
+ return $undefined;
+ }
+ }
+
+ /**
+ * Returns true if the provided valueNode is a variable which is not defined
+ * in the set of variables.
+ *
+ * @param ValueNode&Node $valueNode
+ * @param array<string, mixed>|null $variables
+ */
+ private static function isMissingVariable(ValueNode $valueNode, ?array $variables): bool
+ {
+ return $valueNode instanceof VariableNode
+ && ($variables === null || ! array_key_exists($valueNode->name->value, $variables));
+ }
+
+ /**
+ * Produces a PHP value given a Automattic\WooCommerce\Vendor\GraphQL Value AST.
+ *
+ * Unlike `valueFromAST()`, no type is provided. The resulting PHP value
+ * will reflect the provided Automattic\WooCommerce\Vendor\GraphQL value AST.
+ *
+ * | Automattic\WooCommerce\Vendor\GraphQL Value | PHP Value |
+ * | -------------------- | ------------- |
+ * | Input Object | Assoc Array |
+ * | List | Array |
+ * | Boolean | Boolean |
+ * | String | String |
+ * | Int / Float | Int / Float |
+ * | Enum | Mixed |
+ * | Null | null |
+ *
+ * @param array<string, mixed>|null $variables
+ *
+ * @throws \Exception
+ *
+ * @return mixed
+ *
+ * @api
+ */
+ public static function valueFromASTUntyped(Node $valueNode, ?array $variables = null)
+ {
+ switch (true) {
+ case $valueNode instanceof NullValueNode:
+ return null;
+
+ case $valueNode instanceof IntValueNode:
+ return (int) $valueNode->value;
+
+ case $valueNode instanceof FloatValueNode:
+ return (float) $valueNode->value;
+
+ case $valueNode instanceof StringValueNode:
+ case $valueNode instanceof EnumValueNode:
+ case $valueNode instanceof BooleanValueNode:
+ return $valueNode->value;
+
+ case $valueNode instanceof ListValueNode:
+ $values = [];
+ foreach ($valueNode->values as $node) {
+ $values[] = self::valueFromASTUntyped($node, $variables);
+ }
+
+ return $values;
+
+ case $valueNode instanceof ObjectValueNode:
+ $values = [];
+ foreach ($valueNode->fields as $field) {
+ $values[$field->name->value] = self::valueFromASTUntyped($field->value, $variables);
+ }
+
+ return $values;
+
+ case $valueNode instanceof VariableNode:
+ $variableName = $valueNode->name->value;
+
+ return ($variables ?? []) !== [] && isset($variables[$variableName])
+ ? $variables[$variableName]
+ : null;
+ }
+
+ throw new Error("Unexpected value kind: {$valueNode->kind}");
+ }
+
+ /**
+ * Returns type definition for given AST Type node.
+ *
+ * @param callable(string): ?Type $typeLoader
+ * @param NamedTypeNode|ListTypeNode|NonNullTypeNode $inputTypeNode
+ *
+ * @throws \Exception
+ *
+ * @api
+ */
+ public static function typeFromAST(callable $typeLoader, Node $inputTypeNode): ?Type
+ {
+ if ($inputTypeNode instanceof ListTypeNode) {
+ $innerType = self::typeFromAST($typeLoader, $inputTypeNode->type);
+
+ return $innerType === null
+ ? null
+ : new ListOfType($innerType);
+ }
+
+ if ($inputTypeNode instanceof NonNullTypeNode) {
+ $innerType = self::typeFromAST($typeLoader, $inputTypeNode->type);
+ if ($innerType === null) {
+ return null;
+ }
+
+ assert($innerType instanceof NullableType, 'proven by schema validation');
+
+ return new NonNull($innerType);
+ }
+
+ return $typeLoader($inputTypeNode->name->value);
+ }
+
+ /**
+ * Returns the operation within a document by name.
+ *
+ * If a name is not provided, an operation is only returned if the document has exactly one.
+ *
+ * @api
+ */
+ public static function getOperationAST(DocumentNode $document, ?string $operationName = null): ?OperationDefinitionNode
+ {
+ $operation = null;
+ foreach ($document->definitions->getIterator() as $node) {
+ if (! $node instanceof OperationDefinitionNode) {
+ continue;
+ }
+
+ if ($operationName === null) {
+ // We found a second operation, so we bail instead of returning an ambiguous result.
+ if ($operation !== null) {
+ return null;
+ }
+
+ $operation = $node;
+ } elseif ($node->name instanceof NameNode && $node->name->value === $operationName) {
+ return $node;
+ }
+ }
+
+ return $operation;
+ }
+
+ /**
+ * Provided a collection of ASTs, presumably each from different files,
+ * concatenate the ASTs together into batched AST, useful for validating many
+ * Automattic\WooCommerce\Vendor\GraphQL source files which together represent one conceptual application.
+ *
+ * @param array<DocumentNode> $documents
+ *
+ * @api
+ */
+ public static function concatAST(array $documents): DocumentNode
+ {
+ /** @var array<int, Node&DefinitionNode> $definitions */
+ $definitions = [];
+ foreach ($documents as $document) {
+ foreach ($document->definitions as $definition) {
+ $definitions[] = $definition;
+ }
+ }
+
+ return new DocumentNode(['definitions' => new NodeList($definitions)]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/ASTDefinitionBuilder.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/ASTDefinitionBuilder.php
new file mode 100644
index 00000000000..b96d6fe5f12
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/ASTDefinitionBuilder.php
@@ -0,0 +1,651 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Values;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\OutputType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+
+/**
+ * @see FieldDefinition, InputObjectField
+ *
+ * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition
+ * @phpstan-import-type InputObjectFieldConfig from InputObjectField
+ * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField
+ *
+ * @phpstan-type ResolveType callable(string, Node|null): (Type&NamedType)
+ * @phpstan-type TypeConfigDecorator callable(array<string, mixed>, Node&TypeDefinitionNode, array<string, Node&TypeDefinitionNode>): array<string, mixed>
+ * @phpstan-type FieldConfigDecorator callable(UnnamedFieldDefinitionConfig, FieldDefinitionNode, ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode): UnnamedFieldDefinitionConfig
+ */
+class ASTDefinitionBuilder
+{
+ /** @var array<string, Node&TypeDefinitionNode> */
+ private array $typeDefinitionsMap;
+
+ /**
+ * @var callable
+ *
+ * @phpstan-var ResolveType
+ */
+ private $resolveType;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var TypeConfigDecorator|null
+ */
+ private $typeConfigDecorator;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var FieldConfigDecorator|null
+ */
+ private $fieldConfigDecorator;
+
+ /** @var array<string, Type&NamedType> */
+ private array $cache;
+
+ /** @var array<string, array<int, Node&TypeExtensionNode>> */
+ private array $typeExtensionsMap;
+
+ /**
+ * @param array<string, Node&TypeDefinitionNode> $typeDefinitionsMap
+ * @param array<string, array<int, Node&TypeExtensionNode>> $typeExtensionsMap
+ *
+ * @phpstan-param ResolveType $resolveType
+ * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
+ *
+ * @throws InvariantViolation
+ */
+ public function __construct(
+ array $typeDefinitionsMap,
+ array $typeExtensionsMap,
+ callable $resolveType,
+ ?callable $typeConfigDecorator = null,
+ ?callable $fieldConfigDecorator = null
+ ) {
+ $this->typeDefinitionsMap = $typeDefinitionsMap;
+ $this->typeExtensionsMap = $typeExtensionsMap;
+ $this->resolveType = $resolveType;
+ $this->typeConfigDecorator = $typeConfigDecorator;
+ $this->fieldConfigDecorator = $fieldConfigDecorator;
+
+ $this->cache = Type::builtInTypes();
+ }
+
+ /** @throws \Exception */
+ public function buildDirective(DirectiveDefinitionNode $directiveNode): Directive
+ {
+ $locations = [];
+ foreach ($directiveNode->locations as $location) {
+ $locations[] = $location->value;
+ }
+
+ return new Directive([
+ 'name' => $directiveNode->name->value,
+ 'description' => $directiveNode->description->value ?? null,
+ 'args' => $this->makeInputValues($directiveNode->arguments),
+ 'isRepeatable' => $directiveNode->repeatable,
+ 'locations' => $locations,
+ 'astNode' => $directiveNode,
+ ]);
+ }
+
+ /**
+ * @param NodeList<InputValueDefinitionNode> $values
+ *
+ * @throws \Exception
+ *
+ * @return array<string, UnnamedInputObjectFieldConfig>
+ */
+ private function makeInputValues(NodeList $values): array
+ {
+ /** @var array<string, UnnamedInputObjectFieldConfig> $map */
+ $map = [];
+ foreach ($values as $value) {
+ // Note: While this could make assertions to get the correctly typed
+ // value, that would throw immediately while type system validation
+ // with validateSchema() will produce more actionable results.
+ /** @var Type&InputType $type */
+ $type = $this->buildWrappedType($value->type);
+
+ $config = [
+ 'name' => $value->name->value,
+ 'type' => $type,
+ 'description' => $value->description->value ?? null,
+ 'deprecationReason' => $this->getDeprecationReason($value),
+ 'astNode' => $value,
+ ];
+
+ if ($value->defaultValue !== null) {
+ $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
+ }
+
+ $map[$value->name->value] = $config;
+ }
+
+ return $map;
+ }
+
+ /**
+ * @param array<InputObjectTypeDefinitionNode|InputObjectTypeExtensionNode> $nodes
+ *
+ * @throws \Exception
+ *
+ * @return array<string, UnnamedInputObjectFieldConfig>
+ */
+ private function makeInputFields(array $nodes): array
+ {
+ /** @var array<int, InputValueDefinitionNode> $fields */
+ $fields = [];
+ foreach ($nodes as $node) {
+ array_push($fields, ...$node->fields);
+ }
+
+ return $this->makeInputValues(new NodeList($fields));
+ }
+
+ /**
+ * @param ListTypeNode|NonNullTypeNode|NamedTypeNode $typeNode
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ private function buildWrappedType(TypeNode $typeNode): Type
+ {
+ if ($typeNode instanceof ListTypeNode) {
+ return Type::listOf($this->buildWrappedType($typeNode->type));
+ }
+
+ if ($typeNode instanceof NonNullTypeNode) {
+ // @phpstan-ignore-next-line contained type is NullableType
+ return Type::nonNull($this->buildWrappedType($typeNode->type));
+ }
+
+ return $this->buildType($typeNode);
+ }
+
+ /**
+ * @param string|(Node&NamedTypeNode)|(Node&TypeDefinitionNode) $ref
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return Type&NamedType
+ */
+ public function buildType($ref): Type
+ {
+ if ($ref instanceof TypeDefinitionNode) {
+ return $this->internalBuildType($ref->getName()->value, $ref);
+ }
+ if ($ref instanceof NamedTypeNode) {
+ return $this->internalBuildType($ref->name->value, $ref);
+ }
+
+ return $this->internalBuildType($ref);
+ }
+
+ /**
+ * Calling this method is an equivalent of `typeMap[typeName]` in `graphql-js`.
+ * It is legal to access a type from the map of already-built types that doesn't exist in the map.
+ * Since we build types lazily, and we don't have a such map of built types,
+ * this method provides a way to build a type that may not exist in the SDL definitions and returns null instead.
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return (Type&NamedType)|null
+ */
+ public function maybeBuildType(string $name): ?Type
+ {
+ return isset($this->typeDefinitionsMap[$name])
+ ? $this->buildType($name)
+ : null;
+ }
+
+ /**
+ * @param (Node&NamedTypeNode)|(Node&TypeDefinitionNode)|null $typeNode
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return Type&NamedType
+ */
+ private function internalBuildType(string $typeName, ?Node $typeNode = null): Type
+ {
+ if (isset($this->cache[$typeName])) {
+ return $this->cache[$typeName];
+ }
+
+ if (isset($this->typeDefinitionsMap[$typeName])) {
+ $type = $this->makeSchemaDef($this->typeDefinitionsMap[$typeName]);
+
+ if ($this->typeConfigDecorator !== null) {
+ try {
+ $config = ($this->typeConfigDecorator)(
+ $type->config,
+ $this->typeDefinitionsMap[$typeName],
+ $this->typeDefinitionsMap
+ );
+ } catch (\Throwable $e) {
+ $class = static::class;
+ throw new Error("Type config decorator passed to {$class} threw an error when building {$typeName} type: {$e->getMessage()}", null, null, [], null, $e);
+ }
+
+ // @phpstan-ignore-next-line should not happen, but function types are not enforced by PHP
+ if (! is_array($config) || isset($config[0])) {
+ $class = static::class;
+ $notArray = Utils::printSafe($config);
+ throw new Error("Type config decorator passed to {$class} is expected to return an array, but got {$notArray}");
+ }
+
+ $type = $this->makeSchemaDefFromConfig($this->typeDefinitionsMap[$typeName], $config);
+ }
+
+ return $this->cache[$typeName] = $type;
+ }
+
+ return $this->cache[$typeName] = ($this->resolveType)($typeName, $typeNode);
+ }
+
+ /**
+ * @param TypeDefinitionNode&Node $def
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ *
+ * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
+ */
+ private function makeSchemaDef(Node $def): Type
+ {
+ switch (true) {
+ case $def instanceof ObjectTypeDefinitionNode:
+ return $this->makeTypeDef($def);
+
+ case $def instanceof InterfaceTypeDefinitionNode:
+ return $this->makeInterfaceDef($def);
+
+ case $def instanceof EnumTypeDefinitionNode:
+ return $this->makeEnumDef($def);
+
+ case $def instanceof UnionTypeDefinitionNode:
+ return $this->makeUnionDef($def);
+
+ case $def instanceof ScalarTypeDefinitionNode:
+ return $this->makeScalarDef($def);
+
+ default:
+ assert($def instanceof InputObjectTypeDefinitionNode, 'all implementations are known');
+
+ return $this->makeInputObjectDef($def);
+ }
+ }
+
+ /** @throws InvariantViolation */
+ private function makeTypeDef(ObjectTypeDefinitionNode $def): ObjectType
+ {
+ $name = $def->name->value;
+ /** @var array<ObjectTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
+ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
+ $allNodes = [$def, ...$extensionASTNodes];
+
+ return new ObjectType([
+ 'name' => $name,
+ 'description' => $def->description->value ?? null,
+ 'fields' => fn (): array => $this->makeFieldDefMap($allNodes),
+ 'interfaces' => fn (): array => $this->makeImplementedInterfaces($allNodes),
+ 'astNode' => $def,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /**
+ * @param array<ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode> $nodes
+ *
+ * @throws \Exception
+ *
+ * @phpstan-return array<string, UnnamedFieldDefinitionConfig>
+ */
+ private function makeFieldDefMap(array $nodes): array
+ {
+ $map = [];
+ foreach ($nodes as $node) {
+ foreach ($node->fields as $field) {
+ $map[$field->name->value] = $this->buildField($field, $node);
+ }
+ }
+
+ return $map;
+ }
+
+ /**
+ * @param ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode $node
+ *
+ * @throws \Exception
+ * @throws Error
+ *
+ * @return UnnamedFieldDefinitionConfig
+ */
+ public function buildField(FieldDefinitionNode $field, object $node): array
+ {
+ // Note: While this could make assertions to get the correctly typed
+ // value, that would throw immediately while type system validation
+ // with validateSchema() will produce more actionable results.
+ /** @var OutputType&Type $type */
+ $type = $this->buildWrappedType($field->type);
+
+ $config = [
+ 'type' => $type,
+ 'description' => $field->description->value ?? null,
+ 'args' => $this->makeInputValues($field->arguments),
+ 'deprecationReason' => $this->getDeprecationReason($field),
+ 'astNode' => $field,
+ ];
+
+ if ($this->fieldConfigDecorator !== null) {
+ $config = ($this->fieldConfigDecorator)($config, $field, $node);
+ }
+
+ return $config;
+ }
+
+ /**
+ * Given a collection of directives, returns the string value for the
+ * deprecation reason.
+ *
+ * @param EnumValueDefinitionNode|FieldDefinitionNode|InputValueDefinitionNode $node
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ */
+ private function getDeprecationReason(Node $node): ?string
+ {
+ $deprecated = Values::getDirectiveValues(
+ Directive::deprecatedDirective(),
+ $node
+ );
+
+ return $deprecated['reason'] ?? null;
+ }
+
+ /**
+ * @param array<ObjectTypeDefinitionNode|ObjectTypeExtensionNode|InterfaceTypeDefinitionNode|InterfaceTypeExtensionNode> $nodes
+ *
+ * @throws \Exception
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return array<int, InterfaceType>
+ */
+ private function makeImplementedInterfaces(array $nodes): array
+ {
+ // Note: While this could make early assertions to get the correctly
+ // typed values, that would throw immediately while type system
+ // validation with validateSchema() will produce more actionable results.
+
+ $interfaces = [];
+ foreach ($nodes as $node) {
+ foreach ($node->interfaces as $interface) {
+ $interfaces[] = $this->buildType($interface);
+ }
+ }
+
+ // @phpstan-ignore-next-line generic type will be validated during schema validation
+ return $interfaces;
+ }
+
+ /** @throws InvariantViolation */
+ private function makeInterfaceDef(InterfaceTypeDefinitionNode $def): InterfaceType
+ {
+ $name = $def->name->value;
+ /** @var array<InterfaceTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
+ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
+ $allNodes = [$def, ...$extensionASTNodes];
+
+ return new InterfaceType([
+ 'name' => $name,
+ 'description' => $def->description->value ?? null,
+ 'fields' => fn (): array => $this->makeFieldDefMap($allNodes),
+ 'interfaces' => fn (): array => $this->makeImplementedInterfaces($allNodes),
+ 'astNode' => $def,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /**
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ */
+ private function makeEnumDef(EnumTypeDefinitionNode $def): EnumType
+ {
+ $name = $def->name->value;
+ /** @var array<EnumTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
+ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
+
+ $values = [];
+ foreach ([$def, ...$extensionASTNodes] as $node) {
+ foreach ($node->values as $value) {
+ $values[$value->name->value] = [
+ 'description' => $value->description->value ?? null,
+ 'deprecationReason' => $this->getDeprecationReason($value),
+ 'astNode' => $value,
+ ];
+ }
+ }
+
+ return new EnumType([
+ 'name' => $name,
+ 'description' => $def->description->value ?? null,
+ 'values' => $values,
+ 'astNode' => $def,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /** @throws InvariantViolation */
+ private function makeUnionDef(UnionTypeDefinitionNode $def): UnionType
+ {
+ $name = $def->name->value;
+ /** @var array<UnionTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
+ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
+
+ return new UnionType([
+ 'name' => $name,
+ 'description' => $def->description->value ?? null,
+ // Note: While this could make assertions to get the correctly typed
+ // values below, that would throw immediately while type system
+ // validation with validateSchema() will produce more actionable results.
+ 'types' => function () use ($def, $extensionASTNodes): array {
+ $types = [];
+ foreach ([$def, ...$extensionASTNodes] as $node) {
+ foreach ($node->types as $type) {
+ $types[] = $this->buildType($type);
+ }
+ }
+
+ /** @var array<int, ObjectType> $types */
+ return $types;
+ },
+ 'astNode' => $def,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /** @throws InvariantViolation */
+ private function makeScalarDef(ScalarTypeDefinitionNode $def): CustomScalarType
+ {
+ $name = $def->name->value;
+ /** @var array<ScalarTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
+ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
+
+ return new CustomScalarType([
+ 'name' => $name,
+ 'description' => $def->description->value ?? null,
+ 'serialize' => static fn ($value) => $value,
+ 'astNode' => $def,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /**
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ */
+ private function makeInputObjectDef(InputObjectTypeDefinitionNode $def): InputObjectType
+ {
+ $name = $def->name->value;
+ /** @var array<InputObjectTypeExtensionNode> $extensionASTNodes (proven by schema validation) */
+ $extensionASTNodes = $this->typeExtensionsMap[$name] ?? [];
+
+ $oneOfDirective = Directive::oneOfDirective();
+
+ // Check for @oneOf directive in the definition node
+ $isOneOf = Values::getDirectiveValues($oneOfDirective, $def) !== null;
+
+ // Check for @oneOf directive in extension nodes
+ if (! $isOneOf) {
+ foreach ($extensionASTNodes as $extensionNode) {
+ if (Values::getDirectiveValues($oneOfDirective, $extensionNode) !== null) {
+ $isOneOf = true;
+ break;
+ }
+ }
+ }
+
+ return new InputObjectType([
+ 'name' => $name,
+ 'description' => $def->description->value ?? null,
+ 'isOneOf' => $isOneOf,
+ 'fields' => fn (): array => $this->makeInputFields([$def, ...$extensionASTNodes]),
+ 'astNode' => $def,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /**
+ * @param array<string, mixed> $config
+ *
+ * @throws Error
+ *
+ * @return CustomScalarType|EnumType|InputObjectType|InterfaceType|ObjectType|UnionType
+ */
+ private function makeSchemaDefFromConfig(Node $def, array $config): Type
+ {
+ switch (true) {
+ case $def instanceof ObjectTypeDefinitionNode:
+ // @phpstan-ignore-next-line assume the config matches
+ return new ObjectType($config);
+
+ case $def instanceof InterfaceTypeDefinitionNode:
+ // @phpstan-ignore-next-line assume the config matches
+ return new InterfaceType($config);
+
+ case $def instanceof EnumTypeDefinitionNode:
+ // @phpstan-ignore-next-line assume the config matches
+ return new EnumType($config);
+
+ case $def instanceof UnionTypeDefinitionNode:
+ // @phpstan-ignore-next-line assume the config matches
+ return new UnionType($config);
+
+ case $def instanceof ScalarTypeDefinitionNode:
+ // @phpstan-ignore-next-line assume the config matches
+ return new CustomScalarType($config);
+
+ case $def instanceof InputObjectTypeDefinitionNode:
+ // @phpstan-ignore-next-line assume the config matches
+ return new InputObjectType($config);
+
+ default:
+ throw new Error("Type kind of {$def->kind} not supported.");
+ }
+ }
+
+ /**
+ * @throws \Exception
+ *
+ * @return InputObjectFieldConfig
+ */
+ public function buildInputField(InputValueDefinitionNode $value): array
+ {
+ $type = $this->buildWrappedType($value->type);
+ assert($type instanceof InputType, 'proven by schema validation');
+
+ $config = [
+ 'name' => $value->name->value,
+ 'type' => $type,
+ 'description' => $value->description->value ?? null,
+ 'astNode' => $value,
+ ];
+
+ if ($value->defaultValue !== null) {
+ $config['defaultValue'] = AST::valueFromAST($value->defaultValue, $type);
+ }
+
+ return $config;
+ }
+
+ /**
+ * @throws \Exception
+ *
+ * @return array<string, mixed>
+ */
+ public function buildEnumValue(EnumValueDefinitionNode $value): array
+ {
+ return [
+ 'description' => $value->description->value ?? null,
+ 'deprecationReason' => $this->getDeprecationReason($value),
+ 'astNode' => $value,
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/BreakingChangesFinder.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/BreakingChangesFinder.php
new file mode 100644
index 00000000000..ebc43cc87f3
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/BreakingChangesFinder.php
@@ -0,0 +1,944 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * Utility for finding breaking/dangerous changes between two schemas.
+ *
+ * @phpstan-type Change array{type: string, description: string}
+ * @phpstan-type Changes array{
+ * breakingChanges: array<int, Change>,
+ * dangerousChanges: array<int, Change>
+ * }
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\BreakingChangesFinderTest
+ */
+class BreakingChangesFinder
+{
+ public const BREAKING_CHANGE_FIELD_CHANGED_KIND = 'FIELD_CHANGED_KIND';
+ public const BREAKING_CHANGE_FIELD_REMOVED = 'FIELD_REMOVED';
+ public const BREAKING_CHANGE_TYPE_CHANGED_KIND = 'TYPE_CHANGED_KIND';
+ public const BREAKING_CHANGE_TYPE_REMOVED = 'TYPE_REMOVED';
+ public const BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION = 'TYPE_REMOVED_FROM_UNION';
+ public const BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM = 'VALUE_REMOVED_FROM_ENUM';
+ public const BREAKING_CHANGE_ARG_REMOVED = 'ARG_REMOVED';
+ public const BREAKING_CHANGE_ARG_CHANGED_KIND = 'ARG_CHANGED_KIND';
+ public const BREAKING_CHANGE_REQUIRED_ARG_ADDED = 'REQUIRED_ARG_ADDED';
+ public const BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED = 'REQUIRED_INPUT_FIELD_ADDED';
+ public const BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED = 'IMPLEMENTED_INTERFACE_REMOVED';
+ public const BREAKING_CHANGE_DIRECTIVE_REMOVED = 'DIRECTIVE_REMOVED';
+ public const BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED = 'DIRECTIVE_ARG_REMOVED';
+ public const BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED = 'DIRECTIVE_LOCATION_REMOVED';
+ public const BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED = 'REQUIRED_DIRECTIVE_ARG_ADDED';
+ public const DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED = 'ARG_DEFAULT_VALUE_CHANGE';
+ public const DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM = 'VALUE_ADDED_TO_ENUM';
+ public const DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED = 'IMPLEMENTED_INTERFACE_ADDED';
+ public const DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION = 'TYPE_ADDED_TO_UNION';
+ public const DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED = 'OPTIONAL_INPUT_FIELD_ADDED';
+ public const DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED = 'OPTIONAL_ARG_ADDED';
+
+ /**
+ * Given two schemas, returns an Array containing descriptions of all the types
+ * of breaking changes covered by the other functions down below.
+ *
+ * @throws \TypeError
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findBreakingChanges(Schema $oldSchema, Schema $newSchema): array
+ {
+ return array_merge(
+ self::findRemovedTypes($oldSchema, $newSchema),
+ self::findTypesThatChangedKind($oldSchema, $newSchema),
+ self::findFieldsThatChangedTypeOnObjectOrInterfaceTypes($oldSchema, $newSchema),
+ self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['breakingChanges'],
+ self::findTypesRemovedFromUnions($oldSchema, $newSchema),
+ self::findValuesRemovedFromEnums($oldSchema, $newSchema),
+ self::findArgChanges($oldSchema, $newSchema)['breakingChanges'],
+ self::findInterfacesRemovedFromObjectTypes($oldSchema, $newSchema),
+ self::findRemovedDirectives($oldSchema, $newSchema),
+ self::findRemovedDirectiveArgs($oldSchema, $newSchema),
+ self::findAddedNonNullDirectiveArgs($oldSchema, $newSchema),
+ self::findRemovedDirectiveLocations($oldSchema, $newSchema)
+ );
+ }
+
+ /**
+ * Given two schemas, returns an Array containing descriptions of any breaking
+ * changes in the newSchema related to removing an entire type.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findRemovedTypes(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+
+ $breakingChanges = [];
+ foreach (array_keys($oldTypeMap) as $typeName) {
+ if (! isset($newTypeMap[$typeName])) {
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_TYPE_REMOVED,
+ 'description' => "{$typeName} was removed.",
+ ];
+ }
+ }
+
+ return $breakingChanges;
+ }
+
+ /**
+ * Given two schemas, returns an Array containing descriptions of any breaking
+ * changes in the newSchema related to changing the type of a type.
+ *
+ * @throws \TypeError
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findTypesThatChangedKind(
+ Schema $schemaA,
+ Schema $schemaB
+ ): array {
+ $schemaATypeMap = $schemaA->getTypeMap();
+ $schemaBTypeMap = $schemaB->getTypeMap();
+
+ $breakingChanges = [];
+ foreach ($schemaATypeMap as $typeName => $schemaAType) {
+ if (! isset($schemaBTypeMap[$typeName])) {
+ continue;
+ }
+
+ $schemaBType = $schemaBTypeMap[$typeName];
+ if ($schemaAType instanceof $schemaBType) {
+ continue;
+ }
+
+ if ($schemaBType instanceof $schemaAType) {
+ continue;
+ }
+
+ $schemaATypeKindName = self::typeKindName($schemaAType);
+ $schemaBTypeKindName = self::typeKindName($schemaBType);
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_TYPE_CHANGED_KIND,
+ 'description' => "{$typeName} changed from {$schemaATypeKindName} to {$schemaBTypeKindName}.",
+ ];
+ }
+
+ return $breakingChanges;
+ }
+
+ /**
+ * @param Type&NamedType $type
+ *
+ * @throws \TypeError
+ */
+ private static function typeKindName(NamedType $type): string
+ {
+ if ($type instanceof ScalarType) {
+ return 'a Scalar type';
+ }
+
+ if ($type instanceof ObjectType) {
+ return 'an Object type';
+ }
+
+ if ($type instanceof InterfaceType) {
+ return 'an Interface type';
+ }
+
+ if ($type instanceof UnionType) {
+ return 'a Union type';
+ }
+
+ if ($type instanceof EnumType) {
+ return 'an Enum type';
+ }
+
+ if ($type instanceof InputObjectType) {
+ return 'an Input type';
+ }
+
+ throw new \TypeError('Unknown type: ' . $type->name);
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findFieldsThatChangedTypeOnObjectOrInterfaceTypes(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+
+ $breakingChanges = [];
+ foreach ($oldTypeMap as $typeName => $oldType) {
+ $newType = $newTypeMap[$typeName] ?? null;
+ if (
+ ! $oldType instanceof ObjectType && ! $oldType instanceof InterfaceType
+ || ! $newType instanceof ObjectType && ! $newType instanceof InterfaceType
+ || ! ($newType instanceof $oldType)
+ ) {
+ continue;
+ }
+
+ $oldTypeFieldsDef = $oldType->getFields();
+ $newTypeFieldsDef = $newType->getFields();
+ foreach ($oldTypeFieldsDef as $fieldName => $fieldDefinition) {
+ // Check if the field is missing on the type in the new schema.
+ if (! isset($newTypeFieldsDef[$fieldName])) {
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
+ 'description' => "{$typeName}.{$fieldName} was removed.",
+ ];
+ } else {
+ $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
+ $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
+ $isSafe = self::isChangeSafeForObjectOrInterfaceField(
+ $oldFieldType,
+ $newFieldType
+ );
+ if (! $isSafe) {
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
+ 'description' => "{$typeName}.{$fieldName} changed type from {$oldFieldType} to {$newFieldType}.",
+ ];
+ }
+ }
+ }
+ }
+
+ return $breakingChanges;
+ }
+
+ private static function isChangeSafeForObjectOrInterfaceField(
+ Type $oldType,
+ Type $newType
+ ): bool {
+ if ($oldType instanceof NamedType) {
+ return // if they're both named types, see if their names are equivalent
+ ($newType instanceof NamedType && $oldType->name === $newType->name)
+ // moving from nullable to non-null of the same underlying type is safe
+ || ($newType instanceof NonNull
+ && self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
+ }
+
+ if ($oldType instanceof ListOfType) {
+ return // if they're both lists, make sure the underlying types are compatible
+ ($newType instanceof ListOfType && self::isChangeSafeForObjectOrInterfaceField(
+ $oldType->getWrappedType(),
+ $newType->getWrappedType()
+ ))
+ // moving from nullable to non-null of the same underlying type is safe
+ || ($newType instanceof NonNull
+ && self::isChangeSafeForObjectOrInterfaceField($oldType, $newType->getWrappedType()));
+ }
+
+ if ($oldType instanceof NonNull) {
+ // if they're both non-null, make sure the underlying types are compatible
+ return $newType instanceof NonNull
+ && self::isChangeSafeForObjectOrInterfaceField($oldType->getWrappedType(), $newType->getWrappedType());
+ }
+
+ return false;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return Changes
+ */
+ public static function findFieldsThatChangedTypeOnInputObjectTypes(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+
+ $breakingChanges = [];
+ $dangerousChanges = [];
+ foreach ($oldTypeMap as $typeName => $oldType) {
+ $newType = $newTypeMap[$typeName] ?? null;
+ if (! ($oldType instanceof InputObjectType) || ! ($newType instanceof InputObjectType)) {
+ continue;
+ }
+
+ $oldTypeFieldsDef = $oldType->getFields();
+ $newTypeFieldsDef = $newType->getFields();
+ foreach (array_keys($oldTypeFieldsDef) as $fieldName) {
+ if (! isset($newTypeFieldsDef[$fieldName])) {
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_FIELD_REMOVED,
+ 'description' => "{$typeName}.{$fieldName} was removed.",
+ ];
+ } else {
+ $oldFieldType = $oldTypeFieldsDef[$fieldName]->getType();
+ $newFieldType = $newTypeFieldsDef[$fieldName]->getType();
+
+ $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
+ $oldFieldType,
+ $newFieldType
+ );
+ if (! $isSafe) {
+ $oldFieldTypeString = $oldFieldType instanceof NamedType
+ ? $oldFieldType->name
+ : $oldFieldType;
+ $newFieldTypeString = $newFieldType instanceof NamedType
+ ? $newFieldType->name
+ : $newFieldType;
+
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_FIELD_CHANGED_KIND,
+ 'description' => "{$typeName}.{$fieldName} changed type from {$oldFieldTypeString} to {$newFieldTypeString}.",
+ ];
+ }
+ }
+ }
+
+ // Check if a field was added to the input object type
+ foreach ($newTypeFieldsDef as $fieldName => $fieldDef) {
+ if (isset($oldTypeFieldsDef[$fieldName])) {
+ continue;
+ }
+
+ $newTypeName = $newType->name;
+ if ($fieldDef->isRequired()) {
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_REQUIRED_INPUT_FIELD_ADDED,
+ 'description' => "A required field {$fieldName} on input type {$newTypeName} was added.",
+ ];
+ } else {
+ $dangerousChanges[] = [
+ 'type' => self::DANGEROUS_CHANGE_OPTIONAL_INPUT_FIELD_ADDED,
+ 'description' => "An optional field {$fieldName} on input type {$newTypeName} was added.",
+ ];
+ }
+ }
+ }
+
+ return [
+ 'breakingChanges' => $breakingChanges,
+ 'dangerousChanges' => $dangerousChanges,
+ ];
+ }
+
+ /** @throws InvariantViolation */
+ private static function isChangeSafeForInputObjectFieldOrFieldArg(
+ Type $oldType,
+ Type $newType
+ ): bool {
+ if ($oldType instanceof NamedType) {
+ if (! $newType instanceof NamedType) {
+ return false;
+ }
+
+ // if they're both named types, see if their names are equivalent
+ return $oldType->name === $newType->name;
+ }
+
+ if ($oldType instanceof ListOfType) {
+ // if they're both lists, make sure the underlying types are compatible
+ return $newType instanceof ListOfType
+ && self::isChangeSafeForInputObjectFieldOrFieldArg(
+ $oldType->getWrappedType(),
+ $newType->getWrappedType()
+ );
+ }
+
+ if ($oldType instanceof NonNull) {
+ return // if they're both non-null, make sure the underlying types are compatible
+ ($newType instanceof NonNull && self::isChangeSafeForInputObjectFieldOrFieldArg(
+ $oldType->getWrappedType(),
+ $newType->getWrappedType()
+ ))
+ // moving from non-null to nullable of the same underlying type is safe
+ || ! ($newType instanceof NonNull)
+ && self::isChangeSafeForInputObjectFieldOrFieldArg($oldType->getWrappedType(), $newType);
+ }
+
+ return false;
+ }
+
+ /**
+ * Given two schemas, returns an Array containing descriptions of any breaking
+ * changes in the newSchema related to removing types from a union type.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findTypesRemovedFromUnions(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+
+ $typesRemovedFromUnion = [];
+ foreach ($oldTypeMap as $typeName => $oldType) {
+ $newType = $newTypeMap[$typeName] ?? null;
+ if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
+ continue;
+ }
+
+ $typeNamesInNewUnion = [];
+ foreach ($newType->getTypes() as $type) {
+ $typeNamesInNewUnion[$type->name] = true;
+ }
+
+ foreach ($oldType->getTypes() as $type) {
+ if (! isset($typeNamesInNewUnion[$type->name])) {
+ $typesRemovedFromUnion[] = [
+ 'type' => self::BREAKING_CHANGE_TYPE_REMOVED_FROM_UNION,
+ 'description' => "{$type->name} was removed from union type {$typeName}.",
+ ];
+ }
+ }
+ }
+
+ return $typesRemovedFromUnion;
+ }
+
+ /**
+ * Given two schemas, returns an Array containing descriptions of any breaking
+ * changes in the newSchema related to removing values from an enum type.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findValuesRemovedFromEnums(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+
+ $valuesRemovedFromEnums = [];
+ foreach ($oldTypeMap as $typeName => $oldType) {
+ $newType = $newTypeMap[$typeName] ?? null;
+ if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
+ continue;
+ }
+
+ $valuesInNewEnum = [];
+ foreach ($newType->getValues() as $value) {
+ $valuesInNewEnum[$value->name] = true;
+ }
+
+ foreach ($oldType->getValues() as $value) {
+ if (! isset($valuesInNewEnum[$value->name])) {
+ $valuesRemovedFromEnums[] = [
+ 'type' => self::BREAKING_CHANGE_VALUE_REMOVED_FROM_ENUM,
+ 'description' => "{$value->name} was removed from enum type {$typeName}.",
+ ];
+ }
+ }
+ }
+
+ return $valuesRemovedFromEnums;
+ }
+
+ /**
+ * Given two schemas, returns an Array containing descriptions of any
+ * breaking or dangerous changes in the newSchema related to arguments
+ * (such as removal or change of type of an argument, or a change in an
+ * argument's default value).
+ *
+ * @throws InvariantViolation
+ *
+ * @return Changes
+ */
+ public static function findArgChanges(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+
+ $breakingChanges = [];
+ $dangerousChanges = [];
+
+ foreach ($oldTypeMap as $typeName => $oldType) {
+ $newType = $newTypeMap[$typeName] ?? null;
+ if (
+ ! $oldType instanceof ObjectType && ! $oldType instanceof InterfaceType
+ || ! $newType instanceof ObjectType && ! $newType instanceof InterfaceType
+ || ! ($newType instanceof $oldType)
+ ) {
+ continue;
+ }
+
+ $oldTypeFields = $oldType->getFields();
+ $newTypeFields = $newType->getFields();
+
+ foreach ($oldTypeFields as $fieldName => $oldField) {
+ if (! isset($newTypeFields[$fieldName])) {
+ continue;
+ }
+
+ foreach ($oldField->args as $oldArgDef) {
+ $newArgDef = null;
+ foreach ($newTypeFields[$fieldName]->args as $newArg) {
+ if ($newArg->name === $oldArgDef->name) {
+ $newArgDef = $newArg;
+ }
+ }
+
+ if ($newArgDef !== null) {
+ $isSafe = self::isChangeSafeForInputObjectFieldOrFieldArg(
+ $oldArgDef->getType(),
+ $newArgDef->getType()
+ );
+ $oldArgType = $oldArgDef->getType();
+ $oldArgName = $oldArgDef->name;
+ if (! $isSafe) {
+ $newArgType = $newArgDef->getType();
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_ARG_CHANGED_KIND,
+ 'description' => "{$typeName}.{$fieldName} arg {$oldArgName} has changed type from {$oldArgType} to {$newArgType}",
+ ];
+ } elseif ($oldArgDef->defaultValueExists() && $oldArgDef->defaultValue !== $newArgDef->defaultValue) {
+ $dangerousChanges[] = [
+ 'type' => self::DANGEROUS_CHANGE_ARG_DEFAULT_VALUE_CHANGED,
+ 'description' => "{$typeName}.{$fieldName} arg {$oldArgName} has changed defaultValue",
+ ];
+ }
+ } else {
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_ARG_REMOVED,
+ 'description' => "{$typeName}.{$fieldName} arg {$oldArgDef->name} was removed",
+ ];
+ }
+
+ // Check if arg was added to the field
+ foreach ($newTypeFields[$fieldName]->args as $newTypeFieldArgDef) {
+ $oldArgDef = null;
+ foreach ($oldTypeFields[$fieldName]->args as $oldArg) {
+ if ($oldArg->name === $newTypeFieldArgDef->name) {
+ $oldArgDef = $oldArg;
+ }
+ }
+
+ if ($oldArgDef !== null) {
+ continue;
+ }
+
+ $newTypeName = $newType->name;
+ $newArgName = $newTypeFieldArgDef->name;
+ if ($newTypeFieldArgDef->isRequired()) {
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_REQUIRED_ARG_ADDED,
+ 'description' => "A required arg {$newArgName} on {$newTypeName}.{$fieldName} was added",
+ ];
+ } else {
+ $dangerousChanges[] = [
+ 'type' => self::DANGEROUS_CHANGE_OPTIONAL_ARG_ADDED,
+ 'description' => "An optional arg {$newArgName} on {$newTypeName}.{$fieldName} was added",
+ ];
+ }
+ }
+ }
+ }
+ }
+
+ return [
+ 'breakingChanges' => $breakingChanges,
+ 'dangerousChanges' => $dangerousChanges,
+ ];
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findInterfacesRemovedFromObjectTypes(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+ $breakingChanges = [];
+
+ foreach ($oldTypeMap as $typeName => $oldType) {
+ $newType = $newTypeMap[$typeName] ?? null;
+ if (! ($oldType instanceof ImplementingType) || ! ($newType instanceof ImplementingType)) {
+ continue;
+ }
+
+ $oldInterfaces = $oldType->getInterfaces();
+ $newInterfaces = $newType->getInterfaces();
+ foreach ($oldInterfaces as $oldInterface) {
+ $interfaceWasRemoved = true;
+ foreach ($newInterfaces as $newInterface) {
+ if ($oldInterface->name === $newInterface->name) {
+ $interfaceWasRemoved = false;
+ }
+ }
+
+ if ($interfaceWasRemoved) {
+ $breakingChanges[] = [
+ 'type' => self::BREAKING_CHANGE_IMPLEMENTED_INTERFACE_REMOVED,
+ 'description' => "{$typeName} no longer implements interface {$oldInterface->name}.",
+ ];
+ }
+ }
+ }
+
+ return $breakingChanges;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findRemovedDirectives(Schema $oldSchema, Schema $newSchema): array
+ {
+ $removedDirectives = [];
+
+ $newSchemaDirectiveMap = self::getDirectiveMapForSchema($newSchema);
+ foreach ($oldSchema->getDirectives() as $directive) {
+ if (! isset($newSchemaDirectiveMap[$directive->name])) {
+ $removedDirectives[] = [
+ 'type' => self::BREAKING_CHANGE_DIRECTIVE_REMOVED,
+ 'description' => "{$directive->name} was removed",
+ ];
+ }
+ }
+
+ return $removedDirectives;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<string, Directive>
+ */
+ private static function getDirectiveMapForSchema(Schema $schema): array
+ {
+ $directives = [];
+ foreach ($schema->getDirectives() as $directive) {
+ $directives[$directive->name] = $directive;
+ }
+
+ return $directives;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findRemovedDirectiveArgs(Schema $oldSchema, Schema $newSchema): array
+ {
+ $removedDirectiveArgs = [];
+ $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
+
+ foreach ($newSchema->getDirectives() as $newDirective) {
+ if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
+ continue;
+ }
+
+ foreach (
+ self::findRemovedArgsForDirectives(
+ $oldSchemaDirectiveMap[$newDirective->name],
+ $newDirective
+ ) as $arg
+ ) {
+ $removedDirectiveArgs[] = [
+ 'type' => self::BREAKING_CHANGE_DIRECTIVE_ARG_REMOVED,
+ 'description' => "{$arg->name} was removed from {$newDirective->name}",
+ ];
+ }
+ }
+
+ return $removedDirectiveArgs;
+ }
+
+ /** @return array<int, Argument> */
+ public static function findRemovedArgsForDirectives(Directive $oldDirective, Directive $newDirective): array
+ {
+ $removedArgs = [];
+ $newArgMap = self::getArgumentMapForDirective($newDirective);
+ foreach ($oldDirective->args as $arg) {
+ if (! isset($newArgMap[$arg->name])) {
+ $removedArgs[] = $arg;
+ }
+ }
+
+ return $removedArgs;
+ }
+
+ /** @return array<string, Argument> */
+ private static function getArgumentMapForDirective(Directive $directive): array
+ {
+ $args = [];
+ foreach ($directive->args as $arg) {
+ $args[$arg->name] = $arg;
+ }
+
+ return $args;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findAddedNonNullDirectiveArgs(Schema $oldSchema, Schema $newSchema): array
+ {
+ $addedNonNullableArgs = [];
+ $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
+
+ foreach ($newSchema->getDirectives() as $newDirective) {
+ if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
+ continue;
+ }
+
+ foreach (
+ self::findAddedArgsForDirective(
+ $oldSchemaDirectiveMap[$newDirective->name],
+ $newDirective
+ ) as $arg
+ ) {
+ if ($arg->isRequired()) {
+ $addedNonNullableArgs[] = [
+ 'type' => self::BREAKING_CHANGE_REQUIRED_DIRECTIVE_ARG_ADDED,
+ 'description' => "A required arg {$arg->name} on directive {$newDirective->name} was added",
+ ];
+ }
+ }
+ }
+
+ return $addedNonNullableArgs;
+ }
+
+ /** @return array<int, Argument> */
+ public static function findAddedArgsForDirective(Directive $oldDirective, Directive $newDirective): array
+ {
+ $addedArgs = [];
+ $oldArgMap = self::getArgumentMapForDirective($oldDirective);
+ foreach ($newDirective->args as $arg) {
+ if (! isset($oldArgMap[$arg->name])) {
+ $addedArgs[] = $arg;
+ }
+ }
+
+ return $addedArgs;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findRemovedDirectiveLocations(Schema $oldSchema, Schema $newSchema): array
+ {
+ $removedLocations = [];
+ $oldSchemaDirectiveMap = self::getDirectiveMapForSchema($oldSchema);
+
+ foreach ($newSchema->getDirectives() as $newDirective) {
+ if (! isset($oldSchemaDirectiveMap[$newDirective->name])) {
+ continue;
+ }
+
+ foreach (
+ self::findRemovedLocationsForDirective(
+ $oldSchemaDirectiveMap[$newDirective->name],
+ $newDirective
+ ) as $location
+ ) {
+ $removedLocations[] = [
+ 'type' => self::BREAKING_CHANGE_DIRECTIVE_LOCATION_REMOVED,
+ 'description' => "{$location} was removed from {$newDirective->name}",
+ ];
+ }
+ }
+
+ return $removedLocations;
+ }
+
+ /** @return array<int, string> */
+ public static function findRemovedLocationsForDirective(Directive $oldDirective, Directive $newDirective): array
+ {
+ $removedLocations = [];
+ $newLocationSet = array_flip($newDirective->locations);
+ foreach ($oldDirective->locations as $oldLocation) {
+ if (! array_key_exists($oldLocation, $newLocationSet)) {
+ $removedLocations[] = $oldLocation;
+ }
+ }
+
+ return $removedLocations;
+ }
+
+ /**
+ * Given two schemas, returns an Array containing descriptions of all the types
+ * of potentially dangerous changes covered by the other functions down below.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findDangerousChanges(Schema $oldSchema, Schema $newSchema): array
+ {
+ return array_merge(
+ self::findArgChanges($oldSchema, $newSchema)['dangerousChanges'],
+ self::findValuesAddedToEnums($oldSchema, $newSchema),
+ self::findInterfacesAddedToObjectTypes($oldSchema, $newSchema),
+ self::findTypesAddedToUnions($oldSchema, $newSchema),
+ self::findFieldsThatChangedTypeOnInputObjectTypes($oldSchema, $newSchema)['dangerousChanges']
+ );
+ }
+
+ /**
+ * Given two schemas, returns an Array containing descriptions of any dangerous
+ * changes in the newSchema related to adding values to an enum type.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findValuesAddedToEnums(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+
+ $valuesAddedToEnums = [];
+ foreach ($oldTypeMap as $typeName => $oldType) {
+ $newType = $newTypeMap[$typeName] ?? null;
+ if (! ($oldType instanceof EnumType) || ! ($newType instanceof EnumType)) {
+ continue;
+ }
+
+ $valuesInOldEnum = [];
+ foreach ($oldType->getValues() as $value) {
+ $valuesInOldEnum[$value->name] = true;
+ }
+
+ foreach ($newType->getValues() as $value) {
+ if (! isset($valuesInOldEnum[$value->name])) {
+ $valuesAddedToEnums[] = [
+ 'type' => self::DANGEROUS_CHANGE_VALUE_ADDED_TO_ENUM,
+ 'description' => "{$value->name} was added to enum type {$typeName}.",
+ ];
+ }
+ }
+ }
+
+ return $valuesAddedToEnums;
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findInterfacesAddedToObjectTypes(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+ $interfacesAddedToObjectTypes = [];
+
+ foreach ($newTypeMap as $typeName => $newType) {
+ $oldType = $oldTypeMap[$typeName] ?? null;
+ if (
+ ! $oldType instanceof ObjectType && ! $oldType instanceof InterfaceType
+ || ! $newType instanceof ObjectType && ! $newType instanceof InterfaceType
+ ) {
+ continue;
+ }
+
+ $oldInterfaces = $oldType->getInterfaces();
+ $newInterfaces = $newType->getInterfaces();
+ foreach ($newInterfaces as $newInterface) {
+ $interfaceWasAdded = true;
+ foreach ($oldInterfaces as $oldInterface) {
+ if ($oldInterface->name === $newInterface->name) {
+ $interfaceWasAdded = false;
+ }
+ }
+
+ if ($interfaceWasAdded) {
+ $interfacesAddedToObjectTypes[] = [
+ 'type' => self::DANGEROUS_CHANGE_IMPLEMENTED_INTERFACE_ADDED,
+ 'description' => "{$newInterface->name} added to interfaces implemented by {$typeName}.",
+ ];
+ }
+ }
+ }
+
+ return $interfacesAddedToObjectTypes;
+ }
+
+ /**
+ * Given two schemas, returns an Array containing descriptions of any dangerous
+ * changes in the newSchema related to adding types to a union type.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, Change>
+ */
+ public static function findTypesAddedToUnions(
+ Schema $oldSchema,
+ Schema $newSchema
+ ): array {
+ $oldTypeMap = $oldSchema->getTypeMap();
+ $newTypeMap = $newSchema->getTypeMap();
+
+ $typesAddedToUnion = [];
+ foreach ($newTypeMap as $typeName => $newType) {
+ $oldType = $oldTypeMap[$typeName] ?? null;
+ if (! ($oldType instanceof UnionType) || ! ($newType instanceof UnionType)) {
+ continue;
+ }
+
+ $typeNamesInOldUnion = [];
+ foreach ($oldType->getTypes() as $type) {
+ $typeNamesInOldUnion[$type->name] = true;
+ }
+
+ foreach ($newType->getTypes() as $type) {
+ if (! isset($typeNamesInOldUnion[$type->name])) {
+ $typesAddedToUnion[] = [
+ 'type' => self::DANGEROUS_CHANGE_TYPE_ADDED_TO_UNION,
+ 'description' => "{$type->name} was added to union type {$typeName}.",
+ ];
+ }
+ }
+ }
+
+ return $typesAddedToUnion;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildClientSchema.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildClientSchema.php
new file mode 100644
index 00000000000..a892eb1a472
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildClientSchema.php
@@ -0,0 +1,563 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\OutputType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\TypeKind;
+
+/**
+ * @phpstan-import-type UnnamedFieldDefinitionConfig from FieldDefinition
+ * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField
+ *
+ * @phpstan-type Options array{
+ * assumeValid?: bool
+ * }
+ *
+ * - assumeValid:
+ * When building a schema from a Automattic\WooCommerce\Vendor\GraphQL service's introspection result, it
+ * might be safe to assume the schema is valid. Set to true to assume the
+ * produced schema is valid.
+ *
+ * Default: false
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\BuildClientSchemaTest
+ */
+class BuildClientSchema
+{
+ /** @var array<string, mixed> */
+ private array $introspection;
+
+ /**
+ * @var array<string, bool>
+ *
+ * @phpstan-var Options
+ */
+ private array $options;
+
+ /** @var array<string, NamedType&Type> */
+ private array $typeMap = [];
+
+ /**
+ * @param array<string, mixed> $introspectionQuery
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ */
+ public function __construct(array $introspectionQuery, array $options = [])
+ {
+ $this->introspection = $introspectionQuery;
+ $this->options = $options;
+ }
+
+ /**
+ * Build a schema for use by client tools.
+ *
+ * Given the result of a client running the introspection query, creates and
+ * returns a \Automattic\WooCommerce\Vendor\GraphQL\Type\Schema instance which can be then used with all graphql-php
+ * tools, but cannot be used to execute a query, as introspection does not
+ * represent the "resolver", "parse" or "serialize" functions or any other
+ * server-internal mechanisms.
+ *
+ * This function expects a complete introspection result. Don't forget to check
+ * the "errors" field of a server response before calling this function.
+ *
+ * @param array<string, mixed> $introspectionQuery
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @api
+ *
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public static function build(array $introspectionQuery, array $options = []): Schema
+ {
+ return (new self($introspectionQuery, $options))->buildSchema();
+ }
+
+ /**
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public function buildSchema(): Schema
+ {
+ if (! array_key_exists('__schema', $this->introspection)) {
+ $missingSchemaIntrospection = Utils::printSafeJson($this->introspection);
+ throw new InvariantViolation("Invalid or incomplete introspection result. Ensure that you are passing \"data\" property of introspection response and no \"errors\" was returned alongside: {$missingSchemaIntrospection}.");
+ }
+
+ $schemaIntrospection = $this->introspection['__schema'];
+
+ $builtInTypes = array_merge(
+ Type::builtInScalars(),
+ Introspection::getTypes()
+ );
+
+ foreach ($schemaIntrospection['types'] as $typeIntrospection) {
+ if (! isset($typeIntrospection['name'])) {
+ throw self::invalidOrIncompleteIntrospectionResult($typeIntrospection);
+ }
+
+ $name = $typeIntrospection['name'];
+ if (! is_string($name)) {
+ throw self::invalidOrIncompleteIntrospectionResult($typeIntrospection);
+ }
+
+ // Use the built-in singleton types to avoid reconstruction
+ $this->typeMap[$name] = $builtInTypes[$name]
+ ?? $this->buildType($typeIntrospection);
+ }
+
+ $description = isset($schemaIntrospection['description'])
+ ? $schemaIntrospection['description']
+ : null;
+
+ $queryType = isset($schemaIntrospection['queryType'])
+ ? $this->getObjectType($schemaIntrospection['queryType'])
+ : null;
+
+ $mutationType = isset($schemaIntrospection['mutationType'])
+ ? $this->getObjectType($schemaIntrospection['mutationType'])
+ : null;
+
+ $subscriptionType = isset($schemaIntrospection['subscriptionType'])
+ ? $this->getObjectType($schemaIntrospection['subscriptionType'])
+ : null;
+
+ $directives = isset($schemaIntrospection['directives'])
+ ? array_map(
+ [$this, 'buildDirective'],
+ $schemaIntrospection['directives']
+ )
+ : [];
+
+ return new Schema(
+ (new SchemaConfig())
+ ->setDescription($description)
+ ->setQuery($queryType)
+ ->setMutation($mutationType)
+ ->setSubscription($subscriptionType)
+ ->setTypes($this->typeMap)
+ ->setDirectives($directives)
+ ->setAssumeValid($this->options['assumeValid'] ?? false)
+ );
+ }
+
+ /**
+ * @param array<string, mixed> $typeRef
+ *
+ * @throws InvariantViolation
+ */
+ private function getType(array $typeRef): Type
+ {
+ if (isset($typeRef['kind'])) {
+ if ($typeRef['kind'] === TypeKind::LIST) {
+ if (! isset($typeRef['ofType'])) {
+ throw new InvariantViolation('Decorated type deeper than introspection query.');
+ }
+
+ return new ListOfType($this->getType($typeRef['ofType']));
+ }
+
+ if ($typeRef['kind'] === TypeKind::NON_NULL) {
+ if (! isset($typeRef['ofType'])) {
+ throw new InvariantViolation('Decorated type deeper than introspection query.');
+ }
+
+ // @phpstan-ignore-next-line if the type is not a nullable type, schema validation will catch it
+ return new NonNull($this->getType($typeRef['ofType']));
+ }
+ }
+
+ if (! isset($typeRef['name'])) {
+ $unknownTypeRef = Utils::printSafeJson($typeRef);
+ throw new InvariantViolation("Unknown type reference: {$unknownTypeRef}.");
+ }
+
+ return $this->getNamedType($typeRef['name']);
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @return NamedType&Type
+ */
+ private function getNamedType(string $typeName): NamedType
+ {
+ if (! isset($this->typeMap[$typeName])) {
+ throw new InvariantViolation("Invalid or incomplete schema, unknown type: {$typeName}. Ensure that a full introspection query is used in order to build a client schema.");
+ }
+
+ return $this->typeMap[$typeName];
+ }
+
+ /** @param array<mixed> $type */
+ public static function invalidOrIncompleteIntrospectionResult(array $type): InvariantViolation
+ {
+ $incompleteType = Utils::printSafeJson($type);
+
+ return new InvariantViolation("Invalid or incomplete introspection result. Ensure that a full introspection query is used in order to build a client schema: {$incompleteType}.");
+ }
+
+ /**
+ * @param array<string, mixed> $typeRef
+ *
+ * @throws InvariantViolation
+ *
+ * @return Type&InputType
+ */
+ private function getInputType(array $typeRef): InputType
+ {
+ $type = $this->getType($typeRef);
+
+ if ($type instanceof InputType) {
+ return $type;
+ }
+
+ $notInputType = Utils::printSafe($type);
+ throw new InvariantViolation("Introspection must provide input type for arguments, but received: {$notInputType}.");
+ }
+
+ /**
+ * @param array<string, mixed> $typeRef
+ *
+ * @throws InvariantViolation
+ */
+ private function getOutputType(array $typeRef): OutputType
+ {
+ $type = $this->getType($typeRef);
+
+ if ($type instanceof OutputType) {
+ return $type;
+ }
+
+ $notInputType = Utils::printSafe($type);
+ throw new InvariantViolation("Introspection must provide output type for fields, but received: {$notInputType}.");
+ }
+
+ /**
+ * @param array<string, mixed> $typeRef
+ *
+ * @throws InvariantViolation
+ */
+ private function getObjectType(array $typeRef): ObjectType
+ {
+ $type = $this->getType($typeRef);
+
+ return ObjectType::assertObjectType($type);
+ }
+
+ /**
+ * @param array<string, mixed> $typeRef
+ *
+ * @throws InvariantViolation
+ */
+ public function getInterfaceType(array $typeRef): InterfaceType
+ {
+ $type = $this->getType($typeRef);
+
+ return InterfaceType::assertInterfaceType($type);
+ }
+
+ /**
+ * @param array<string, mixed> $type
+ *
+ * @throws InvariantViolation
+ *
+ * @return Type&NamedType
+ */
+ private function buildType(array $type): NamedType
+ {
+ if (! array_key_exists('kind', $type)) {
+ throw self::invalidOrIncompleteIntrospectionResult($type);
+ }
+
+ switch ($type['kind']) {
+ case TypeKind::SCALAR:
+ return $this->buildScalarDef($type);
+ case TypeKind::OBJECT:
+ return $this->buildObjectDef($type);
+ case TypeKind::INTERFACE:
+ return $this->buildInterfaceDef($type);
+ case TypeKind::UNION:
+ return $this->buildUnionDef($type);
+ case TypeKind::ENUM:
+ return $this->buildEnumDef($type);
+ case TypeKind::INPUT_OBJECT:
+ return $this->buildInputObjectDef($type);
+ default:
+ $unknownKindType = Utils::printSafeJson($type);
+ throw new InvariantViolation("Invalid or incomplete introspection result. Received type with unknown kind: {$unknownKindType}.");
+ }
+ }
+
+ /**
+ * @param array<string, string> $scalar
+ *
+ * @throws InvariantViolation
+ */
+ private function buildScalarDef(array $scalar): ScalarType
+ {
+ return new CustomScalarType([
+ 'name' => $scalar['name'],
+ 'description' => $scalar['description'],
+ 'serialize' => static fn ($value) => $value,
+ ]);
+ }
+
+ /**
+ * @param array<string, mixed> $implementingIntrospection
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, InterfaceType>
+ */
+ private function buildImplementationsList(array $implementingIntrospection): array
+ {
+ // TODO: Temporary workaround until Automattic\WooCommerce\Vendor\GraphQL ecosystem will fully support 'interfaces' on interface types.
+ if (
+ array_key_exists('interfaces', $implementingIntrospection)
+ && $implementingIntrospection['interfaces'] === null
+ && $implementingIntrospection['kind'] === TypeKind::INTERFACE
+ ) {
+ return [];
+ }
+
+ if (! array_key_exists('interfaces', $implementingIntrospection)) {
+ $safeIntrospection = Utils::printSafeJson($implementingIntrospection);
+ throw new InvariantViolation("Introspection result missing interfaces: {$safeIntrospection}.");
+ }
+
+ return array_map(
+ [$this, 'getInterfaceType'],
+ $implementingIntrospection['interfaces']
+ );
+ }
+
+ /**
+ * @param array<string, mixed> $object
+ *
+ * @throws InvariantViolation
+ */
+ private function buildObjectDef(array $object): ObjectType
+ {
+ return new ObjectType([
+ 'name' => $object['name'],
+ 'description' => $object['description'],
+ 'interfaces' => fn (): array => $this->buildImplementationsList($object),
+ 'fields' => fn (): array => $this->buildFieldDefMap($object),
+ ]);
+ }
+
+ /**
+ * @param array<string, mixed> $interface
+ *
+ * @throws InvariantViolation
+ */
+ private function buildInterfaceDef(array $interface): InterfaceType
+ {
+ return new InterfaceType([
+ 'name' => $interface['name'],
+ 'description' => $interface['description'],
+ 'fields' => fn (): array => $this->buildFieldDefMap($interface),
+ 'interfaces' => fn (): array => $this->buildImplementationsList($interface),
+ ]);
+ }
+
+ /**
+ * @param array<string, mixed> $union
+ *
+ * @throws InvariantViolation
+ */
+ private function buildUnionDef(array $union): UnionType
+ {
+ if (! array_key_exists('possibleTypes', $union)) {
+ $safeUnion = Utils::printSafeJson($union);
+ throw new InvariantViolation("Introspection result missing possibleTypes: {$safeUnion}.");
+ }
+
+ return new UnionType([
+ 'name' => $union['name'],
+ 'description' => $union['description'],
+ 'types' => fn (): array => array_map(
+ [$this, 'getObjectType'],
+ $union['possibleTypes']
+ ),
+ ]);
+ }
+
+ /**
+ * @param array<string, mixed> $enum
+ *
+ * @throws InvariantViolation
+ */
+ private function buildEnumDef(array $enum): EnumType
+ {
+ if (! array_key_exists('enumValues', $enum)) {
+ $safeEnum = Utils::printSafeJson($enum);
+ throw new InvariantViolation("Introspection result missing enumValues: {$safeEnum}.");
+ }
+
+ $values = [];
+ foreach ($enum['enumValues'] as $value) {
+ $values[$value['name']] = [
+ 'description' => $value['description'],
+ 'deprecationReason' => $value['deprecationReason'],
+ ];
+ }
+
+ return new EnumType([
+ 'name' => $enum['name'],
+ 'description' => $enum['description'],
+ 'values' => $values,
+ ]);
+ }
+
+ /**
+ * @param array<string, mixed> $inputObject
+ *
+ * @throws InvariantViolation
+ */
+ private function buildInputObjectDef(array $inputObject): InputObjectType
+ {
+ if (! array_key_exists('inputFields', $inputObject)) {
+ $safeInputObject = Utils::printSafeJson($inputObject);
+ throw new InvariantViolation("Introspection result missing inputFields: {$safeInputObject}.");
+ }
+
+ return new InputObjectType([
+ 'name' => $inputObject['name'],
+ 'description' => $inputObject['description'],
+ 'fields' => fn (): array => $this->buildInputValueDefMap($inputObject['inputFields']),
+ ]);
+ }
+
+ /**
+ * @param array<string, mixed> $typeIntrospection
+ *
+ * @throws \Exception
+ * @throws InvariantViolation
+ *
+ * @return array<string, UnnamedFieldDefinitionConfig>
+ */
+ private function buildFieldDefMap(array $typeIntrospection): array
+ {
+ if (! array_key_exists('fields', $typeIntrospection)) {
+ $safeType = Utils::printSafeJson($typeIntrospection);
+ throw new InvariantViolation("Introspection result missing fields: {$safeType}.");
+ }
+
+ /** @var array<string, UnnamedFieldDefinitionConfig> $map */
+ $map = [];
+ foreach ($typeIntrospection['fields'] as $field) {
+ if (! array_key_exists('args', $field)) {
+ $safeField = Utils::printSafeJson($field);
+ throw new InvariantViolation("Introspection result missing field args: {$safeField}.");
+ }
+
+ $map[$field['name']] = [
+ 'description' => $field['description'],
+ 'deprecationReason' => $field['deprecationReason'],
+ 'type' => $this->getOutputType($field['type']),
+ 'args' => $this->buildInputValueDefMap($field['args']),
+ ];
+ }
+
+ // @phpstan-ignore-next-line unless the returned name was numeric, this works
+ return $map;
+ }
+
+ /**
+ * @param array<int, array<string, mixed>> $inputValueIntrospections
+ *
+ * @throws \Exception
+ *
+ * @return array<string, UnnamedInputObjectFieldConfig>
+ */
+ private function buildInputValueDefMap(array $inputValueIntrospections): array
+ {
+ /** @var array<string, UnnamedInputObjectFieldConfig> $map */
+ $map = [];
+ foreach ($inputValueIntrospections as $value) {
+ $map[$value['name']] = $this->buildInputValue($value);
+ }
+
+ return $map;
+ }
+
+ /**
+ * @param array<string, mixed> $inputValueIntrospection
+ *
+ * @throws \Exception
+ * @throws SyntaxError
+ *
+ * @return UnnamedInputObjectFieldConfig
+ */
+ public function buildInputValue(array $inputValueIntrospection): array
+ {
+ $type = $this->getInputType($inputValueIntrospection['type']);
+
+ $inputValue = [
+ 'description' => $inputValueIntrospection['description'],
+ 'type' => $type,
+ ];
+
+ if (isset($inputValueIntrospection['defaultValue'])) {
+ $inputValue['defaultValue'] = AST::valueFromAST(
+ Parser::parseValue($inputValueIntrospection['defaultValue']),
+ $type
+ );
+ }
+
+ return $inputValue;
+ }
+
+ /**
+ * @param array<string, mixed> $directive
+ *
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public function buildDirective(array $directive): Directive
+ {
+ if (! array_key_exists('args', $directive)) {
+ $safeDirective = Utils::printSafeJson($directive);
+ throw new InvariantViolation("Introspection result missing directive args: {$safeDirective}.");
+ }
+
+ if (! array_key_exists('locations', $directive)) {
+ $safeDirective = Utils::printSafeJson($directive);
+ throw new InvariantViolation("Introspection result missing directive locations: {$safeDirective}.");
+ }
+
+ return new Directive([
+ 'name' => $directive['name'],
+ 'description' => $directive['description'],
+ 'args' => $this->buildInputValueDefMap($directive['args']),
+ 'isRepeatable' => $directive['isRepeatable'] ?? false,
+ 'locations' => $directive['locations'],
+ ]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildSchema.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildSchema.php
new file mode 100644
index 00000000000..707579a1d1c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/BuildSchema.php
@@ -0,0 +1,282 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Source;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
+
+/**
+ * Build instance of @see \Automattic\WooCommerce\Vendor\GraphQL\Type\Schema out of schema language definition (string or parsed AST).
+ *
+ * See [schema definition language docs](schema-definition-language.md) for details.
+ *
+ * @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder
+ * @phpstan-import-type FieldConfigDecorator from ASTDefinitionBuilder
+ *
+ * @phpstan-type BuildSchemaOptions array{
+ * assumeValid?: bool,
+ * assumeValidSDL?: bool
+ * }
+ *
+ * - assumeValid:
+ * When building a schema from a Automattic\WooCommerce\Vendor\GraphQL service's introspection result, it might be safe to assume the schema is valid.
+ * Set to true to assume the produced schema is valid.
+ * Default: false
+ *
+ * - assumeValidSDL:
+ * Set to true to assume the SDL is valid.
+ * Default: false
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\BuildSchemaTest
+ */
+class BuildSchema
+{
+ private DocumentNode $ast;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var TypeConfigDecorator|null
+ */
+ private $typeConfigDecorator;
+
+ /**
+ * @var callable|null
+ *
+ * @phpstan-var FieldConfigDecorator|null
+ */
+ private $fieldConfigDecorator;
+
+ /**
+ * @var array<string, bool>
+ *
+ * @phpstan-var BuildSchemaOptions
+ */
+ private array $options;
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
+ * @phpstan-param BuildSchemaOptions $options
+ */
+ public function __construct(
+ DocumentNode $ast,
+ ?callable $typeConfigDecorator = null,
+ array $options = [],
+ ?callable $fieldConfigDecorator = null
+ ) {
+ $this->ast = $ast;
+ $this->typeConfigDecorator = $typeConfigDecorator;
+ $this->options = $options;
+ $this->fieldConfigDecorator = $fieldConfigDecorator;
+ }
+
+ /**
+ * A helper function to build a GraphQLSchema directly from a source
+ * document.
+ *
+ * @param DocumentNode|Source|string $source
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
+ * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
+ * @phpstan-param BuildSchemaOptions $options
+ *
+ * @api
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ * @throws SyntaxError
+ */
+ public static function build(
+ $source,
+ ?callable $typeConfigDecorator = null,
+ array $options = [],
+ ?callable $fieldConfigDecorator = null
+ ): Schema {
+ $doc = $source instanceof DocumentNode
+ ? $source
+ : Parser::parse($source);
+
+ return self::buildAST($doc, $typeConfigDecorator, $options, $fieldConfigDecorator);
+ }
+
+ /**
+ * This takes the AST of a schema from @see \Automattic\WooCommerce\Vendor\GraphQL\Language\Parser::parse().
+ *
+ * If no schema definition is provided, then it will look for types named Query and Mutation.
+ *
+ * Given that AST it constructs a @see \Automattic\WooCommerce\Vendor\GraphQL\Type\Schema. The resulting schema
+ * has no resolve methods, so execution will use default resolvers.
+ *
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
+ * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
+ * @phpstan-param BuildSchemaOptions $options
+ *
+ * @api
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public static function buildAST(
+ DocumentNode $ast,
+ ?callable $typeConfigDecorator = null,
+ array $options = [],
+ ?callable $fieldConfigDecorator = null
+ ): Schema {
+ return (new self($ast, $typeConfigDecorator, $options, $fieldConfigDecorator))->buildSchema();
+ }
+
+ /**
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ public function buildSchema(): Schema
+ {
+ if (
+ ! ($this->options['assumeValid'] ?? false)
+ && ! ($this->options['assumeValidSDL'] ?? false)
+ ) {
+ DocumentValidator::assertValidSDL($this->ast);
+ }
+
+ $schemaDef = null;
+
+ /** @var array<string, Node&TypeDefinitionNode> */
+ $typeDefinitionsMap = [];
+
+ /** @var array<string, array<int, Node&TypeExtensionNode>> $typeExtensionsMap */
+ $typeExtensionsMap = [];
+
+ /** @var array<int, DirectiveDefinitionNode> $directiveDefs */
+ $directiveDefs = [];
+
+ foreach ($this->ast->definitions as $definition) {
+ switch (true) {
+ case $definition instanceof SchemaDefinitionNode:
+ $schemaDef = $definition;
+ break;
+ case $definition instanceof TypeDefinitionNode:
+ $name = $definition->getName()->value;
+ $typeDefinitionsMap[$name] = $definition;
+ break;
+ case $definition instanceof TypeExtensionNode:
+ $name = $definition->getName()->value;
+ $typeExtensionsMap[$name][] = $definition;
+ break;
+ case $definition instanceof DirectiveDefinitionNode:
+ $directiveDefs[] = $definition;
+ break;
+ }
+ }
+
+ $operationTypes = $schemaDef !== null
+ ? $this->getOperationTypes($schemaDef)
+ : [
+ 'query' => 'Query',
+ 'mutation' => 'Mutation',
+ 'subscription' => 'Subscription',
+ ];
+
+ $definitionBuilder = new ASTDefinitionBuilder(
+ $typeDefinitionsMap,
+ $typeExtensionsMap,
+ static function (string $typeName): Type {
+ throw self::unknownType($typeName);
+ },
+ $this->typeConfigDecorator,
+ $this->fieldConfigDecorator
+ );
+
+ $directives = array_map(
+ [$definitionBuilder, 'buildDirective'],
+ $directiveDefs
+ );
+
+ $directivesByName = [];
+ foreach ($directives as $directive) {
+ $directivesByName[$directive->name][] = $directive;
+ }
+
+ // If specified directives were not explicitly declared, add them.
+ if (! isset($directivesByName['include'])) {
+ $directives[] = Directive::includeDirective();
+ }
+ if (! isset($directivesByName['skip'])) {
+ $directives[] = Directive::skipDirective();
+ }
+ if (! isset($directivesByName['deprecated'])) {
+ $directives[] = Directive::deprecatedDirective();
+ }
+ if (! isset($directivesByName['oneOf'])) {
+ $directives[] = Directive::oneOfDirective();
+ }
+
+ // Note: While this could make early assertions to get the correctly
+ // typed values below, that would throw immediately while type system
+ // validation with validateSchema() will produce more actionable results.
+ return new Schema(
+ (new SchemaConfig())
+ ->setDescription($schemaDef->description->value ?? null)
+ // @phpstan-ignore-next-line
+ ->setQuery(isset($operationTypes['query'])
+ ? $definitionBuilder->maybeBuildType($operationTypes['query'])
+ : null)
+ // @phpstan-ignore-next-line
+ ->setMutation(isset($operationTypes['mutation'])
+ ? $definitionBuilder->maybeBuildType($operationTypes['mutation'])
+ : null)
+ // @phpstan-ignore-next-line
+ ->setSubscription(isset($operationTypes['subscription'])
+ ? $definitionBuilder->maybeBuildType($operationTypes['subscription'])
+ : null)
+ ->setTypeLoader(static fn (string $name): ?Type => $definitionBuilder->maybeBuildType($name))
+ ->setDirectives($directives)
+ ->setAstNode($schemaDef)
+ ->setTypes(fn (): array => array_map(
+ static fn (TypeDefinitionNode $def): Type => $definitionBuilder->buildType($def->getName()->value),
+ $typeDefinitionsMap,
+ ))
+ );
+ }
+
+ /** @return array<string, string> */
+ private function getOperationTypes(SchemaDefinitionNode $schemaDef): array
+ {
+ /** @var array<string, string> $operationTypes */
+ $operationTypes = [];
+ foreach ($schemaDef->operationTypes as $operationType) {
+ $operationTypes[$operationType->operation] = $operationType->type->name->value;
+ }
+
+ return $operationTypes;
+ }
+
+ public static function unknownType(string $typeName): Error
+ {
+ return new Error("Unknown type: \"{$typeName}\".");
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/InterfaceImplementations.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/InterfaceImplementations.php
new file mode 100644
index 00000000000..2ad53929629
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/InterfaceImplementations.php
@@ -0,0 +1,42 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+
+/**
+ * A way to track interface implementations.
+ *
+ * Distinguishes between implementations by ObjectTypes and InterfaceTypes.
+ */
+class InterfaceImplementations
+{
+ /** @var array<int, ObjectType> */
+ private $objects;
+
+ /** @var array<int, InterfaceType> */
+ private $interfaces;
+
+ /**
+ * @param array<int, ObjectType> $objects
+ * @param array<int, InterfaceType> $interfaces
+ */
+ public function __construct(array $objects, array $interfaces)
+ {
+ $this->objects = $objects;
+ $this->interfaces = $interfaces;
+ }
+
+ /** @return array<int, ObjectType> */
+ public function objects(): array
+ {
+ return $this->objects;
+ }
+
+ /** @return array<int, InterfaceType> */
+ public function interfaces(): array
+ {
+ return $this->interfaces;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/LazyException.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/LazyException.php
new file mode 100644
index 00000000000..3e91265b236
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/LazyException.php
@@ -0,0 +1,15 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+/**
+ * Allows lazy calculation of a complex message when the exception is used in `assert()`.
+ */
+class LazyException extends \Exception
+{
+ /** @param callable(): string $makeMessage */
+ public function __construct(callable $makeMessage)
+ {
+ parent::__construct($makeMessage());
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/LexicalDistance.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/LexicalDistance.php
new file mode 100644
index 00000000000..20e17015010
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/LexicalDistance.php
@@ -0,0 +1,129 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+/**
+ * Computes the lexical distance between strings A and B.
+ *
+ * The "distance" between two strings is given by counting the minimum number
+ * of edits needed to transform string A into string B. An edit can be an
+ * insertion, deletion, or substitution of a single character, or a swap of two
+ * adjacent characters.
+ *
+ * Includes a custom alteration from Damerau-Levenshtein to treat case changes
+ * as a single edit which helps identify mis-cased values with an edit distance
+ * of 1.
+ *
+ * This distance can be useful for detecting typos in input or sorting
+ *
+ * Unlike the native levenshtein() function that always returns int, LexicalDistance::measure() returns int|null.
+ * It takes into account the threshold and returns null if the measured distance is bigger.
+ */
+class LexicalDistance
+{
+ private string $input;
+
+ private string $inputLowerCase;
+
+ /**
+ * List of char codes in the input string.
+ *
+ * @var array<int>
+ */
+ private array $inputArray;
+
+ public function __construct(string $input)
+ {
+ $this->input = $input;
+ $this->inputLowerCase = strtolower($input);
+ $this->inputArray = self::stringToArray($this->inputLowerCase);
+ }
+
+ public function measure(string $option, float $threshold): ?int
+ {
+ if ($this->input === $option) {
+ return 0;
+ }
+
+ $optionLowerCase = strtolower($option);
+
+ // Any case change counts as a single edit
+ if ($this->inputLowerCase === $optionLowerCase) {
+ return 1;
+ }
+
+ $a = self::stringToArray($optionLowerCase);
+ $b = $this->inputArray;
+
+ if (count($a) < count($b)) {
+ $tmp = $a;
+ $a = $b;
+ $b = $tmp;
+ }
+
+ $aLength = count($a);
+ $bLength = count($b);
+
+ if ($aLength - $bLength > $threshold) {
+ return null;
+ }
+
+ /** @var array<array<int>> $rows */
+ $rows = [];
+ for ($i = 0; $i <= $bLength; ++$i) {
+ $rows[0][$i] = $i;
+ }
+
+ for ($i = 1; $i <= $aLength; ++$i) {
+ $upRow = &$rows[($i - 1) % 3];
+ $currentRow = &$rows[$i % 3];
+
+ $smallestCell = ($currentRow[0] = $i);
+ for ($j = 1; $j <= $bLength; ++$j) {
+ $cost = $a[$i - 1] === $b[$j - 1] ? 0 : 1;
+
+ $currentCell = min(
+ $upRow[$j] + 1, // delete
+ $currentRow[$j - 1] + 1, // insert
+ $upRow[$j - 1] + $cost, // substitute
+ );
+
+ if ($i > 1 && $j > 1 && $a[$i - 1] === $b[$j - 2] && $a[$i - 2] === $b[$j - 1]) {
+ // transposition
+ $doubleDiagonalCell = $rows[($i - 2) % 3][$j - 2];
+ $currentCell = min($currentCell, $doubleDiagonalCell + 1);
+ }
+
+ if ($currentCell < $smallestCell) {
+ $smallestCell = $currentCell;
+ }
+
+ $currentRow[$j] = $currentCell;
+ }
+
+ // Early exit, since distance can't go smaller than smallest element of the previous row.
+ if ($smallestCell > $threshold) {
+ return null;
+ }
+ }
+
+ $distance = $rows[$aLength % 3][$bLength];
+
+ return $distance <= $threshold ? $distance : null;
+ }
+
+ /**
+ * Returns a list of char codes in the given string.
+ *
+ * @return array<int>
+ */
+ private static function stringToArray(string $str): array
+ {
+ $array = [];
+ foreach (mb_str_split($str) as $char) {
+ $array[] = mb_ord($char);
+ }
+
+ return $array;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/MixedStore.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/MixedStore.php
new file mode 100644
index 00000000000..a3200b0e739
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/MixedStore.php
@@ -0,0 +1,210 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+/**
+ * Similar to PHP array, but allows any type of data to act as key (including arrays, objects, scalars).
+ *
+ * When storing array as key, access and modification is O(N). Avoid if possible.
+ *
+ * @template TValue of mixed
+ *
+ * @implements \ArrayAccess<mixed, TValue>
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\MixedStoreTest
+ */
+class MixedStore implements \ArrayAccess
+{
+ /** @var array<TValue> */
+ private array $standardStore = [];
+
+ /** @var array<TValue> */
+ private array $floatStore = [];
+
+ /** @var \SplObjectStorage<object, TValue> */
+ private \SplObjectStorage $objectStore;
+
+ /** @var array<int, array<mixed>> */
+ private array $arrayKeys = [];
+
+ /** @var array<int, TValue> */
+ private array $arrayValues = [];
+
+ /** @var array<mixed> */
+ private ?array $lastArrayKey = null;
+
+ /** @var TValue|null */
+ private $lastArrayValue;
+
+ /** @var TValue|null */
+ private $nullValue;
+
+ private bool $nullValueIsSet = false;
+
+ /** @var TValue|null */
+ private $trueValue;
+
+ private bool $trueValueIsSet = false;
+
+ /** @var TValue|null */
+ private $falseValue;
+
+ private bool $falseValueIsSet = false;
+
+ public function __construct()
+ {
+ $this->objectStore = new \SplObjectStorage();
+ }
+
+ /** @param mixed $offset */
+ #[\ReturnTypeWillChange]
+ public function offsetExists($offset): bool
+ {
+ if ($offset === false) {
+ return $this->falseValueIsSet;
+ }
+
+ if ($offset === true) {
+ return $this->trueValueIsSet;
+ }
+
+ if (is_int($offset) || is_string($offset)) {
+ return array_key_exists($offset, $this->standardStore);
+ }
+
+ if (is_float($offset)) {
+ return array_key_exists((string) $offset, $this->floatStore);
+ }
+
+ if (is_object($offset)) {
+ return $this->objectStore->offsetExists($offset);
+ }
+
+ if (is_array($offset)) {
+ foreach ($this->arrayKeys as $index => $entry) {
+ if ($entry === $offset) {
+ $this->lastArrayKey = $offset;
+ $this->lastArrayValue = $this->arrayValues[$index];
+
+ return true;
+ }
+ }
+ }
+
+ if ($offset === null) {
+ return $this->nullValueIsSet;
+ }
+
+ return false;
+ }
+
+ /**
+ * @param mixed $offset
+ *
+ * @return TValue|null
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetGet($offset)
+ {
+ if ($offset === true) {
+ return $this->trueValue;
+ }
+
+ if ($offset === false) {
+ return $this->falseValue;
+ }
+
+ if (is_int($offset) || is_string($offset)) {
+ return $this->standardStore[$offset];
+ }
+
+ if (is_float($offset)) {
+ return $this->floatStore[(string) $offset];
+ }
+
+ if (is_object($offset)) {
+ return $this->objectStore->offsetGet($offset);
+ }
+
+ if (is_array($offset)) {
+ // offsetGet is often called directly after offsetExists, so optimize to avoid second loop:
+ if ($this->lastArrayKey === $offset) {
+ return $this->lastArrayValue;
+ }
+
+ foreach ($this->arrayKeys as $index => $entry) {
+ if ($entry === $offset) {
+ return $this->arrayValues[$index];
+ }
+ }
+ }
+
+ if ($offset === null) {
+ return $this->nullValue;
+ }
+
+ return null;
+ }
+
+ /**
+ * @param mixed $offset
+ * @param TValue $value
+ *
+ * @throws \InvalidArgumentException
+ */
+ #[\ReturnTypeWillChange]
+ public function offsetSet($offset, $value): void
+ {
+ if ($offset === false) {
+ $this->falseValue = $value;
+ $this->falseValueIsSet = true;
+ } elseif ($offset === true) {
+ $this->trueValue = $value;
+ $this->trueValueIsSet = true;
+ } elseif (is_int($offset) || is_string($offset)) {
+ $this->standardStore[$offset] = $value;
+ } elseif (is_float($offset)) {
+ $this->floatStore[(string) $offset] = $value;
+ } elseif (is_object($offset)) {
+ $this->objectStore[$offset] = $value;
+ } elseif (is_array($offset)) {
+ $this->arrayKeys[] = $offset;
+ $this->arrayValues[] = $value;
+ } elseif ($offset === null) {
+ $this->nullValue = $value;
+ $this->nullValueIsSet = true;
+ } else {
+ $unexpectedOffset = Utils::printSafe($offset);
+ throw new \InvalidArgumentException("Unexpected offset type: {$unexpectedOffset}");
+ }
+ }
+
+ /** @param mixed $offset */
+ #[\ReturnTypeWillChange]
+ public function offsetUnset($offset): void
+ {
+ if ($offset === true) {
+ $this->trueValue = null;
+ $this->trueValueIsSet = false;
+ } elseif ($offset === false) {
+ $this->falseValue = null;
+ $this->falseValueIsSet = false;
+ } elseif (is_int($offset) || is_string($offset)) {
+ unset($this->standardStore[$offset]);
+ } elseif (is_float($offset)) {
+ unset($this->floatStore[(string) $offset]);
+ } elseif (is_object($offset)) {
+ $this->objectStore->offsetUnset($offset);
+ } elseif (is_array($offset)) {
+ $index = array_search($offset, $this->arrayKeys, true);
+
+ if ($index !== false) {
+ array_splice($this->arrayKeys, $index, 1);
+ array_splice($this->arrayValues, $index, 1);
+ }
+ } elseif ($offset === null) {
+ $this->nullValue = null;
+ $this->nullValueIsSet = false;
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/PairSet.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/PairSet.php
new file mode 100644
index 00000000000..2268037a9d5
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/PairSet.php
@@ -0,0 +1,43 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+/**
+ * A way to keep track of pairs of things when the ordering of the pair does
+ * not matter. We do this by maintaining a sort of double adjacency sets.
+ */
+class PairSet
+{
+ /** @var array<string, array<string, bool>> */
+ private array $data = [];
+
+ public function has(string $a, string $b, bool $areMutuallyExclusive): bool
+ {
+ $first = $this->data[$a] ?? null;
+ $result = $first !== null && isset($first[$b]) ? $first[$b] : null;
+ if ($result === null) {
+ return false;
+ }
+
+ // areMutuallyExclusive being false is a superset of being true,
+ // hence if we want to know if this PairSet "has" these two with no
+ // exclusivity, we have to ensure it was added as such.
+ if ($areMutuallyExclusive === false) {
+ return $result === false;
+ }
+
+ return true;
+ }
+
+ public function add(string $a, string $b, bool $areMutuallyExclusive): void
+ {
+ $this->pairSetAdd($a, $b, $areMutuallyExclusive);
+ $this->pairSetAdd($b, $a, $areMutuallyExclusive);
+ }
+
+ private function pairSetAdd(string $a, string $b, bool $areMutuallyExclusive): void
+ {
+ $this->data[$a] ??= [];
+ $this->data[$a][$b] = $areMutuallyExclusive;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/PhpDoc.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/PhpDoc.php
new file mode 100644
index 00000000000..620620d9642
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/PhpDoc.php
@@ -0,0 +1,52 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+class PhpDoc
+{
+ /** @param string|false|null $docBlock */
+ public static function unwrap($docBlock): ?string
+ {
+ if ($docBlock === false || $docBlock === null) {
+ return null;
+ }
+
+ $content = preg_replace('~([\r\n]) \* (.*)~i', '$1$2', $docBlock); // strip *
+ assert(is_string($content), 'regex is statically known to be valid');
+
+ $content = preg_replace('~([\r\n])[\* ]+([\r\n])~i', '$1$2', $content); // strip single-liner *
+ assert(is_string($content), 'regex is statically known to be valid');
+
+ $content = substr($content, 3); // strip leading /**
+ $content = substr($content, 0, -2); // strip trailing */
+
+ return static::nonEmptyOrNull($content);
+ }
+
+ /** @param string|false|null $docBlock */
+ public static function unpad($docBlock): ?string
+ {
+ if ($docBlock === false || $docBlock === null) {
+ return null;
+ }
+
+ $lines = explode("\n", $docBlock);
+ $lines = array_map(
+ static fn (string $line): string => ' ' . trim($line),
+ $lines
+ );
+
+ $content = implode("\n", $lines);
+
+ return static::nonEmptyOrNull($content);
+ }
+
+ protected static function nonEmptyOrNull(string $maybeEmptyString): ?string
+ {
+ $trimmed = trim($maybeEmptyString);
+
+ return $trimmed === ''
+ ? null
+ : $trimmed;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaExtender.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaExtender.php
new file mode 100644
index 00000000000..2e11bbf9eda
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaExtender.php
@@ -0,0 +1,669 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\SchemaConfig;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
+
+/**
+ * @phpstan-import-type TypeConfigDecorator from ASTDefinitionBuilder
+ * @phpstan-import-type FieldConfigDecorator from ASTDefinitionBuilder
+ * @phpstan-import-type UnnamedArgumentConfig from Argument
+ * @phpstan-import-type UnnamedInputObjectFieldConfig from InputObjectField
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\SchemaExtenderTest
+ */
+class SchemaExtender
+{
+ /** @var array<string, Type> */
+ protected array $extendTypeCache = [];
+
+ /** @var array<string, array<TypeExtensionNode>> */
+ protected array $typeExtensionsMap = [];
+
+ protected ASTDefinitionBuilder $astBuilder;
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
+ * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
+ *
+ * @api
+ *
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public static function extend(
+ Schema $schema,
+ DocumentNode $documentAST,
+ array $options = [],
+ ?callable $typeConfigDecorator = null,
+ ?callable $fieldConfigDecorator = null
+ ): Schema {
+ return (new static())->doExtend($schema, $documentAST, $options, $typeConfigDecorator, $fieldConfigDecorator);
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param TypeConfigDecorator|null $typeConfigDecorator
+ * @phpstan-param FieldConfigDecorator|null $fieldConfigDecorator
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ */
+ protected function doExtend(
+ Schema $schema,
+ DocumentNode $documentAST,
+ array $options = [],
+ ?callable $typeConfigDecorator = null,
+ ?callable $fieldConfigDecorator = null
+ ): Schema {
+ if (
+ ! ($options['assumeValid'] ?? false)
+ && ! ($options['assumeValidSDL'] ?? false)
+ ) {
+ DocumentValidator::assertValidSDLExtension($documentAST, $schema);
+ }
+
+ /** @var array<string, Node&TypeDefinitionNode> $typeDefinitionMap */
+ $typeDefinitionMap = [];
+
+ /** @var array<int, DirectiveDefinitionNode> $directiveDefinitions */
+ $directiveDefinitions = [];
+
+ /** @var SchemaDefinitionNode|null $schemaDef */
+ $schemaDef = null;
+
+ /** @var array<int, SchemaExtensionNode> $schemaExtensions */
+ $schemaExtensions = [];
+
+ foreach ($documentAST->definitions as $def) {
+ if ($def instanceof SchemaDefinitionNode) {
+ $schemaDef = $def;
+ } elseif ($def instanceof SchemaExtensionNode) {
+ $schemaExtensions[] = $def;
+ } elseif ($def instanceof TypeDefinitionNode) {
+ $name = $def->getName()->value;
+ $typeDefinitionMap[$name] = $def;
+ } elseif ($def instanceof TypeExtensionNode) {
+ $name = $def->getName()->value;
+ $this->typeExtensionsMap[$name][] = $def;
+ } elseif ($def instanceof DirectiveDefinitionNode) {
+ $directiveDefinitions[] = $def;
+ }
+ }
+
+ if (
+ $this->typeExtensionsMap === []
+ && $typeDefinitionMap === []
+ && $directiveDefinitions === []
+ && $schemaExtensions === []
+ && $schemaDef === null
+ ) {
+ return $schema;
+ }
+
+ $this->astBuilder = new ASTDefinitionBuilder(
+ $typeDefinitionMap,
+ [],
+ function (string $typeName) use ($schema): Type {
+ $existingType = $schema->getType($typeName);
+ if ($existingType === null) {
+ throw new InvariantViolation("Unknown type: \"{$typeName}\".");
+ }
+
+ return $this->extendNamedType($existingType);
+ },
+ $typeConfigDecorator,
+ $fieldConfigDecorator
+ );
+
+ $this->extendTypeCache = [];
+
+ $types = [];
+
+ // Iterate through all types, getting the type definition for each, ensuring
+ // that any type not directly referenced by a field will get created.
+ foreach ($schema->getTypeMap() as $type) {
+ $types[] = $this->extendNamedType($type);
+ }
+
+ // Do the same with new types.
+ foreach ($typeDefinitionMap as $type) {
+ $types[] = $this->astBuilder->buildType($type);
+ }
+
+ $operationTypes = [
+ 'query' => $this->extendMaybeNamedType($schema->getQueryType()),
+ 'mutation' => $this->extendMaybeNamedType($schema->getMutationType()),
+ 'subscription' => $this->extendMaybeNamedType($schema->getSubscriptionType()),
+ ];
+
+ if ($schemaDef !== null) {
+ foreach ($schemaDef->operationTypes as $operationType) {
+ $operationTypes[$operationType->operation] = $this->astBuilder->buildType($operationType->type);
+ }
+ }
+
+ foreach ($schemaExtensions as $schemaExtension) {
+ foreach ($schemaExtension->operationTypes as $operationType) {
+ $operationTypes[$operationType->operation] = $this->astBuilder->buildType($operationType->type);
+ }
+ }
+
+ $schemaConfig = (new SchemaConfig())
+ ->setDescription($schemaDef->description->value ?? $schema->description ?? null)
+ // @phpstan-ignore-next-line the root types may be invalid, but just passing them leads to more actionable errors
+ ->setQuery($operationTypes['query'])
+ // @phpstan-ignore-next-line the root types may be invalid, but just passing them leads to more actionable errors
+ ->setMutation($operationTypes['mutation'])
+ // @phpstan-ignore-next-line the root types may be invalid, but just passing them leads to more actionable errors
+ ->setSubscription($operationTypes['subscription'])
+ ->setTypes($types)
+ ->setDirectives($this->getMergedDirectives($schema, $directiveDefinitions))
+ ->setAstNode($schema->astNode ?? $schemaDef)
+ ->setExtensionASTNodes([...$schema->extensionASTNodes, ...$schemaExtensions]);
+
+ return new Schema($schemaConfig);
+ }
+
+ /**
+ * @param Type&NamedType $type
+ *
+ * @return array<TypeExtensionNode>|null
+ */
+ protected function extensionASTNodes(NamedType $type): ?array
+ {
+ return [
+ ...$type->extensionASTNodes ?? [],
+ ...$this->typeExtensionsMap[$type->name] ?? [],
+ ];
+ }
+
+ /**
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ */
+ protected function extendScalarType(ScalarType $type): CustomScalarType
+ {
+ /** @var array<ScalarTypeExtensionNode> $extensionASTNodes */
+ $extensionASTNodes = $this->extensionASTNodes($type);
+
+ return new CustomScalarType([
+ 'name' => $type->name,
+ 'description' => $type->description,
+ 'serialize' => [$type, 'serialize'],
+ 'parseValue' => [$type, 'parseValue'],
+ 'parseLiteral' => [$type, 'parseLiteral'],
+ 'astNode' => $type->astNode,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /** @throws InvariantViolation */
+ protected function extendUnionType(UnionType $type): UnionType
+ {
+ /** @var array<UnionTypeExtensionNode> $extensionASTNodes */
+ $extensionASTNodes = $this->extensionASTNodes($type);
+
+ return new UnionType([
+ 'name' => $type->name,
+ 'description' => $type->description,
+ 'types' => fn (): array => $this->extendUnionPossibleTypes($type),
+ 'resolveType' => [$type, 'resolveType'],
+ 'astNode' => $type->astNode,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /**
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ */
+ protected function extendEnumType(EnumType $type): EnumType
+ {
+ /** @var array<EnumTypeExtensionNode> $extensionASTNodes */
+ $extensionASTNodes = $this->extensionASTNodes($type);
+
+ return new EnumType([
+ 'name' => $type->name,
+ 'description' => $type->description,
+ 'values' => $this->extendEnumValueMap($type),
+ 'astNode' => $type->astNode,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /** @throws InvariantViolation */
+ protected function extendInputObjectType(InputObjectType $type): InputObjectType
+ {
+ /** @var array<InputObjectTypeExtensionNode> $extensionASTNodes */
+ $extensionASTNodes = $this->extensionASTNodes($type);
+
+ return new InputObjectType([
+ 'name' => $type->name,
+ 'description' => $type->description,
+ 'fields' => fn (): array => $this->extendInputFieldMap($type),
+ 'parseValue' => [$type, 'parseValue'],
+ 'astNode' => $type->astNode,
+ 'extensionASTNodes' => $extensionASTNodes,
+ 'isOneOf' => $type->isOneOf,
+ ]);
+ }
+
+ /**
+ * @throws \Exception
+ * @throws InvariantViolation
+ *
+ * @return array<string, UnnamedInputObjectFieldConfig>
+ */
+ protected function extendInputFieldMap(InputObjectType $type): array
+ {
+ /** @var array<string, UnnamedInputObjectFieldConfig> $newFieldMap */
+ $newFieldMap = [];
+
+ $oldFieldMap = $type->getFields();
+ foreach ($oldFieldMap as $fieldName => $field) {
+ $extendedType = $this->extendType($field->getType());
+
+ $newFieldConfig = [
+ 'description' => $field->description,
+ 'type' => $extendedType,
+ 'deprecationReason' => $field->deprecationReason,
+ 'astNode' => $field->astNode,
+ ];
+
+ if ($field->defaultValueExists()) {
+ $newFieldConfig['defaultValue'] = $field->defaultValue;
+ }
+
+ $newFieldMap[$fieldName] = $newFieldConfig;
+ }
+
+ if (isset($this->typeExtensionsMap[$type->name])) {
+ foreach ($this->typeExtensionsMap[$type->name] as $extension) {
+ assert($extension instanceof InputObjectTypeExtensionNode, 'proven by schema validation');
+
+ foreach ($extension->fields as $field) {
+ $newFieldMap[$field->name->value] = $this->astBuilder->buildInputField($field);
+ }
+ }
+ }
+
+ return $newFieldMap;
+ }
+
+ /**
+ * @throws \Exception
+ * @throws InvariantViolation
+ *
+ * @return array<string, array<string, mixed>>
+ */
+ protected function extendEnumValueMap(EnumType $type): array
+ {
+ $newValueMap = [];
+
+ foreach ($type->getValues() as $value) {
+ $newValueMap[$value->name] = [
+ 'name' => $value->name,
+ 'description' => $value->description,
+ 'value' => $value->value,
+ 'deprecationReason' => $value->deprecationReason,
+ 'astNode' => $value->astNode,
+ ];
+ }
+
+ if (isset($this->typeExtensionsMap[$type->name])) {
+ foreach ($this->typeExtensionsMap[$type->name] as $extension) {
+ assert($extension instanceof EnumTypeExtensionNode, 'proven by schema validation');
+
+ foreach ($extension->values as $value) {
+ $newValueMap[$value->name->value] = $this->astBuilder->buildEnumValue($value);
+ }
+ }
+ }
+
+ return $newValueMap;
+ }
+
+ /**
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return array<int, ObjectType>
+ */
+ protected function extendUnionPossibleTypes(UnionType $type): array
+ {
+ $possibleTypes = array_map(
+ [$this, 'extendNamedType'],
+ $type->getTypes()
+ );
+
+ if (isset($this->typeExtensionsMap[$type->name])) {
+ foreach ($this->typeExtensionsMap[$type->name] as $extension) {
+ assert($extension instanceof UnionTypeExtensionNode, 'proven by schema validation');
+
+ foreach ($extension->types as $namedType) {
+ $possibleTypes[] = $this->astBuilder->buildType($namedType);
+ }
+ }
+ }
+
+ // @phpstan-ignore-next-line proven by schema validation
+ return $possibleTypes;
+ }
+
+ /**
+ * @param ObjectType|InterfaceType $type
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return array<int, InterfaceType>
+ */
+ protected function extendImplementedInterfaces(ImplementingType $type): array
+ {
+ $interfaces = array_map(
+ [$this, 'extendNamedType'],
+ $type->getInterfaces()
+ );
+
+ if (isset($this->typeExtensionsMap[$type->name])) {
+ foreach ($this->typeExtensionsMap[$type->name] as $extension) {
+ assert(
+ $extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode,
+ 'proven by schema validation'
+ );
+
+ foreach ($extension->interfaces as $namedType) {
+ $interface = $this->astBuilder->buildType($namedType);
+ assert($interface instanceof InterfaceType, 'we know this, but PHP templates cannot express it');
+
+ $interfaces[] = $interface;
+ }
+ }
+ }
+
+ return $interfaces;
+ }
+
+ /**
+ * @template T of Type
+ *
+ * @param T $typeDef
+ *
+ * @return T
+ */
+ protected function extendType(Type $typeDef): Type
+ {
+ if ($typeDef instanceof ListOfType) {
+ // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input
+ return Type::listOf($this->extendType($typeDef->getWrappedType()));
+ }
+
+ if ($typeDef instanceof NonNull) {
+ // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input
+ return Type::nonNull($this->extendType($typeDef->getWrappedType()));
+ }
+
+ // @phpstan-ignore-next-line PHPStan does not understand this is the same generic type as the input
+ return $this->extendNamedType($typeDef);
+ }
+
+ /**
+ * @param array<Argument> $args
+ *
+ * @return array<string, UnnamedArgumentConfig>
+ */
+ protected function extendArgs(array $args): array
+ {
+ $extended = [];
+ foreach ($args as $arg) {
+ $extendedType = $this->extendType($arg->getType());
+
+ $def = [
+ 'type' => $extendedType,
+ 'description' => $arg->description,
+ 'deprecationReason' => $arg->deprecationReason,
+ 'astNode' => $arg->astNode,
+ ];
+
+ if ($arg->defaultValueExists()) {
+ $def['defaultValue'] = $arg->defaultValue;
+ }
+
+ $extended[$arg->name] = $def;
+ }
+
+ return $extended;
+ }
+
+ /**
+ * @param InterfaceType|ObjectType $type
+ *
+ * @throws \Exception
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @return array<string, array<string, mixed>>
+ */
+ protected function extendFieldMap(Type $type): array
+ {
+ $newFieldMap = [];
+ $oldFieldMap = $type->getFields();
+
+ foreach (array_keys($oldFieldMap) as $fieldName) {
+ $field = $oldFieldMap[$fieldName];
+
+ $newFieldMap[$fieldName] = [
+ 'name' => $fieldName,
+ 'description' => $field->description,
+ 'deprecationReason' => $field->deprecationReason,
+ 'type' => $this->extendType($field->getType()),
+ 'args' => $this->extendArgs($field->args),
+ 'resolve' => $field->resolveFn,
+ 'argsMapper' => $field->argsMapper,
+ 'astNode' => $field->astNode,
+ ];
+ }
+
+ if (isset($this->typeExtensionsMap[$type->name])) {
+ foreach ($this->typeExtensionsMap[$type->name] as $extension) {
+ assert(
+ $extension instanceof ObjectTypeExtensionNode || $extension instanceof InterfaceTypeExtensionNode,
+ 'proven by schema validation'
+ );
+
+ foreach ($extension->fields as $field) {
+ $newFieldMap[$field->name->value] = $this->astBuilder->buildField($field, $extension);
+ }
+ }
+ }
+
+ return $newFieldMap;
+ }
+
+ /** @throws InvariantViolation */
+ protected function extendObjectType(ObjectType $type): ObjectType
+ {
+ /** @var array<ObjectTypeExtensionNode> $extensionASTNodes */
+ $extensionASTNodes = $this->extensionASTNodes($type);
+
+ return new ObjectType([
+ 'name' => $type->name,
+ 'description' => $type->description,
+ 'interfaces' => fn (): array => $this->extendImplementedInterfaces($type),
+ 'fields' => fn (): array => $this->extendFieldMap($type),
+ 'isTypeOf' => [$type, 'isTypeOf'],
+ 'resolveField' => $type->resolveFieldFn,
+ 'argsMapper' => $type->argsMapper,
+ 'astNode' => $type->astNode,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ /** @throws InvariantViolation */
+ protected function extendInterfaceType(InterfaceType $type): InterfaceType
+ {
+ /** @var array<InterfaceTypeExtensionNode> $extensionASTNodes */
+ $extensionASTNodes = $this->extensionASTNodes($type);
+
+ return new InterfaceType([
+ 'name' => $type->name,
+ 'description' => $type->description,
+ 'interfaces' => fn (): array => $this->extendImplementedInterfaces($type),
+ 'fields' => fn (): array => $this->extendFieldMap($type),
+ 'resolveType' => [$type, 'resolveType'],
+ 'astNode' => $type->astNode,
+ 'extensionASTNodes' => $extensionASTNodes,
+ ]);
+ }
+
+ protected function isSpecifiedScalarType(Type $type): bool
+ {
+ return $type instanceof NamedType
+ && in_array($type->name, [
+ Type::STRING,
+ Type::INT,
+ Type::FLOAT,
+ Type::BOOLEAN,
+ Type::ID,
+ ], true);
+ }
+
+ /**
+ * @template T of Type
+ *
+ * @param T&NamedType $type
+ *
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ *
+ * @return T&NamedType
+ */
+ protected function extendNamedType(Type $type): Type
+ {
+ if (Introspection::isIntrospectionType($type) || $this->isSpecifiedScalarType($type)) {
+ return $type;
+ }
+
+ // @phpstan-ignore-next-line the subtypes line up
+ return $this->extendTypeCache[$type->name] ??= $this->extendNamedTypeWithoutCache($type);
+ }
+
+ /** @throws \Exception */
+ protected function extendNamedTypeWithoutCache(Type $type): Type
+ {
+ switch (true) {
+ case $type instanceof ScalarType: return $this->extendScalarType($type);
+ case $type instanceof ObjectType: return $this->extendObjectType($type);
+ case $type instanceof InterfaceType: return $this->extendInterfaceType($type);
+ case $type instanceof UnionType: return $this->extendUnionType($type);
+ case $type instanceof EnumType: return $this->extendEnumType($type);
+ case $type instanceof InputObjectType: return $this->extendInputObjectType($type);
+ default:
+ $unconsideredType = get_class($type);
+ throw new \Exception("Unconsidered type: {$unconsideredType}.");
+ }
+ }
+
+ /**
+ * @template T of Type
+ *
+ * @param (T&NamedType)|null $type
+ *
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ *
+ * @return (T&NamedType)|null
+ */
+ protected function extendMaybeNamedType(?Type $type = null): ?Type
+ {
+ if ($type !== null) {
+ return $this->extendNamedType($type);
+ }
+
+ return null;
+ }
+
+ /**
+ * @param array<DirectiveDefinitionNode> $directiveDefinitions
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ *
+ * @return array<int, Directive>
+ */
+ protected function getMergedDirectives(Schema $schema, array $directiveDefinitions): array
+ {
+ $directives = array_map(
+ [$this, 'extendDirective'],
+ $schema->getDirectives()
+ );
+
+ if ($directives === []) {
+ throw new InvariantViolation('Schema must have default directives.');
+ }
+
+ foreach ($directiveDefinitions as $directive) {
+ $directives[] = $this->astBuilder->buildDirective($directive);
+ }
+
+ return $directives;
+ }
+
+ protected function extendDirective(Directive $directive): Directive
+ {
+ return new Directive([
+ 'name' => $directive->name,
+ 'description' => $directive->description,
+ 'locations' => $directive->locations,
+ 'args' => $this->extendArgs($directive->args),
+ 'isRepeatable' => $directive->isRepeatable,
+ 'astNode' => $directive->astNode,
+ ]);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaPrinter.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaPrinter.php
new file mode 100644
index 00000000000..9594ed86993
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/SchemaPrinter.php
@@ -0,0 +1,579 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\SerializationError;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\BlockString;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumValueDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectField;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * Prints the contents of a Schema in schema definition language.
+ *
+ * All sorting options sort alphabetically. If not given or `false`, the original schema definition order will be used.
+ *
+ * @phpstan-type Options array{
+ * sortArguments?: bool,
+ * sortEnumValues?: bool,
+ * sortFields?: bool,
+ * sortInputFields?: bool,
+ * sortTypes?: bool,
+ * }
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Tests\Utils\SchemaPrinterTest
+ */
+class SchemaPrinter
+{
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @api
+ *
+ * @throws \JsonException
+ * @throws Error
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ public static function doPrint(Schema $schema, array $options = []): string
+ {
+ return static::printFilteredSchema(
+ $schema,
+ static fn (Directive $directive): bool => ! Directive::isSpecifiedDirective($directive),
+ static fn (NamedType $type): bool => ! $type->isBuiltInType(),
+ $options
+ );
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @api
+ *
+ * @throws \JsonException
+ * @throws Error
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ public static function printIntrospectionSchema(Schema $schema, array $options = []): string
+ {
+ return static::printFilteredSchema(
+ $schema,
+ [Directive::class, 'isSpecifiedDirective'],
+ [Introspection::class, 'isIntrospectionType'],
+ $options
+ );
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws Error
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ public static function printType(Type $type, array $options = []): string
+ {
+ if ($type instanceof ScalarType) {
+ return static::printScalar($type, $options);
+ }
+
+ if ($type instanceof ObjectType) {
+ return static::printObject($type, $options);
+ }
+
+ if ($type instanceof InterfaceType) {
+ return static::printInterface($type, $options);
+ }
+
+ if ($type instanceof UnionType) {
+ return static::printUnion($type, $options);
+ }
+
+ if ($type instanceof EnumType) {
+ return static::printEnum($type, $options);
+ }
+
+ if ($type instanceof InputObjectType) {
+ return static::printInputObject($type, $options);
+ }
+
+ $unknownType = Utils::printSafe($type);
+ throw new Error("Unknown type: {$unknownType}.");
+ }
+
+ /**
+ * @param callable(Directive $directive): bool $directiveFilter
+ * @param callable(Type&NamedType $type): bool $typeFilter
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws Error
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printFilteredSchema(Schema $schema, callable $directiveFilter, callable $typeFilter, array $options): string
+ {
+ $directives = array_filter($schema->getDirectives(), $directiveFilter);
+ $types = array_filter($schema->getTypeMap(), $typeFilter);
+
+ if (isset($options['sortTypes']) && $options['sortTypes']) {
+ ksort($types);
+ }
+
+ $elements = [static::printSchemaDefinition($schema)];
+
+ foreach ($directives as $directive) {
+ $elements[] = static::printDirective($directive, $options);
+ }
+
+ foreach ($types as $type) {
+ $elements[] = static::printType($type, $options);
+ }
+
+ /** @phpstan-ignore arrayFilter.strict */
+ return implode("\n\n", array_filter($elements)) . "\n";
+ }
+
+ /**
+ * @throws \JsonException
+ * @throws InvariantViolation
+ */
+ protected static function printSchemaDefinition(Schema $schema): ?string
+ {
+ $queryType = $schema->getQueryType();
+ $mutationType = $schema->getMutationType();
+ $subscriptionType = $schema->getSubscriptionType();
+
+ // Special case: When a schema has no root operation types, no valid schema
+ // definition can be printed.
+ if ($queryType === null && $mutationType === null && $subscriptionType === null) {
+ return null;
+ }
+
+ // Only print a schema definition if there is a description or if it should
+ // not be omitted because of having default type names.
+ if ($schema->description !== null || ! static::hasDefaultRootOperationTypes($schema)) {
+ return static::printDescription([], $schema) . "schema {\n"
+ . ($queryType !== null ? " query: {$queryType->name}\n" : '')
+ . ($mutationType !== null ? " mutation: {$mutationType->name}\n" : '')
+ . ($subscriptionType !== null ? " subscription: {$subscriptionType->name}\n" : '')
+ . '}';
+ }
+
+ return null;
+ }
+
+ /**
+ * Automattic\WooCommerce\Vendor\GraphQL schema define root types for each type of operation. These types are
+ * the same as any other type and can be named in any manner, however there is
+ * a common naming convention:.
+ *
+ * ```graphql
+ * schema {
+ * query: Query
+ * mutation: Mutation
+ * subscription: Subscription
+ * }
+ * ```
+ *
+ * When using this naming convention, the schema description can be omitted.
+ * When using this naming convention, the schema description can be omitted so
+ * long as these names are only used for operation types.
+ *
+ * Note however that if any of these default names are used elsewhere in the
+ * schema but not as a root operation type, the schema definition must still
+ * be printed to avoid ambiguity.
+ *
+ * @throws InvariantViolation
+ */
+ protected static function hasDefaultRootOperationTypes(Schema $schema): bool
+ {
+ return $schema->getQueryType() === $schema->getType('Query')
+ && $schema->getMutationType() === $schema->getType('Mutation')
+ && $schema->getSubscriptionType() === $schema->getType('Subscription');
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printDirective(Directive $directive, array $options): string
+ {
+ return static::printDescription($options, $directive)
+ . 'directive @' . $directive->name
+ . static::printArgs($options, $directive->args)
+ . ($directive->isRepeatable ? ' repeatable' : '')
+ . ' on ' . implode(' | ', $directive->locations);
+ }
+
+ /**
+ * @param array<string, bool> $options
+ * @param (Type&NamedType)|Directive|EnumValueDefinition|Argument|FieldDefinition|InputObjectField|Schema $def
+ *
+ * @throws \JsonException
+ */
+ protected static function printDescription(array $options, $def, string $indentation = '', bool $firstInBlock = true): string
+ {
+ $description = $def->description;
+ if ($description === null) {
+ return '';
+ }
+
+ $prefix = $indentation !== '' && ! $firstInBlock
+ ? "\n{$indentation}"
+ : $indentation;
+
+ if (count(Utils::splitLines($description)) === 1) {
+ $description = json_encode($description, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+ } else {
+ $description = BlockString::print($description);
+ $description = $indentation !== ''
+ ? str_replace("\n", "\n{$indentation}", $description)
+ : $description;
+ }
+
+ return "{$prefix}{$description}\n";
+ }
+
+ /**
+ * @param array<string, bool> $options
+ * @param array<int, Argument> $args
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printArgs(array $options, array $args, string $indentation = ''): string
+ {
+ if ($args === []) {
+ return '';
+ }
+
+ if (isset($options['sortArguments']) && $options['sortArguments']) {
+ usort($args, static fn (Argument $left, Argument $right): int => $left->name <=> $right->name);
+ }
+
+ $allArgsWithoutDescription = true;
+ foreach ($args as $arg) {
+ $description = $arg->description;
+ if ($description !== null && $description !== '') {
+ $allArgsWithoutDescription = false;
+ break;
+ }
+ }
+
+ if ($allArgsWithoutDescription) {
+ return '('
+ . implode(
+ ', ',
+ array_map(
+ [static::class, 'printInputValue'],
+ $args
+ )
+ )
+ . ')';
+ }
+
+ $argsStrings = [];
+ $firstInBlock = true;
+ $previousHasDescription = false;
+ foreach ($args as $arg) {
+ $hasDescription = $arg->description !== null;
+ if ($previousHasDescription && ! $hasDescription) {
+ $argsStrings[] = '';
+ }
+
+ $argsStrings[] = static::printDescription($options, $arg, ' ' . $indentation, $firstInBlock)
+ . ' '
+ . $indentation
+ . static::printInputValue($arg);
+ $firstInBlock = false;
+ $previousHasDescription = $hasDescription;
+ }
+
+ return "(\n"
+ . implode("\n", $argsStrings)
+ . "\n"
+ . $indentation
+ . ')';
+ }
+
+ /**
+ * @param InputObjectField|Argument $arg
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printInputValue($arg): string
+ {
+ $argDecl = "{$arg->name}: {$arg->getType()->toString()}";
+
+ if ($arg->defaultValueExists()) {
+ $defaultValueAST = AST::astFromValue($arg->defaultValue, $arg->getType());
+
+ if ($defaultValueAST === null) {
+ $inconvertibleDefaultValue = Utils::printSafe($arg->defaultValue);
+ throw new InvariantViolation("Unable to convert defaultValue of argument {$arg->name} into AST: {$inconvertibleDefaultValue}.");
+ }
+
+ $printedDefaultValue = Printer::doPrint($defaultValueAST);
+ $argDecl .= " = {$printedDefaultValue}";
+ }
+
+ return $argDecl . static::printDeprecated($arg);
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ */
+ protected static function printScalar(ScalarType $type, array $options): string
+ {
+ return static::printDescription($options, $type)
+ . "scalar {$type->name}";
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printObject(ObjectType $type, array $options): string
+ {
+ return static::printDescription($options, $type)
+ . "type {$type->name}"
+ . static::printImplementedInterfaces($type)
+ . static::printFields($options, $type);
+ }
+
+ /**
+ * @param array<string, bool> $options
+ * @param ObjectType|InterfaceType $type
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printFields(array $options, $type): string
+ {
+ $fields = [];
+ $firstInBlock = true;
+ $previousHasDescription = false;
+ $fieldDefinitions = $type->getFields();
+
+ if (isset($options['sortFields']) && $options['sortFields']) {
+ ksort($fieldDefinitions);
+ }
+
+ foreach ($fieldDefinitions as $f) {
+ $hasDescription = $f->description !== null;
+ if ($previousHasDescription && ! $hasDescription) {
+ $fields[] = '';
+ }
+
+ $fields[] = static::printDescription($options, $f, ' ', $firstInBlock)
+ . ' '
+ . $f->name
+ . static::printArgs($options, $f->args, ' ')
+ . ': '
+ . $f->getType()->toString()
+ . static::printDeprecated($f);
+ $firstInBlock = false;
+ $previousHasDescription = $hasDescription;
+ }
+
+ return static::printBlock($fields);
+ }
+
+ /**
+ * @param FieldDefinition|EnumValueDefinition|InputObjectField|Argument $deprecation
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printDeprecated($deprecation): string
+ {
+ $reason = $deprecation->deprecationReason;
+ if ($reason === null) {
+ return '';
+ }
+
+ if ($reason === '' || $reason === Directive::DEFAULT_DEPRECATION_REASON) {
+ return ' @deprecated';
+ }
+
+ $reasonAST = AST::astFromValue($reason, Type::string());
+ assert($reasonAST instanceof StringValueNode);
+
+ $reasonASTString = Printer::doPrint($reasonAST);
+
+ return " @deprecated(reason: {$reasonASTString})";
+ }
+
+ protected static function printImplementedInterfaces(ImplementingType $type): string
+ {
+ $interfaces = $type->getInterfaces();
+
+ return $interfaces === []
+ ? ''
+ : ' implements ' . implode(
+ ' & ',
+ array_map(
+ static fn (InterfaceType $interface): string => $interface->name,
+ $interfaces
+ )
+ );
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printInterface(InterfaceType $type, array $options): string
+ {
+ return static::printDescription($options, $type)
+ . "interface {$type->name}"
+ . static::printImplementedInterfaces($type)
+ . static::printFields($options, $type);
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ */
+ protected static function printUnion(UnionType $type, array $options): string
+ {
+ $types = $type->getTypes();
+ $types = $types === []
+ ? ''
+ : ' = ' . implode(' | ', $types);
+
+ return static::printDescription($options, $type) . 'union ' . $type->name . $types;
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printEnum(EnumType $type, array $options): string
+ {
+ $values = [];
+ $firstInBlock = true;
+ $valueDefinitions = $type->getValues();
+
+ if (isset($options['sortEnumValues']) && $options['sortEnumValues']) {
+ usort($valueDefinitions, static fn (EnumValueDefinition $left, EnumValueDefinition $right): int => $left->name <=> $right->name);
+ }
+
+ foreach ($valueDefinitions as $value) {
+ $values[] = static::printDescription($options, $value, ' ', $firstInBlock)
+ . ' '
+ . $value->name
+ . static::printDeprecated($value);
+ $firstInBlock = false;
+ }
+
+ return static::printDescription($options, $type)
+ . "enum {$type->name}"
+ . static::printBlock($values);
+ }
+
+ /**
+ * @param array<string, bool> $options
+ *
+ * @phpstan-param Options $options
+ *
+ * @throws \JsonException
+ * @throws InvariantViolation
+ * @throws SerializationError
+ */
+ protected static function printInputObject(InputObjectType $type, array $options): string
+ {
+ $fields = [];
+ $firstInBlock = true;
+ $fieldDefinitions = $type->getFields();
+
+ if (isset($options['sortInputFields']) && $options['sortInputFields']) {
+ ksort($fieldDefinitions);
+ }
+
+ foreach ($fieldDefinitions as $field) {
+ $fields[] = static::printDescription($options, $field, ' ', $firstInBlock)
+ . ' '
+ . static::printInputValue($field);
+ $firstInBlock = false;
+ }
+
+ return static::printDescription($options, $type)
+ . "input {$type->name}"
+ . ($type->isOneOf() ? ' @oneOf' : '')
+ . static::printBlock($fields);
+ }
+
+ /** @param array<string> $items */
+ protected static function printBlock(array $items): string
+ {
+ return $items === []
+ ? ''
+ : " {\n" . implode("\n", $items) . "\n}";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeComparators.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeComparators.php
new file mode 100644
index 00000000000..5532ccc02a4
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeComparators.php
@@ -0,0 +1,106 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+class TypeComparators
+{
+ /** Provided two types, return true if the types are equal (invariant). */
+ public static function isEqualType(Type $typeA, Type $typeB): bool
+ {
+ // Equivalent types are equal.
+ if ($typeA === $typeB) {
+ return true;
+ }
+
+ if (self::areSameBuiltInScalar($typeA, $typeB)) {
+ return true;
+ }
+
+ // If either type is non-null, the other must also be non-null.
+ if ($typeA instanceof NonNull && $typeB instanceof NonNull) {
+ return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
+ }
+
+ // If either type is a list, the other must also be a list.
+ if ($typeA instanceof ListOfType && $typeB instanceof ListOfType) {
+ return self::isEqualType($typeA->getWrappedType(), $typeB->getWrappedType());
+ }
+
+ // Otherwise the types are not equal.
+ return false;
+ }
+
+ /**
+ * Provided a type and a super type, return true if the first type is either
+ * equal or a subset of the second super type (covariant).
+ *
+ * @throws InvariantViolation
+ */
+ public static function isTypeSubTypeOf(Schema $schema, Type $maybeSubType, Type $superType): bool
+ {
+ // Equivalent type is a valid subtype
+ if ($maybeSubType === $superType) {
+ return true;
+ }
+
+ if (self::areSameBuiltInScalar($maybeSubType, $superType)) {
+ return true;
+ }
+
+ // If superType is non-null, maybeSubType must also be nullable.
+ if ($superType instanceof NonNull) {
+ if ($maybeSubType instanceof NonNull) {
+ return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
+ }
+
+ return false;
+ }
+
+ if ($maybeSubType instanceof NonNull) {
+ // If superType is nullable, maybeSubType may be non-null.
+ return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType);
+ }
+
+ // If superType type is a list, maybeSubType type must also be a list.
+ if ($superType instanceof ListOfType) {
+ if ($maybeSubType instanceof ListOfType) {
+ return self::isTypeSubTypeOf($schema, $maybeSubType->getWrappedType(), $superType->getWrappedType());
+ }
+
+ return false;
+ }
+
+ if ($maybeSubType instanceof ListOfType) {
+ // If superType is not a list, maybeSubType must also be not a list.
+ return false;
+ }
+
+ if (Type::isAbstractType($superType)) {
+ // If superType type is an abstract type, maybeSubType type may be a currently
+ // possible object or interface type.
+
+ return $maybeSubType instanceof ImplementingType
+ && $schema->isSubType($superType, $maybeSubType);
+ }
+
+ return false;
+ }
+
+ /**
+ * Built-in scalars may exist as different instances when a type loader
+ * overrides them. Compare by name to handle this case.
+ */
+ private static function areSameBuiltInScalar(Type $typeA, Type $typeB): bool
+ {
+ return Type::isBuiltInScalar($typeA)
+ && Type::isBuiltInScalar($typeB)
+ && $typeA->name() === $typeB->name();
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeInfo.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeInfo.php
new file mode 100644
index 00000000000..869ad4efe07
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/TypeInfo.php
@@ -0,0 +1,431 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CompositeType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\HasFieldsType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ImplementingType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\WrappingType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+class TypeInfo
+{
+ private Schema $schema;
+
+ /** @var array<int, Type|null> */
+ private array $typeStack = [];
+
+ /** @var array<int, (CompositeType&Type)|null> */
+ private array $parentTypeStack = [];
+
+ /** @var array<int, (InputType&Type)|null> */
+ private array $inputTypeStack = [];
+
+ /** @var array<int, FieldDefinition|null> */
+ private array $fieldDefStack = [];
+
+ /** @var array<int, mixed> */
+ private array $defaultValueStack = [];
+
+ private ?Directive $directive = null;
+
+ private ?Argument $argument = null;
+
+ /** @var mixed */
+ private $enumValue;
+
+ public function __construct(Schema $schema)
+ {
+ $this->schema = $schema;
+ }
+
+ /** @return array<int, (CompositeType&Type)|null> */
+ public function getParentTypeStack(): array
+ {
+ return $this->parentTypeStack;
+ }
+
+ /** @return array<int, FieldDefinition|null> */
+ public function getFieldDefStack(): array
+ {
+ return $this->fieldDefStack;
+ }
+
+ /**
+ * Given root type scans through all fields to find nested types.
+ *
+ * Returns array where keys are for type name
+ * and value contains corresponding type instance.
+ *
+ * Example output:
+ * [
+ * 'String' => $instanceOfStringType,
+ * 'MyType' => $instanceOfMyType,
+ * ...
+ * ]
+ *
+ * @param (Type&NamedType)|(Type&WrappingType) $type
+ * @param array<string, Type&NamedType> $typeMap
+ *
+ * @throws InvariantViolation
+ */
+ public static function extractTypes(Type $type, array &$typeMap): void
+ {
+ if ($type instanceof WrappingType) {
+ self::extractTypes($type->getInnermostType(), $typeMap);
+
+ return;
+ }
+
+ $name = $type->name;
+ assert(is_string($name));
+
+ if (isset($typeMap[$name])) {
+ if ($typeMap[$name] !== $type) {
+ throw new InvariantViolation("Schema must contain unique named types but contains multiple types named \"{$type}\" (see https://webonyx.github.io/graphql-php/type-definitions/#type-registry).");
+ }
+
+ return;
+ }
+
+ $typeMap[$name] = $type;
+
+ if ($type instanceof UnionType) {
+ foreach ($type->getTypes() as $member) {
+ self::extractTypes($member, $typeMap);
+ }
+
+ return;
+ }
+
+ if ($type instanceof InputObjectType) {
+ foreach ($type->getFields() as $field) {
+ $fieldType = $field->getType();
+ assert($fieldType instanceof NamedType || $fieldType instanceof WrappingType);
+ self::extractTypes($fieldType, $typeMap);
+ }
+
+ return;
+ }
+
+ if ($type instanceof ImplementingType) {
+ foreach ($type->getInterfaces() as $interface) {
+ self::extractTypes($interface, $typeMap);
+ }
+ }
+
+ if ($type instanceof HasFieldsType) {
+ foreach ($type->getFields() as $field) {
+ foreach ($field->args as $arg) {
+ $argType = $arg->getType();
+ assert($argType instanceof NamedType || $argType instanceof WrappingType);
+ self::extractTypes($argType, $typeMap);
+ }
+
+ $fieldType = $field->getType();
+ assert($fieldType instanceof NamedType || $fieldType instanceof WrappingType);
+ self::extractTypes($fieldType, $typeMap);
+ }
+ }
+ }
+
+ /**
+ * @param array<string, Type&NamedType> $typeMap
+ *
+ * @throws InvariantViolation
+ */
+ public static function extractTypesFromDirectives(Directive $directive, array &$typeMap): void
+ {
+ foreach ($directive->args as $arg) {
+ $argType = $arg->getType();
+ assert($argType instanceof NamedType || $argType instanceof WrappingType);
+ self::extractTypes($argType, $typeMap);
+ }
+ }
+
+ /** @return (Type&InputType)|null */
+ public function getParentInputType(): ?InputType
+ {
+ return $this->inputTypeStack[count($this->inputTypeStack) - 2] ?? null;
+ }
+
+ public function getArgument(): ?Argument
+ {
+ return $this->argument;
+ }
+
+ /** @return mixed */
+ public function getEnumValue()
+ {
+ return $this->enumValue;
+ }
+
+ /**
+ * @throws \Exception
+ * @throws InvariantViolation
+ */
+ public function enter(Node $node): void
+ {
+ $schema = $this->schema;
+
+ // Note: many of the types below are explicitly typed as "mixed" to drop
+ // any assumptions of a valid schema to ensure runtime types are properly
+ // checked before continuing since TypeInfo is used as part of validation
+ // which occurs before guarantees of schema and document validity.
+ switch (true) {
+ case $node instanceof SelectionSetNode:
+ $namedType = Type::getNamedType($this->getType());
+ $this->parentTypeStack[] = Type::isCompositeType($namedType) ? $namedType : null;
+ break;
+
+ case $node instanceof FieldNode:
+ $parentType = $this->getParentType();
+
+ $fieldDef = $parentType === null
+ ? null
+ : self::getFieldDefinition($schema, $parentType, $node);
+
+ $fieldType = $fieldDef === null
+ ? null
+ : $fieldDef->getType();
+
+ $this->fieldDefStack[] = $fieldDef;
+ $this->typeStack[] = $fieldType;
+ break;
+
+ case $node instanceof DirectiveNode:
+ $this->directive = $schema->getDirective($node->name->value);
+ break;
+
+ case $node instanceof OperationDefinitionNode:
+ if ($node->operation === 'query') {
+ $type = $schema->getQueryType();
+ } elseif ($node->operation === 'mutation') {
+ $type = $schema->getMutationType();
+ } else {
+ // Only other option
+ $type = $schema->getSubscriptionType();
+ }
+
+ $this->typeStack[] = Type::isOutputType($type)
+ ? $type
+ : null;
+ break;
+
+ case $node instanceof InlineFragmentNode:
+ case $node instanceof FragmentDefinitionNode:
+ $typeConditionNode = $node->typeCondition;
+ $outputType = $typeConditionNode === null
+ ? Type::getNamedType($this->getType())
+ : AST::typeFromAST([$schema, 'getType'], $typeConditionNode);
+ $this->typeStack[] = Type::isOutputType($outputType) ? $outputType : null;
+ break;
+
+ case $node instanceof VariableDefinitionNode:
+ $inputType = AST::typeFromAST([$schema, 'getType'], $node->type);
+ $this->inputTypeStack[] = Type::isInputType($inputType) ? $inputType : null; // push
+ break;
+
+ case $node instanceof ArgumentNode:
+ $fieldOrDirective = $this->getDirective() ?? $this->getFieldDef();
+ $argDef = null;
+ $argType = null;
+ if ($fieldOrDirective !== null) {
+ foreach ($fieldOrDirective->args as $arg) {
+ if ($arg->name === $node->name->value) {
+ $argDef = $arg;
+ $argType = $arg->getType();
+ }
+ }
+ }
+
+ $this->argument = $argDef;
+ $this->defaultValueStack[] = $argDef !== null && $argDef->defaultValueExists()
+ ? $argDef->defaultValue
+ : Utils::undefined();
+ $this->inputTypeStack[] = Type::isInputType($argType) ? $argType : null;
+ break;
+
+ case $node instanceof ListValueNode:
+ $type = $this->getInputType();
+ $listType = $type instanceof NonNull
+ ? $type->getWrappedType()
+ : $type;
+ $itemType = $listType instanceof ListOfType
+ ? $listType->getWrappedType()
+ : $listType;
+ // List positions never have a default value.
+ $this->defaultValueStack[] = Utils::undefined();
+ $this->inputTypeStack[] = Type::isInputType($itemType) ? $itemType : null;
+ break;
+
+ case $node instanceof ObjectFieldNode:
+ $objectType = Type::getNamedType($this->getInputType());
+ $inputField = null;
+ $inputFieldType = null;
+ if ($objectType instanceof InputObjectType) {
+ $tmp = $objectType->getFields();
+ $inputField = $tmp[$node->name->value] ?? null;
+ $inputFieldType = $inputField === null
+ ? null
+ : $inputField->getType();
+ }
+
+ $this->defaultValueStack[] = $inputField !== null && $inputField->defaultValueExists()
+ ? $inputField->defaultValue
+ : Utils::undefined();
+ $this->inputTypeStack[] = Type::isInputType($inputFieldType)
+ ? $inputFieldType
+ : null;
+ break;
+
+ case $node instanceof EnumValueNode:
+ $enumType = Type::getNamedType($this->getInputType());
+
+ $this->enumValue = $enumType instanceof EnumType
+ ? $enumType->getValue($node->value)
+ : null;
+ break;
+ }
+ }
+
+ public function getType(): ?Type
+ {
+ return $this->typeStack[count($this->typeStack) - 1] ?? null;
+ }
+
+ /** @return (CompositeType&Type)|null */
+ public function getParentType(): ?CompositeType
+ {
+ return $this->parentTypeStack[count($this->parentTypeStack) - 1] ?? null;
+ }
+
+ /**
+ * Not exactly the same as the executor's definition of getFieldDef, in this
+ * statically evaluated environment we do not always have an Object type,
+ * and need to handle Interface and Union types.
+ *
+ * @throws InvariantViolation
+ */
+ private static function getFieldDefinition(Schema $schema, Type $parentType, FieldNode $fieldNode): ?FieldDefinition
+ {
+ $name = $fieldNode->name->value;
+ $schemaMeta = Introspection::schemaMetaFieldDef();
+ if ($name === $schemaMeta->name && $schema->getQueryType() === $parentType) {
+ return $schemaMeta;
+ }
+
+ $typeMeta = Introspection::typeMetaFieldDef();
+ if ($name === $typeMeta->name && $schema->getQueryType() === $parentType) {
+ return $typeMeta;
+ }
+
+ $typeNameMeta = Introspection::typeNameMetaFieldDef();
+ if ($name === $typeNameMeta->name && $parentType instanceof CompositeType) {
+ return $typeNameMeta;
+ }
+
+ if (
+ $parentType instanceof ObjectType
+ || $parentType instanceof InterfaceType
+ ) {
+ return $parentType->findField($name);
+ }
+
+ return null;
+ }
+
+ public function getDirective(): ?Directive
+ {
+ return $this->directive;
+ }
+
+ public function getFieldDef(): ?FieldDefinition
+ {
+ return $this->fieldDefStack[count($this->fieldDefStack) - 1] ?? null;
+ }
+
+ /** @return mixed any value is possible */
+ public function getDefaultValue()
+ {
+ return $this->defaultValueStack[count($this->defaultValueStack) - 1] ?? null;
+ }
+
+ /** @return (InputType&Type)|null */
+ public function getInputType(): ?InputType
+ {
+ return $this->inputTypeStack[count($this->inputTypeStack) - 1] ?? null;
+ }
+
+ public function leave(Node $node): void
+ {
+ switch ($node->kind) {
+ case NodeKind::SELECTION_SET:
+ array_pop($this->parentTypeStack);
+ break;
+
+ case NodeKind::FIELD:
+ array_pop($this->fieldDefStack);
+ array_pop($this->typeStack);
+ break;
+
+ case NodeKind::DIRECTIVE:
+ $this->directive = null;
+ break;
+
+ case NodeKind::OPERATION_DEFINITION:
+ case NodeKind::INLINE_FRAGMENT:
+ case NodeKind::FRAGMENT_DEFINITION:
+ array_pop($this->typeStack);
+ break;
+
+ case NodeKind::VARIABLE_DEFINITION:
+ array_pop($this->inputTypeStack);
+ break;
+
+ case NodeKind::ARGUMENT:
+ $this->argument = null;
+ array_pop($this->defaultValueStack);
+ array_pop($this->inputTypeStack);
+ break;
+
+ case NodeKind::LST:
+ case NodeKind::OBJECT_FIELD:
+ array_pop($this->defaultValueStack);
+ array_pop($this->inputTypeStack);
+ break;
+
+ case NodeKind::ENUM:
+ $this->enumValue = null;
+ break;
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/Utils.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/Utils.php
new file mode 100644
index 00000000000..10fbd1dac9c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/Utils.php
@@ -0,0 +1,294 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Warning;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+
+class Utils
+{
+ public static function undefined(): \stdClass
+ {
+ static $undefined;
+
+ return $undefined ??= new \stdClass();
+ }
+
+ /** @param array<string, mixed> $vars */
+ public static function assign(object $obj, array $vars): object
+ {
+ foreach ($vars as $key => $value) {
+ if (! property_exists($obj, $key)) {
+ $cls = get_class($obj);
+ Warning::warn(
+ "Trying to set non-existing property '{$key}' on class '{$cls}'",
+ Warning::WARNING_ASSIGN
+ );
+ }
+
+ $obj->{$key} = $value;
+ }
+
+ return $obj;
+ }
+
+ /**
+ * Print a value that came from JSON for debugging purposes.
+ *
+ * @param mixed $value
+ */
+ public static function printSafeJson($value): string
+ {
+ if ($value instanceof \stdClass) {
+ return static::jsonEncodeOrSerialize($value);
+ }
+
+ return static::printSafeInternal($value);
+ }
+
+ /**
+ * Print a value that came from PHP for debugging purposes.
+ *
+ * @param mixed $value
+ */
+ public static function printSafe($value): string
+ {
+ if (is_object($value)) {
+ if (method_exists($value, '__toString')) {
+ return $value->__toString();
+ }
+
+ return 'instance of ' . get_class($value);
+ }
+
+ return static::printSafeInternal($value);
+ }
+
+ /** @param \stdClass|array<mixed> $value */
+ protected static function jsonEncodeOrSerialize($value): string
+ {
+ try {
+ return json_encode($value, JSON_THROW_ON_ERROR);
+ } catch (\JsonException $jsonException) {
+ return serialize($value);
+ }
+ }
+
+ /** @param mixed $value */
+ protected static function printSafeInternal($value): string
+ {
+ if (is_array($value)) {
+ return static::jsonEncodeOrSerialize($value);
+ }
+
+ if ($value === '') {
+ return '(empty string)';
+ }
+
+ if ($value === null) {
+ return 'null';
+ }
+
+ if ($value === false) {
+ return 'false';
+ }
+
+ if ($value === true) {
+ return 'true';
+ }
+
+ if (is_string($value)) {
+ return "\"{$value}\"";
+ }
+
+ if (is_scalar($value)) {
+ return (string) $value;
+ }
+
+ return gettype($value);
+ }
+
+ /** UTF-8 compatible chr(). */
+ public static function chr(int $ord, string $encoding = 'UTF-8'): string
+ {
+ if ($encoding === 'UCS-4BE') {
+ return pack('N', $ord);
+ }
+
+ return mb_convert_encoding(self::chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE');
+ }
+
+ /** UTF-8 compatible ord(). */
+ public static function ord(string $char, string $encoding = 'UTF-8'): int
+ {
+ if (! isset($char[1])) {
+ return ord($char);
+ }
+
+ if ($encoding !== 'UCS-4BE') {
+ $char = mb_convert_encoding($char, 'UCS-4BE', $encoding);
+ assert(is_string($char), 'format string is statically known to be correct');
+ }
+
+ $unpacked = unpack('N', $char);
+ assert(is_array($unpacked), 'format string is statically known to be correct');
+
+ return $unpacked[1];
+ }
+
+ /** Returns UTF-8 char code at given $positing of the $string. */
+ public static function charCodeAt(string $string, int $position): int
+ {
+ $char = mb_substr($string, $position, 1, 'UTF-8');
+
+ return self::ord($char);
+ }
+
+ /** @throws \JsonException */
+ public static function printCharCode(?int $code): string
+ {
+ if ($code === null) {
+ return '<EOF>';
+ }
+
+ return $code < 0x007F
+ // Trust JSON for ASCII
+ ? json_encode(self::chr($code), JSON_THROW_ON_ERROR)
+ // Otherwise, print the escaped form
+ : '"\\u' . dechex($code) . '"';
+ }
+
+ /**
+ * Upholds the spec rules about naming.
+ *
+ * @throws Error
+ */
+ public static function assertValidName(string $name): void
+ {
+ $error = self::isValidNameError($name);
+ if ($error !== null) {
+ throw $error;
+ }
+ }
+
+ /** Returns an Error if a name is invalid. */
+ public static function isValidNameError(string $name, ?Node $node = null): ?Error
+ {
+ if (isset($name[1]) && $name[0] === '_' && $name[1] === '_') {
+ return new Error(
+ "Name \"{$name}\" must not begin with \"__\", which is reserved by Automattic\WooCommerce\Vendor\GraphQL introspection.",
+ $node
+ );
+ }
+
+ if (preg_match('/^[_a-zA-Z][_a-zA-Z0-9]*$/', $name) !== 1) {
+ return new Error(
+ "Names must match /^[_a-zA-Z][_a-zA-Z0-9]*\$/ but \"{$name}\" does not.",
+ $node
+ );
+ }
+
+ return null;
+ }
+
+ /** @param array<string> $items */
+ public static function quotedOrList(array $items): string
+ {
+ $quoted = array_map(
+ static fn (string $item): string => "\"{$item}\"",
+ $items
+ );
+
+ return self::orList($quoted);
+ }
+
+ /** @param array<string> $items */
+ public static function orList(array $items): string
+ {
+ if ($items === []) {
+ return '';
+ }
+
+ $selected = array_slice($items, 0, 5);
+ $selectedLength = count($selected);
+ $firstSelected = $selected[0];
+
+ if ($selectedLength === 1) {
+ return $firstSelected;
+ }
+
+ return array_reduce(
+ range(1, $selectedLength - 1),
+ static fn ($list, $index): string => $list
+ . ($selectedLength > 2 ? ', ' : ' ')
+ . ($index === $selectedLength - 1 ? 'or ' : '')
+ . $selected[$index],
+ $firstSelected
+ );
+ }
+
+ /**
+ * Given an invalid input string and a list of valid options, returns a filtered
+ * list of valid options sorted based on their similarity with the input.
+ *
+ * @param array<string> $options
+ *
+ * @return array<int, string>
+ */
+ public static function suggestionList(string $input, array $options): array
+ {
+ /** @var array<string, int> $optionsByDistance */
+ $optionsByDistance = [];
+ $lexicalDistance = new LexicalDistance($input);
+ $threshold = mb_strlen($input) * 0.4 + 1;
+ foreach ($options as $option) {
+ $distance = $lexicalDistance->measure($option, $threshold);
+
+ if ($distance !== null) {
+ $optionsByDistance[$option] = $distance;
+ }
+ }
+
+ uksort($optionsByDistance, static function (string $a, string $b) use ($optionsByDistance) {
+ $distanceDiff = $optionsByDistance[$a] - $optionsByDistance[$b];
+
+ return $distanceDiff !== 0 ? $distanceDiff : strnatcmp($a, $b);
+ });
+
+ return array_map('strval', array_keys($optionsByDistance));
+ }
+
+ /**
+ * Try to extract the value for a key from an object like value.
+ *
+ * @param mixed $objectLikeValue
+ *
+ * @return mixed
+ */
+ public static function extractKey($objectLikeValue, string $key)
+ {
+ if (is_array($objectLikeValue) || $objectLikeValue instanceof \ArrayAccess) {
+ return $objectLikeValue[$key] ?? null;
+ }
+
+ if (is_object($objectLikeValue)) {
+ return $objectLikeValue->{$key} ?? null;
+ }
+
+ return null;
+ }
+
+ /**
+ * Split a string that has either Unix, Windows or Mac style newlines into lines.
+ *
+ * @return list<string>
+ */
+ public static function splitLines(string $value): array
+ {
+ $lines = preg_split("/\r\n|\r|\n/", $value);
+ assert(is_array($lines), 'given the regex is valid');
+
+ return $lines;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Utils/Value.php b/plugins/woocommerce/lib/packages/GraphQL/Utils/Value.php
new file mode 100644
index 00000000000..06e36a77fa2
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Utils/Value.php
@@ -0,0 +1,248 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Utils;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\ClientAware;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\CoercionError;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+/**
+ * @phpstan-type CoercedValue array{errors: null, value: mixed}
+ * @phpstan-type CoercedErrors array{errors: array<int, CoercionError>, value: null}
+ *
+ * @phpstan-import-type InputPath from CoercionError
+ */
+class Value
+{
+ /**
+ * Coerce the given value to match the given Automattic\WooCommerce\Vendor\GraphQL Input Type.
+ *
+ * Returns either a value which is valid for the provided type,
+ * or a list of encountered coercion errors.
+ *
+ * @param mixed $value
+ * @param InputType&Type $type
+ *
+ * @phpstan-param InputPath|null $path
+ *
+ * @throws InvariantViolation
+ *
+ * @phpstan-return CoercedValue|CoercedErrors
+ */
+ public static function coerceInputValue($value, InputType $type, ?array $path = null, ?Schema $schema = null): array
+ {
+ if ($type instanceof NonNull) {
+ if ($value === null) {
+ return self::ofErrors([
+ CoercionError::make("Expected non-nullable type \"{$type}\" not to be null.", $path, $value),
+ ]);
+ }
+
+ // @phpstan-ignore-next-line wrapped type is known to be input type after schema validation
+ return self::coerceInputValue($value, $type->getWrappedType(), $path, $schema);
+ }
+
+ if ($value === null) {
+ // Explicitly return the value null.
+ return self::ofValue(null);
+ }
+
+ // Account for type loader returning a different scalar instance than
+ // the built-in singleton used in field definitions. Resolve the actual
+ // type from the schema to ensure the correct parseValue() is called.
+ if ($schema !== null && Type::isBuiltInScalar($type)) {
+ $schemaType = $schema->getType($type->name);
+ assert($schemaType instanceof ScalarType, "Schema must provide a ScalarType for built-in scalar \"{$type->name}\".");
+ $type = $schemaType;
+ }
+
+ if ($type instanceof ScalarType || $type instanceof EnumType) {
+ try {
+ return self::ofValue($type->parseValue($value));
+ } catch (\Throwable $error) {
+ if (
+ $error instanceof Error
+ || ($error instanceof ClientAware && $error->isClientSafe())
+ ) {
+ return self::ofErrors([
+ CoercionError::make($error->getMessage(), $path, $value, $error),
+ ]);
+ }
+
+ return self::ofErrors([
+ CoercionError::make("Expected type \"{$type->name}\".", $path, $value, $error),
+ ]);
+ }
+ }
+
+ if ($type instanceof ListOfType) {
+ $itemType = $type->getWrappedType();
+ assert($itemType instanceof InputType, 'known through schema validation');
+
+ if (is_iterable($value)) {
+ $errors = [];
+ $coercedValue = [];
+ foreach ($value as $index => $itemValue) {
+ $coercedItem = self::coerceInputValue(
+ $itemValue,
+ $itemType,
+ [...$path ?? [], $index],
+ $schema,
+ );
+
+ if (isset($coercedItem['errors'])) {
+ $errors = self::add($errors, $coercedItem['errors']);
+ } else {
+ $coercedValue[] = $coercedItem['value'];
+ }
+ }
+
+ return $errors === []
+ ? self::ofValue($coercedValue)
+ : self::ofErrors($errors);
+ }
+
+ // Lists accept a non-list value as a list of one.
+ $coercedItem = self::coerceInputValue($value, $itemType, null, $schema);
+
+ return isset($coercedItem['errors'])
+ ? $coercedItem
+ : self::ofValue([$coercedItem['value']]);
+ }
+
+ assert($type instanceof InputObjectType, 'we handled all other cases at this point');
+
+ if ($value instanceof \stdClass) {
+ // Cast objects to associative array before checking the fields.
+ // Note that the coerced value will be an array.
+ $value = (array) $value;
+ } elseif (! is_array($value)) {
+ return self::ofErrors([
+ CoercionError::make("Expected type \"{$type->name}\" to be an object.", $path, $value),
+ ]);
+ }
+
+ $errors = [];
+ $coercedValue = [];
+ $fields = $type->getFields();
+ foreach ($fields as $fieldName => $field) {
+ if (array_key_exists($fieldName, $value)) {
+ $fieldValue = $value[$fieldName];
+ $coercedField = self::coerceInputValue(
+ $fieldValue,
+ $field->getType(),
+ [...$path ?? [], $fieldName],
+ $schema,
+ );
+
+ if (isset($coercedField['errors'])) {
+ $errors = self::add($errors, $coercedField['errors']);
+ } else {
+ $coercedValue[$fieldName] = $coercedField['value'];
+ }
+ } elseif ($field->defaultValueExists()) {
+ $coercedValue[$fieldName] = $field->defaultValue;
+ } elseif ($field->getType() instanceof NonNull) {
+ $errors = self::add(
+ $errors,
+ CoercionError::make("Field \"{$fieldName}\" of required type \"{$field->getType()->toString()}\" was not provided.", $path, $value)
+ );
+ }
+ }
+
+ // Ensure every provided field is defined.
+ foreach ($value as $fieldName => $field) {
+ if (array_key_exists($fieldName, $fields)) {
+ continue;
+ }
+
+ $suggestions = Utils::suggestionList(
+ (string) $fieldName,
+ array_keys($fields)
+ );
+ $message = "Field \"{$fieldName}\" is not defined by type \"{$type->name}\"."
+ . ($suggestions === []
+ ? ''
+ : ' Did you mean ' . Utils::quotedOrList($suggestions) . '?');
+
+ $errors = self::add(
+ $errors,
+ CoercionError::make($message, $path, $value)
+ );
+ }
+
+ // Validate OneOf constraints if this is a OneOf input type
+ if ($type->isOneOf()) {
+ $providedFieldCount = count($coercedValue);
+ $nullFieldName = null;
+
+ if ($providedFieldCount !== 1) {
+ $errors = self::add(
+ $errors,
+ CoercionError::make("OneOf input object \"{$type->name}\" must specify exactly one field.", $path, $value)
+ );
+ } else {
+ foreach ($coercedValue as $fieldName => $fieldValue) {
+ if ($fieldValue === null) {
+ $nullFieldName = $fieldName;
+ }
+ }
+
+ if ($nullFieldName !== null) {
+ $errors = self::add(
+ $errors,
+ CoercionError::make("OneOf input object \"{$type->name}\" field \"{$nullFieldName}\" must be non-null.", $path, $value)
+ );
+ }
+ }
+ }
+
+ return $errors === []
+ ? self::ofValue($type->parseValue($coercedValue))
+ : self::ofErrors($errors);
+ }
+
+ /**
+ * @param array<int, CoercionError> $errors
+ *
+ * @phpstan-return CoercedErrors
+ */
+ private static function ofErrors(array $errors): array
+ {
+ return ['errors' => $errors, 'value' => null];
+ }
+
+ /**
+ * @param mixed $value any value
+ *
+ * @phpstan-return CoercedValue
+ */
+ private static function ofValue($value): array
+ {
+ return ['errors' => null, 'value' => $value];
+ }
+
+ /**
+ * @param array<int, CoercionError> $errors
+ * @param CoercionError|array<int, CoercionError> $errorOrErrors
+ *
+ * @return array<int, CoercionError>
+ */
+ private static function add(array $errors, $errorOrErrors): array
+ {
+ $moreErrors = is_array($errorOrErrors)
+ ? $errorOrErrors
+ : [$errorOrErrors];
+
+ return array_merge($errors, $moreErrors);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/DocumentValidator.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/DocumentValidator.php
new file mode 100644
index 00000000000..1dad7900f05
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/DocumentValidator.php
@@ -0,0 +1,330 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\DisableIntrospection;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ExecutableDefinitions;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\FieldsOnCorrectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\FragmentsOnCompositeTypes;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownArgumentNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownArgumentNamesOnDirectives;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownDirectives;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownFragmentNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\KnownTypeNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\LoneAnonymousOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\LoneSchemaDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\NoFragmentCycles;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\NoUndefinedVariables;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\NoUnusedFragments;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\NoUnusedVariables;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\OneOfInputObjectsRule;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\PossibleFragmentSpreads;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\PossibleTypeExtensions;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ProvidedRequiredArguments;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ProvidedRequiredArgumentsOnDirectives;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryComplexity;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryDepth;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QuerySecurityRule;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ScalarLeafs;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\SingleFieldSubscription;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueArgumentDefinitionNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueArgumentNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueDirectiveNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueDirectivesPerLocation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueEnumValueNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueFieldDefinitionNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueFragmentNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueInputFieldNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueOperationNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueOperationTypes;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueTypeNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\UniqueVariableNames;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValidationRule;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValuesOfCorrectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\VariablesAreInputTypes;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\VariablesInAllowedPosition;
+
+/**
+ * Implements the "Validation" section of the spec.
+ *
+ * Validation runs synchronously, returning an array of encountered errors, or
+ * an empty array if no errors were encountered and the document is valid.
+ *
+ * A list of specific validation rules may be provided. If not provided, the
+ * default list of rules defined by the Automattic\WooCommerce\Vendor\GraphQL specification will be used.
+ *
+ * Each validation rule is an instance of Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\ValidationRule
+ * which returns a visitor (see the [Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor API](class-reference.md#graphqllanguagevisitor)).
+ *
+ * Visitor methods are expected to return an instance of [Automattic\WooCommerce\Vendor\GraphQL\Error\Error](class-reference.md#graphqlerrorerror),
+ * or array of such instances when invalid.
+ *
+ * Optionally a custom TypeInfo instance may be provided. If not provided, one
+ * will be created from the provided schema.
+ */
+class DocumentValidator
+{
+ /** @var array<string, ValidationRule> */
+ private static array $rules = [];
+
+ /** @var array<class-string<ValidationRule>, ValidationRule> */
+ private static array $defaultRules;
+
+ /** @var array<class-string<QuerySecurityRule>, QuerySecurityRule> */
+ private static array $securityRules;
+
+ /** @var array<class-string<ValidationRule>, ValidationRule> */
+ private static array $sdlRules;
+
+ private static bool $initRules = false;
+
+ /**
+ * Validate a Automattic\WooCommerce\Vendor\GraphQL query against a schema.
+ *
+ * @param array<ValidationRule>|null $rules Defaults to using all available rules
+ *
+ * @throws \Exception
+ *
+ * @return list<Error>
+ *
+ * @api
+ */
+ public static function validate(
+ Schema $schema,
+ DocumentNode $ast,
+ ?array $rules = null,
+ ?TypeInfo $typeInfo = null
+ ): array {
+ $rules ??= static::allRules();
+
+ if ($rules === []) {
+ return [];
+ }
+
+ $typeInfo ??= new TypeInfo($schema);
+
+ $context = new QueryValidationContext($schema, $ast, $typeInfo);
+
+ $visitors = [];
+ foreach ($rules as $rule) {
+ $visitors[] = $rule->getVisitor($context);
+ }
+
+ Visitor::visit(
+ $ast,
+ Visitor::visitWithTypeInfo(
+ $typeInfo,
+ Visitor::visitInParallel($visitors)
+ )
+ );
+
+ return $context->getErrors();
+ }
+
+ /**
+ * Returns all global validation rules.
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return array<string, ValidationRule>
+ *
+ * @api
+ */
+ public static function allRules(): array
+ {
+ if (! self::$initRules) {
+ self::$rules = array_merge(
+ static::defaultRules(),
+ self::securityRules(),
+ self::$rules
+ );
+ self::$initRules = true;
+ }
+
+ return self::$rules;
+ }
+
+ /** @return array<class-string<ValidationRule>, ValidationRule> */
+ public static function defaultRules(): array
+ {
+ return self::$defaultRules ??= [
+ ExecutableDefinitions::class => new ExecutableDefinitions(),
+ UniqueOperationNames::class => new UniqueOperationNames(),
+ LoneAnonymousOperation::class => new LoneAnonymousOperation(),
+ SingleFieldSubscription::class => new SingleFieldSubscription(),
+ KnownTypeNames::class => new KnownTypeNames(),
+ FragmentsOnCompositeTypes::class => new FragmentsOnCompositeTypes(),
+ VariablesAreInputTypes::class => new VariablesAreInputTypes(),
+ ScalarLeafs::class => new ScalarLeafs(),
+ FieldsOnCorrectType::class => new FieldsOnCorrectType(),
+ UniqueFragmentNames::class => new UniqueFragmentNames(),
+ KnownFragmentNames::class => new KnownFragmentNames(),
+ NoUnusedFragments::class => new NoUnusedFragments(),
+ PossibleFragmentSpreads::class => new PossibleFragmentSpreads(),
+ NoFragmentCycles::class => new NoFragmentCycles(),
+ UniqueVariableNames::class => new UniqueVariableNames(),
+ NoUndefinedVariables::class => new NoUndefinedVariables(),
+ NoUnusedVariables::class => new NoUnusedVariables(),
+ KnownDirectives::class => new KnownDirectives(),
+ UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
+ KnownArgumentNames::class => new KnownArgumentNames(),
+ UniqueArgumentNames::class => new UniqueArgumentNames(),
+ ValuesOfCorrectType::class => new ValuesOfCorrectType(),
+ ProvidedRequiredArguments::class => new ProvidedRequiredArguments(),
+ VariablesInAllowedPosition::class => new VariablesInAllowedPosition(),
+ OverlappingFieldsCanBeMerged::class => new OverlappingFieldsCanBeMerged(),
+ UniqueInputFieldNames::class => new UniqueInputFieldNames(),
+ OneOfInputObjectsRule::class => new OneOfInputObjectsRule(),
+ ];
+ }
+
+ /**
+ * @deprecated just add rules via @see DocumentValidator::addRule()
+ *
+ * @throws \InvalidArgumentException
+ *
+ * @return array<class-string<QuerySecurityRule>, QuerySecurityRule>
+ */
+ public static function securityRules(): array
+ {
+ return self::$securityRules ??= [
+ DisableIntrospection::class => new DisableIntrospection(DisableIntrospection::DISABLED),
+ QueryDepth::class => new QueryDepth(QueryDepth::DISABLED),
+ QueryComplexity::class => new QueryComplexity(QueryComplexity::DISABLED),
+ ];
+ }
+
+ /** @return array<class-string<ValidationRule>, ValidationRule> */
+ public static function sdlRules(): array
+ {
+ return self::$sdlRules ??= [
+ LoneSchemaDefinition::class => new LoneSchemaDefinition(),
+ UniqueOperationTypes::class => new UniqueOperationTypes(),
+ UniqueTypeNames::class => new UniqueTypeNames(),
+ UniqueEnumValueNames::class => new UniqueEnumValueNames(),
+ UniqueFieldDefinitionNames::class => new UniqueFieldDefinitionNames(),
+ UniqueArgumentDefinitionNames::class => new UniqueArgumentDefinitionNames(),
+ UniqueDirectiveNames::class => new UniqueDirectiveNames(),
+ KnownTypeNames::class => new KnownTypeNames(),
+ KnownDirectives::class => new KnownDirectives(),
+ UniqueDirectivesPerLocation::class => new UniqueDirectivesPerLocation(),
+ PossibleTypeExtensions::class => new PossibleTypeExtensions(),
+ KnownArgumentNamesOnDirectives::class => new KnownArgumentNamesOnDirectives(),
+ UniqueArgumentNames::class => new UniqueArgumentNames(),
+ UniqueInputFieldNames::class => new UniqueInputFieldNames(),
+ ProvidedRequiredArgumentsOnDirectives::class => new ProvidedRequiredArgumentsOnDirectives(),
+ ];
+ }
+
+ /**
+ * Returns global validation rule by name.
+ *
+ * Standard rules are named by class name, so example usage for such rules:
+ *
+ * @example DocumentValidator::getRule(Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryComplexity::class);
+ *
+ * @api
+ *
+ * @throws \InvalidArgumentException
+ */
+ public static function getRule(string $name): ?ValidationRule
+ {
+ return static::allRules()[$name] ?? null;
+ }
+
+ /**
+ * Add rule to list of global validation rules.
+ *
+ * @api
+ */
+ public static function addRule(ValidationRule $rule): void
+ {
+ self::$rules[$rule->getName()] = $rule;
+ }
+
+ /**
+ * Remove rule from list of global validation rules.
+ *
+ * @api
+ */
+ public static function removeRule(ValidationRule $rule): void
+ {
+ unset(self::$rules[$rule->getName()]);
+ }
+
+ /**
+ * Validate a Automattic\WooCommerce\Vendor\GraphQL document defined through schema definition language.
+ *
+ * @param array<ValidationRule>|null $rules
+ *
+ * @throws \Exception
+ *
+ * @return list<Error>
+ */
+ public static function validateSDL(
+ DocumentNode $documentAST,
+ ?Schema $schemaToExtend = null,
+ ?array $rules = null
+ ): array {
+ $rules ??= self::sdlRules();
+
+ if ($rules === []) {
+ return [];
+ }
+
+ $context = new SDLValidationContext($documentAST, $schemaToExtend);
+
+ $visitors = [];
+ foreach ($rules as $rule) {
+ $visitors[] = $rule->getSDLVisitor($context);
+ }
+
+ Visitor::visit(
+ $documentAST,
+ Visitor::visitInParallel($visitors)
+ );
+
+ return $context->getErrors();
+ }
+
+ /**
+ * @throws \Exception
+ * @throws Error
+ */
+ public static function assertValidSDL(DocumentNode $documentAST): void
+ {
+ $errors = self::validateSDL($documentAST);
+ if ($errors !== []) {
+ throw new Error(self::combineErrorMessages($errors));
+ }
+ }
+
+ /**
+ * @throws \Exception
+ * @throws Error
+ */
+ public static function assertValidSDLExtension(DocumentNode $documentAST, Schema $schema): void
+ {
+ $errors = self::validateSDL($documentAST, $schema);
+ if ($errors !== []) {
+ throw new Error(self::combineErrorMessages($errors));
+ }
+ }
+
+ /** @param array<Error> $errors */
+ private static function combineErrorMessages(array $errors): string
+ {
+ $messages = [];
+ foreach ($errors as $error) {
+ $messages[] = $error->getMessage();
+ }
+
+ return implode("\n\n", $messages);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/QueryValidationContext.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/QueryValidationContext.php
new file mode 100644
index 00000000000..74488a103e4
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/QueryValidationContext.php
@@ -0,0 +1,276 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\HasSelectionSet;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CompositeType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeInfo;
+
+/**
+ * An instance of this class is passed as the "this" context to all validators,
+ * allowing access to commonly useful contextual information from within a
+ * validation rule.
+ *
+ * @phpstan-type VariableUsage array{node: VariableNode, type: (Type&InputType)|null, defaultValue: mixed}
+ */
+class QueryValidationContext implements ValidationContext
+{
+ protected Schema $schema;
+
+ protected DocumentNode $ast;
+
+ /** @var list<Error> */
+ protected array $errors = [];
+
+ private TypeInfo $typeInfo;
+
+ /** @var array<string, FragmentDefinitionNode> */
+ private array $fragments;
+
+ /** @var \SplObjectStorage<HasSelectionSet, array<int, FragmentSpreadNode>> */
+ private \SplObjectStorage $fragmentSpreads;
+
+ /** @var \SplObjectStorage<OperationDefinitionNode, array<int, FragmentDefinitionNode>> */
+ private \SplObjectStorage $recursivelyReferencedFragments;
+
+ /** @var \SplObjectStorage<HasSelectionSet, array<int, VariableUsage>> */
+ private \SplObjectStorage $variableUsages;
+
+ /** @var \SplObjectStorage<HasSelectionSet, array<int, VariableUsage>> */
+ private \SplObjectStorage $recursiveVariableUsages;
+
+ public function __construct(Schema $schema, DocumentNode $ast, TypeInfo $typeInfo)
+ {
+ $this->schema = $schema;
+ $this->ast = $ast;
+ $this->typeInfo = $typeInfo;
+
+ $this->fragmentSpreads = new \SplObjectStorage();
+ $this->recursivelyReferencedFragments = new \SplObjectStorage();
+ $this->variableUsages = new \SplObjectStorage();
+ $this->recursiveVariableUsages = new \SplObjectStorage();
+ }
+
+ public function reportError(Error $error): void
+ {
+ $this->errors[] = $error;
+ }
+
+ /** @return list<Error> */
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+
+ public function getDocument(): DocumentNode
+ {
+ return $this->ast;
+ }
+
+ public function getSchema(): Schema
+ {
+ return $this->schema;
+ }
+
+ /**
+ * @throws \Exception
+ *
+ * @phpstan-return array<int, VariableUsage>
+ */
+ public function getRecursiveVariableUsages(OperationDefinitionNode $operation): array
+ {
+ $usages = $this->recursiveVariableUsages[$operation] ?? null;
+
+ if ($usages === null) {
+ $usages = $this->getVariableUsages($operation);
+ $fragments = $this->getRecursivelyReferencedFragments($operation);
+
+ $allUsages = [$usages];
+ foreach ($fragments as $fragment) {
+ $allUsages[] = $this->getVariableUsages($fragment);
+ }
+
+ $usages = array_merge(...$allUsages);
+ $this->recursiveVariableUsages[$operation] = $usages;
+ }
+
+ return $usages;
+ }
+
+ /**
+ * @param HasSelectionSet&Node $node
+ *
+ * @throws \Exception
+ *
+ * @phpstan-return array<int, VariableUsage>
+ */
+ private function getVariableUsages(HasSelectionSet $node): array
+ {
+ if (! isset($this->variableUsages[$node])) {
+ $usages = [];
+ $typeInfo = new TypeInfo($this->schema);
+ Visitor::visit(
+ $node,
+ Visitor::visitWithTypeInfo(
+ $typeInfo,
+ [
+ NodeKind::VARIABLE_DEFINITION => static fn () => Visitor::skipNode(),
+ NodeKind::VARIABLE => static function (VariableNode $variable) use (&$usages, $typeInfo): void {
+ $usages[] = [
+ 'node' => $variable,
+ 'type' => $typeInfo->getInputType(),
+ 'defaultValue' => $typeInfo->getDefaultValue(),
+ ];
+ },
+ ]
+ )
+ );
+
+ return $this->variableUsages[$node] = $usages;
+ }
+
+ return $this->variableUsages[$node];
+ }
+
+ /** @return array<int, FragmentDefinitionNode> */
+ public function getRecursivelyReferencedFragments(OperationDefinitionNode $operation): array
+ {
+ $fragments = $this->recursivelyReferencedFragments[$operation] ?? null;
+
+ if ($fragments === null) {
+ $fragments = [];
+ $collectedNames = [];
+ $nodesToVisit = [$operation];
+ while ($nodesToVisit !== []) {
+ $node = array_pop($nodesToVisit);
+ $spreads = $this->getFragmentSpreads($node);
+ foreach ($spreads as $spread) {
+ $fragName = $spread->name->value;
+
+ if ($collectedNames[$fragName] ?? false) {
+ continue;
+ }
+
+ $collectedNames[$fragName] = true;
+ $fragment = $this->getFragment($fragName);
+ if ($fragment === null) {
+ continue;
+ }
+
+ $fragments[] = $fragment;
+ $nodesToVisit[] = $fragment;
+ }
+ }
+
+ $this->recursivelyReferencedFragments[$operation] = $fragments;
+ }
+
+ return $fragments;
+ }
+
+ /**
+ * @param OperationDefinitionNode|FragmentDefinitionNode $node
+ *
+ * @return array<int, FragmentSpreadNode>
+ */
+ public function getFragmentSpreads(HasSelectionSet $node): array
+ {
+ $spreads = $this->fragmentSpreads[$node] ?? null;
+ if ($spreads === null) {
+ $spreads = [];
+
+ $setsToVisit = [$node->getSelectionSet()];
+ while ($setsToVisit !== []) {
+ $set = array_pop($setsToVisit);
+
+ foreach ($set->selections as $selection) {
+ if ($selection instanceof FragmentSpreadNode) {
+ $spreads[] = $selection;
+ } else {
+ assert($selection instanceof FieldNode || $selection instanceof InlineFragmentNode);
+
+ $selectionSet = $selection->selectionSet;
+ if ($selectionSet !== null) {
+ $setsToVisit[] = $selectionSet;
+ }
+ }
+ }
+ }
+
+ $this->fragmentSpreads[$node] = $spreads;
+ }
+
+ return $spreads;
+ }
+
+ public function getFragment(string $name): ?FragmentDefinitionNode
+ {
+ if (! isset($this->fragments)) {
+ $fragments = [];
+ foreach ($this->getDocument()->definitions as $statement) {
+ if ($statement instanceof FragmentDefinitionNode) {
+ $fragments[$statement->name->value] = $statement;
+ }
+ }
+
+ $this->fragments = $fragments;
+ }
+
+ return $this->fragments[$name] ?? null;
+ }
+
+ public function getType(): ?Type
+ {
+ return $this->typeInfo->getType();
+ }
+
+ /** @return (CompositeType&Type)|null */
+ public function getParentType(): ?CompositeType
+ {
+ return $this->typeInfo->getParentType();
+ }
+
+ /** @return (Type&InputType)|null */
+ public function getInputType(): ?InputType
+ {
+ return $this->typeInfo->getInputType();
+ }
+
+ /** @return (Type&InputType)|null */
+ public function getParentInputType(): ?InputType
+ {
+ return $this->typeInfo->getParentInputType();
+ }
+
+ public function getFieldDef(): ?FieldDefinition
+ {
+ return $this->typeInfo->getFieldDef();
+ }
+
+ public function getDirective(): ?Directive
+ {
+ return $this->typeInfo->getDirective();
+ }
+
+ public function getArgument(): ?Argument
+ {
+ return $this->typeInfo->getArgument();
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/CustomValidationRule.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/CustomValidationRule.php
new file mode 100644
index 00000000000..32c90019f10
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/CustomValidationRule.php
@@ -0,0 +1,36 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
+
+/**
+ * @see Node, VisitorOperation
+ *
+ * @phpstan-type NodeVisitorFnResult VisitorOperation|mixed|null
+ * @phpstan-type VisitorFnResult array<string, callable(Node): NodeVisitorFnResult>|array<string, array<string, callable(Node): NodeVisitorFnResult>>
+ * @phpstan-type VisitorFn callable(ValidationContext): VisitorFnResult
+ */
+class CustomValidationRule extends ValidationRule
+{
+ /**
+ * @var callable
+ *
+ * @phpstan-var VisitorFn
+ */
+ protected $visitorFn;
+
+ /** @phpstan-param VisitorFn $visitorFn */
+ public function __construct(string $name, callable $visitorFn)
+ {
+ $this->name = $name;
+ $this->visitorFn = $visitorFn;
+ }
+
+ public function getVisitor(ValidationContext $context): array
+ {
+ return ($this->visitorFn)($context);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/DisableIntrospection.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/DisableIntrospection.php
new file mode 100644
index 00000000000..378299ccd93
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/DisableIntrospection.php
@@ -0,0 +1,54 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class DisableIntrospection extends QuerySecurityRule
+{
+ public const ENABLED = 1;
+
+ protected int $isEnabled;
+
+ public function __construct(int $enabled)
+ {
+ $this->setEnabled($enabled);
+ }
+
+ public function setEnabled(int $enabled): void
+ {
+ $this->isEnabled = $enabled;
+ }
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return $this->invokeIfNeeded(
+ $context,
+ [
+ NodeKind::FIELD => static function (FieldNode $node) use ($context): void {
+ if ($node->name->value !== '__type' && $node->name->value !== '__schema') {
+ return;
+ }
+
+ $context->reportError(new Error(
+ static::introspectionDisabledMessage(),
+ [$node]
+ ));
+ },
+ ]
+ );
+ }
+
+ public static function introspectionDisabledMessage(): string
+ {
+ return 'Automattic\WooCommerce\Vendor\GraphQL introspection is not allowed, but the query contained __schema or __type';
+ }
+
+ protected function isEnabled(): bool
+ {
+ return $this->isEnabled !== self::DISABLED;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ExecutableDefinitions.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ExecutableDefinitions.php
new file mode 100644
index 00000000000..1270f7517d2
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ExecutableDefinitions.php
@@ -0,0 +1,57 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ExecutableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+/**
+ * Executable definitions.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL document is only valid for execution if all definitions are either
+ * operation or fragment definitions.
+ */
+class ExecutableDefinitions extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::DOCUMENT => static function (DocumentNode $node) use ($context): VisitorOperation {
+ foreach ($node->definitions as $definition) {
+ if (! $definition instanceof ExecutableDefinitionNode) {
+ if ($definition instanceof SchemaDefinitionNode || $definition instanceof SchemaExtensionNode) {
+ $defName = 'schema';
+ } else {
+ assert(
+ $definition instanceof TypeDefinitionNode || $definition instanceof TypeExtensionNode,
+ 'only other option'
+ );
+ $defName = "\"{$definition->getName()->value}\"";
+ }
+
+ $context->reportError(new Error(
+ static::nonExecutableDefinitionMessage($defName),
+ [$definition]
+ ));
+ }
+ }
+
+ return Visitor::skipNode();
+ },
+ ];
+ }
+
+ public static function nonExecutableDefinitionMessage(string $defName): string
+ {
+ return "The {$defName} definition is not executable.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FieldsOnCorrectType.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FieldsOnCorrectType.php
new file mode 100644
index 00000000000..23a25401a4c
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FieldsOnCorrectType.php
@@ -0,0 +1,148 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\HasFieldsType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class FieldsOnCorrectType extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::FIELD => function (FieldNode $node) use ($context): void {
+ $fieldDef = $context->getFieldDef();
+ if ($fieldDef !== null && $fieldDef->isVisible()) {
+ return;
+ }
+
+ $type = $context->getParentType();
+ if (! $type instanceof NamedType) {
+ return;
+ }
+
+ // This isn't valid. Let's find suggestions, if any.
+ $schema = $context->getSchema();
+ $fieldName = $node->name->value;
+ // First determine if there are any suggested types to condition on.
+ $suggestedTypeNames = $this->getSuggestedTypeNames($schema, $type, $fieldName);
+ // If there are no suggested types, then perhaps this was a typo?
+ $suggestedFieldNames = $suggestedTypeNames === []
+ ? $this->getSuggestedFieldNames($type, $fieldName)
+ : [];
+
+ // Report an error, including helpful suggestions.
+ $context->reportError(new Error(
+ static::undefinedFieldMessage(
+ $node->name->value,
+ $type->name,
+ $suggestedTypeNames,
+ $suggestedFieldNames
+ ),
+ [$node]
+ ));
+ },
+ ];
+ }
+
+ /**
+ * Go through all implementations of a type, as well as the interfaces
+ * that it implements. If any of those types include the provided field,
+ * suggest them, sorted by how often the type is referenced, starting
+ * with interfaces.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, string>
+ */
+ protected function getSuggestedTypeNames(Schema $schema, Type $type, string $fieldName): array
+ {
+ if (Type::isAbstractType($type)) {
+ $suggestedObjectTypes = [];
+ $interfaceUsageCount = [];
+
+ foreach ($schema->getPossibleTypes($type) as $possibleType) {
+ if (! $possibleType->hasField($fieldName)) {
+ continue;
+ }
+
+ // This object type defines this field.
+ $suggestedObjectTypes[] = $possibleType->name;
+ foreach ($possibleType->getInterfaces() as $possibleInterface) {
+ if (! $possibleInterface->hasField($fieldName)) {
+ continue;
+ }
+
+ // This interface type defines this field.
+ $interfaceUsageCount[$possibleInterface->name] = isset($interfaceUsageCount[$possibleInterface->name])
+ ? $interfaceUsageCount[$possibleInterface->name] + 1
+ : 0;
+ }
+ }
+
+ // Suggest interface types based on how common they are.
+ arsort($interfaceUsageCount);
+ $suggestedInterfaceTypes = array_keys($interfaceUsageCount);
+
+ // Suggest both interface and object types.
+ return array_merge($suggestedInterfaceTypes, $suggestedObjectTypes);
+ }
+
+ // Otherwise, must be an Object type, which does not have suggested types.
+ return [];
+ }
+
+ /**
+ * For the field name provided, determine if there are any similar field names
+ * that may be the result of a typo.
+ *
+ * @throws InvariantViolation
+ *
+ * @return array<int, string>
+ */
+ protected function getSuggestedFieldNames(Type $type, string $fieldName): array
+ {
+ if ($type instanceof HasFieldsType) {
+ return Utils::suggestionList(
+ $fieldName,
+ $type->getFieldNames()
+ );
+ }
+
+ // Otherwise, must be a Union type, which does not define fields.
+ return [];
+ }
+
+ /**
+ * @param array<string> $suggestedTypeNames
+ * @param array<string> $suggestedFieldNames
+ */
+ public static function undefinedFieldMessage(
+ string $fieldName,
+ string $type,
+ array $suggestedTypeNames,
+ array $suggestedFieldNames
+ ): string {
+ $message = "Cannot query field \"{$fieldName}\" on type \"{$type}\".";
+
+ if ($suggestedTypeNames !== []) {
+ $suggestions = Utils::quotedOrList($suggestedTypeNames);
+
+ $message .= " Did you mean to use an inline fragment on {$suggestions}?";
+ } elseif ($suggestedFieldNames !== []) {
+ $suggestions = Utils::quotedOrList($suggestedFieldNames);
+
+ $message .= " Did you mean {$suggestions}?";
+ }
+
+ return $message;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FragmentsOnCompositeTypes.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FragmentsOnCompositeTypes.php
new file mode 100644
index 00000000000..6e539b41a82
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/FragmentsOnCompositeTypes.php
@@ -0,0 +1,61 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class FragmentsOnCompositeTypes extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::INLINE_FRAGMENT => static function (InlineFragmentNode $node) use ($context): void {
+ if ($node->typeCondition === null) {
+ return;
+ }
+
+ $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->typeCondition);
+ if ($type === null || Type::isCompositeType($type)) {
+ return;
+ }
+
+ $context->reportError(new Error(
+ static::inlineFragmentOnNonCompositeErrorMessage($type->toString()),
+ [$node->typeCondition]
+ ));
+ },
+ NodeKind::FRAGMENT_DEFINITION => static function (FragmentDefinitionNode $node) use ($context): void {
+ $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->typeCondition);
+
+ if ($type === null || Type::isCompositeType($type)) {
+ return;
+ }
+
+ $context->reportError(new Error(
+ static::fragmentOnNonCompositeErrorMessage(
+ $node->name->value,
+ Printer::doPrint($node->typeCondition)
+ ),
+ [$node->typeCondition]
+ ));
+ },
+ ];
+ }
+
+ public static function inlineFragmentOnNonCompositeErrorMessage(string $type): string
+ {
+ return "Fragment cannot condition on non composite type \"{$type}\".";
+ }
+
+ public static function fragmentOnNonCompositeErrorMessage(string $fragName, string $type): string
+ {
+ return "Fragment \"{$fragName}\" cannot condition on non composite type \"{$type}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNames.php
new file mode 100644
index 00000000000..095258fd417
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNames.php
@@ -0,0 +1,75 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+/**
+ * Known argument names.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL field is only valid if all supplied arguments are defined by
+ * that field.
+ */
+class KnownArgumentNames extends ValidationRule
+{
+ /** @throws InvariantViolation */
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $knownArgumentNamesOnDirectives = new KnownArgumentNamesOnDirectives();
+
+ return $knownArgumentNamesOnDirectives->getVisitor($context) + [
+ NodeKind::ARGUMENT => static function (ArgumentNode $node) use ($context): void {
+ $argDef = $context->getArgument();
+ if ($argDef !== null) {
+ return;
+ }
+
+ $fieldDef = $context->getFieldDef();
+ if ($fieldDef === null) {
+ return;
+ }
+
+ $parentType = $context->getParentType();
+ if (! $parentType instanceof NamedType) {
+ return;
+ }
+
+ $context->reportError(new Error(
+ static::unknownArgMessage(
+ $node->name->value,
+ $fieldDef->name,
+ $parentType->name,
+ Utils::suggestionList(
+ $node->name->value,
+ array_map(
+ static fn (Argument $arg): string => $arg->name,
+ $fieldDef->args
+ )
+ )
+ ),
+ [$node]
+ ));
+ },
+ ];
+ }
+
+ /** @param array<string> $suggestedArgs */
+ public static function unknownArgMessage(string $argName, string $fieldName, string $typeName, array $suggestedArgs): string
+ {
+ $message = "Unknown argument \"{$argName}\" on field \"{$fieldName}\" of type \"{$typeName}\".";
+
+ if ($suggestedArgs !== []) {
+ $suggestions = Utils::quotedOrList($suggestedArgs);
+ $message .= " Did you mean {$suggestions}?";
+ }
+
+ return $message;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNamesOnDirectives.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNamesOnDirectives.php
new file mode 100644
index 00000000000..00f800b980e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownArgumentNamesOnDirectives.php
@@ -0,0 +1,110 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
+
+/**
+ * Known argument names on directives.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL directive is only valid if all supplied arguments are defined by
+ * that field.
+ *
+ * @phpstan-import-type VisitorArray from Visitor
+ */
+class KnownArgumentNamesOnDirectives extends ValidationRule
+{
+ /** @param array<string> $suggestedArgs */
+ public static function unknownDirectiveArgMessage(string $argName, string $directiveName, array $suggestedArgs): string
+ {
+ $message = "Unknown argument \"{$argName}\" on directive \"@{$directiveName}\".";
+
+ if (isset($suggestedArgs[0])) {
+ $suggestions = Utils::quotedOrList($suggestedArgs);
+ $message .= " Did you mean {$suggestions}?";
+ }
+
+ return $message;
+ }
+
+ /** @throws InvariantViolation */
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /** @throws InvariantViolation */
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @phpstan-return VisitorArray
+ */
+ public function getASTVisitor(ValidationContext $context): array
+ {
+ $directiveArgs = [];
+ $schema = $context->getSchema();
+ $definedDirectives = $schema !== null
+ ? $schema->getDirectives()
+ : Directive::getInternalDirectives();
+
+ foreach ($definedDirectives as $directive) {
+ $directiveArgs[$directive->name] = array_map(
+ static fn (Argument $arg): string => $arg->name,
+ $directive->args
+ );
+ }
+
+ $astDefinitions = $context->getDocument()->definitions;
+ foreach ($astDefinitions as $def) {
+ if ($def instanceof DirectiveDefinitionNode) {
+ $argNames = [];
+ foreach ($def->arguments as $arg) {
+ $argNames[] = $arg->name->value;
+ }
+
+ $directiveArgs[$def->name->value] = $argNames;
+ }
+ }
+
+ return [
+ NodeKind::DIRECTIVE => static function (DirectiveNode $directiveNode) use ($directiveArgs, $context): VisitorOperation {
+ $directiveName = $directiveNode->name->value;
+
+ if (! isset($directiveArgs[$directiveName])) {
+ return Visitor::skipNode();
+ }
+ $knownArgs = $directiveArgs[$directiveName];
+
+ foreach ($directiveNode->arguments as $argNode) {
+ $argName = $argNode->name->value;
+ if (! in_array($argName, $knownArgs, true)) {
+ $suggestions = Utils::suggestionList($argName, $knownArgs);
+ $context->reportError(new Error(
+ static::unknownDirectiveArgMessage($argName, $directiveName, $suggestions),
+ [$argNode]
+ ));
+ }
+ }
+
+ return Visitor::skipNode();
+ },
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownDirectives.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownDirectives.php
new file mode 100644
index 00000000000..ff4e7c18f18
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownDirectives.php
@@ -0,0 +1,204 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ScalarTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\UnionTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\DirectiveLocation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
+
+/**
+ * @phpstan-import-type VisitorArray from Visitor
+ */
+class KnownDirectives extends ValidationRule
+{
+ /** @throws InvariantViolation */
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /** @throws InvariantViolation */
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @phpstan-return VisitorArray
+ */
+ public function getASTVisitor(ValidationContext $context): array
+ {
+ $locationsMap = [];
+ $schema = $context->getSchema();
+ $definedDirectives = $schema === null
+ ? Directive::getInternalDirectives()
+ : $schema->getDirectives();
+
+ foreach ($definedDirectives as $directive) {
+ $locationsMap[$directive->name] = $directive->locations;
+ }
+
+ $astDefinition = $context->getDocument()->definitions;
+
+ foreach ($astDefinition as $def) {
+ if ($def instanceof DirectiveDefinitionNode) {
+ $locationNames = [];
+ foreach ($def->locations as $location) {
+ $locationNames[] = $location->value;
+ }
+
+ $locationsMap[$def->name->value] = $locationNames;
+ }
+ }
+
+ return [
+ NodeKind::DIRECTIVE => function (
+ DirectiveNode $node,
+ $key,
+ $parent,
+ $path,
+ $ancestors
+ ) use (
+ $context,
+ $locationsMap
+ ): void {
+ $name = $node->name->value;
+ $locations = $locationsMap[$name] ?? null;
+
+ if ($locations === null) {
+ $context->reportError(new Error(
+ static::unknownDirectiveMessage($name),
+ [$node]
+ ));
+
+ return;
+ }
+
+ $candidateLocation = $this->getDirectiveLocationForASTPath($ancestors);
+
+ if ($candidateLocation === '' || in_array($candidateLocation, $locations, true)) {
+ return;
+ }
+
+ $context->reportError(
+ new Error(
+ static::misplacedDirectiveMessage($name, $candidateLocation),
+ [$node]
+ )
+ );
+ },
+ ];
+ }
+
+ public static function unknownDirectiveMessage(string $directiveName): string
+ {
+ return "Unknown directive \"@{$directiveName}\".";
+ }
+
+ /**
+ * @param array<Node|NodeList<Node>> $ancestors
+ *
+ * @throws \Exception
+ */
+ protected function getDirectiveLocationForASTPath(array $ancestors): string
+ {
+ $appliedTo = $ancestors[count($ancestors) - 1];
+
+ switch (true) {
+ case $appliedTo instanceof OperationDefinitionNode:
+ switch ($appliedTo->operation) {
+ case 'query':
+ return DirectiveLocation::QUERY;
+ case 'mutation':
+ return DirectiveLocation::MUTATION;
+ case 'subscription':
+ return DirectiveLocation::SUBSCRIPTION;
+ }
+ // no break, since all possible cases were handled
+ case $appliedTo instanceof FieldNode:
+ return DirectiveLocation::FIELD;
+ case $appliedTo instanceof FragmentSpreadNode:
+ return DirectiveLocation::FRAGMENT_SPREAD;
+ case $appliedTo instanceof InlineFragmentNode:
+ return DirectiveLocation::INLINE_FRAGMENT;
+ case $appliedTo instanceof FragmentDefinitionNode:
+ return DirectiveLocation::FRAGMENT_DEFINITION;
+ case $appliedTo instanceof VariableDefinitionNode:
+ return DirectiveLocation::VARIABLE_DEFINITION;
+ case $appliedTo instanceof SchemaDefinitionNode:
+ case $appliedTo instanceof SchemaExtensionNode:
+ return DirectiveLocation::SCHEMA;
+ case $appliedTo instanceof ScalarTypeDefinitionNode:
+ case $appliedTo instanceof ScalarTypeExtensionNode:
+ return DirectiveLocation::SCALAR;
+ case $appliedTo instanceof ObjectTypeDefinitionNode:
+ case $appliedTo instanceof ObjectTypeExtensionNode:
+ return DirectiveLocation::OBJECT;
+ case $appliedTo instanceof FieldDefinitionNode:
+ return DirectiveLocation::FIELD_DEFINITION;
+ case $appliedTo instanceof InterfaceTypeDefinitionNode:
+ case $appliedTo instanceof InterfaceTypeExtensionNode:
+ return DirectiveLocation::IFACE;
+ case $appliedTo instanceof UnionTypeDefinitionNode:
+ case $appliedTo instanceof UnionTypeExtensionNode:
+ return DirectiveLocation::UNION;
+ case $appliedTo instanceof EnumTypeDefinitionNode:
+ case $appliedTo instanceof EnumTypeExtensionNode:
+ return DirectiveLocation::ENUM;
+ case $appliedTo instanceof EnumValueDefinitionNode:
+ return DirectiveLocation::ENUM_VALUE;
+ case $appliedTo instanceof InputObjectTypeDefinitionNode:
+ case $appliedTo instanceof InputObjectTypeExtensionNode:
+ return DirectiveLocation::INPUT_OBJECT;
+ case $appliedTo instanceof InputValueDefinitionNode:
+ $parentNode = $ancestors[count($ancestors) - 3];
+
+ return $parentNode instanceof InputObjectTypeDefinitionNode
+ ? DirectiveLocation::INPUT_FIELD_DEFINITION
+ : DirectiveLocation::ARGUMENT_DEFINITION;
+ default:
+ $unknownLocation = get_class($appliedTo);
+ throw new \Exception("Unknown directive location: {$unknownLocation}.");
+ }
+ }
+
+ public static function misplacedDirectiveMessage(string $directiveName, string $location): string
+ {
+ return "Directive \"{$directiveName}\" may not be used on \"{$location}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownFragmentNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownFragmentNames.php
new file mode 100644
index 00000000000..86be2006118
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownFragmentNames.php
@@ -0,0 +1,34 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class KnownFragmentNames extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::FRAGMENT_SPREAD => static function (FragmentSpreadNode $node) use ($context): void {
+ $fragmentName = $node->name->value;
+ $fragment = $context->getFragment($fragmentName);
+ if ($fragment !== null) {
+ return;
+ }
+
+ $context->reportError(new Error(
+ static::unknownFragmentMessage($fragmentName),
+ [$node->name]
+ ));
+ },
+ ];
+ }
+
+ public static function unknownFragmentMessage(string $fragName): string
+ {
+ return "Unknown fragment \"{$fragName}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownTypeNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownTypeNames.php
new file mode 100644
index 00000000000..20e9f72b57a
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/KnownTypeNames.php
@@ -0,0 +1,102 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NamedTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeSystemDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeSystemExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
+
+/**
+ * Known type names.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if referenced types (specifically
+ * variable definitions and fragment conditions) are defined by the type schema.
+ *
+ * @phpstan-import-type VisitorArray from \Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor
+ */
+class KnownTypeNames extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /** @phpstan-return VisitorArray */
+ public function getASTVisitor(ValidationContext $context): array
+ {
+ /** @var array<int, string> $definedTypes */
+ $definedTypes = [];
+ foreach ($context->getDocument()->definitions as $def) {
+ if ($def instanceof TypeDefinitionNode) {
+ $definedTypes[] = $def->getName()->value;
+ }
+ }
+
+ return [
+ NodeKind::NAMED_TYPE => static function (NamedTypeNode $node, $_1, $parent, $_2, $ancestors) use ($context, $definedTypes): void {
+ $typeName = $node->name->value;
+ $schema = $context->getSchema();
+
+ if (in_array($typeName, $definedTypes, true)) {
+ return;
+ }
+
+ if ($schema !== null && $schema->hasType($typeName)) {
+ return;
+ }
+
+ $definitionNode = $ancestors[2] ?? $parent;
+ $isSDL = $definitionNode instanceof TypeSystemDefinitionNode || $definitionNode instanceof TypeSystemExtensionNode;
+ if ($isSDL && in_array($typeName, Type::BUILT_IN_TYPE_NAMES, true)) {
+ return;
+ }
+
+ $existingTypesMap = $schema !== null
+ ? $schema->getTypeMap()
+ : [];
+ $typeNames = [
+ ...array_keys($existingTypesMap),
+ ...$definedTypes,
+ ];
+ $context->reportError(new Error(
+ static::unknownTypeMessage(
+ $typeName,
+ Utils::suggestionList(
+ $typeName,
+ $isSDL
+ ? [...Type::BUILT_IN_TYPE_NAMES, ...$typeNames]
+ : $typeNames
+ )
+ ),
+ [$node]
+ ));
+ },
+ ];
+ }
+
+ /** @param array<string> $suggestedTypes */
+ public static function unknownTypeMessage(string $type, array $suggestedTypes): string
+ {
+ $message = "Unknown type \"{$type}\".";
+
+ if ($suggestedTypes !== []) {
+ $suggestionList = Utils::quotedOrList($suggestedTypes);
+ $message .= " Did you mean {$suggestionList}?";
+ }
+
+ return $message;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneAnonymousOperation.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneAnonymousOperation.php
new file mode 100644
index 00000000000..660cb90edf9
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneAnonymousOperation.php
@@ -0,0 +1,48 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+/**
+ * Lone anonymous operation.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if when it contains an anonymous operation
+ * (the query shorthand) that it contains only that one operation definition.
+ */
+class LoneAnonymousOperation extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $operationCount = 0;
+
+ return [
+ NodeKind::DOCUMENT => static function (DocumentNode $node) use (&$operationCount): void {
+ $operationCount = 0;
+ foreach ($node->definitions as $definition) {
+ if ($definition instanceof OperationDefinitionNode) {
+ ++$operationCount;
+ }
+ }
+ },
+ NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use (&$operationCount, $context): void {
+ if ($node->name !== null || $operationCount <= 1) {
+ return;
+ }
+
+ $context->reportError(
+ new Error(static::anonOperationNotAloneMessage(), [$node])
+ );
+ },
+ ];
+ }
+
+ public static function anonOperationNotAloneMessage(): string
+ {
+ return 'This anonymous operation must be the only defined operation.';
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneSchemaDefinition.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneSchemaDefinition.php
new file mode 100644
index 00000000000..284fa7a3679
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/LoneSchemaDefinition.php
@@ -0,0 +1,57 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+
+/**
+ * Lone schema definition.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if it contains only one schema definition.
+ */
+class LoneSchemaDefinition extends ValidationRule
+{
+ public static function schemaDefinitionNotAloneMessage(): string
+ {
+ return 'Must provide only one schema definition.';
+ }
+
+ public static function canNotDefineSchemaWithinExtensionMessage(): string
+ {
+ return 'Cannot define a new schema within a schema extension.';
+ }
+
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ $oldSchema = $context->getSchema();
+ $alreadyDefined = $oldSchema === null
+ ? false
+ : (
+ $oldSchema->astNode !== null
+ || $oldSchema->getQueryType() !== null
+ || $oldSchema->getMutationType() !== null
+ || $oldSchema->getSubscriptionType() !== null
+ );
+
+ $schemaDefinitionsCount = 0;
+
+ return [
+ NodeKind::SCHEMA_DEFINITION => static function (SchemaDefinitionNode $node) use ($alreadyDefined, $context, &$schemaDefinitionsCount): void {
+ if ($alreadyDefined) {
+ $context->reportError(new Error(static::canNotDefineSchemaWithinExtensionMessage(), $node));
+
+ return;
+ }
+
+ if ($schemaDefinitionsCount > 0) {
+ $context->reportError(new Error(static::schemaDefinitionNotAloneMessage(), $node));
+ }
+
+ ++$schemaDefinitionsCount;
+ },
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoFragmentCycles.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoFragmentCycles.php
new file mode 100644
index 00000000000..c3792272ef7
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoFragmentCycles.php
@@ -0,0 +1,101 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class NoFragmentCycles extends ValidationRule
+{
+ /** @var array<string, bool> */
+ protected array $visitedFrags;
+
+ /** @var array<int, FragmentSpreadNode> */
+ protected array $spreadPath;
+
+ /** @var array<string, int|null> */
+ protected array $spreadPathIndexByName;
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ // Tracks already visited fragments to maintain O(N) and to ensure that cycles
+ // are not redundantly reported.
+ $this->visitedFrags = [];
+
+ // Array of AST nodes used to produce meaningful errors
+ $this->spreadPath = [];
+
+ // Position in the spread path
+ $this->spreadPathIndexByName = [];
+
+ return [
+ NodeKind::OPERATION_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(),
+ NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context): VisitorOperation {
+ $this->detectCycleRecursive($node, $context);
+
+ return Visitor::skipNode();
+ },
+ ];
+ }
+
+ protected function detectCycleRecursive(FragmentDefinitionNode $fragment, QueryValidationContext $context): void
+ {
+ if (isset($this->visitedFrags[$fragment->name->value])) {
+ return;
+ }
+
+ $fragmentName = $fragment->name->value;
+ $this->visitedFrags[$fragmentName] = true;
+
+ $spreadNodes = $context->getFragmentSpreads($fragment);
+
+ if ($spreadNodes === []) {
+ return;
+ }
+
+ $this->spreadPathIndexByName[$fragmentName] = count($this->spreadPath);
+
+ foreach ($spreadNodes as $spreadNode) {
+ $spreadName = $spreadNode->name->value;
+ $cycleIndex = $this->spreadPathIndexByName[$spreadName] ?? null;
+
+ $this->spreadPath[] = $spreadNode;
+ if ($cycleIndex === null) {
+ $spreadFragment = $context->getFragment($spreadName);
+ if ($spreadFragment !== null) {
+ $this->detectCycleRecursive($spreadFragment, $context);
+ }
+ } else {
+ $cyclePath = array_slice($this->spreadPath, $cycleIndex);
+ $fragmentNames = [];
+ foreach (array_slice($cyclePath, 0, -1) as $frag) {
+ $fragmentNames[] = $frag->name->value;
+ }
+
+ $context->reportError(new Error(
+ static::cycleErrorMessage($spreadName, $fragmentNames),
+ $cyclePath
+ ));
+ }
+
+ array_pop($this->spreadPath);
+ }
+
+ $this->spreadPathIndexByName[$fragmentName] = null;
+ }
+
+ /** @param array<string> $spreadNames */
+ public static function cycleErrorMessage(string $fragName, array $spreadNames = []): string
+ {
+ $via = $spreadNames === []
+ ? ''
+ : ' via ' . implode(', ', $spreadNames);
+
+ return "Cannot spread fragment \"{$fragName}\" within itself{$via}.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUndefinedVariables.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUndefinedVariables.php
new file mode 100644
index 00000000000..1d5878a403e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUndefinedVariables.php
@@ -0,0 +1,60 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+/**
+ * A Automattic\WooCommerce\Vendor\GraphQL operation is only valid if all variables encountered, both directly
+ * and via fragment spreads, are defined by that operation.
+ */
+class NoUndefinedVariables extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ /** @var array<string, true> $variableNameDefined */
+ $variableNameDefined = [];
+
+ return [
+ NodeKind::OPERATION_DEFINITION => [
+ 'enter' => static function () use (&$variableNameDefined): void {
+ $variableNameDefined = [];
+ },
+ 'leave' => static function (OperationDefinitionNode $operation) use (&$variableNameDefined, $context): void {
+ $usages = $context->getRecursiveVariableUsages($operation);
+
+ foreach ($usages as $usage) {
+ $node = $usage['node'];
+ $varName = $node->name->value;
+
+ if (! isset($variableNameDefined[$varName])) {
+ $context->reportError(new Error(
+ static::undefinedVarMessage(
+ $varName,
+ $operation->name !== null
+ ? $operation->name->value
+ : null
+ ),
+ [$node, $operation]
+ ));
+ }
+ }
+ },
+ ],
+ NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $def) use (&$variableNameDefined): void {
+ $variableNameDefined[$def->variable->name->value] = true;
+ },
+ ];
+ }
+
+ public static function undefinedVarMessage(string $varName, ?string $opName): string
+ {
+ return $opName === null
+ ? "Variable \"\${$varName}\" is not defined by operation \"{$opName}\"."
+ : "Variable \"\${$varName}\" is not defined.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedFragments.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedFragments.php
new file mode 100644
index 00000000000..6cd72e7d4ce
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedFragments.php
@@ -0,0 +1,66 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class NoUnusedFragments extends ValidationRule
+{
+ /** @var array<int, OperationDefinitionNode> */
+ protected array $operationDefs;
+
+ /** @var array<int, FragmentDefinitionNode> */
+ protected array $fragmentDefs;
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $this->operationDefs = [];
+ $this->fragmentDefs = [];
+
+ return [
+ NodeKind::OPERATION_DEFINITION => function ($node): VisitorOperation {
+ $this->operationDefs[] = $node;
+
+ return Visitor::skipNode();
+ },
+ NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $def): VisitorOperation {
+ $this->fragmentDefs[] = $def;
+
+ return Visitor::skipNode();
+ },
+ NodeKind::DOCUMENT => [
+ 'leave' => function () use ($context): void {
+ $fragmentNameUsed = [];
+
+ foreach ($this->operationDefs as $operation) {
+ foreach ($context->getRecursivelyReferencedFragments($operation) as $fragment) {
+ $fragmentNameUsed[$fragment->name->value] = true;
+ }
+ }
+
+ foreach ($this->fragmentDefs as $fragmentDef) {
+ $fragName = $fragmentDef->name->value;
+
+ if (! isset($fragmentNameUsed[$fragName])) {
+ $context->reportError(new Error(
+ static::unusedFragMessage($fragName),
+ [$fragmentDef]
+ ));
+ }
+ }
+ },
+ ],
+ ];
+ }
+
+ public static function unusedFragMessage(string $fragName): string
+ {
+ return "Fragment \"{$fragName}\" is never used.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedVariables.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedVariables.php
new file mode 100644
index 00000000000..2c3f95e33f5
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/NoUnusedVariables.php
@@ -0,0 +1,61 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class NoUnusedVariables extends ValidationRule
+{
+ /** @var array<int, VariableDefinitionNode> */
+ protected array $variableDefs;
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $this->variableDefs = [];
+
+ return [
+ NodeKind::OPERATION_DEFINITION => [
+ 'enter' => function (): void {
+ $this->variableDefs = [];
+ },
+ 'leave' => function (OperationDefinitionNode $operation) use ($context): void {
+ $variableNameUsed = [];
+ $usages = $context->getRecursiveVariableUsages($operation);
+ $opName = $operation->name !== null
+ ? $operation->name->value
+ : null;
+
+ foreach ($usages as $usage) {
+ $node = $usage['node'];
+ $variableNameUsed[$node->name->value] = true;
+ }
+
+ foreach ($this->variableDefs as $variableDef) {
+ $variableName = $variableDef->variable->name->value;
+
+ if (! isset($variableNameUsed[$variableName])) {
+ $context->reportError(new Error(
+ static::unusedVariableMessage($variableName, $opName),
+ [$variableDef]
+ ));
+ }
+ }
+ },
+ ],
+ NodeKind::VARIABLE_DEFINITION => function ($def): void {
+ $this->variableDefs[] = $def;
+ },
+ ];
+ }
+
+ public static function unusedVariableMessage(string $varName, ?string $opName = null): string
+ {
+ return $opName !== null
+ ? "Variable \"\${$varName}\" is never used in operation \"{$opName}\"."
+ : "Variable \"\${$varName}\" is never used.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OneOfInputObjectsRule.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OneOfInputObjectsRule.php
new file mode 100644
index 00000000000..37e95e84df8
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OneOfInputObjectsRule.php
@@ -0,0 +1,94 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+/**
+ * OneOf Input Objects validation rule.
+ *
+ * Validates that OneOf Input Objects have exactly one non-null field provided.
+ */
+class OneOfInputObjectsRule extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::OBJECT => static function (ObjectValueNode $node) use ($context): void {
+ $type = $context->getInputType();
+
+ if ($type === null) {
+ return;
+ }
+
+ $namedType = Type::getNamedType($type);
+ if (! ($namedType instanceof InputObjectType)
+ || ! $namedType->isOneOf()
+ ) {
+ return;
+ }
+
+ $providedFields = [];
+ $nullFields = [];
+
+ foreach ($node->fields as $fieldNode) {
+ $fieldName = $fieldNode->name->value;
+ $providedFields[] = $fieldName;
+
+ // Check if the field value is explicitly null
+ if ($fieldNode->value->kind === NodeKind::NULL) {
+ $nullFields[] = $fieldName;
+ }
+ }
+
+ $fieldCount = count($providedFields);
+
+ if ($fieldCount === 0) {
+ $context->reportError(new Error(
+ static::oneOfInputObjectExpectedExactlyOneFieldMessage($namedType->name),
+ [$node]
+ ));
+
+ return;
+ }
+
+ if ($fieldCount > 1) {
+ $context->reportError(new Error(
+ static::oneOfInputObjectExpectedExactlyOneFieldMessage($namedType->name, $fieldCount),
+ [$node]
+ ));
+
+ return;
+ }
+
+ // At this point, $fieldCount === 1
+ if (count($nullFields) > 0) {
+ // Exactly one field provided, but it's null
+ $context->reportError(new Error(
+ static::oneOfInputObjectFieldValueMustNotBeNullMessage($namedType->name, $nullFields[0]),
+ [$node]
+ ));
+ }
+ },
+ ];
+ }
+
+ public static function oneOfInputObjectExpectedExactlyOneFieldMessage(string $typeName, ?int $providedCount = null): string
+ {
+ if ($providedCount === null) {
+ return "OneOf input object '{$typeName}' must specify exactly one field.";
+ }
+
+ return "OneOf input object '{$typeName}' must specify exactly one field, but {$providedCount} fields were provided.";
+ }
+
+ public static function oneOfInputObjectFieldValueMustNotBeNullMessage(string $typeName, string $fieldName): string
+ {
+ return "OneOf input object '{$typeName}' field '{$fieldName}' must be non-null.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OverlappingFieldsCanBeMerged.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OverlappingFieldsCanBeMerged.php
new file mode 100644
index 00000000000..e72c899a568
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/OverlappingFieldsCanBeMerged.php
@@ -0,0 +1,962 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\PairSet;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+/**
+ * ReasonOrReasons is recursive, but PHPStan does not support that.
+ *
+ * @phpstan-type ReasonOrReasons string|array<array{string, string|array<mixed>}>
+ * @phpstan-type Conflict array{array{string, ReasonOrReasons}, array<int, FieldNode>, array<int, FieldNode>}
+ * @phpstan-type FieldInfo array{Type|null, FieldNode, FieldDefinition|null}
+ * @phpstan-type FieldMap array<string, array<int, FieldInfo>>
+ */
+class OverlappingFieldsCanBeMerged extends ValidationRule
+{
+ /**
+ * A memoization for when two fragments are compared "between" each other for
+ * conflicts. Two fragments may be compared many times, so memoizing this can
+ * dramatically improve the performance of this validator.
+ */
+ protected PairSet $comparedFragmentPairs;
+
+ /**
+ * A cache for the "field map" and list of fragment names found in any given
+ * selection set. Selection sets may be asked for this information multiple
+ * times, so this improves the performance of this validator.
+ *
+ * @phpstan-var \SplObjectStorage<SelectionSetNode, array{FieldMap, array<int, string>}>
+ */
+ protected \SplObjectStorage $cachedFieldsAndFragmentNames;
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $this->comparedFragmentPairs = new PairSet();
+ $this->cachedFieldsAndFragmentNames = new \SplObjectStorage();
+
+ return [
+ NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context): void {
+ $conflicts = $this->findConflictsWithinSelectionSet(
+ $context,
+ $context->getParentType(),
+ $selectionSet
+ );
+
+ foreach ($conflicts as $conflict) {
+ [[$responseName, $reason], $fields1, $fields2] = $conflict;
+
+ $context->reportError(new Error(
+ static::fieldsConflictMessage($responseName, $reason),
+ array_merge($fields1, $fields2)
+ ));
+ }
+ },
+ ];
+ }
+
+ /**
+ * Find all conflicts found "within" a selection set, including those found
+ * via spreading in fragments. Called when visiting each SelectionSet in the
+ * Automattic\WooCommerce\Vendor\GraphQL Document.
+ *
+ * @throws \Exception
+ *
+ * @phpstan-return array<int, Conflict>
+ */
+ protected function findConflictsWithinSelectionSet(
+ QueryValidationContext $context,
+ ?Type $parentType,
+ SelectionSetNode $selectionSet
+ ): array {
+ [$fieldMap, $fragmentNames] = $this->getFieldsAndFragmentNames(
+ $context,
+ $parentType,
+ $selectionSet
+ );
+
+ $conflicts = [];
+
+ // (A) Find all conflicts "within" the fields of this selection set.
+ // Note: this is the *only place* `collectConflictsWithin` is called.
+ $this->collectConflictsWithin(
+ $context,
+ $conflicts,
+ $fieldMap
+ );
+
+ $fragmentNamesLength = count($fragmentNames);
+ if ($fragmentNamesLength !== 0) {
+ // (B) Then collect conflicts between these fields and those represented by
+ // each spread fragment name found.
+ $comparedFragments = [];
+ for ($i = 0; $i < $fragmentNamesLength; ++$i) {
+ $this->collectConflictsBetweenFieldsAndFragment(
+ $context,
+ $conflicts,
+ $comparedFragments,
+ false,
+ $fieldMap,
+ $fragmentNames[$i]
+ );
+ // (C) Then compare this fragment with all other fragments found in this
+ // selection set to collect conflicts between fragments spread together.
+ // This compares each item in the list of fragment names to every other item
+ // in that same list (except for itself).
+ for ($j = $i + 1; $j < $fragmentNamesLength; ++$j) {
+ $this->collectConflictsBetweenFragments(
+ $context,
+ $conflicts,
+ false,
+ $fragmentNames[$i],
+ $fragmentNames[$j]
+ );
+ }
+ }
+ }
+
+ return $conflicts;
+ }
+
+ /**
+ * Given a selection set, return the collection of fields (a mapping of response
+ * name to field ASTs and definitions) as well as a list of fragment names
+ * referenced via fragment spreads.
+ *
+ * @throws \Exception
+ *
+ * @return array{FieldMap, array<int, string>}
+ */
+ protected function getFieldsAndFragmentNames(
+ QueryValidationContext $context,
+ ?Type $parentType,
+ SelectionSetNode $selectionSet
+ ): array {
+ if (! isset($this->cachedFieldsAndFragmentNames[$selectionSet])) {
+ /** @phpstan-var FieldMap $astAndDefs */
+ $astAndDefs = [];
+
+ /** @var array<string, bool> $fragmentNames */
+ $fragmentNames = [];
+
+ $this->internalCollectFieldsAndFragmentNames(
+ $context,
+ $parentType,
+ $selectionSet,
+ $astAndDefs,
+ $fragmentNames
+ );
+
+ return $this->cachedFieldsAndFragmentNames[$selectionSet] = [$astAndDefs, array_keys($fragmentNames)];
+ }
+
+ return $this->cachedFieldsAndFragmentNames[$selectionSet];
+ }
+
+ /**
+ * Algorithm:.
+ *
+ * Conflicts occur when two fields exist in a query which will produce the same
+ * response name, but represent differing values, thus creating a conflict.
+ * The algorithm below finds all conflicts via making a series of comparisons
+ * between fields. In order to compare as few fields as possible, this makes
+ * a series of comparisons "within" sets of fields and "between" sets of fields.
+ *
+ * Given any selection set, a collection produces both a set of fields by
+ * also including all inline fragments, as well as a list of fragments
+ * referenced by fragment spreads.
+ *
+ * A) Each selection set represented in the document first compares "within" its
+ * collected set of fields, finding any conflicts between every pair of
+ * overlapping fields.
+ * Note: This is the *only time* that a the fields "within" a set are compared
+ * to each other. After this only fields "between" sets are compared.
+ *
+ * B) Also, if any fragment is referenced in a selection set, then a
+ * comparison is made "between" the original set of fields and the
+ * referenced fragment.
+ *
+ * C) Also, if multiple fragments are referenced, then comparisons
+ * are made "between" each referenced fragment.
+ *
+ * D) When comparing "between" a set of fields and a referenced fragment, first
+ * a comparison is made between each field in the original set of fields and
+ * each field in the the referenced set of fields.
+ *
+ * E) Also, if any fragment is referenced in the referenced selection set,
+ * then a comparison is made "between" the original set of fields and the
+ * referenced fragment (recursively referring to step D).
+ *
+ * F) When comparing "between" two fragments, first a comparison is made between
+ * each field in the first referenced set of fields and each field in the the
+ * second referenced set of fields.
+ *
+ * G) Also, any fragments referenced by the first must be compared to the
+ * second, and any fragments referenced by the second must be compared to the
+ * first (recursively referring to step F).
+ *
+ * H) When comparing two fields, if both have selection sets, then a comparison
+ * is made "between" both selection sets, first comparing the set of fields in
+ * the first selection set with the set of fields in the second.
+ *
+ * I) Also, if any fragment is referenced in either selection set, then a
+ * comparison is made "between" the other set of fields and the
+ * referenced fragment.
+ *
+ * J) Also, if two fragments are referenced in both selection sets, then a
+ * comparison is made "between" the two fragments.
+ */
+
+ /**
+ * Given a reference to a fragment, return the represented collection of fields
+ * as well as a list of nested fragment names referenced via fragment spreads.
+ *
+ * @param array<string, bool> $fragmentNames
+ *
+ * @phpstan-param FieldMap $astAndDefs
+ *
+ * @throws \Exception
+ */
+ protected function internalCollectFieldsAndFragmentNames(
+ QueryValidationContext $context,
+ ?Type $parentType,
+ SelectionSetNode $selectionSet,
+ array &$astAndDefs,
+ array &$fragmentNames
+ ): void {
+ foreach ($selectionSet->selections as $selection) {
+ switch (true) {
+ case $selection instanceof FieldNode:
+ $fieldName = $selection->name->value;
+ $fieldDef = null;
+ if (
+ ($parentType instanceof ObjectType || $parentType instanceof InterfaceType)
+ && $parentType->hasField($fieldName)
+ ) {
+ $fieldDef = $parentType->getField($fieldName);
+ }
+
+ $responseName = $selection->alias->value ?? $fieldName;
+
+ $astAndDefs[$responseName] ??= [];
+ $astAndDefs[$responseName][] = [$parentType, $selection, $fieldDef];
+ break;
+ case $selection instanceof FragmentSpreadNode:
+ $fragmentNames[$selection->name->value] = true;
+ break;
+ case $selection instanceof InlineFragmentNode:
+ $typeCondition = $selection->typeCondition;
+ $inlineFragmentType = $typeCondition === null
+ ? $parentType
+ : AST::typeFromAST([$context->getSchema(), 'getType'], $typeCondition);
+
+ $this->internalCollectFieldsAndFragmentNames(
+ $context,
+ $inlineFragmentType,
+ $selection->selectionSet,
+ $astAndDefs,
+ $fragmentNames
+ );
+ break;
+ }
+ }
+ }
+
+ /**
+ * Collect all Conflicts "within" one collection of fields.
+ *
+ * @param array<int, Conflict> $conflicts
+ *
+ * @phpstan-param FieldMap $fieldMap
+ *
+ * @throws \Exception
+ */
+ protected function collectConflictsWithin(
+ QueryValidationContext $context,
+ array &$conflicts,
+ array $fieldMap
+ ): void {
+ // A field map is a keyed collection, where each key represents a response
+ // name and the value at that key is a list of all fields which provide that
+ // response name. For every response name, if there are multiple fields, they
+ // must be compared to find a potential conflict.
+ foreach ($fieldMap as $responseName => $fields) {
+ // This compares every field in the list to every other field in this list
+ // (except to itself). If the list only has one item, nothing needs to
+ // be compared.
+ $fieldsLength = count($fields);
+ if ($fieldsLength <= 1) {
+ continue;
+ }
+
+ // Deduplicate structurally identical fields to avoid O(n²) blowup
+ // when a query repeats the same field many times.
+ $fields = $this->deduplicateFields($fields);
+ $fieldsLength = count($fields);
+ if ($fieldsLength <= 1) {
+ continue;
+ }
+
+ for ($i = 0; $i < $fieldsLength; ++$i) {
+ for ($j = $i + 1; $j < $fieldsLength; ++$j) {
+ $conflict = $this->findConflict(
+ $context,
+ false, // within one collection is never mutually exclusive
+ $responseName,
+ $fields[$i],
+ $fields[$j]
+ );
+ if ($conflict !== null) {
+ $conflicts[] = $conflict;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * @phpstan-param array<int, FieldInfo> $fields
+ *
+ * @throws \JsonException
+ *
+ * @phpstan-return array<int, FieldInfo>
+ */
+ protected function deduplicateFields(array $fields): array
+ {
+ $unique = [];
+ $seen = [];
+ foreach ($fields as $field) {
+ $key = $this->fieldFingerprint($field);
+ if (! isset($seen[$key])) {
+ $seen[$key] = true;
+ $unique[] = $field;
+ }
+ }
+
+ return $unique;
+ }
+
+ /**
+ * @phpstan-param FieldInfo $field
+ *
+ * @throws \JsonException
+ */
+ protected function fieldFingerprint(array $field): string
+ {
+ [$parentType, $ast] = $field;
+
+ $parentTypeId = $parentType !== null
+ ? spl_object_id($parentType)
+ : '';
+ $name = $ast->name->value;
+ $selectionSetId = $ast->selectionSet !== null
+ ? spl_object_id($ast->selectionSet)
+ : '';
+
+ $fingerprint = "{$parentTypeId}:{$name}:{$selectionSetId}";
+
+ foreach ($ast->arguments as $argument) {
+ $fingerprint .= ":{$argument->name->value}=" . Printer::doPrint($argument->value);
+ }
+
+ return $fingerprint;
+ }
+
+ /**
+ * Determines if there is a conflict between two particular fields, including
+ * comparing their sub-fields.
+ *
+ * @param array{Type|null, FieldNode, FieldDefinition|null} $field1
+ * @param array{Type|null, FieldNode, FieldDefinition|null} $field2
+ *
+ * @throws \Exception
+ *
+ * @phpstan-return Conflict|null
+ */
+ protected function findConflict(
+ QueryValidationContext $context,
+ bool $parentFieldsAreMutuallyExclusive,
+ string $responseName,
+ array $field1,
+ array $field2
+ ): ?array {
+ [$parentType1, $ast1, $def1] = $field1;
+ [$parentType2, $ast2, $def2] = $field2;
+
+ // If it is known that two fields could not possibly apply at the same
+ // time, due to the parent types, then it is safe to permit them to diverge
+ // in aliased field or arguments used as they will not present any ambiguity
+ // by differing.
+ // It is known that two parent types could never overlap if they are
+ // different Object types. Interface or Union types might overlap - if not
+ // in the current state of the schema, then perhaps in some future version,
+ // thus may not safely diverge.
+ $areMutuallyExclusive = $parentFieldsAreMutuallyExclusive
+ || (
+ $parentType1 !== $parentType2
+ && $parentType1 instanceof ObjectType
+ && $parentType2 instanceof ObjectType
+ );
+
+ // The return type for each field.
+ $type1 = $def1 === null
+ ? null
+ : $def1->getType();
+ $type2 = $def2 === null
+ ? null
+ : $def2->getType();
+
+ if (! $areMutuallyExclusive) {
+ // Two aliases must refer to the same field.
+ $name1 = $ast1->name->value;
+ $name2 = $ast2->name->value;
+ if ($name1 !== $name2) {
+ return [
+ [$responseName, "{$name1} and {$name2} are different fields"],
+ [$ast1],
+ [$ast2],
+ ];
+ }
+
+ if (! $this->sameArguments($ast1->arguments, $ast2->arguments)) {
+ return [
+ [$responseName, 'they have differing arguments'],
+ [$ast1],
+ [$ast2],
+ ];
+ }
+ }
+
+ if (
+ $type1 !== null
+ && $type2 !== null
+ && $this->doTypesConflict($type1, $type2)
+ ) {
+ return [
+ [$responseName, "they return conflicting types {$type1} and {$type2}"],
+ [$ast1],
+ [$ast2],
+ ];
+ }
+
+ // Collect and compare sub-fields. Use the same "visited fragment names" list
+ // for both collections so fields in a fragment reference are never
+ // compared to themselves.
+ $selectionSet1 = $ast1->selectionSet;
+ $selectionSet2 = $ast2->selectionSet;
+ if ($selectionSet1 !== null && $selectionSet2 !== null) {
+ $conflicts = $this->findConflictsBetweenSubSelectionSets(
+ $context,
+ $areMutuallyExclusive,
+ Type::getNamedType($type1),
+ $selectionSet1,
+ Type::getNamedType($type2),
+ $selectionSet2
+ );
+
+ return $this->subfieldConflicts(
+ $conflicts,
+ $responseName,
+ $ast1,
+ $ast2
+ );
+ }
+
+ return null;
+ }
+
+ /**
+ * @param NodeList<ArgumentNode> $arguments1 keep
+ * @param NodeList<ArgumentNode> $arguments2 keep
+ *
+ * @throws \JsonException
+ */
+ protected function sameArguments(NodeList $arguments1, NodeList $arguments2): bool
+ {
+ if (count($arguments1) !== count($arguments2)) {
+ return false;
+ }
+
+ foreach ($arguments1 as $argument1) {
+ $argument2 = null;
+ foreach ($arguments2 as $argument) {
+ if ($argument->name->value === $argument1->name->value) {
+ $argument2 = $argument;
+ break;
+ }
+ }
+
+ if ($argument2 === null) {
+ return false;
+ }
+
+ if (! $this->sameValue($argument1->value, $argument2->value)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /** @throws \JsonException */
+ protected function sameValue(Node $value1, Node $value2): bool
+ {
+ return Printer::doPrint($value1) === Printer::doPrint($value2);
+ }
+
+ /**
+ * Two types conflict if both types could not apply to a value simultaneously.
+ *
+ * Composite types are ignored as their individual field types will be compared
+ * later recursively. However, List and Non-Null types must match.
+ */
+ protected function doTypesConflict(Type $type1, Type $type2): bool
+ {
+ if ($type1 instanceof ListOfType) {
+ return $type2 instanceof ListOfType
+ ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType())
+ : true;
+ }
+
+ if ($type2 instanceof ListOfType) {
+ return true;
+ }
+
+ if ($type1 instanceof NonNull) {
+ return $type2 instanceof NonNull
+ ? $this->doTypesConflict($type1->getWrappedType(), $type2->getWrappedType())
+ : true;
+ }
+
+ if ($type2 instanceof NonNull) {
+ return true;
+ }
+
+ if (Type::isLeafType($type1) || Type::isLeafType($type2)) {
+ return $type1 !== $type2;
+ }
+
+ return false;
+ }
+
+ /**
+ * Find all conflicts found between two selection sets, including those found
+ * via spreading in fragments. Called when determining if conflicts exist
+ * between the sub-fields of two overlapping fields.
+ *
+ * @throws \Exception
+ *
+ * @return array<int, Conflict>
+ */
+ protected function findConflictsBetweenSubSelectionSets(
+ QueryValidationContext $context,
+ bool $areMutuallyExclusive,
+ ?Type $parentType1,
+ SelectionSetNode $selectionSet1,
+ ?Type $parentType2,
+ SelectionSetNode $selectionSet2
+ ): array {
+ $conflicts = [];
+
+ [$fieldMap1, $fragmentNames1] = $this->getFieldsAndFragmentNames(
+ $context,
+ $parentType1,
+ $selectionSet1
+ );
+ [$fieldMap2, $fragmentNames2] = $this->getFieldsAndFragmentNames(
+ $context,
+ $parentType2,
+ $selectionSet2
+ );
+
+ // (H) First, collect all conflicts between these two collections of field.
+ $this->collectConflictsBetween(
+ $context,
+ $conflicts,
+ $areMutuallyExclusive,
+ $fieldMap1,
+ $fieldMap2
+ );
+
+ // (I) Then collect conflicts between the first collection of fields and
+ // those referenced by each fragment name associated with the second.
+ $fragmentNames2Length = count($fragmentNames2);
+ if ($fragmentNames2Length !== 0) {
+ $comparedFragments = [];
+ for ($j = 0; $j < $fragmentNames2Length; ++$j) {
+ $this->collectConflictsBetweenFieldsAndFragment(
+ $context,
+ $conflicts,
+ $comparedFragments,
+ $areMutuallyExclusive,
+ $fieldMap1,
+ $fragmentNames2[$j]
+ );
+ }
+ }
+
+ // (I) Then collect conflicts between the second collection of fields and
+ // those referenced by each fragment name associated with the first.
+ $fragmentNames1Length = count($fragmentNames1);
+ if ($fragmentNames1Length !== 0) {
+ $comparedFragments = [];
+ for ($i = 0; $i < $fragmentNames1Length; ++$i) {
+ $this->collectConflictsBetweenFieldsAndFragment(
+ $context,
+ $conflicts,
+ $comparedFragments,
+ $areMutuallyExclusive,
+ $fieldMap2,
+ $fragmentNames1[$i]
+ );
+ }
+ }
+
+ // (J) Also collect conflicts between any fragment names by the first and
+ // fragment names by the second. This compares each item in the first set of
+ // names to each item in the second set of names.
+ for ($i = 0; $i < $fragmentNames1Length; ++$i) {
+ for ($j = 0; $j < $fragmentNames2Length; ++$j) {
+ $this->collectConflictsBetweenFragments(
+ $context,
+ $conflicts,
+ $areMutuallyExclusive,
+ $fragmentNames1[$i],
+ $fragmentNames2[$j]
+ );
+ }
+ }
+
+ return $conflicts;
+ }
+
+ /**
+ * Collect all Conflicts between two collections of fields. This is similar to,
+ * but different from the `collectConflictsWithin` function above. This check
+ * assumes that `collectConflictsWithin` has already been called on each
+ * provided collection of fields. This is true because this validator traverses
+ * each individual selection set.
+ *
+ * @phpstan-param array<int, Conflict> $conflicts
+ * @phpstan-param FieldMap $fieldMap1
+ * @phpstan-param FieldMap $fieldMap2
+ *
+ * @throws \Exception
+ */
+ protected function collectConflictsBetween(
+ QueryValidationContext $context,
+ array &$conflicts,
+ bool $parentFieldsAreMutuallyExclusive,
+ array $fieldMap1,
+ array $fieldMap2
+ ): void {
+ // A field map is a keyed collection, where each key represents a response
+ // name and the value at that key is a list of all fields which provide that
+ // response name. For any response name which appears in both provided field
+ // maps, each field from the first field map must be compared to every field
+ // in the second field map to find potential conflicts.
+ foreach ($fieldMap1 as $responseName => $fields1) {
+ if (! isset($fieldMap2[$responseName])) {
+ continue;
+ }
+
+ $fields2 = $fieldMap2[$responseName];
+ $fields1Length = count($fields1);
+ $fields2Length = count($fields2);
+ for ($i = 0; $i < $fields1Length; ++$i) {
+ for ($j = 0; $j < $fields2Length; ++$j) {
+ $conflict = $this->findConflict(
+ $context,
+ $parentFieldsAreMutuallyExclusive,
+ $responseName,
+ $fields1[$i],
+ $fields2[$j]
+ );
+ if ($conflict !== null) {
+ $conflicts[] = $conflict;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Collect all conflicts found between a set of fields and a fragment reference
+ * including via spreading in any nested fragments.
+ *
+ * @param array<string, true> $comparedFragments
+ *
+ * @phpstan-param array<int, Conflict> $conflicts
+ * @phpstan-param FieldMap $fieldMap
+ *
+ * @throws \Exception
+ */
+ protected function collectConflictsBetweenFieldsAndFragment(
+ QueryValidationContext $context,
+ array &$conflicts,
+ array &$comparedFragments,
+ bool $areMutuallyExclusive,
+ array $fieldMap,
+ string $fragmentName
+ ): void {
+ if (isset($comparedFragments[$fragmentName])) {
+ return;
+ }
+
+ $comparedFragments[$fragmentName] = true;
+
+ $fragment = $context->getFragment($fragmentName);
+ if ($fragment === null) {
+ return;
+ }
+
+ [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames(
+ $context,
+ $fragment
+ );
+
+ if ($fieldMap === $fieldMap2) {
+ return;
+ }
+
+ // (D) First collect any conflicts between the provided collection of fields
+ // and the collection of fields represented by the given fragment.
+ $this->collectConflictsBetween(
+ $context,
+ $conflicts,
+ $areMutuallyExclusive,
+ $fieldMap,
+ $fieldMap2
+ );
+
+ // (E) Then collect any conflicts between the provided collection of fields
+ // and any fragment names found in the given fragment.
+ $fragmentNames2Length = count($fragmentNames2);
+ for ($i = 0; $i < $fragmentNames2Length; ++$i) {
+ $this->collectConflictsBetweenFieldsAndFragment(
+ $context,
+ $conflicts,
+ $comparedFragments,
+ $areMutuallyExclusive,
+ $fieldMap,
+ $fragmentNames2[$i]
+ );
+ }
+ }
+
+ /**
+ * Given a reference to a fragment, return the represented collection of fields
+ * as well as a list of nested fragment names referenced via fragment spreads.
+ *
+ * @throws \Exception
+ *
+ * @phpstan-return array{FieldMap, array<int, string>}
+ */
+ protected function getReferencedFieldsAndFragmentNames(
+ QueryValidationContext $context,
+ FragmentDefinitionNode $fragment
+ ): array {
+ // Short-circuit building a type from the AST if possible.
+ if (isset($this->cachedFieldsAndFragmentNames[$fragment->selectionSet])) {
+ return $this->cachedFieldsAndFragmentNames[$fragment->selectionSet];
+ }
+
+ $fragmentType = AST::typeFromAST([$context->getSchema(), 'getType'], $fragment->typeCondition);
+
+ return $this->getFieldsAndFragmentNames(
+ $context,
+ $fragmentType,
+ $fragment->selectionSet
+ );
+ }
+
+ /**
+ * Collect all conflicts found between two fragments, including via spreading in
+ * any nested fragments.
+ *
+ * @phpstan-param array<int, Conflict> $conflicts
+ *
+ * @throws \Exception
+ */
+ protected function collectConflictsBetweenFragments(
+ QueryValidationContext $context,
+ array &$conflicts,
+ bool $areMutuallyExclusive,
+ string $fragmentName1,
+ string $fragmentName2
+ ): void {
+ // No need to compare a fragment to itself.
+ if ($fragmentName1 === $fragmentName2) {
+ return;
+ }
+
+ // Memoize so two fragments are not compared for conflicts more than once.
+ if (
+ $this->comparedFragmentPairs->has(
+ $fragmentName1,
+ $fragmentName2,
+ $areMutuallyExclusive
+ )
+ ) {
+ return;
+ }
+
+ $this->comparedFragmentPairs->add(
+ $fragmentName1,
+ $fragmentName2,
+ $areMutuallyExclusive
+ );
+
+ $fragment1 = $context->getFragment($fragmentName1);
+ $fragment2 = $context->getFragment($fragmentName2);
+ if ($fragment1 === null || $fragment2 === null) {
+ return;
+ }
+
+ [$fieldMap1, $fragmentNames1] = $this->getReferencedFieldsAndFragmentNames(
+ $context,
+ $fragment1
+ );
+ [$fieldMap2, $fragmentNames2] = $this->getReferencedFieldsAndFragmentNames(
+ $context,
+ $fragment2
+ );
+
+ // (F) First, collect all conflicts between these two collections of fields
+ // (not including any nested fragments).
+ $this->collectConflictsBetween(
+ $context,
+ $conflicts,
+ $areMutuallyExclusive,
+ $fieldMap1,
+ $fieldMap2
+ );
+
+ // (G) Then collect conflicts between the first fragment and any nested
+ // fragments spread in the second fragment.
+ $fragmentNames2Length = count($fragmentNames2);
+ for ($j = 0; $j < $fragmentNames2Length; ++$j) {
+ $this->collectConflictsBetweenFragments(
+ $context,
+ $conflicts,
+ $areMutuallyExclusive,
+ $fragmentName1,
+ $fragmentNames2[$j]
+ );
+ }
+
+ // (G) Then collect conflicts between the second fragment and any nested
+ // fragments spread in the first fragment.
+ $fragmentNames1Length = count($fragmentNames1);
+ for ($i = 0; $i < $fragmentNames1Length; ++$i) {
+ $this->collectConflictsBetweenFragments(
+ $context,
+ $conflicts,
+ $areMutuallyExclusive,
+ $fragmentNames1[$i],
+ $fragmentName2
+ );
+ }
+ }
+
+ /**
+ * Merge Conflicts between two sub-fields into a single Conflict.
+ *
+ * @phpstan-param array<int, Conflict> $conflicts
+ *
+ * @phpstan-return Conflict|null
+ */
+ protected function subfieldConflicts(
+ array $conflicts,
+ string $responseName,
+ FieldNode $ast1,
+ FieldNode $ast2
+ ): ?array {
+ if ($conflicts === []) {
+ return null;
+ }
+
+ $reasons = [];
+ foreach ($conflicts as $conflict) {
+ $reasons[] = $conflict[0];
+ }
+
+ $fields1 = [$ast1];
+ foreach ($conflicts as $conflict) {
+ foreach ($conflict[1] as $field) {
+ $fields1[] = $field;
+ }
+ }
+
+ $fields2 = [$ast2];
+ foreach ($conflicts as $conflict) {
+ foreach ($conflict[2] as $field) {
+ $fields2[] = $field;
+ }
+ }
+
+ return [
+ [
+ $responseName,
+ $reasons,
+ ],
+ $fields1,
+ $fields2,
+ ];
+ }
+
+ /**
+ * @param string|array $reasonOrReasons
+ *
+ * @phpstan-param ReasonOrReasons $reasonOrReasons
+ */
+ public static function fieldsConflictMessage(string $responseName, $reasonOrReasons): string
+ {
+ $reasonMessage = static::reasonMessage($reasonOrReasons);
+
+ return "Fields \"{$responseName}\" conflict because {$reasonMessage}. Use different aliases on the fields to fetch both if this was intentional.";
+ }
+
+ /**
+ * @param string|array $reasonOrReasons
+ *
+ * @phpstan-param ReasonOrReasons $reasonOrReasons
+ */
+ public static function reasonMessage($reasonOrReasons): string
+ {
+ if (is_array($reasonOrReasons)) {
+ $reasons = array_map(
+ static function (array $reason): string {
+ [$responseName, $subReason] = $reason;
+ $reasonMessage = static::reasonMessage($subReason);
+
+ return "subfields \"{$responseName}\" conflict because {$reasonMessage}";
+ },
+ $reasonOrReasons
+ );
+
+ return implode(' and ', $reasons);
+ }
+
+ return $reasonOrReasons;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleFragmentSpreads.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleFragmentSpreads.php
new file mode 100644
index 00000000000..9140f64b5e3
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleFragmentSpreads.php
@@ -0,0 +1,165 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\AbstractType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CompositeType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class PossibleFragmentSpreads extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::INLINE_FRAGMENT => function (InlineFragmentNode $node) use ($context): void {
+ $fragType = $context->getType();
+ $parentType = $context->getParentType();
+
+ if (
+ ! $fragType instanceof CompositeType
+ || ! $parentType instanceof CompositeType
+ || $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)
+ ) {
+ return;
+ }
+
+ $context->reportError(new Error(
+ static::typeIncompatibleAnonSpreadMessage($parentType->toString(), $fragType->toString()),
+ [$node]
+ ));
+ },
+ NodeKind::FRAGMENT_SPREAD => function (FragmentSpreadNode $node) use ($context): void {
+ $fragName = $node->name->value;
+ $fragType = $this->getFragmentType($context, $fragName);
+ $parentType = $context->getParentType();
+
+ if (
+ $fragType === null
+ || $parentType === null
+ || $this->doTypesOverlap($context->getSchema(), $fragType, $parentType)
+ ) {
+ return;
+ }
+
+ $context->reportError(new Error(
+ static::typeIncompatibleSpreadMessage($fragName, $parentType->toString(), $fragType->toString()),
+ [$node]
+ ));
+ },
+ ];
+ }
+
+ /**
+ * @param CompositeType&Type $fragType
+ * @param CompositeType&Type $parentType
+ *
+ * @throws InvariantViolation
+ */
+ protected function doTypesOverlap(Schema $schema, CompositeType $fragType, CompositeType $parentType): bool
+ {
+ // Checking in the order of the most frequently used scenarios:
+ // Parent type === fragment type
+ if ($parentType === $fragType) {
+ return true;
+ }
+
+ // Parent type is interface or union, fragment type is object type
+ if ($parentType instanceof AbstractType && $fragType instanceof ObjectType) {
+ return $schema->isSubType($parentType, $fragType);
+ }
+
+ // Parent type is object type, fragment type is interface (or rather rare - union)
+ if ($parentType instanceof ObjectType && $fragType instanceof AbstractType) {
+ return $schema->isSubType($fragType, $parentType);
+ }
+
+ // Both are object types:
+ if ($parentType instanceof ObjectType && $fragType instanceof ObjectType) {
+ return $parentType === $fragType;
+ }
+
+ // Both are interfaces
+ // This case may be assumed valid only when implementations of two interfaces intersect
+ // But we don't have information about all implementations at runtime
+ // (getting this information via $schema->getPossibleTypes() requires scanning through whole schema
+ // which is very costly to do at each request due to PHP "shared nothing" architecture)
+ //
+ // So in this case we just make it pass - invalid fragment spreads will be simply ignored during execution
+ // See also https://github.com/webonyx/graphql-php/issues/69#issuecomment-283954602
+ if ($parentType instanceof InterfaceType && $fragType instanceof InterfaceType) {
+ return true;
+
+ // Note that there is one case when we do have information about all implementations:
+ // When schema descriptor is defined ($schema->hasDescriptor())
+ // BUT we must avoid situation when some query that worked in development had suddenly stopped
+ // working in production. So staying consistent and always validate.
+ }
+
+ // Interface within union
+ if ($parentType instanceof UnionType && $fragType instanceof InterfaceType) {
+ foreach ($parentType->getTypes() as $type) {
+ if ($type->implementsInterface($fragType)) {
+ return true;
+ }
+ }
+ }
+
+ if ($parentType instanceof InterfaceType && $fragType instanceof UnionType) {
+ foreach ($fragType->getTypes() as $type) {
+ if ($type->implementsInterface($parentType)) {
+ return true;
+ }
+ }
+ }
+
+ if ($parentType instanceof UnionType && $fragType instanceof UnionType) {
+ foreach ($fragType->getTypes() as $type) {
+ if ($parentType->isPossibleType($type)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public static function typeIncompatibleAnonSpreadMessage(string $parentType, string $fragType): string
+ {
+ return "Fragment cannot be spread here as objects of type \"{$parentType}\" can never be of type \"{$fragType}\".";
+ }
+
+ /**
+ * @throws \Exception
+ *
+ * @return (CompositeType&Type)|null
+ */
+ protected function getFragmentType(QueryValidationContext $context, string $name): ?Type
+ {
+ $frag = $context->getFragment($name);
+ if ($frag === null) {
+ return null;
+ }
+
+ $type = AST::typeFromAST([$context->getSchema(), 'getType'], $frag->typeCondition);
+
+ return $type instanceof CompositeType
+ ? $type
+ : null;
+ }
+
+ public static function typeIncompatibleSpreadMessage(string $fragName, string $parentType, string $fragType): string
+ {
+ return "Fragment \"{$fragName}\" cannot be spread here as objects of type \"{$parentType}\" can never be of type \"{$fragType}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleTypeExtensions.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleTypeExtensions.php
new file mode 100644
index 00000000000..16402c0f958
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/PossibleTypeExtensions.php
@@ -0,0 +1,163 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\TypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\UnionType;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+
+/**
+ * Possible type extensions.
+ *
+ * A type extension is only valid if the type is defined and has the same kind.
+ */
+class PossibleTypeExtensions extends ValidationRule
+{
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ $schema = $context->getSchema();
+
+ /** @var array<string, TypeDefinitionNode&Node> $definedTypes */
+ $definedTypes = [];
+ foreach ($context->getDocument()->definitions as $def) {
+ if ($def instanceof TypeDefinitionNode) {
+ $name = $def->getName()->value;
+ $definedTypes[$name] = $def;
+ }
+ }
+
+ $checkTypeExtension = static function ($node) use ($context, $schema, &$definedTypes): ?VisitorOperation {
+ $typeName = $node->name->value;
+ $defNode = $definedTypes[$typeName] ?? null;
+ $existingType = $schema !== null
+ ? $schema->getType($typeName)
+ : null;
+
+ $expectedKind = null;
+ if ($defNode !== null) {
+ $expectedKind = self::defKindToExtKind($defNode->kind);
+ } elseif ($existingType !== null) {
+ $expectedKind = self::typeToExtKind($existingType);
+ }
+
+ if ($expectedKind !== null) {
+ if ($expectedKind !== $node->kind) {
+ $kindStr = self::extensionKindToTypeName($node->kind);
+ $context->reportError(
+ new Error(
+ "Cannot extend non-{$kindStr} type \"{$typeName}\".",
+ $defNode !== null
+ ? [$defNode, $node]
+ : $node,
+ ),
+ );
+ }
+ } else {
+ $existingTypesMap = $schema !== null
+ ? $schema->getTypeMap()
+ : [];
+ $allTypeNames = [
+ ...array_keys($definedTypes),
+ ...array_keys($existingTypesMap),
+ ];
+ $suggestedTypes = Utils::suggestionList($typeName, $allTypeNames);
+ $didYouMean = $suggestedTypes === []
+ ? ''
+ : ' Did you mean ' . Utils::quotedOrList($suggestedTypes) . '?';
+ $context->reportError(
+ new Error(
+ "Cannot extend type \"{$typeName}\" because it is not defined.{$didYouMean}",
+ $node->name,
+ ),
+ );
+ }
+
+ return null;
+ };
+
+ return [
+ NodeKind::SCALAR_TYPE_EXTENSION => $checkTypeExtension,
+ NodeKind::OBJECT_TYPE_EXTENSION => $checkTypeExtension,
+ NodeKind::INTERFACE_TYPE_EXTENSION => $checkTypeExtension,
+ NodeKind::UNION_TYPE_EXTENSION => $checkTypeExtension,
+ NodeKind::ENUM_TYPE_EXTENSION => $checkTypeExtension,
+ NodeKind::INPUT_OBJECT_TYPE_EXTENSION => $checkTypeExtension,
+ ];
+ }
+
+ /** @throws InvariantViolation */
+ private static function defKindToExtKind(string $kind): string
+ {
+ switch ($kind) {
+ case NodeKind::SCALAR_TYPE_DEFINITION:
+ return NodeKind::SCALAR_TYPE_EXTENSION;
+ case NodeKind::OBJECT_TYPE_DEFINITION:
+ return NodeKind::OBJECT_TYPE_EXTENSION;
+ case NodeKind::INTERFACE_TYPE_DEFINITION:
+ return NodeKind::INTERFACE_TYPE_EXTENSION;
+ case NodeKind::UNION_TYPE_DEFINITION:
+ return NodeKind::UNION_TYPE_EXTENSION;
+ case NodeKind::ENUM_TYPE_DEFINITION:
+ return NodeKind::ENUM_TYPE_EXTENSION;
+ case NodeKind::INPUT_OBJECT_TYPE_DEFINITION:
+ return NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
+ default:
+ throw new InvariantViolation("Unexpected definition kind: {$kind}.");
+ }
+ }
+
+ /** @throws InvariantViolation */
+ private static function typeToExtKind(NamedType $type): string
+ {
+ switch (true) {
+ case $type instanceof ScalarType:
+ return NodeKind::SCALAR_TYPE_EXTENSION;
+ case $type instanceof ObjectType:
+ return NodeKind::OBJECT_TYPE_EXTENSION;
+ case $type instanceof InterfaceType:
+ return NodeKind::INTERFACE_TYPE_EXTENSION;
+ case $type instanceof UnionType:
+ return NodeKind::UNION_TYPE_EXTENSION;
+ case $type instanceof EnumType:
+ return NodeKind::ENUM_TYPE_EXTENSION;
+ case $type instanceof InputObjectType:
+ return NodeKind::INPUT_OBJECT_TYPE_EXTENSION;
+ default:
+ $unexpectedType = Utils::printSafe($type);
+ throw new InvariantViolation("Unexpected type: {$unexpectedType}.");
+ }
+ }
+
+ /** @throws InvariantViolation */
+ private static function extensionKindToTypeName(string $kind): string
+ {
+ switch ($kind) {
+ case NodeKind::SCALAR_TYPE_EXTENSION:
+ return 'scalar';
+ case NodeKind::OBJECT_TYPE_EXTENSION:
+ return 'object';
+ case NodeKind::INTERFACE_TYPE_EXTENSION:
+ return 'interface';
+ case NodeKind::UNION_TYPE_EXTENSION:
+ return 'union';
+ case NodeKind::ENUM_TYPE_EXTENSION:
+ return 'enum';
+ case NodeKind::INPUT_OBJECT_TYPE_EXTENSION:
+ return 'input object';
+ default:
+ throw new InvariantViolation("Unexpected extension kind: {$kind}.");
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArguments.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArguments.php
new file mode 100644
index 00000000000..0974950a56b
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArguments.php
@@ -0,0 +1,55 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class ProvidedRequiredArguments extends ValidationRule
+{
+ /** @throws \Exception */
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $providedRequiredArgumentsOnDirectives = new ProvidedRequiredArgumentsOnDirectives();
+
+ return $providedRequiredArgumentsOnDirectives->getVisitor($context) + [
+ NodeKind::FIELD => [
+ 'leave' => static function (FieldNode $fieldNode) use ($context): ?VisitorOperation {
+ $fieldDef = $context->getFieldDef();
+
+ if ($fieldDef === null) {
+ return Visitor::skipNode();
+ }
+
+ $argNodes = $fieldNode->arguments;
+
+ $argNodeMap = [];
+ foreach ($argNodes as $argNode) {
+ $argNodeMap[$argNode->name->value] = $argNode;
+ }
+
+ foreach ($fieldDef->args as $argDef) {
+ $argNode = $argNodeMap[$argDef->name] ?? null;
+ if ($argNode === null && $argDef->isRequired()) {
+ $context->reportError(new Error(
+ static::missingFieldArgMessage($fieldNode->name->value, $argDef->name, $argDef->getType()->toString()),
+ [$fieldNode]
+ ));
+ }
+ }
+
+ return null;
+ },
+ ],
+ ];
+ }
+
+ public static function missingFieldArgMessage(string $fieldName, string $argName, string $type): string
+ {
+ return "Field \"{$fieldName}\" argument \"{$argName}\" of type \"{$type}\" is required but not provided.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php
new file mode 100644
index 00000000000..1ee38758f2e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ProvidedRequiredArgumentsOnDirectives.php
@@ -0,0 +1,122 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NonNullTypeNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Argument;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
+
+/**
+ * Provided required arguments on directives.
+ *
+ * A directive is only valid if all required (non-null without a
+ * default value) field arguments have been provided.
+ *
+ * @phpstan-import-type VisitorArray from Visitor
+ */
+class ProvidedRequiredArgumentsOnDirectives extends ValidationRule
+{
+ public static function missingDirectiveArgMessage(string $directiveName, string $argName, string $type): string
+ {
+ return "Directive \"@{$directiveName}\" argument \"{$argName}\" of type \"{$type}\" is required but not provided.";
+ }
+
+ /** @throws \Exception */
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /** @throws \Exception */
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /**
+ * @throws \Exception
+ * @throws \InvalidArgumentException
+ * @throws \ReflectionException
+ * @throws Error
+ * @throws InvariantViolation
+ *
+ * @phpstan-return VisitorArray
+ */
+ public function getASTVisitor(ValidationContext $context): array
+ {
+ $requiredArgsMap = [];
+ $schema = $context->getSchema();
+ $definedDirectives = $schema === null
+ ? Directive::getInternalDirectives()
+ : $schema->getDirectives();
+
+ foreach ($definedDirectives as $directive) {
+ $directiveArgs = [];
+ foreach ($directive->args as $arg) {
+ if ($arg->isRequired()) {
+ $directiveArgs[$arg->name] = $arg;
+ }
+ }
+
+ $requiredArgsMap[$directive->name] = $directiveArgs;
+ }
+
+ $astDefinition = $context->getDocument()->definitions;
+ foreach ($astDefinition as $def) {
+ if ($def instanceof DirectiveDefinitionNode) {
+ $arguments = $def->arguments;
+
+ $requiredArgs = [];
+ foreach ($arguments as $argument) {
+ if ($argument->type instanceof NonNullTypeNode && ! isset($argument->defaultValue)) {
+ $requiredArgs[$argument->name->value] = $argument;
+ }
+ }
+
+ $requiredArgsMap[$def->name->value] = $requiredArgs;
+ }
+ }
+
+ return [
+ NodeKind::DIRECTIVE => [
+ // Validate on leave to allow for deeper errors to appear first.
+ 'leave' => static function (DirectiveNode $directiveNode) use ($requiredArgsMap, $context): ?string {
+ $directiveName = $directiveNode->name->value;
+ $requiredArgs = $requiredArgsMap[$directiveName] ?? null;
+ if ($requiredArgs === null || $requiredArgs === []) {
+ return null;
+ }
+
+ $argNodeMap = [];
+ foreach ($directiveNode->arguments as $arg) {
+ $argNodeMap[$arg->name->value] = $arg;
+ }
+
+ foreach ($requiredArgs as $argName => $arg) {
+ if (! isset($argNodeMap[$argName])) {
+ $argType = $arg instanceof Argument
+ ? $arg->getType()->toString()
+ : Printer::doPrint($arg->type);
+
+ $context->reportError(
+ new Error(static::missingDirectiveArgMessage($directiveName, $argName, $argType), [$directiveNode])
+ );
+ }
+ }
+
+ return null;
+ },
+ ],
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryComplexity.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryComplexity.php
new file mode 100644
index 00000000000..aa25931aa97
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryComplexity.php
@@ -0,0 +1,302 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Executor\Values;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+/**
+ * @phpstan-import-type ASTAndDefs from QuerySecurityRule
+ */
+class QueryComplexity extends QuerySecurityRule
+{
+ protected int $maxQueryComplexity;
+
+ protected int $queryComplexity;
+
+ /** @var array<string, mixed> */
+ protected array $rawVariableValues = [];
+
+ /** @var NodeList<VariableDefinitionNode> */
+ protected NodeList $variableDefs;
+
+ /** @phpstan-var ASTAndDefs */
+ protected \ArrayObject $fieldNodeAndDefs;
+
+ protected QueryValidationContext $context;
+
+ /** @throws \InvalidArgumentException */
+ public function __construct(int $maxQueryComplexity)
+ {
+ $this->setMaxQueryComplexity($maxQueryComplexity);
+ }
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $this->queryComplexity = 0;
+ $this->context = $context;
+ $this->variableDefs = new NodeList([]);
+ $this->fieldNodeAndDefs = new \ArrayObject();
+
+ return $this->invokeIfNeeded(
+ $context,
+ [
+ NodeKind::SELECTION_SET => function (SelectionSetNode $selectionSet) use ($context): void {
+ $this->fieldNodeAndDefs = $this->collectFieldASTsAndDefs(
+ $context,
+ $context->getParentType(),
+ $selectionSet,
+ null,
+ $this->fieldNodeAndDefs
+ );
+ },
+ NodeKind::VARIABLE_DEFINITION => function ($def): VisitorOperation {
+ $this->variableDefs[] = $def;
+
+ return Visitor::skipNode();
+ },
+ NodeKind::DOCUMENT => [
+ 'leave' => function (DocumentNode $document) use ($context): void {
+ $errors = $context->getErrors();
+
+ if ($errors !== []) {
+ return;
+ }
+
+ if ($this->maxQueryComplexity === self::DISABLED) {
+ return;
+ }
+
+ foreach ($document->definitions as $definition) {
+ if (! $definition instanceof OperationDefinitionNode) {
+ continue;
+ }
+
+ $this->queryComplexity = $this->fieldComplexity($definition->selectionSet);
+
+ if ($this->queryComplexity > $this->maxQueryComplexity) {
+ $context->reportError(
+ new Error(static::maxQueryComplexityErrorMessage(
+ $this->maxQueryComplexity,
+ $this->queryComplexity
+ ))
+ );
+
+ return;
+ }
+ }
+ },
+ ],
+ ]
+ );
+ }
+
+ /** @throws \Exception */
+ protected function fieldComplexity(SelectionSetNode $selectionSet): int
+ {
+ $complexity = 0;
+
+ foreach ($selectionSet->selections as $selection) {
+ $complexity += $this->nodeComplexity($selection);
+ }
+
+ return $complexity;
+ }
+
+ /** @throws \Exception */
+ protected function nodeComplexity(SelectionNode $node): int
+ {
+ switch (true) {
+ case $node instanceof FieldNode:
+ // Exclude __schema field and all nested content from complexity calculation
+ if ($node->name->value === Introspection::SCHEMA_FIELD_NAME) {
+ return 0;
+ }
+
+ if ($this->directiveExcludesField($node)) {
+ return 0;
+ }
+
+ $childrenComplexity = isset($node->selectionSet)
+ ? $this->fieldComplexity($node->selectionSet)
+ : 0;
+
+ $fieldDef = $this->fieldDefinition($node);
+ if ($fieldDef instanceof FieldDefinition && $fieldDef->complexityFn !== null) {
+ $fieldArguments = $this->buildFieldArguments($node);
+
+ return ($fieldDef->complexityFn)($childrenComplexity, $fieldArguments);
+ }
+
+ return $childrenComplexity + 1;
+
+ case $node instanceof InlineFragmentNode:
+ return $this->fieldComplexity($node->selectionSet);
+
+ case $node instanceof FragmentSpreadNode:
+ $fragment = $this->getFragment($node);
+
+ if ($fragment !== null) {
+ return $this->fieldComplexity($fragment->selectionSet);
+ }
+ }
+
+ return 0;
+ }
+
+ protected function fieldDefinition(FieldNode $field): ?FieldDefinition
+ {
+ foreach ($this->fieldNodeAndDefs[$this->getFieldName($field)] ?? [] as [$node, $def]) {
+ if ($node === $field) {
+ return $def;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Will the given field be executed at all, given the directives placed upon it?
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ */
+ protected function directiveExcludesField(FieldNode $node): bool
+ {
+ foreach ($node->directives as $directiveNode) {
+ if ($directiveNode->name->value === Directive::DEPRECATED_NAME) {
+ return false;
+ }
+
+ [$errors, $variableValues] = Values::getVariableValues(
+ $this->context->getSchema(),
+ $this->variableDefs,
+ $this->getRawVariableValues()
+ );
+ if ($errors !== null && $errors !== []) {
+ throw new Error(implode("\n\n", array_map(static fn (Error $error): string => $error->getMessage(), $errors)));
+ }
+
+ if ($directiveNode->name->value === Directive::INCLUDE_NAME) {
+ $includeArguments = Values::getArgumentValues(
+ Directive::includeDirective(),
+ $directiveNode,
+ $variableValues
+ );
+ assert(is_bool($includeArguments['if']), 'ensured by query validation');
+
+ return ! $includeArguments['if'];
+ }
+
+ if ($directiveNode->name->value === Directive::SKIP_NAME) {
+ $skipArguments = Values::getArgumentValues(
+ Directive::skipDirective(),
+ $directiveNode,
+ $variableValues
+ );
+ assert(is_bool($skipArguments['if']), 'ensured by query validation');
+
+ return $skipArguments['if'];
+ }
+ }
+
+ return false;
+ }
+
+ /** @return array<string, mixed> */
+ public function getRawVariableValues(): array
+ {
+ return $this->rawVariableValues;
+ }
+
+ /** @param array<string, mixed>|null $rawVariableValues */
+ public function setRawVariableValues(?array $rawVariableValues = null): void
+ {
+ $this->rawVariableValues = $rawVariableValues ?? [];
+ }
+
+ /**
+ * @throws \Exception
+ * @throws Error
+ *
+ * @return array<string, mixed>
+ */
+ protected function buildFieldArguments(FieldNode $node): array
+ {
+ $rawVariableValues = $this->getRawVariableValues();
+ $fieldDef = $this->fieldDefinition($node);
+
+ /** @var array<string, mixed> $args */
+ $args = [];
+
+ if ($fieldDef instanceof FieldDefinition) {
+ [$errors, $variableValues] = Values::getVariableValues(
+ $this->context->getSchema(),
+ $this->variableDefs,
+ $rawVariableValues
+ );
+
+ if (is_array($errors) && $errors !== []) {
+ throw new Error(implode("\n\n", array_map(static fn ($error) => $error->getMessage(), $errors)));
+ }
+
+ $args = Values::getArgumentValues($fieldDef, $node, $variableValues);
+ }
+
+ return $args;
+ }
+
+ public function getMaxQueryComplexity(): int
+ {
+ return $this->maxQueryComplexity;
+ }
+
+ /**
+ * Complexity of the first operation exceeding the defined limit, or, in case no operation
+ * exceeds the limit, complexity of the last defined operation.
+ */
+ public function getQueryComplexity(): int
+ {
+ return $this->queryComplexity;
+ }
+
+ /**
+ * Set max query complexity. If equal to 0 no check is done. Must be greater or equal to 0.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setMaxQueryComplexity(int $maxQueryComplexity): void
+ {
+ $this->checkIfGreaterOrEqualToZero('maxQueryComplexity', $maxQueryComplexity);
+
+ $this->maxQueryComplexity = $maxQueryComplexity;
+ }
+
+ public static function maxQueryComplexityErrorMessage(int $max, int $count): string
+ {
+ return "Max query complexity should be {$max} but got {$count}.";
+ }
+
+ protected function isEnabled(): bool
+ {
+ return $this->maxQueryComplexity !== self::DISABLED;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryDepth.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryDepth.php
new file mode 100644
index 00000000000..5a21d715ed6
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QueryDepth.php
@@ -0,0 +1,130 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class QueryDepth extends QuerySecurityRule
+{
+ /** @var array<string, bool> Fragment names which are already calculated in recursion */
+ protected array $calculatedFragments = [];
+
+ protected int $maxQueryDepth;
+
+ /** @throws \InvalidArgumentException */
+ public function __construct(int $maxQueryDepth)
+ {
+ $this->setMaxQueryDepth($maxQueryDepth);
+ }
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return $this->invokeIfNeeded(
+ $context,
+ [
+ NodeKind::OPERATION_DEFINITION => [
+ 'leave' => function (OperationDefinitionNode $operationDefinition) use ($context): void {
+ $maxDepth = $this->fieldDepth($operationDefinition);
+
+ if ($maxDepth <= $this->maxQueryDepth) {
+ return;
+ }
+
+ $context->reportError(
+ new Error(static::maxQueryDepthErrorMessage($this->maxQueryDepth, $maxDepth))
+ );
+ },
+ ],
+ ]
+ );
+ }
+
+ /** @param OperationDefinitionNode|FieldNode|InlineFragmentNode|FragmentDefinitionNode $node */
+ protected function fieldDepth(Node $node, int $depth = 0, int $maxDepth = 0): int
+ {
+ if ($node->selectionSet instanceof SelectionSetNode) {
+ foreach ($node->selectionSet->selections as $childNode) {
+ $maxDepth = $this->nodeDepth($childNode, $depth, $maxDepth);
+ }
+ }
+
+ return $maxDepth;
+ }
+
+ protected function nodeDepth(Node $node, int $depth = 0, int $maxDepth = 0): int
+ {
+ switch (true) {
+ case $node instanceof FieldNode:
+ // node has children?
+ if ($node->selectionSet !== null) {
+ // update maxDepth if needed
+ if ($depth > $maxDepth) {
+ $maxDepth = $depth;
+ }
+
+ $maxDepth = $this->fieldDepth($node, $depth + 1, $maxDepth);
+ }
+
+ break;
+
+ case $node instanceof InlineFragmentNode:
+ $maxDepth = $this->fieldDepth($node, $depth, $maxDepth);
+
+ break;
+
+ case $node instanceof FragmentSpreadNode:
+ $fragment = $this->getFragment($node);
+
+ if ($fragment !== null) {
+ $name = $fragment->name->value;
+ if (isset($this->calculatedFragments[$name])) {
+ return $this->maxQueryDepth + 1;
+ }
+
+ $this->calculatedFragments[$name] = true;
+ $maxDepth = $this->fieldDepth($fragment, $depth, $maxDepth);
+ unset($this->calculatedFragments[$name]);
+ }
+
+ break;
+ }
+
+ return $maxDepth;
+ }
+
+ public function getMaxQueryDepth(): int
+ {
+ return $this->maxQueryDepth;
+ }
+
+ /**
+ * Set max query depth. If equal to 0 no check is done. Must be greater or equal to 0.
+ *
+ * @throws \InvalidArgumentException
+ */
+ public function setMaxQueryDepth(int $maxQueryDepth): void
+ {
+ $this->checkIfGreaterOrEqualToZero('maxQueryDepth', $maxQueryDepth);
+
+ $this->maxQueryDepth = $maxQueryDepth;
+ }
+
+ public static function maxQueryDepthErrorMessage(int $max, int $count): string
+ {
+ return "Max query depth should be {$max} but got {$count}.";
+ }
+
+ protected function isEnabled(): bool
+ {
+ return $this->maxQueryDepth !== self::DISABLED;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QuerySecurityRule.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QuerySecurityRule.php
new file mode 100644
index 00000000000..69f761ebb81
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/QuerySecurityRule.php
@@ -0,0 +1,184 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\FieldDefinition;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\HasFieldsType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Introspection;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+/**
+ * @see Visitor, FieldDefinition
+ *
+ * @phpstan-import-type VisitorArray from Visitor
+ *
+ * @phpstan-type ASTAndDefs \ArrayObject<string, \ArrayObject<int, array{FieldNode, FieldDefinition|null}>>
+ */
+abstract class QuerySecurityRule extends ValidationRule
+{
+ public const DISABLED = 0;
+
+ /** @var array<string, FragmentDefinitionNode> */
+ protected array $fragments = [];
+
+ /** @throws \InvalidArgumentException */
+ protected function checkIfGreaterOrEqualToZero(string $name, int $value): void
+ {
+ if ($value < 0) {
+ throw new \InvalidArgumentException("\${$name} argument must be greater or equal to 0.");
+ }
+ }
+
+ protected function getFragment(FragmentSpreadNode $fragmentSpread): ?FragmentDefinitionNode
+ {
+ return $this->fragments[$fragmentSpread->name->value] ?? null;
+ }
+
+ /** @return array<string, FragmentDefinitionNode> */
+ protected function getFragments(): array
+ {
+ return $this->fragments;
+ }
+
+ /**
+ * @phpstan-param VisitorArray $validators
+ *
+ * @phpstan-return VisitorArray
+ */
+ protected function invokeIfNeeded(QueryValidationContext $context, array $validators): array
+ {
+ if (! $this->isEnabled()) {
+ return [];
+ }
+
+ $this->gatherFragmentDefinition($context);
+
+ return $validators;
+ }
+
+ abstract protected function isEnabled(): bool;
+
+ protected function gatherFragmentDefinition(QueryValidationContext $context): void
+ {
+ // Gather all the fragment definition.
+ // Importantly this does not include inline fragments.
+ $definitions = $context->getDocument()->definitions;
+ foreach ($definitions as $node) {
+ if ($node instanceof FragmentDefinitionNode) {
+ $this->fragments[$node->name->value] = $node;
+ }
+ }
+ }
+
+ /**
+ * Given a selectionSet, adds all fields in that selection to
+ * the passed in map of fields, and returns it at the end.
+ *
+ * Note: This is not the same as execution's collectFields because at static
+ * time we do not know what object type will be used, so we unconditionally
+ * spread in all fragments.
+ *
+ * @see \Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\OverlappingFieldsCanBeMerged
+ *
+ * @param \ArrayObject<string, true>|null $visitedFragmentNames
+ *
+ * @phpstan-param ASTAndDefs|null $astAndDefs
+ *
+ * @throws \Exception
+ * @throws \ReflectionException
+ * @throws InvariantViolation
+ *
+ * @phpstan-return ASTAndDefs
+ */
+ protected function collectFieldASTsAndDefs(
+ QueryValidationContext $context,
+ ?Type $parentType,
+ SelectionSetNode $selectionSet,
+ ?\ArrayObject $visitedFragmentNames = null,
+ ?\ArrayObject $astAndDefs = null
+ ): \ArrayObject {
+ $visitedFragmentNames ??= new \ArrayObject();
+ $astAndDefs ??= new \ArrayObject();
+
+ foreach ($selectionSet->selections as $selection) {
+ if ($selection instanceof FieldNode) {
+ $fieldName = $selection->name->value;
+
+ $fieldDef = null;
+ if ($parentType instanceof HasFieldsType) {
+ $schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
+ $typeMetaFieldDef = Introspection::typeMetaFieldDef();
+ $typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
+
+ $queryType = $context->getSchema()->getQueryType();
+
+ if ($fieldName === $schemaMetaFieldDef->name && $queryType === $parentType) {
+ $fieldDef = $schemaMetaFieldDef;
+ } elseif ($fieldName === $typeMetaFieldDef->name && $queryType === $parentType) {
+ $fieldDef = $typeMetaFieldDef;
+ } elseif ($fieldName === $typeNameMetaFieldDef->name) {
+ $fieldDef = $typeNameMetaFieldDef;
+ } elseif ($parentType->hasField($fieldName)) {
+ $fieldDef = $parentType->getField($fieldName);
+ }
+ }
+
+ $responseName = $this->getFieldName($selection);
+ $responseContext = $astAndDefs[$responseName] ??= new \ArrayObject();
+ $responseContext[] = [$selection, $fieldDef];
+ } elseif ($selection instanceof InlineFragmentNode) {
+ $typeCondition = $selection->typeCondition;
+ $fragmentParentType = $typeCondition === null
+ ? $parentType
+ : AST::typeFromAST([$context->getSchema(), 'getType'], $typeCondition);
+ $astAndDefs = $this->collectFieldASTsAndDefs(
+ $context,
+ $fragmentParentType,
+ $selection->selectionSet,
+ $visitedFragmentNames,
+ $astAndDefs
+ );
+ } elseif ($selection instanceof FragmentSpreadNode) {
+ $fragName = $selection->name->value;
+
+ if (isset($visitedFragmentNames[$fragName])) {
+ continue;
+ }
+ $visitedFragmentNames[$fragName] = true;
+
+ $fragment = $context->getFragment($fragName);
+ if ($fragment === null) {
+ continue;
+ }
+
+ $astAndDefs = $this->collectFieldASTsAndDefs(
+ $context,
+ AST::typeFromAST([$context->getSchema(), 'getType'], $fragment->typeCondition),
+ $fragment->selectionSet,
+ $visitedFragmentNames,
+ $astAndDefs
+ );
+ }
+ }
+
+ return $astAndDefs;
+ }
+
+ protected function getFieldName(FieldNode $node): string
+ {
+ $fieldName = $node->name->value;
+
+ return $node->alias === null
+ ? $fieldName
+ : $node->alias->value;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ScalarLeafs.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ScalarLeafs.php
new file mode 100644
index 00000000000..2b7c66bf837
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ScalarLeafs.php
@@ -0,0 +1,48 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class ScalarLeafs extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::FIELD => static function (FieldNode $node) use ($context): void {
+ $type = $context->getType();
+ if ($type === null) {
+ return;
+ }
+
+ if (Type::isLeafType(Type::getNamedType($type))) {
+ if ($node->selectionSet !== null) {
+ $context->reportError(new Error(
+ static::noSubselectionAllowedMessage($node->name->value, $type->toString()),
+ [$node->selectionSet]
+ ));
+ }
+ } elseif ($node->selectionSet === null) {
+ $context->reportError(new Error(
+ static::requiredSubselectionMessage($node->name->value, $type->toString()),
+ [$node]
+ ));
+ }
+ },
+ ];
+ }
+
+ public static function noSubselectionAllowedMessage(string $field, string $type): string
+ {
+ return "Field \"{$field}\" of type \"{$type}\" must not have a sub selection.";
+ }
+
+ public static function requiredSubselectionMessage(string $field, string $type): string
+ {
+ return "Field \"{$field}\" of type \"{$type}\" must have a sub selection.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/SingleFieldSubscription.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/SingleFieldSubscription.php
new file mode 100644
index 00000000000..e9163507b76
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/SingleFieldSubscription.php
@@ -0,0 +1,44 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class SingleFieldSubscription extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::OPERATION_DEFINITION => static function (OperationDefinitionNode $node) use ($context): VisitorOperation {
+ if ($node->operation === 'subscription') {
+ $selections = $node->selectionSet->selections;
+
+ if (count($selections) > 1) {
+ $offendingSelections = $selections->splice(1, count($selections));
+
+ $context->reportError(new Error(
+ static::multipleFieldsInOperation($node->name->value ?? null),
+ $offendingSelections
+ ));
+ }
+ }
+
+ return Visitor::skipNode();
+ },
+ ];
+ }
+
+ public static function multipleFieldsInOperation(?string $operationName): string
+ {
+ if ($operationName === null) {
+ return 'Anonymous Subscription must select only one top level field.';
+ }
+
+ return "Subscription \"{$operationName}\" must select only one top level field.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentDefinitionNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentDefinitionNames.php
new file mode 100644
index 00000000000..0f11d73c2ad
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentDefinitionNames.php
@@ -0,0 +1,73 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputValueDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeList;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+
+/**
+ * Unique argument definition names.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL Object or Interface type is only valid if all its fields have uniquely named arguments.
+ * A Automattic\WooCommerce\Vendor\GraphQL Directive is only valid if all its arguments are uniquely named.
+ */
+class UniqueArgumentDefinitionNames extends ValidationRule
+{
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ $checkArgUniquenessPerField = static function ($node) use ($context): VisitorOperation {
+ assert(
+ $node instanceof InterfaceTypeDefinitionNode
+ || $node instanceof InterfaceTypeExtensionNode
+ || $node instanceof ObjectTypeDefinitionNode
+ || $node instanceof ObjectTypeExtensionNode
+ );
+
+ foreach ($node->fields as $fieldDef) {
+ self::checkArgUniqueness("{$node->name->value}.{$fieldDef->name->value}", $fieldDef->arguments, $context);
+ }
+
+ return Visitor::skipNode();
+ };
+
+ return [
+ NodeKind::DIRECTIVE_DEFINITION => static fn (DirectiveDefinitionNode $node): VisitorOperation => self::checkArgUniqueness("@{$node->name->value}", $node->arguments, $context),
+ NodeKind::INTERFACE_TYPE_DEFINITION => $checkArgUniquenessPerField,
+ NodeKind::INTERFACE_TYPE_EXTENSION => $checkArgUniquenessPerField,
+ NodeKind::OBJECT_TYPE_DEFINITION => $checkArgUniquenessPerField,
+ NodeKind::OBJECT_TYPE_EXTENSION => $checkArgUniquenessPerField,
+ ];
+ }
+
+ /** @param NodeList<InputValueDefinitionNode> $arguments */
+ private static function checkArgUniqueness(string $parentName, NodeList $arguments, SDLValidationContext $context): VisitorOperation
+ {
+ $seenArgs = [];
+ foreach ($arguments as $argument) {
+ $seenArgs[$argument->name->value][] = $argument;
+ }
+
+ foreach ($seenArgs as $argName => $argNodes) {
+ if (count($argNodes) > 1) {
+ $context->reportError(
+ new Error(
+ "Argument \"{$parentName}({$argName}:)\" can only be defined once.",
+ $argNodes,
+ ),
+ );
+ }
+ }
+
+ return Visitor::skipNode();
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentNames.php
new file mode 100644
index 00000000000..b6d5134059e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueArgumentNames.php
@@ -0,0 +1,65 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
+
+/**
+ * @phpstan-import-type VisitorArray from Visitor
+ */
+class UniqueArgumentNames extends ValidationRule
+{
+ /** @var array<string, NameNode> */
+ protected array $knownArgNames;
+
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /** @phpstan-return VisitorArray */
+ public function getASTVisitor(ValidationContext $context): array
+ {
+ $this->knownArgNames = [];
+
+ return [
+ NodeKind::FIELD => function (): void {
+ $this->knownArgNames = [];
+ },
+ NodeKind::DIRECTIVE => function (): void {
+ $this->knownArgNames = [];
+ },
+ NodeKind::ARGUMENT => function (ArgumentNode $node) use ($context): VisitorOperation {
+ $argName = $node->name->value;
+ if (isset($this->knownArgNames[$argName])) {
+ $context->reportError(new Error(
+ static::duplicateArgMessage($argName),
+ [$this->knownArgNames[$argName], $node->name]
+ ));
+ } else {
+ $this->knownArgNames[$argName] = $node->name;
+ }
+
+ return Visitor::skipNode();
+ },
+ ];
+ }
+
+ public static function duplicateArgMessage(string $argName): string
+ {
+ return "There can be only one argument named \"{$argName}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectiveNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectiveNames.php
new file mode 100644
index 00000000000..c80867ceca6
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectiveNames.php
@@ -0,0 +1,59 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+
+/**
+ * Unique directive names.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if all defined directives have unique names.
+ */
+class UniqueDirectiveNames extends ValidationRule
+{
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ $schema = $context->getSchema();
+
+ /** @var array<string, NameNode> $knownDirectiveNames */
+ $knownDirectiveNames = [];
+
+ return [
+ NodeKind::DIRECTIVE_DEFINITION => static function ($node) use ($context, $schema, &$knownDirectiveNames): ?VisitorOperation {
+ $directiveName = $node->name->value;
+
+ if ($schema !== null && $schema->getDirective($directiveName) !== null) {
+ $context->reportError(
+ new Error(
+ 'Directive "@' . $directiveName . '" already exists in the schema. It cannot be redefined.',
+ $node->name,
+ ),
+ );
+
+ return null;
+ }
+
+ if (isset($knownDirectiveNames[$directiveName])) {
+ $context->reportError(
+ new Error(
+ 'There can be only one directive named "@' . $directiveName . '".',
+ [
+ $knownDirectiveNames[$directiveName],
+ $node->name,
+ ]
+ ),
+ );
+ } else {
+ $knownDirectiveNames[$directiveName] = $node->name;
+ }
+
+ return Visitor::skipNode();
+ },
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectivesPerLocation.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectivesPerLocation.php
new file mode 100644
index 00000000000..750a527ddb6
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueDirectivesPerLocation.php
@@ -0,0 +1,96 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DirectiveDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\Node;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Directive;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
+
+/**
+ * Unique directive names per location.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if all non-repeatable directives at
+ * a given location are uniquely named.
+ *
+ * @phpstan-import-type VisitorArray from Visitor
+ */
+class UniqueDirectivesPerLocation extends ValidationRule
+{
+ /** @throws InvariantViolation */
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /** @throws InvariantViolation */
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /**
+ * @throws InvariantViolation
+ *
+ * @phpstan-return VisitorArray
+ */
+ public function getASTVisitor(ValidationContext $context): array
+ {
+ /** @var array<string, true> $uniqueDirectiveMap */
+ $uniqueDirectiveMap = [];
+
+ $schema = $context->getSchema();
+ $definedDirectives = $schema !== null
+ ? $schema->getDirectives()
+ : Directive::getInternalDirectives();
+ foreach ($definedDirectives as $directive) {
+ if (! $directive->isRepeatable) {
+ $uniqueDirectiveMap[$directive->name] = true;
+ }
+ }
+
+ $astDefinitions = $context->getDocument()->definitions;
+ foreach ($astDefinitions as $definition) {
+ if ($definition instanceof DirectiveDefinitionNode
+ && ! $definition->repeatable
+ ) {
+ $uniqueDirectiveMap[$definition->name->value] = true;
+ }
+ }
+
+ return [
+ 'enter' => static function (Node $node) use ($uniqueDirectiveMap, $context): void {
+ if (! property_exists($node, 'directives')) {
+ return;
+ }
+
+ $knownDirectives = [];
+
+ foreach ($node->directives as $directive) {
+ $directiveName = $directive->name->value;
+
+ if (isset($uniqueDirectiveMap[$directiveName])) {
+ if (isset($knownDirectives[$directiveName])) {
+ $context->reportError(new Error(
+ static::duplicateDirectiveMessage($directiveName),
+ [$knownDirectives[$directiveName], $directive]
+ ));
+ } else {
+ $knownDirectives[$directiveName] = $directive;
+ }
+ }
+ }
+ },
+ ];
+ }
+
+ public static function duplicateDirectiveMessage(string $directiveName): string
+ {
+ return "The directive \"{$directiveName}\" can only be used once at this location.";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueEnumValueNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueEnumValueNames.php
new file mode 100644
index 00000000000..c83c0eec2d6
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueEnumValueNames.php
@@ -0,0 +1,68 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+
+class UniqueEnumValueNames extends ValidationRule
+{
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ /** @var array<string, array<string, EnumValueNode>> $knownValueNames */
+ $knownValueNames = [];
+
+ /**
+ * @param EnumTypeDefinitionNode|EnumTypeExtensionNode $enum
+ */
+ $checkValueUniqueness = static function ($enum) use ($context, &$knownValueNames): VisitorOperation {
+ $typeName = $enum->name->value;
+
+ $schema = $context->getSchema();
+ $existingType = $schema !== null
+ ? $schema->getType($typeName)
+ : null;
+
+ $valueNodes = $enum->values;
+
+ if (! isset($knownValueNames[$typeName])) {
+ $knownValueNames[$typeName] = [];
+ }
+
+ $valueNames = &$knownValueNames[$typeName];
+
+ foreach ($valueNodes as $valueDef) {
+ $valueNameNode = $valueDef->name;
+ $valueName = $valueNameNode->value;
+
+ if ($existingType instanceof EnumType && $existingType->getValue($valueName) !== null) {
+ $context->reportError(new Error(
+ "Enum value \"{$typeName}.{$valueName}\" already exists in the schema. It cannot also be defined in this type extension.",
+ $valueNameNode
+ ));
+ } elseif (isset($valueNames[$valueName])) {
+ $context->reportError(new Error(
+ "Enum value \"{$typeName}.{$valueName}\" can only be defined once.",
+ [$valueNames[$valueName], $valueNameNode]
+ ));
+ } else {
+ $valueNames[$valueName] = $valueNameNode;
+ }
+ }
+
+ return Visitor::skipNode();
+ };
+
+ return [
+ NodeKind::ENUM_TYPE_DEFINITION => $checkValueUniqueness,
+ NodeKind::ENUM_TYPE_EXTENSION => $checkValueUniqueness,
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFieldDefinitionNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFieldDefinitionNames.php
new file mode 100644
index 00000000000..66d7ce65751
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFieldDefinitionNames.php
@@ -0,0 +1,99 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InputObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InterfaceTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectTypeExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NamedType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+
+/**
+ * Unique field definition names.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL complex type is only valid if all its fields are uniquely named.
+ */
+class UniqueFieldDefinitionNames extends ValidationRule
+{
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ $schema = $context->getSchema();
+
+ /** @var array<string, array<int, NameNode>> $knownFieldNames */
+ $knownFieldNames = [];
+
+ $checkFieldUniqueness = static function ($node) use ($context, $schema, &$knownFieldNames): VisitorOperation {
+ assert(
+ $node instanceof InputObjectTypeDefinitionNode
+ || $node instanceof InputObjectTypeExtensionNode
+ || $node instanceof InterfaceTypeDefinitionNode
+ || $node instanceof InterfaceTypeExtensionNode
+ || $node instanceof ObjectTypeDefinitionNode
+ || $node instanceof ObjectTypeExtensionNode
+ );
+
+ $typeName = $node->name->value;
+
+ $knownFieldNames[$typeName] ??= [];
+ $fieldNames = &$knownFieldNames[$typeName];
+
+ foreach ($node->fields as $fieldDef) {
+ $fieldName = $fieldDef->name->value;
+
+ $existingType = $schema !== null
+ ? $schema->getType($typeName)
+ : null;
+ if (self::hasField($existingType, $fieldName)) {
+ $context->reportError(
+ new Error(
+ "Field \"{$typeName}.{$fieldName}\" already exists in the schema. It cannot also be defined in this type extension.",
+ $fieldDef->name,
+ ),
+ );
+ } elseif (isset($fieldNames[$fieldName])) {
+ $context->reportError(
+ new Error(
+ "Field \"{$typeName}.{$fieldName}\" can only be defined once.",
+ [$fieldNames[$fieldName], $fieldDef->name],
+ ),
+ );
+ } else {
+ $fieldNames[$fieldName] = $fieldDef->name;
+ }
+ }
+
+ return Visitor::skipNode();
+ };
+
+ return [
+ NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $checkFieldUniqueness,
+ NodeKind::INPUT_OBJECT_TYPE_EXTENSION => $checkFieldUniqueness,
+ NodeKind::INTERFACE_TYPE_DEFINITION => $checkFieldUniqueness,
+ NodeKind::INTERFACE_TYPE_EXTENSION => $checkFieldUniqueness,
+ NodeKind::OBJECT_TYPE_DEFINITION => $checkFieldUniqueness,
+ NodeKind::OBJECT_TYPE_EXTENSION => $checkFieldUniqueness,
+ ];
+ }
+
+ /** @throws InvariantViolation */
+ private static function hasField(?NamedType $type, string $fieldName): bool
+ {
+ if ($type instanceof ObjectType || $type instanceof InterfaceType || $type instanceof InputObjectType) {
+ return $type->hasField($fieldName);
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFragmentNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFragmentNames.php
new file mode 100644
index 00000000000..62c0e788f41
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueFragmentNames.php
@@ -0,0 +1,44 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class UniqueFragmentNames extends ValidationRule
+{
+ /** @var array<string, NameNode> */
+ protected array $knownFragmentNames;
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $this->knownFragmentNames = [];
+
+ return [
+ NodeKind::OPERATION_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(),
+ NodeKind::FRAGMENT_DEFINITION => function (FragmentDefinitionNode $node) use ($context): VisitorOperation {
+ $fragmentName = $node->name->value;
+ if (! isset($this->knownFragmentNames[$fragmentName])) {
+ $this->knownFragmentNames[$fragmentName] = $node->name;
+ } else {
+ $context->reportError(new Error(
+ static::duplicateFragmentNameMessage($fragmentName),
+ [$this->knownFragmentNames[$fragmentName], $node->name]
+ ));
+ }
+
+ return Visitor::skipNode();
+ },
+ ];
+ }
+
+ public static function duplicateFragmentNameMessage(string $fragName): string
+ {
+ return "There can be only one fragment named \"{$fragName}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueInputFieldNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueInputFieldNames.php
new file mode 100644
index 00000000000..9bea56dad25
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueInputFieldNames.php
@@ -0,0 +1,76 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\ValidationContext;
+
+/**
+ * @phpstan-import-type VisitorArray from Visitor
+ */
+class UniqueInputFieldNames extends ValidationRule
+{
+ /** @var array<string, NameNode> */
+ protected array $knownNames;
+
+ /** @var array<array<string, NameNode>> */
+ protected array $knownNameStack;
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ return $this->getASTVisitor($context);
+ }
+
+ /** @phpstan-return VisitorArray */
+ public function getASTVisitor(ValidationContext $context): array
+ {
+ $this->knownNames = [];
+ $this->knownNameStack = [];
+
+ return [
+ NodeKind::OBJECT => [
+ 'enter' => function (): void {
+ $this->knownNameStack[] = $this->knownNames;
+ $this->knownNames = [];
+ },
+ 'leave' => function (): void {
+ $knownNames = array_pop($this->knownNameStack);
+ assert(is_array($knownNames), 'should not happen if the visitor works correctly');
+
+ $this->knownNames = $knownNames;
+ },
+ ],
+ NodeKind::OBJECT_FIELD => function (ObjectFieldNode $node) use ($context): VisitorOperation {
+ $fieldName = $node->name->value;
+
+ if (isset($this->knownNames[$fieldName])) {
+ $context->reportError(new Error(
+ static::duplicateInputFieldMessage($fieldName),
+ [$this->knownNames[$fieldName], $node->name]
+ ));
+ } else {
+ $this->knownNames[$fieldName] = $node->name;
+ }
+
+ return Visitor::skipNode();
+ },
+ ];
+ }
+
+ public static function duplicateInputFieldMessage(string $fieldName): string
+ {
+ return "There can be only one input field named \"{$fieldName}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationNames.php
new file mode 100644
index 00000000000..482bc63f285
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationNames.php
@@ -0,0 +1,47 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class UniqueOperationNames extends ValidationRule
+{
+ /** @var array<string, NameNode> */
+ protected array $knownOperationNames;
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $this->knownOperationNames = [];
+
+ return [
+ NodeKind::OPERATION_DEFINITION => function (OperationDefinitionNode $node) use ($context): VisitorOperation {
+ $operationName = $node->name;
+
+ if ($operationName !== null) {
+ if (! isset($this->knownOperationNames[$operationName->value])) {
+ $this->knownOperationNames[$operationName->value] = $operationName;
+ } else {
+ $context->reportError(new Error(
+ static::duplicateOperationNameMessage($operationName->value),
+ [$this->knownOperationNames[$operationName->value], $operationName]
+ ));
+ }
+ }
+
+ return Visitor::skipNode();
+ },
+ NodeKind::FRAGMENT_DEFINITION => static fn (): VisitorOperation => Visitor::skipNode(),
+ ];
+ }
+
+ public static function duplicateOperationNameMessage(string $operationName): string
+ {
+ return "There can be only one operation named \"{$operationName}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationTypes.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationTypes.php
new file mode 100644
index 00000000000..9bb92fb43b7
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueOperationTypes.php
@@ -0,0 +1,67 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SchemaExtensionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+
+/**
+ * Unique operation types.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if it has only one type per operation.
+ */
+class UniqueOperationTypes extends ValidationRule
+{
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ $schema = $context->getSchema();
+ $definedOperationTypes = [];
+ $existingOperationTypes = $schema !== null
+ ? [
+ 'query' => $schema->getQueryType(),
+ 'mutation' => $schema->getMutationType(),
+ 'subscription' => $schema->getSubscriptionType(),
+ ]
+ : [];
+
+ /**
+ * @param SchemaDefinitionNode|SchemaExtensionNode $node
+ */
+ $checkOperationTypes = static function ($node) use ($context, &$definedOperationTypes, $existingOperationTypes): VisitorOperation {
+ foreach ($node->operationTypes as $operationType) {
+ $operation = $operationType->operation;
+ $alreadyDefinedOperationType = $definedOperationTypes[$operation] ?? null;
+
+ if (isset($existingOperationTypes[$operation])) {
+ $context->reportError(
+ new Error(
+ "Type for {$operation} already defined in the schema. It cannot be redefined.",
+ $operationType,
+ ),
+ );
+ } elseif ($alreadyDefinedOperationType !== null) {
+ $context->reportError(
+ new Error(
+ "There can be only one {$operation} type in schema.",
+ [$alreadyDefinedOperationType, $operationType],
+ ),
+ );
+ } else {
+ $definedOperationTypes[$operation] = $operationType;
+ }
+ }
+
+ return Visitor::skipNode();
+ };
+
+ return [
+ NodeKind::SCHEMA_DEFINITION => $checkOperationTypes,
+ NodeKind::SCHEMA_EXTENSION => $checkOperationTypes,
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueTypeNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueTypeNames.php
new file mode 100644
index 00000000000..a55d0e6d3f1
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueTypeNames.php
@@ -0,0 +1,64 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+
+/**
+ * Unique type names.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if all defined types have unique names.
+ */
+class UniqueTypeNames extends ValidationRule
+{
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ $schema = $context->getSchema();
+ /** @var array<string, NameNode> $knownTypeNames */
+ $knownTypeNames = [];
+ $checkTypeName = static function ($node) use ($context, $schema, &$knownTypeNames): ?VisitorOperation {
+ $typeName = $node->name->value;
+
+ if ($schema !== null && $schema->getType($typeName) !== null) {
+ $context->reportError(
+ new Error(
+ "Type \"{$typeName}\" already exists in the schema. It cannot also be defined in this type definition.",
+ $node->name,
+ ),
+ );
+
+ return null;
+ }
+
+ if (array_key_exists($typeName, $knownTypeNames)) {
+ $context->reportError(
+ new Error(
+ "There can be only one type named \"{$typeName}\".",
+ [
+ $knownTypeNames[$typeName],
+ $node->name,
+ ]
+ ),
+ );
+ } else {
+ $knownTypeNames[$typeName] = $node->name;
+ }
+
+ return Visitor::skipNode();
+ };
+
+ return [
+ NodeKind::SCALAR_TYPE_DEFINITION => $checkTypeName,
+ NodeKind::OBJECT_TYPE_DEFINITION => $checkTypeName,
+ NodeKind::INTERFACE_TYPE_DEFINITION => $checkTypeName,
+ NodeKind::UNION_TYPE_DEFINITION => $checkTypeName,
+ NodeKind::ENUM_TYPE_DEFINITION => $checkTypeName,
+ NodeKind::INPUT_OBJECT_TYPE_DEFINITION => $checkTypeName,
+ ];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueVariableNames.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueVariableNames.php
new file mode 100644
index 00000000000..510e6a4a4ff
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/UniqueVariableNames.php
@@ -0,0 +1,42 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NameNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class UniqueVariableNames extends ValidationRule
+{
+ /** @var array<string, NameNode> */
+ protected array $knownVariableNames;
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ $this->knownVariableNames = [];
+
+ return [
+ NodeKind::OPERATION_DEFINITION => function (): void {
+ $this->knownVariableNames = [];
+ },
+ NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $node) use ($context): void {
+ $variableName = $node->variable->name->value;
+ if (! isset($this->knownVariableNames[$variableName])) {
+ $this->knownVariableNames[$variableName] = $node->variable->name;
+ } else {
+ $context->reportError(new Error(
+ static::duplicateVariableMessage($variableName),
+ [$this->knownVariableNames[$variableName], $node->variable->name]
+ ));
+ }
+ },
+ ];
+ }
+
+ public static function duplicateVariableMessage(string $variableName): string
+ {
+ return "There can be only one variable named \"{$variableName}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValidationRule.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValidationRule.php
new file mode 100644
index 00000000000..857d077251e
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValidationRule.php
@@ -0,0 +1,40 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\SDLValidationContext;
+
+/**
+ * @phpstan-import-type VisitorArray from Visitor
+ */
+abstract class ValidationRule
+{
+ protected string $name;
+
+ public function getName(): string
+ {
+ return $this->name ?? static::class;
+ }
+
+ /**
+ * Returns structure suitable for @see \Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor.
+ *
+ * @phpstan-return VisitorArray
+ */
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [];
+ }
+
+ /**
+ * Returns structure suitable for @see \Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor.
+ *
+ * @phpstan-return VisitorArray
+ */
+ public function getSDLVisitor(SDLValidationContext $context): array
+ {
+ return [];
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValuesOfCorrectType.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValuesOfCorrectType.php
new file mode 100644
index 00000000000..7dd4db18faa
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/ValuesOfCorrectType.php
@@ -0,0 +1,192 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\BooleanValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\EnumValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FloatValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\IntValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ListValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectFieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ObjectValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Visitor;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\VisitorOperation;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\LeafType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ListOfType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+/**
+ * Value literals of correct type.
+ *
+ * A Automattic\WooCommerce\Vendor\GraphQL document is only valid if all value literals are of the type
+ * expected at their position.
+ */
+class ValuesOfCorrectType extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::NULL => static function (NullValueNode $node) use ($context): void {
+ $type = $context->getInputType();
+ if ($type instanceof NonNull) {
+ $typeStr = Utils::printSafe($type);
+ $nodeStr = Printer::doPrint($node);
+ $context->reportError(
+ new Error(
+ "Expected value of type \"{$typeStr}\", found {$nodeStr}.",
+ $node
+ )
+ );
+ }
+ },
+ NodeKind::LST => function (ListValueNode $node) use ($context): ?VisitorOperation {
+ // Note: TypeInfo will traverse into a list's item type, so look to the
+ // parent input type to check if it is a list.
+ $parentType = $context->getParentInputType();
+ $type = $parentType === null
+ ? null
+ : Type::getNullableType($parentType);
+ if (! $type instanceof ListOfType) {
+ $this->isValidValueNode($context, $node);
+
+ return Visitor::skipNode();
+ }
+
+ return null;
+ },
+ NodeKind::OBJECT => function (ObjectValueNode $node) use ($context): ?VisitorOperation {
+ $type = Type::getNamedType($context->getInputType());
+ if (! $type instanceof InputObjectType) {
+ $this->isValidValueNode($context, $node);
+
+ return Visitor::skipNode();
+ }
+
+ // Ensure every required field exists.
+ $inputFields = $type->getFields();
+
+ $fieldNodeMap = [];
+ foreach ($node->fields as $field) {
+ $fieldNodeMap[$field->name->value] = $field;
+ }
+
+ foreach ($inputFields as $inputFieldName => $fieldDef) {
+ if (! isset($fieldNodeMap[$inputFieldName]) && $fieldDef->isRequired()) {
+ $fieldType = Utils::printSafe($fieldDef->getType());
+ $context->reportError(
+ new Error(
+ "Field {$type->name}.{$inputFieldName} of required type {$fieldType} was not provided.",
+ $node
+ )
+ );
+ }
+ }
+
+ return null;
+ },
+ NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) use ($context): void {
+ $parentType = Type::getNamedType($context->getParentInputType());
+ if (! $parentType instanceof InputObjectType) {
+ return;
+ }
+
+ if ($context->getInputType() !== null) {
+ return;
+ }
+
+ $suggestions = Utils::suggestionList(
+ $node->name->value,
+ array_keys($parentType->getFields())
+ );
+ $didYouMean = $suggestions === []
+ ? null
+ : ' Did you mean ' . Utils::quotedOrList($suggestions) . '?';
+
+ $context->reportError(
+ new Error(
+ "Field \"{$node->name->value}\" is not defined by type \"{$parentType->name}\".{$didYouMean}",
+ $node
+ )
+ );
+ },
+ NodeKind::ENUM => function (EnumValueNode $node) use ($context): void {
+ $this->isValidValueNode($context, $node);
+ },
+ NodeKind::INT => function (IntValueNode $node) use ($context): void {
+ $this->isValidValueNode($context, $node);
+ },
+ NodeKind::FLOAT => function (FloatValueNode $node) use ($context): void {
+ $this->isValidValueNode($context, $node);
+ },
+ NodeKind::STRING => function (StringValueNode $node) use ($context): void {
+ $this->isValidValueNode($context, $node);
+ },
+ NodeKind::BOOLEAN => function (BooleanValueNode $node) use ($context): void {
+ $this->isValidValueNode($context, $node);
+ },
+ ];
+ }
+
+ /**
+ * @param VariableNode|NullValueNode|IntValueNode|FloatValueNode|StringValueNode|BooleanValueNode|EnumValueNode|ListValueNode|ObjectValueNode $node
+ *
+ * @throws \JsonException
+ */
+ protected function isValidValueNode(QueryValidationContext $context, ValueNode $node): void
+ {
+ // Report any error at the full type expected by the location.
+ $locationType = $context->getInputType();
+ if ($locationType === null) {
+ return;
+ }
+
+ $type = Type::getNamedType($locationType);
+
+ if (! $type instanceof LeafType) {
+ $typeStr = Utils::printSafe($type);
+ $nodeStr = Printer::doPrint($node);
+ $context->reportError(
+ new Error(
+ "Expected value of type \"{$typeStr}\", found {$nodeStr}.",
+ $node
+ )
+ );
+
+ return;
+ }
+
+ // Scalars determine if a literal value is valid via parseLiteral() which
+ // may throw to indicate failure.
+ try {
+ $type->parseLiteral($node);
+ } catch (\Throwable $error) {
+ if ($error instanceof Error) {
+ $context->reportError($error);
+ } else {
+ $typeStr = Utils::printSafe($type);
+ $nodeStr = Printer::doPrint($node);
+ $context->reportError(
+ new Error(
+ "Expected value of type \"{$typeStr}\", found {$nodeStr}; {$error->getMessage()}",
+ $node,
+ null,
+ [],
+ null,
+ $error // Ensure a reference to the original error is maintained.
+ )
+ );
+ }
+ }
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesAreInputTypes.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesAreInputTypes.php
new file mode 100644
index 00000000000..29957dba420
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesAreInputTypes.php
@@ -0,0 +1,39 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Printer;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class VariablesAreInputTypes extends ValidationRule
+{
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::VARIABLE_DEFINITION => static function (VariableDefinitionNode $node) use ($context): void {
+ $type = AST::typeFromAST([$context->getSchema(), 'getType'], $node->type);
+
+ // If the variable type is not an input type, return an error.
+ if ($type === null || Type::isInputType($type)) {
+ return;
+ }
+
+ $variableName = $node->variable->name->value;
+ $context->reportError(new Error(
+ static::nonInputTypeOnVarMessage($variableName, Printer::doPrint($node->type)),
+ [$node->type]
+ ));
+ },
+ ];
+ }
+
+ public static function nonInputTypeOnVarMessage(string $variableName, string $typeName): string
+ {
+ return "Variable \"\${$variableName}\" cannot be non-input type \"{$typeName}\".";
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesInAllowedPosition.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesInAllowedPosition.php
new file mode 100644
index 00000000000..0f373e7f15a
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/Rules/VariablesInAllowedPosition.php
@@ -0,0 +1,110 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\InvariantViolation;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NodeKind;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\NullValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ValueNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\NonNull;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\TypeComparators;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\Utils;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\QueryValidationContext;
+
+class VariablesInAllowedPosition extends ValidationRule
+{
+ /**
+ * A map from variable names to their definition nodes.
+ *
+ * @var array<string, VariableDefinitionNode>
+ */
+ protected array $varDefMap;
+
+ public function getVisitor(QueryValidationContext $context): array
+ {
+ return [
+ NodeKind::OPERATION_DEFINITION => [
+ 'enter' => function (): void {
+ $this->varDefMap = [];
+ },
+ 'leave' => function (OperationDefinitionNode $operation) use ($context): void {
+ $usages = $context->getRecursiveVariableUsages($operation);
+
+ foreach ($usages as $usage) {
+ $node = $usage['node'];
+ $type = $usage['type'];
+ $defaultValue = $usage['defaultValue'];
+ $varName = $node->name->value;
+ $varDef = $this->varDefMap[$varName] ?? null;
+
+ if ($varDef === null || $type === null) {
+ continue;
+ }
+
+ // A var type is allowed if it is the same or more strict (e.g. is
+ // a subtype of) than the expected type. It can be more strict if
+ // the variable type is non-null when the expected type is nullable.
+ // If both are list types, the variable item type can be more strict
+ // than the expected item type (contravariant).
+ $schema = $context->getSchema();
+ $varType = AST::typeFromAST([$schema, 'getType'], $varDef->type);
+
+ if ($varType !== null && ! $this->allowedVariableUsage($schema, $varType, $varDef->defaultValue, $type, $defaultValue)) {
+ $context->reportError(new Error(
+ static::badVarPosMessage($varName, $varType->toString(), $type->toString()),
+ [$varDef, $node]
+ ));
+ }
+ }
+ },
+ ],
+ NodeKind::VARIABLE_DEFINITION => function (VariableDefinitionNode $varDefNode): void {
+ $this->varDefMap[$varDefNode->variable->name->value] = $varDefNode;
+ },
+ ];
+ }
+
+ /**
+ * A var type is allowed if it is the same or more strict than the expected
+ * type. It can be more strict if the variable type is non-null when the
+ * expected type is nullable. If both are list types, the variable item type can
+ * be more strict than the expected item type.
+ */
+ public static function badVarPosMessage(string $varName, string $varType, string $expectedType): string
+ {
+ return "Variable \"\${$varName}\" of type \"{$varType}\" used in position expecting type \"{$expectedType}\".";
+ }
+
+ /**
+ * Returns true if the variable is allowed in the location it was found,
+ * which includes considering if default values exist for either the variable
+ * or the location at which it is located.
+ *
+ * @param ValueNode|null $varDefaultValue
+ * @param mixed $locationDefaultValue
+ *
+ * @throws InvariantViolation
+ */
+ protected function allowedVariableUsage(Schema $schema, Type $varType, $varDefaultValue, Type $locationType, $locationDefaultValue): bool
+ {
+ if ($locationType instanceof NonNull && ! $varType instanceof NonNull) {
+ $hasNonNullVariableDefaultValue = $varDefaultValue !== null && ! $varDefaultValue instanceof NullValueNode;
+ $hasLocationDefaultValue = Utils::undefined() !== $locationDefaultValue;
+ if (! $hasNonNullVariableDefaultValue && ! $hasLocationDefaultValue) {
+ return false;
+ }
+
+ $nullableLocationType = $locationType->getWrappedType();
+
+ return TypeComparators::isTypeSubTypeOf($schema, $varType, $nullableLocationType);
+ }
+
+ return TypeComparators::isTypeSubTypeOf($schema, $varType, $locationType);
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/SDLValidationContext.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/SDLValidationContext.php
new file mode 100644
index 00000000000..f6227362ae6
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/SDLValidationContext.php
@@ -0,0 +1,43 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+class SDLValidationContext implements ValidationContext
+{
+ protected DocumentNode $ast;
+
+ protected ?Schema $schema;
+
+ /** @var list<Error> */
+ protected array $errors = [];
+
+ public function __construct(DocumentNode $ast, ?Schema $schema)
+ {
+ $this->ast = $ast;
+ $this->schema = $schema;
+ }
+
+ public function reportError(Error $error): void
+ {
+ $this->errors[] = $error;
+ }
+
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+
+ public function getDocument(): DocumentNode
+ {
+ return $this->ast;
+ }
+
+ public function getSchema(): ?Schema
+ {
+ return $this->schema;
+ }
+}
diff --git a/plugins/woocommerce/lib/packages/GraphQL/Validator/ValidationContext.php b/plugins/woocommerce/lib/packages/GraphQL/Validator/ValidationContext.php
new file mode 100644
index 00000000000..f6a43b3a3da
--- /dev/null
+++ b/plugins/woocommerce/lib/packages/GraphQL/Validator/ValidationContext.php
@@ -0,0 +1,19 @@
+<?php declare(strict_types=1);
+
+namespace Automattic\WooCommerce\Vendor\GraphQL\Validator;
+
+use Automattic\WooCommerce\Vendor\GraphQL\Error\Error;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+
+interface ValidationContext
+{
+ public function reportError(Error $error): void;
+
+ /** @return list<Error> */
+ public function getErrors(): array;
+
+ public function getDocument(): DocumentNode;
+
+ public function getSchema(): ?Schema;
+}
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateCoupon.php
index 3bf7a736ea3..273e579a2a4 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateCoupon.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateCoupon.php
@@ -10,8 +10,8 @@ use Automattic\WooCommerce\Internal\Api\QueryInfoExtractor;
use Automattic\WooCommerce\Internal\Api\Utils;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\CreateCoupon as CreateCouponInput;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class CreateCoupon {
public static function get_field_definition(): array {
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateProduct.php
index cce22433600..8bac27defe5 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateProduct.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/CreateProduct.php
@@ -10,8 +10,8 @@ use Automattic\WooCommerce\Internal\Api\QueryInfoExtractor;
use Automattic\WooCommerce\Internal\Api\Utils;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\CreateProduct as CreateProductInput;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class CreateProduct {
public static function get_field_definition(): array {
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteCoupon.php
index 68a812df341..9676979c287 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteCoupon.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteCoupon.php
@@ -9,8 +9,8 @@ use Automattic\WooCommerce\Api\Mutations\Coupons\DeleteCoupon as DeleteCouponCom
use Automattic\WooCommerce\Internal\Api\QueryInfoExtractor;
use Automattic\WooCommerce\Internal\Api\Utils;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\DeleteCouponResult as DeleteCouponResultType;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class DeleteCoupon {
public static function get_field_definition(): array {
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteProduct.php
index 202eaab89c8..2ed4910cefc 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteProduct.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/DeleteProduct.php
@@ -8,14 +8,14 @@ namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations;
use Automattic\WooCommerce\Api\Mutations\Products\DeleteProduct as DeleteProductCommand;
use Automattic\WooCommerce\Internal\Api\QueryInfoExtractor;
use Automattic\WooCommerce\Internal\Api\Utils;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class DeleteProduct {
public static function get_field_definition(): array {
return array(
'type' => Type::nonNull(
- new \GraphQL\Type\Definition\ObjectType(
+ new \Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType(
array(
'name' => 'DeleteProductResult',
'fields' => array(
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateCoupon.php
index 73dc05ab0a4..ca89b31d21d 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateCoupon.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateCoupon.php
@@ -10,8 +10,8 @@ use Automattic\WooCommerce\Internal\Api\QueryInfoExtractor;
use Automattic\WooCommerce\Internal\Api\Utils;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\UpdateCoupon as UpdateCouponInput;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class UpdateCoupon {
public static function get_field_definition(): array {
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateProduct.php
index c8706bc73f2..25b65f851a6 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateProduct.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLMutations/UpdateProduct.php
@@ -10,8 +10,8 @@ use Automattic\WooCommerce\Internal\Api\QueryInfoExtractor;
use Automattic\WooCommerce\Internal\Api\Utils;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\UpdateProduct as UpdateProductInput;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class UpdateProduct {
public static function get_field_definition(): array {
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetCoupon.php
index 3be3f63b5ea..589fec2ba50 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetCoupon.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetCoupon.php
@@ -9,8 +9,8 @@ use Automattic\WooCommerce\Api\Queries\Coupons\GetCoupon as GetCouponCommand;
use Automattic\WooCommerce\Internal\Api\QueryInfoExtractor;
use Automattic\WooCommerce\Internal\Api\Utils;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class GetCoupon {
public static function get_field_definition(): array {
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetProduct.php
index 74c9abd2459..f9c641c2cd1 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetProduct.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/GetProduct.php
@@ -9,8 +9,8 @@ use Automattic\WooCommerce\Api\Queries\Products\GetProduct as GetProductCommand;
use Automattic\WooCommerce\Internal\Api\QueryInfoExtractor;
use Automattic\WooCommerce\Internal\Api\Utils;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class GetProduct {
public static function get_field_definition(): array {
@@ -43,7 +43,7 @@ class GetProduct {
'_preauthorized' => current_user_can( 'read_product' ),
)
) ) {
- throw new \GraphQL\Error\Error(
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error(
'You do not have permission to perform this action.',
extensions: array( 'code' => 'UNAUTHORIZED' )
);
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListCoupons.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListCoupons.php
index 31ba564f3bc..690735d0717 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListCoupons.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListCoupons.php
@@ -10,8 +10,8 @@ use Automattic\WooCommerce\Internal\Api\QueryInfoExtractor;
use Automattic\WooCommerce\Internal\Api\Utils;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\CouponConnection as CouponConnectionType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\CouponStatus as CouponStatusType;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ListCoupons {
public static function get_field_definition(): array {
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListProducts.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListProducts.php
index 288c0a7fb0c..19ef206a7d4 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListProducts.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLQueries/ListProducts.php
@@ -12,8 +12,8 @@ use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\Pr
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\StockStatus as StockStatusType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ListProducts {
public static function get_field_definition(): array {
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/CouponStatus.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/CouponStatus.php
index 9fa17254a17..70d2a043b62 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/CouponStatus.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/CouponStatus.php
@@ -6,7 +6,7 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus as CouponStatusEnum;
-use GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
class CouponStatus {
private static ?EnumType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/DiscountType.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/DiscountType.php
index 2b105555b3c..8c65fc559ad 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/DiscountType.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/DiscountType.php
@@ -6,7 +6,7 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
use Automattic\WooCommerce\Api\Enums\Coupons\DiscountType as DiscountTypeEnum;
-use GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
class DiscountType {
private static ?EnumType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductStatus.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductStatus.php
index ca81d4016a0..006dcb95f4d 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductStatus.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductStatus.php
@@ -6,7 +6,7 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
use Automattic\WooCommerce\Api\Enums\Products\ProductStatus as ProductStatusEnum;
-use GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
class ProductStatus {
private static ?EnumType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductType.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductType.php
index c24684d12e7..d335706cacf 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductType.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/ProductType.php
@@ -6,7 +6,7 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
use Automattic\WooCommerce\Api\Enums\Products\ProductType as ProductTypeEnum;
-use GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
class ProductType {
private static ?EnumType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/StockStatus.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/StockStatus.php
index a28c18b46ab..d48329f324f 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/StockStatus.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Enums/StockStatus.php
@@ -6,7 +6,7 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums;
use Automattic\WooCommerce\Api\Enums\Products\StockStatus as StockStatusEnum;
-use GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
class StockStatus {
private static ?EnumType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateCoupon.php
index 85df973eaee..f20309ba37a 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateCoupon.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateCoupon.php
@@ -7,8 +7,8 @@ namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\DiscountType as DiscountTypeType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\CouponStatus as CouponStatusType;
-use GraphQL\Type\Definition\InputObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class CreateCoupon {
private static ?InputObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateProduct.php
index e2b5306964b..60df57d398c 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateProduct.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/CreateProduct.php
@@ -8,8 +8,8 @@ namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\Dimensions as DimensionsInput;
-use GraphQL\Type\Definition\InputObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class CreateProduct {
private static ?InputObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/Dimensions.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/Dimensions.php
index fa2b8018763..0c701c4df05 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/Dimensions.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/Dimensions.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
-use GraphQL\Type\Definition\InputObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class Dimensions {
private static ?InputObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/ProductFilter.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/ProductFilter.php
index a7c039c5d1d..639ea1d246c 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/ProductFilter.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/ProductFilter.php
@@ -7,8 +7,8 @@ namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\StockStatus as StockStatusType;
-use GraphQL\Type\Definition\InputObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductFilter {
private static ?InputObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateCoupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateCoupon.php
index e03a703981a..64306d7e93f 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateCoupon.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateCoupon.php
@@ -7,8 +7,8 @@ namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\DiscountType as DiscountTypeType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\CouponStatus as CouponStatusType;
-use GraphQL\Type\Definition\InputObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class UpdateCoupon {
private static ?InputObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateProduct.php
index 765a0083c2d..76eec48eec2 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateProduct.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Input/UpdateProduct.php
@@ -8,8 +8,8 @@ namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductStatus as ProductStatusType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\ProductType as ProductTypeType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Input\Dimensions as DimensionsInput;
-use GraphQL\Type\Definition\InputObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class UpdateProduct {
private static ?InputObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/ObjectWithId.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/ObjectWithId.php
index 3b0cbb3fb3e..617ce3fdf62 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/ObjectWithId.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/ObjectWithId.php
@@ -6,8 +6,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use GraphQL\Type\Definition\InterfaceType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ObjectWithId {
private static ?InterfaceType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/Product.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/Product.php
index 713d01ba311..0b2ab53aa4d 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/Product.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Interfaces/Product.php
@@ -17,8 +17,8 @@ use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Produc
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ExternalProduct as ExternalProductType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\VariableProduct as VariableProductType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\SimpleProduct as SimpleProductType;
-use GraphQL\Type\Definition\InterfaceType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class Product {
private static ?InterfaceType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/Coupon.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/Coupon.php
index 5828b0f4bb7..8ec2751eab3 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/Coupon.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/Coupon.php
@@ -9,8 +9,8 @@ use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\Discoun
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Enums\CouponStatus as CouponStatusType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\ObjectWithId as ObjectWithIdInterface;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class Coupon {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/DeleteCouponResult.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/DeleteCouponResult.php
index e71fad9b810..0d026955221 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/DeleteCouponResult.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/DeleteCouponResult.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class DeleteCouponResult {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ExternalProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ExternalProduct.php
index c9a3c0e3e05..fd69028cb98 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ExternalProduct.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ExternalProduct.php
@@ -14,8 +14,8 @@ use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Produc
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductReviewConnection as ProductReviewConnectionType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ExternalProduct {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductAttribute.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductAttribute.php
index d0ed3df49d3..b656ff6a06c 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductAttribute.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductAttribute.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductAttribute {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductDimensions.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductDimensions.php
index 78ecacf7e16..8d5d5ee9b40 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductDimensions.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductDimensions.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductDimensions {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductImage.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductImage.php
index 4131b2975ab..053fb21c574 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductImage.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductImage.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductImage {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductReview.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductReview.php
index c1e865ec835..112d7698438 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductReview.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductReview.php
@@ -6,8 +6,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductReview {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductVariation.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductVariation.php
index e1a36ab38c3..cc2b3ff80a3 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductVariation.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/ProductVariation.php
@@ -15,8 +15,8 @@ use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Produc
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductReviewConnection as ProductReviewConnectionType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductVariation {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SelectedAttribute.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SelectedAttribute.php
index 2bec5ace603..6ede3d2c5d2 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SelectedAttribute.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SelectedAttribute.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class SelectedAttribute {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SimpleProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SimpleProduct.php
index 00d45b97576..a18e5f09016 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SimpleProduct.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/SimpleProduct.php
@@ -14,8 +14,8 @@ use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Produc
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination\ProductReviewConnection as ProductReviewConnectionType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateTime as DateTimeType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class SimpleProduct {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/VariableProduct.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/VariableProduct.php
index c9519c8e7a1..87b9014f44f 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/VariableProduct.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Output/VariableProduct.php
@@ -17,8 +17,8 @@ use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars\DateT
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductInterface;
use Automattic\WooCommerce\Api\Pagination\Connection;
use Automattic\WooCommerce\Internal\Api\Utils;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class VariableProduct {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponConnection.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponConnection.php
index 0b26171b4eb..2db08798e1c 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponConnection.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponConnection.php
@@ -7,8 +7,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class CouponConnection {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponEdge.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponEdge.php
index 71435aec5de..a9c685ada9b 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponEdge.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/CouponEdge.php
@@ -7,8 +7,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\Coupon as CouponType;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class CouponEdge {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/PageInfo.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/PageInfo.php
index 821ed70c2bf..7827521503c 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/PageInfo.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/PageInfo.php
@@ -5,8 +5,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class PageInfo {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductConnection.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductConnection.php
index c37c757f2f3..52f72836c85 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductConnection.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductConnection.php
@@ -7,8 +7,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductType;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductConnection {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductEdge.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductEdge.php
index 097ad21a2b1..25509671c2d 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductEdge.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductEdge.php
@@ -7,8 +7,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Interfaces\Product as ProductType;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductEdge {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewConnection.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewConnection.php
index 86e38a7c4c1..343576bdcb6 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewConnection.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewConnection.php
@@ -7,8 +7,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductReview as ProductReviewType;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductReviewConnection {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewEdge.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewEdge.php
index ed955e17b2c..b2cae95a1ba 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewEdge.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductReviewEdge.php
@@ -7,8 +7,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductReview as ProductReviewType;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductReviewEdge {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationConnection.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationConnection.php
index 3cfd78864d5..6df67642e82 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationConnection.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationConnection.php
@@ -7,8 +7,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductVariation as ProductVariationType;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductVariationConnection {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationEdge.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationEdge.php
index 7f87da82f72..267caeec866 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationEdge.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Pagination/ProductVariationEdge.php
@@ -7,8 +7,8 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Pagination;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Output\ProductVariation as ProductVariationType;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class ProductVariationEdge {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Scalars/DateTime.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Scalars/DateTime.php
index 4bc6c3ea798..419f8fe054a 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Scalars/DateTime.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/GraphQLTypes/Scalars/DateTime.php
@@ -6,7 +6,7 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLTypes\Scalars;
use Automattic\WooCommerce\Api\Scalars\DateTime as DateTimeScalar;
-use GraphQL\Type\Definition\CustomScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType;
class DateTime {
private static ?CustomScalarType $instance = null;
@@ -22,18 +22,18 @@ class DateTime {
try {
return DateTimeScalar::parse( $value );
} catch ( \InvalidArgumentException $e ) {
- throw new \GraphQL\Error\Error( $e->getMessage() );
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error( $e->getMessage() );
}
},
'parseLiteral' => function ( $value_node, ?array $variables = null ) {
- if ( $value_node instanceof \GraphQL\Language\AST\StringValueNode ) {
+ if ( $value_node instanceof \Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode ) {
try {
return DateTimeScalar::parse( $value_node->value );
} catch ( \InvalidArgumentException $e ) {
- throw new \GraphQL\Error\Error( $e->getMessage() );
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error( $e->getMessage() );
}
}
- throw new \GraphQL\Error\Error(
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error(
'DateTime must be a string, got: ' . $value_node->kind
);
},
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/RootMutationType.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/RootMutationType.php
index ce9b639d5ec..d632029ff23 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/RootMutationType.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/RootMutationType.php
@@ -11,7 +11,7 @@ use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\DeletePro
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\DeleteCoupon;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\CreateCoupon;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLMutations\UpdateCoupon;
-use GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
class RootMutationType {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/RootQueryType.php b/plugins/woocommerce/src/Internal/Api/Autogenerated/RootQueryType.php
index b2a8dde5585..3e0872a7d87 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/RootQueryType.php
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/RootQueryType.php
@@ -9,7 +9,7 @@ use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries\ListProduct
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries\GetProduct;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries\GetCoupon;
use Automattic\WooCommerce\Internal\Api\Autogenerated\GraphQLQueries\ListCoupons;
-use GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
class RootQueryType {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/Autogenerated/api_generation_date.txt b/plugins/woocommerce/src/Internal/Api/Autogenerated/api_generation_date.txt
index e6f606063e3..ec38f7ba76e 100644
--- a/plugins/woocommerce/src/Internal/Api/Autogenerated/api_generation_date.txt
+++ b/plugins/woocommerce/src/Internal/Api/Autogenerated/api_generation_date.txt
@@ -1 +1 @@
-2026-04-21T07:16:03+00:00
\ No newline at end of file
+2026-04-22T09:37:36+00:00
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Scripts/ApiBuilder.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Scripts/ApiBuilder.php
index 36b96d0138b..bf148564fc1 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Scripts/ApiBuilder.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Scripts/ApiBuilder.php
@@ -1188,8 +1188,8 @@ class ApiBuilder {
$code .= "// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.\n\n";
$code .= "namespace {$namespace};\n\n";
$code .= "use {$node_type_namespace}\\{$node_type_class} as {$node_type_alias};\n";
- $code .= "use GraphQL\\Type\\Definition\\ObjectType;\n";
- $code .= "use GraphQL\\Type\\Definition\\Type;\n\n";
+ $code .= "use Automattic\\WooCommerce\\Vendor\\GraphQL\\Type\\Definition\\ObjectType;\n";
+ $code .= "use Automattic\\WooCommerce\\Vendor\\GraphQL\\Type\\Definition\\Type;\n\n";
$code .= "class {$connection_class_name} {\n";
$code .= "\tprivate static ?ObjectType \$instance = null;\n\n";
$code .= "\tpublic static function get(): ObjectType {\n";
@@ -1232,8 +1232,8 @@ class ApiBuilder {
$code .= "// THIS FILE IS AUTO-GENERATED. DO NOT EDIT MANUALLY.\n\n";
$code .= "namespace {$namespace};\n\n";
$code .= "use {$node_type_namespace}\\{$node_type_class} as {$node_type_alias};\n";
- $code .= "use GraphQL\\Type\\Definition\\ObjectType;\n";
- $code .= "use GraphQL\\Type\\Definition\\Type;\n\n";
+ $code .= "use Automattic\\WooCommerce\\Vendor\\GraphQL\\Type\\Definition\\ObjectType;\n";
+ $code .= "use Automattic\\WooCommerce\\Vendor\\GraphQL\\Type\\Definition\\Type;\n\n";
$code .= "class {$edge_class_name} {\n";
$code .= "\tprivate static ?ObjectType \$instance = null;\n\n";
$code .= "\tpublic static function get(): ObjectType {\n";
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/EnumTypeTemplate.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/EnumTypeTemplate.php
index 388f3c0df96..a9f4d597fd8 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/EnumTypeTemplate.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/EnumTypeTemplate.php
@@ -22,7 +22,7 @@ declare(strict_types=1);
namespace <?php echo $namespace; ?>;
use <?php echo $enum_fqcn; ?> as <?php echo $enum_alias; ?>;
-use GraphQL\Type\Definition\EnumType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\EnumType;
class <?php echo $class_name; ?> {
private static ?EnumType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/InputObjectTypeTemplate.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/InputObjectTypeTemplate.php
index 8c446288e60..a406f488a61 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/InputObjectTypeTemplate.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/InputObjectTypeTemplate.php
@@ -23,8 +23,8 @@ namespace <?php echo $namespace; ?>;
<?php foreach ( $use_statements as $use ) : ?>
use <?php echo $use; ?>;
<?php endforeach; ?>
-use GraphQL\Type\Definition\InputObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InputObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class <?php echo $class_name; ?> {
private static ?InputObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/InterfaceTypeTemplate.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/InterfaceTypeTemplate.php
index 5624825876c..37351291f0e 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/InterfaceTypeTemplate.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/InterfaceTypeTemplate.php
@@ -24,8 +24,8 @@ namespace <?php echo $namespace; ?>;
<?php foreach ( $use_statements as $use ) : ?>
use <?php echo $use; ?>;
<?php endforeach; ?>
-use GraphQL\Type\Definition\InterfaceType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\InterfaceType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class <?php echo $class_name; ?> {
private static ?InterfaceType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/ObjectTypeTemplate.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/ObjectTypeTemplate.php
index 96cdc825a42..c6c954fbf68 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/ObjectTypeTemplate.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/ObjectTypeTemplate.php
@@ -37,8 +37,8 @@ use <?php echo $use; ?>;
use Automattic\WooCommerce\Api\Pagination\Connection;
use Automattic\WooCommerce\Internal\Api\Utils;
<?php endif; ?>
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class <?php echo $class_name; ?> {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/PageInfoTypeTemplate.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/PageInfoTypeTemplate.php
index 1534a69d7ac..6b14497f119 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/PageInfoTypeTemplate.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/PageInfoTypeTemplate.php
@@ -13,8 +13,8 @@ declare(strict_types=1);
namespace <?php echo $namespace; ?>;
-use GraphQL\Type\Definition\ObjectType;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class PageInfo {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/QueryResolverTemplate.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/QueryResolverTemplate.php
index 94351b88a8f..dd228569dad 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/QueryResolverTemplate.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/QueryResolverTemplate.php
@@ -41,14 +41,14 @@ use Automattic\WooCommerce\Internal\Api\Utils;
<?php foreach ( $use_statements as $use ) : ?>
use <?php echo $use; ?>;
<?php endforeach; ?>
-use GraphQL\Type\Definition\ResolveInfo;
-use GraphQL\Type\Definition\Type;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\Type;
class <?php echo $class_name; ?> {
public static function get_field_definition(): array {
return array(
<?php if ( $scalar_return ) : ?>
- 'type' => Type::nonNull(new \GraphQL\Type\Definition\ObjectType(array(
+ 'type' => Type::nonNull(new \Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType(array(
'name' => '<?php echo $class_name; ?>Result',
'fields' => array(
'result' => array( 'type' => <?php echo $return_type_expr; ?> ),
@@ -126,7 +126,7 @@ foreach ( $execute_params as $param ) :
'_preauthorized' => <?php echo $preauthorized_expr; ?>,
<?php endif; ?>
) ) ) {
- throw new \GraphQL\Error\Error(
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error(
'You do not have permission to perform this action.',
extensions: array( 'code' => 'UNAUTHORIZED' )
);
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/RootMutationTypeTemplate.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/RootMutationTypeTemplate.php
index 241617d85c3..d50d44265b3 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/RootMutationTypeTemplate.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/RootMutationTypeTemplate.php
@@ -17,7 +17,7 @@ namespace <?php echo $namespace; ?>;
<?php foreach ( $mutations as $mutation ) : ?>
use <?php echo $mutation['fqcn']; ?>;
<?php endforeach; ?>
-use GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
class RootMutationType {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/RootQueryTypeTemplate.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/RootQueryTypeTemplate.php
index 54ed76c8868..7c4f402569c 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/RootQueryTypeTemplate.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/RootQueryTypeTemplate.php
@@ -17,7 +17,7 @@ namespace <?php echo $namespace; ?>;
<?php foreach ( $queries as $query ) : ?>
use <?php echo $query['fqcn']; ?>;
<?php endforeach; ?>
-use GraphQL\Type\Definition\ObjectType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ObjectType;
class RootQueryType {
private static ?ObjectType $instance = null;
diff --git a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/ScalarTypeTemplate.php b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/ScalarTypeTemplate.php
index 6316734c7e3..c4677f2fd03 100644
--- a/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/ScalarTypeTemplate.php
+++ b/plugins/woocommerce/src/Internal/Api/DesignTime/Templates/ScalarTypeTemplate.php
@@ -21,7 +21,7 @@ declare(strict_types=1);
namespace <?php echo $namespace; ?>;
use <?php echo $scalar_fqcn; ?> as <?php echo $scalar_alias; ?>;
-use GraphQL\Type\Definition\CustomScalarType;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\CustomScalarType;
class <?php echo $class_name; ?> {
private static ?CustomScalarType $instance = null;
@@ -39,18 +39,18 @@ class <?php echo $class_name; ?> {
try {
return <?php echo $scalar_alias; ?>::parse( $value );
} catch ( \InvalidArgumentException $e ) {
- throw new \GraphQL\Error\Error( $e->getMessage() );
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error( $e->getMessage() );
}
},
'parseLiteral' => function ( $value_node, ?array $variables = null ) {
- if ( $value_node instanceof \GraphQL\Language\AST\StringValueNode ) {
+ if ( $value_node instanceof \Automattic\WooCommerce\Vendor\GraphQL\Language\AST\StringValueNode ) {
try {
return <?php echo $scalar_alias; ?>::parse( $value_node->value );
} catch ( \InvalidArgumentException $e ) {
- throw new \GraphQL\Error\Error( $e->getMessage() );
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error( $e->getMessage() );
}
}
- throw new \GraphQL\Error\Error(
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error(
'<?php echo $graphql_name; ?> must be a string, got: ' . $value_node->kind
);
},
diff --git a/plugins/woocommerce/src/Internal/Api/GraphQLController.php b/plugins/woocommerce/src/Internal/Api/GraphQLController.php
index 456bca157ed..4238804b3c2 100644
--- a/plugins/woocommerce/src/Internal/Api/GraphQLController.php
+++ b/plugins/woocommerce/src/Internal/Api/GraphQLController.php
@@ -8,18 +8,18 @@ use Automattic\WooCommerce\Api\ApiException;
use Automattic\WooCommerce\Internal\Api\Autogenerated\RootQueryType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\RootMutationType;
use Automattic\WooCommerce\Internal\Api\Autogenerated\TypeRegistry;
-use GraphQL\GraphQL;
-use GraphQL\Language\AST\DocumentNode;
-use GraphQL\Language\AST\FieldNode;
-use GraphQL\Language\AST\InlineFragmentNode;
-use GraphQL\Language\AST\OperationDefinitionNode;
-use GraphQL\Language\AST\SelectionSetNode;
-use GraphQL\Type\Schema;
-use GraphQL\Error\DebugFlag;
-use GraphQL\Validator\DocumentValidator;
-use GraphQL\Validator\Rules\DisableIntrospection;
-use GraphQL\Validator\Rules\QueryComplexity;
-use GraphQL\Validator\Rules\QueryDepth;
+use Automattic\WooCommerce\Vendor\GraphQL\GraphQL;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\OperationDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Schema;
+use Automattic\WooCommerce\Vendor\GraphQL\Error\DebugFlag;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\DocumentValidator;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\DisableIntrospection;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryComplexity;
+use Automattic\WooCommerce\Vendor\GraphQL\Validator\Rules\QueryDepth;
/**
* Handles incoming GraphQL requests over the WooCommerce REST API.
@@ -201,10 +201,10 @@ class GraphQLController {
$debug_mode = $this->is_debug_mode( $request );
$result->setErrorFormatter(
function ( \Throwable $error ) use ( $debug_mode ): array {
- $formatted = \GraphQL\Error\FormattedError::createFromException( $error );
+ $formatted = \Automattic\WooCommerce\Vendor\GraphQL\Error\FormattedError::createFromException( $error );
if ( ! isset( $formatted['extensions']['code'] ) ) {
- $client_safe = $error instanceof \GraphQL\Error\ClientAware && $error->isClientSafe();
+ $client_safe = $error instanceof \Automattic\WooCommerce\Vendor\GraphQL\Error\ClientAware && $error->isClientSafe();
$formatted['extensions']['code'] = $client_safe ? 'BAD_USER_INPUT' : 'INTERNAL_ERROR';
}
diff --git a/plugins/woocommerce/src/Internal/Api/QueryCache.php b/plugins/woocommerce/src/Internal/Api/QueryCache.php
index 9f1c5f2d0e1..f304ac8978b 100644
--- a/plugins/woocommerce/src/Internal/Api/QueryCache.php
+++ b/plugins/woocommerce/src/Internal/Api/QueryCache.php
@@ -4,9 +4,9 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api;
-use GraphQL\Language\AST\DocumentNode;
-use GraphQL\Language\Parser;
-use GraphQL\Utils\AST;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\DocumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\Parser;
+use Automattic\WooCommerce\Vendor\GraphQL\Utils\AST;
/**
* Caches parsed GraphQL ASTs in the WP object cache and implements the
@@ -131,7 +131,7 @@ class QueryCache {
private function parse_and_cache( string $query, string $hash ) {
try {
$document = Parser::parse( $query, array( 'noLocation' => true ) );
- } catch ( \GraphQL\Error\SyntaxError $e ) {
+ } catch ( \Automattic\WooCommerce\Vendor\GraphQL\Error\SyntaxError $e ) {
return $this->error_response( 'GraphQL syntax error: ' . $e->getMessage(), 'GRAPHQL_PARSE_ERROR' );
}
diff --git a/plugins/woocommerce/src/Internal/Api/QueryInfoExtractor.php b/plugins/woocommerce/src/Internal/Api/QueryInfoExtractor.php
index 353787c6b5a..87b44b11c73 100644
--- a/plugins/woocommerce/src/Internal/Api/QueryInfoExtractor.php
+++ b/plugins/woocommerce/src/Internal/Api/QueryInfoExtractor.php
@@ -4,13 +4,13 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Internal\Api;
-use GraphQL\Language\AST\ArgumentNode;
-use GraphQL\Language\AST\FieldNode;
-use GraphQL\Language\AST\FragmentDefinitionNode;
-use GraphQL\Language\AST\FragmentSpreadNode;
-use GraphQL\Language\AST\InlineFragmentNode;
-use GraphQL\Language\AST\SelectionSetNode;
-use GraphQL\Type\Definition\ResolveInfo;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\ArgumentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FieldNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentDefinitionNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\FragmentSpreadNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\InlineFragmentNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Language\AST\SelectionSetNode;
+use Automattic\WooCommerce\Vendor\GraphQL\Type\Definition\ResolveInfo;
/**
* Extracts a unified query info tree from a GraphQL ResolveInfo.
@@ -173,10 +173,10 @@ class QueryInfoExtractor {
private static function resolve_argument_value( ArgumentNode $arg, array $variable_values ): mixed {
$value_node = $arg->value;
- if ( $value_node instanceof \GraphQL\Language\AST\VariableNode ) {
+ if ( $value_node instanceof \Automattic\WooCommerce\Vendor\GraphQL\Language\AST\VariableNode ) {
return $variable_values[ $value_node->name->value ] ?? null;
}
- return \GraphQL\Utils\AST::valueFromASTUntyped( $value_node, $variable_values );
+ return \Automattic\WooCommerce\Vendor\GraphQL\Utils\AST::valueFromASTUntyped( $value_node, $variable_values );
}
}
diff --git a/plugins/woocommerce/src/Internal/Api/Utils.php b/plugins/woocommerce/src/Internal/Api/Utils.php
index 682dd0ff95c..24e2ebed1c9 100644
--- a/plugins/woocommerce/src/Internal/Api/Utils.php
+++ b/plugins/woocommerce/src/Internal/Api/Utils.php
@@ -17,11 +17,11 @@ class Utils {
*
* @param string $capability A WordPress capability slug.
*
- * @throws \GraphQL\Error\Error When the current user lacks the capability.
+ * @throws \Automattic\WooCommerce\Vendor\GraphQL\Error\Error When the current user lacks the capability.
*/
public static function check_current_user_can( string $capability ): void {
if ( ! current_user_can( $capability ) ) {
- throw new \GraphQL\Error\Error(
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error(
'You do not have permission to perform this action.',
extensions: array( 'code' => 'UNAUTHORIZED' )
);
@@ -60,7 +60,7 @@ class Utils {
* @param array $args The GraphQL field arguments.
*
* @return \Automattic\WooCommerce\Api\Pagination\PaginationParams
- * @throws \GraphQL\Error\Error When a pagination value is out of range.
+ * @throws \Automattic\WooCommerce\Vendor\GraphQL\Error\Error When a pagination value is out of range.
*/
public static function create_pagination_params( array $args ): \Automattic\WooCommerce\Api\Pagination\PaginationParams {
return self::create_input(
@@ -84,14 +84,14 @@ class Utils {
* @param callable $factory A callable that returns the constructed object.
*
* @return mixed The return value of the factory.
- * @throws \GraphQL\Error\Error When the factory throws InvalidArgumentException.
+ * @throws \Automattic\WooCommerce\Vendor\GraphQL\Error\Error When the factory throws InvalidArgumentException.
*/
public static function create_input( callable $factory ): mixed {
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
try {
return $factory();
} catch ( \InvalidArgumentException $e ) {
- throw new \GraphQL\Error\Error(
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error(
$e->getMessage(),
extensions: array( 'code' => 'INVALID_ARGUMENT' )
);
@@ -107,7 +107,7 @@ class Utils {
* @param array $execute_args Named arguments to pass to execute().
*
* @return mixed The return value of execute().
- * @throws \GraphQL\Error\Error On any exception from the command.
+ * @throws \Automattic\WooCommerce\Vendor\GraphQL\Error\Error On any exception from the command.
*/
public static function execute_command( object $command, array $execute_args ): mixed {
return self::translate_exceptions(
@@ -129,7 +129,7 @@ class Utils {
* @param array $authorize_args Named arguments to pass to authorize().
*
* @return bool The return value of authorize().
- * @throws \GraphQL\Error\Error On any exception from the authorize method.
+ * @throws \Automattic\WooCommerce\Vendor\GraphQL\Error\Error On any exception from the authorize method.
*/
public static function authorize_command( object $command, array $authorize_args ): bool {
return self::translate_exceptions(
@@ -154,7 +154,7 @@ class Utils {
* @param callable $operation Callable to invoke.
*
* @return mixed The return value of the callable.
- * @throws \GraphQL\Error\Error On any exception from the callable.
+ * @throws \Automattic\WooCommerce\Vendor\GraphQL\Error\Error On any exception from the callable.
*/
public static function translate_exceptions( callable $operation ): mixed {
// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Not HTML; serialized as JSON.
@@ -165,7 +165,7 @@ class Utils {
// getErrorCode() can't be silently overridden by an extensions
// entry keyed 'code'. The invariant "the code on the wire
// equals ApiException::getErrorCode()" is worth enforcing.
- throw new \GraphQL\Error\Error(
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error(
$e->getMessage(),
extensions: array_merge(
$e->getExtensions(),
@@ -173,12 +173,12 @@ class Utils {
)
);
} catch ( \InvalidArgumentException $e ) {
- throw new \GraphQL\Error\Error(
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error(
$e->getMessage(),
extensions: array( 'code' => 'INVALID_ARGUMENT' )
);
} catch ( \Throwable $e ) {
- throw new \GraphQL\Error\Error(
+ throw new \Automattic\WooCommerce\Vendor\GraphQL\Error\Error(
'An unexpected error occurred.',
previous: $e,
extensions: array( 'code' => 'INTERNAL_ERROR' )