diff --git a/CHANGELOG.md b/CHANGELOG.md index 35d55c0506..1ab2920709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ You can find and compare releases at the [GitHub release page](https://github.co ## Unreleased +## v6.52.0 + +### Added + +- Expose query complexity score in lifecycle event `BuildExtensionsResponse` https://github.com/nuwave/lighthouse/pull/2637 + ## v6.51.2 ### Fixed diff --git a/docs/6/api-reference/events.md b/docs/6/api-reference/events.md index 4e2923a8d2..64164a9085 100644 --- a/docs/6/api-reference/events.md +++ b/docs/6/api-reference/events.md @@ -175,31 +175,35 @@ class StartExecution ### BuildExtensionsResponse ```php +use GraphQL\Executor\ExecutionResult; + /** * Fires after a query was resolved. * * Listeners may return a @see \Nuwave\Lighthouse\Execution\ExtensionsResponse * to include in the response. */ -class BuildExtensionsResponse {} +class BuildExtensionsResponse +{ + public function __construct( + /** The result of resolving a single operation. */ + public ExecutionResult $result, + /** The calculated query complexity score of the operation, only available if the validation rule is enabled. */ + public ?int $queryComplexity, + ) {} +} ``` ```php namespace Nuwave\Lighthouse\Execution; -/** - * May be returned from listeners of @see \Nuwave\Lighthouse\Events\BuildExtensionsResponse. - */ +/** May be returned from listeners of @see \Nuwave\Lighthouse\Events\BuildExtensionsResponse. */ class ExtensionsResponse { public function __construct( - /** - * Will be used as the key in the response map. - */ + /** Will be used as the key in the response map. */ public string $key, - /** - * JSON-encodable content of the extension. - */ + /** JSON-encodable content of the extension. */ public mixed $content, ) {} } diff --git a/docs/master/api-reference/events.md b/docs/master/api-reference/events.md index 4e2923a8d2..64164a9085 100644 --- a/docs/master/api-reference/events.md +++ b/docs/master/api-reference/events.md @@ -175,31 +175,35 @@ class StartExecution ### BuildExtensionsResponse ```php +use GraphQL\Executor\ExecutionResult; + /** * Fires after a query was resolved. * * Listeners may return a @see \Nuwave\Lighthouse\Execution\ExtensionsResponse * to include in the response. */ -class BuildExtensionsResponse {} +class BuildExtensionsResponse +{ + public function __construct( + /** The result of resolving a single operation. */ + public ExecutionResult $result, + /** The calculated query complexity score of the operation, only available if the validation rule is enabled. */ + public ?int $queryComplexity, + ) {} +} ``` ```php namespace Nuwave\Lighthouse\Execution; -/** - * May be returned from listeners of @see \Nuwave\Lighthouse\Events\BuildExtensionsResponse. - */ +/** May be returned from listeners of @see \Nuwave\Lighthouse\Events\BuildExtensionsResponse. */ class ExtensionsResponse { public function __construct( - /** - * Will be used as the key in the response map. - */ + /** Will be used as the key in the response map. */ public string $key, - /** - * JSON-encodable content of the extension. - */ + /** JSON-encodable content of the extension. */ public mixed $content, ) {} } diff --git a/src/Events/BuildExtensionsResponse.php b/src/Events/BuildExtensionsResponse.php index d3594d6a0a..2c1a9216f8 100644 --- a/src/Events/BuildExtensionsResponse.php +++ b/src/Events/BuildExtensionsResponse.php @@ -15,5 +15,7 @@ class BuildExtensionsResponse public function __construct( /** The result of resolving a single operation. */ public ExecutionResult $result, + /** The calculated query complexity score of the operation, only available if the validation rule is enabled. */ + public ?int $queryComplexity, ) {} } diff --git a/src/Execution/ExtensionsResponse.php b/src/Execution/ExtensionsResponse.php index 79fa74462c..50d60f0798 100644 --- a/src/Execution/ExtensionsResponse.php +++ b/src/Execution/ExtensionsResponse.php @@ -2,19 +2,13 @@ namespace Nuwave\Lighthouse\Execution; -/** - * May be returned from listeners of @see \Nuwave\Lighthouse\Events\BuildExtensionsResponse. - */ +/** May be returned from listeners of @see \Nuwave\Lighthouse\Events\BuildExtensionsResponse. */ class ExtensionsResponse { public function __construct( - /** - * Will be used as the key in the response map. - */ + /** Will be used as the key in the response map. */ public string $key, - /** - * JSON-encodable content of the extension. - */ + /** JSON-encodable content of the extension. */ public mixed $content, ) {} } diff --git a/src/GraphQL.php b/src/GraphQL.php index 62be5a6faf..b5938791c2 100644 --- a/src/GraphQL.php +++ b/src/GraphQL.php @@ -137,14 +137,16 @@ public function executeParsedQueryRaw( ); if ($this->providesValidationRules instanceof CacheableValidationRulesProvider) { - $validationRules = $this->providesValidationRules->cacheableValidationRules(); + $cacheableValidationRules = $this->providesValidationRules->cacheableValidationRules(); - $errors = $this->validateCacheableRules($validationRules, $schema, $this->schemaBuilder->schemaHash(), $query, $queryHash); + $errors = $this->validateCacheableRules($cacheableValidationRules, $schema, $this->schemaBuilder->schemaHash(), $query, $queryHash); if ($errors !== []) { return new ExecutionResult(null, $errors); } } + $validationRules = $this->providesValidationRules->validationRules(); + $result = GraphQLBase::executeQuery( $schema, $query, @@ -153,12 +155,19 @@ public function executeParsedQueryRaw( $variables, $operationName, null, - $this->providesValidationRules->validationRules(), + $validationRules, ); + $queryComplexityRule = $validationRules[QueryComplexity::class] ?? null; + $queryComplexity = $queryComplexityRule instanceof QueryComplexity + // TODO remove this check when updating the required version of webonyx/graphql-php + && method_exists($queryComplexityRule, 'getQueryComplexity') + ? $queryComplexityRule->getQueryComplexity() + : null; + /** @var array<\Nuwave\Lighthouse\Execution\ExtensionsResponse|null> $extensionsResponses */ $extensionsResponses = (array) $this->eventDispatcher->dispatch( - new BuildExtensionsResponse($result), + new BuildExtensionsResponse($result, $queryComplexity), ); foreach ($extensionsResponses as $extensionsResponse) { diff --git a/src/Tracing/FederatedTracing/Proto/FieldStat.php b/src/Tracing/FederatedTracing/Proto/FieldStat.php index eeeaa606d2..15c5bfc09d 100644 --- a/src/Tracing/FederatedTracing/Proto/FieldStat.php +++ b/src/Tracing/FederatedTracing/Proto/FieldStat.php @@ -82,16 +82,17 @@ class FieldStat extends \Google\Protobuf\Internal\Message * * @var string $return_type * required; eg "String!" for User.email:String! - * @var int|string $errors_count + * + * @type int|string $errors_count * Number of errors whose path is this field. Note that we assume that error * tracking does *not* require field-level instrumentation so this *will* * include errors from requests that don't contribute to the * `observed_execution_count` field (and does not need to be scaled by * field_execution_weight). - * @var int|string $observed_execution_count + * @type int|string $observed_execution_count * Number of times that the resolver for this field is directly observed being - * executed - * @var int|string $estimated_execution_count + * executed. + * @type int|string $estimated_execution_count * Same as `observed_execution_count` but potentially scaled upwards if the server was only * performing field-level instrumentation on a sampling of operations. For * example, if the server randomly instruments 1% of requests for this @@ -100,13 +101,13 @@ class FieldStat extends \Google\Protobuf\Internal\Message * this number goes up by the trace's `field_execution_weight` for each * observed field execution, while `observed_execution_count` above goes * up by 1.) - * @var int|string $requests_with_errors_count + * @type int|string $requests_with_errors_count * Number of times the resolver for this field is executed that resulted in * at least one error. "Request" is a misnomer here as this corresponds to * resolver calls, not overall operations. Like `errors_count` above, this * includes all requests rather than just requests with field-level * instrumentation. - * @var array|array|\Google\Protobuf\Internal\RepeatedField $latency_count + * @type array|array|\Google\Protobuf\Internal\RepeatedField $latency_count * Duration histogram for the latency of this field. Note that it is scaled in * the same way as estimated_execution_count so its "total count" might be * greater than `observed_execution_count` and may not exactly equal diff --git a/src/Tracing/FederatedTracing/Proto/ReportHeader.php b/src/Tracing/FederatedTracing/Proto/ReportHeader.php index 993e32baad..facca667cb 100644 --- a/src/Tracing/FederatedTracing/Proto/ReportHeader.php +++ b/src/Tracing/FederatedTracing/Proto/ReportHeader.php @@ -78,16 +78,18 @@ class ReportHeader extends \Google\Protobuf\Internal\Message * * @var string $graph_ref * eg "mygraph@myvariant" - * @var string $hostname + * + * @type string $hostname * eg "host-01.example.com" - * @var string $agent_version + * @type string $agent_version * eg "engineproxy 0.1.0" - * @var string $service_version + * @type string $service_version * eg "prod-4279-20160804T065423Z-5-g3cf0aa8" (taken from `git describe --tags`) - * @var string $runtime_version + * @type string $runtime_version * eg "node v4.6.0" - * @var string $uname + * @type string $uname * eg "Linux box 4.6.5-1-ec2 #1 SMP Mon Aug 1 02:31:38 PDT 2016 x86_64 GNU/Linux" + * * @var string $executable_schema_id * An id that is used to represent the schema to Apollo Graph Manager * Using this in place of what used to be schema_hash, since that is no longer diff --git a/src/Tracing/FederatedTracing/Proto/Trace.php b/src/Tracing/FederatedTracing/Proto/Trace.php index 0b5f445ee1..115ea74632 100644 --- a/src/Tracing/FederatedTracing/Proto/Trace.php +++ b/src/Tracing/FederatedTracing/Proto/Trace.php @@ -183,10 +183,11 @@ class Trace extends \Google\Protobuf\Internal\Message * @var int|string $duration_ns * High precision duration of the trace; may not equal end_time-start_time * (eg, if your machine's clock changed during the trace) - * @var \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\Node $root + * + * @type \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\Node $root * A tree containing information about all resolvers run directly by this - * service, including errors - * @var bool $is_incomplete + * service, including errors. + * @type bool $is_incomplete * If this is true, the trace is potentially missing some nodes that were * present on the query plan. This can happen if the trace span buffer used * in the Router fills up and some spans have to be dropped. In these cases @@ -194,7 +195,7 @@ class Trace extends \Google\Protobuf\Internal\Message * be missing some referenced or executed fields, and some nodes may be * missing. If this is true we should display a warning to the user when they * view the trace in Explorer. - * @var string $signature + * @type string $signature * In addition to details.raw_query, we include a "signature" of the query, * which can be normalized: for example, you may want to discard aliases, drop * unused operations and fragments, sort fields, etc. The most important thing @@ -205,43 +206,44 @@ class Trace extends \Google\Protobuf\Internal\Message * that signature is in the key of traces_per_query rather than in this field. * Engineproxy provides the signature in legacy_signature_needs_resigning * instead. - * @var string $unexecutedOperationBody + * @type string $unexecutedOperationBody * Optional: when GraphQL parsing or validation against the GraphQL schema fails, these fields * can include reference to the operation being sent for users to dig into the set of operations - * that are failing validation - * @var string $unexecutedOperationName - * @var \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\Details $details - * @var string $client_name - * @var string $client_version - * @var string $operation_type - * @var string $operation_subtype - * @var \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\HTTP $http - * @var \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\CachePolicy $cache_policy - * @var \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\QueryPlanNode $query_plan + * that are failing validation. + * @type string $unexecutedOperationName + * @type \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\Details $details + * @type string $client_name + * @type string $client_version + * @type string $operation_type + * @type string $operation_subtype + * @type \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\HTTP $http + * @type \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\CachePolicy $cache_policy + * @type \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\QueryPlanNode $query_plan * If this Trace was created by a Router/Gateway, this is the query plan, including * sub-Traces for subgraphs. Note that the 'root' tree on the * top-level Trace won't contain any resolvers (though it could contain errors * that occurred in the Router/Gateway itself). - * @var bool $full_query_cache_hit + * @type bool $full_query_cache_hit * Was this response served from a full query response cache? (In that case * the node tree will have no resolvers.) - * @var bool $persisted_query_hit + * @type bool $persisted_query_hit * Was this query specified successfully as a persisted query hash? - * @var bool $persisted_query_register + * @type bool $persisted_query_register * Did this query contain both a full query string and a persisted query hash? * (This typically means that a previous request was rejected as an unknown * persisted query.) - * @var bool $registered_operation + * @type bool $registered_operation * Was this operation registered and a part of the safelist? - * @var bool $forbidden_operation + * @type bool $forbidden_operation * Was this operation forbidden due to lack of safelisting? - * @var float $field_execution_weight + * @type float $field_execution_weight * Some servers don't do field-level instrumentation for every request and assign * each request a "weight" for each request that they do instrument. When this * trace is aggregated into field usage stats, it should count as this value * towards the estimated_execution_count rather than just 1. This value should * typically be at least 1. * 0 is treated as 1 for backwards compatibility. + * * @var \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\Limits $limits * The limits information of the query. * } diff --git a/src/Tracing/FederatedTracing/Proto/Trace/Node.php b/src/Tracing/FederatedTracing/Proto/Trace/Node.php index c8e93bc5bf..d5c8502457 100644 --- a/src/Tracing/FederatedTracing/Proto/Trace/Node.php +++ b/src/Tracing/FederatedTracing/Proto/Trace/Node.php @@ -71,13 +71,15 @@ class Node extends \Google\Protobuf\Internal\Message * @var string $original_field_name * @var string $type * The field's return type; e.g. "String!" for User.email:String! - * @var string $parent_type + * + * @type string $parent_type * The field's parent type; e.g. "User" for User.email:String! - * @var \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\CachePolicy $cache_policy - * @var int|string $start_time + * @type \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\CachePolicy $cache_policy + * @type int|string $start_time * relative to the trace's start_time, in ns - * @var int|string $end_time + * @type int|string $end_time * relative to the trace's start_time, in ns + * * @var array<\Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\Error>|\Google\Protobuf\Internal\RepeatedField $error * @var array<\Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\Trace\Node>|\Google\Protobuf\Internal\RepeatedField $child * } diff --git a/src/Tracing/FederatedTracing/Proto/TracesAndStats.php b/src/Tracing/FederatedTracing/Proto/TracesAndStats.php index af59bbf3e0..0c8607c8f7 100644 --- a/src/Tracing/FederatedTracing/Proto/TracesAndStats.php +++ b/src/Tracing/FederatedTracing/Proto/TracesAndStats.php @@ -59,7 +59,8 @@ class TracesAndStats extends \Google\Protobuf\Internal\Message * `@include` or `@skip`, etc). It also may be missing fields that show up in FieldStats * (as FieldStats will include the concrete object type for fields referenced * via an interface type). - * @var \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\QueryMetadata $query_metadata + * + * @type \Nuwave\Lighthouse\Tracing\FederatedTracing\Proto\QueryMetadata $query_metadata * This is an optional field that is used to provide more context to the key of this object within the * traces_per_query map. If it's omitted, we assume the key is a standard operation name and signature key. * } diff --git a/tests/Unit/Schema/Directives/ComplexityDirectiveTest.php b/tests/Unit/Schema/Directives/ComplexityDirectiveTest.php index 5ccee1d121..418349a44d 100644 --- a/tests/Unit/Schema/Directives/ComplexityDirectiveTest.php +++ b/tests/Unit/Schema/Directives/ComplexityDirectiveTest.php @@ -4,6 +4,8 @@ use GraphQL\Validator\Rules\QueryComplexity; use Illuminate\Contracts\Config\Repository as ConfigRepository; +use Illuminate\Contracts\Events\Dispatcher as EventsDispatcher; +use Nuwave\Lighthouse\Events\BuildExtensionsResponse; use Tests\TestCase; use Tests\Utils\Queries\Foo; @@ -13,6 +15,14 @@ final class ComplexityDirectiveTest extends TestCase public function testDefaultComplexity(): void { + $eventsDispatcher = $this->app->make(EventsDispatcher::class); + + /** @var array $events */ + $events = []; + $eventsDispatcher->listen(BuildExtensionsResponse::class, static function (BuildExtensionsResponse $event) use (&$events): void { + $events[] = $event; + }); + $max = 1; $this->setMaxQueryComplexity($max); @@ -33,6 +43,14 @@ public function testDefaultComplexity(): void } } ')->assertGraphQLErrorMessage(QueryComplexity::maxQueryComplexityErrorMessage($max, 2)); + + $this->assertCount(1, $events); + + // TODO remove this check when updating the required version of webonyx/graphql-php + if (method_exists(QueryComplexity::class, 'getQueryComplexity')) { + $event = $events[0]; + $this->assertSame(2, $event->queryComplexity); + } } public function testKnowsPagination(): void