From 4171d5f9cd41731b857c53a186270ba0626baedf Mon Sep 17 00:00:00 2001 From: Antoine Bluchet Date: Tue, 15 Oct 2024 09:47:05 +0200 Subject: [PATCH] fix(graphql): register query parameter arguments with filters (#6726) --- src/GraphQl/Type/FieldsBuilder.php | 129 ++++++++++++---------- src/Laravel/Tests/GraphQlTest.php | 19 ++++ src/Laravel/workbench/app/Models/Book.php | 11 ++ 3 files changed, 99 insertions(+), 60 deletions(-) diff --git a/src/GraphQl/Type/FieldsBuilder.php b/src/GraphQl/Type/FieldsBuilder.php index 3a9884f45f..0952929981 100644 --- a/src/GraphQl/Type/FieldsBuilder.php +++ b/src/GraphQl/Type/FieldsBuilder.php @@ -287,61 +287,6 @@ public function resolveResourceArgs(array $args, Operation $operation): array $args[$id]['type'] = $this->typeConverter->resolveType($arg['type']); } - /* - * This is @experimental, read the comment on the parameterToObjectType function as additional information. - */ - foreach ($operation->getParameters() ?? [] as $parameter) { - $key = $parameter->getKey(); - - if (str_contains($key, ':property')) { - if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) { - continue; - } - - $filter = $this->filterLocator->get($filterId); - $parsedKey = explode('[:property]', $key); - $flattenFields = []; - - if ($filter instanceof FilterInterface) { - foreach ($filter->getDescription($operation->getClass()) as $name => $value) { - $values = []; - parse_str($name, $values); - if (isset($values[$parsedKey[0]])) { - $values = $values[$parsedKey[0]]; - } - - $name = key($values); - $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string']; - } - - $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]); - } - - if ($filter instanceof OpenApiParameterFilterInterface) { - foreach ($filter->getOpenApiParameters($parameter) as $value) { - $values = []; - parse_str($value->getName(), $values); - if (isset($values[$parsedKey[0]])) { - $values = $values[$parsedKey[0]]; - } - - $name = key($values); - $flattenFields[] = ['name' => $name, 'required' => $value->getRequired(), 'description' => $value->getDescription(), 'leafs' => $values[$name], 'type' => $value->getSchema()['type'] ?? 'string']; - } - - $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0].$operation->getShortName().$operation->getName()); - } - - continue; - } - - $args[$key] = ['type' => GraphQLType::string()]; - - if ($parameter->getRequired()) { - $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']); - } - } - return $args; } @@ -463,12 +408,15 @@ private function getResourceFieldConfiguration(?string $property, ?string $field $args = []; - if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType && $isCollectionType) { - if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) { - $args = $this->getGraphQlPaginationArgs($resourceOperation); - } + if (!$input && !$rootOperation instanceof Mutation && !$rootOperation instanceof Subscription && !$isStandardGraphqlType) { + if ($isCollectionType) { + if (!$this->isEnumClass($resourceClass) && $this->pagination->isGraphQlEnabled($resourceOperation)) { + $args = $this->getGraphQlPaginationArgs($resourceOperation); + } - $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); + $args = $this->getFilterArgs($args, $resourceClass, $rootResource, $resourceOperation, $rootOperation, $property, $depth); + $args = $this->getParameterArgs($rootOperation, $args); + } } if ($isStandardGraphqlType || $input) { @@ -491,6 +439,67 @@ private function getResourceFieldConfiguration(?string $property, ?string $field return null; } + /* + * This function is @experimental, read the comment on the parameterToObjectType function for additional information. + * @experimental + */ + private function getParameterArgs(Operation $operation, array $args = []): array + { + foreach ($operation->getParameters() ?? [] as $parameter) { + $key = $parameter->getKey(); + + if (!str_contains($key, ':property')) { + $args[$key] = ['type' => GraphQLType::string()]; + + if ($parameter->getRequired()) { + $args[$key]['type'] = GraphQLType::nonNull($args[$key]['type']); + } + + continue; + } + + if (!($filterId = $parameter->getFilter()) || !$this->filterLocator->has($filterId)) { + continue; + } + + $filter = $this->filterLocator->get($filterId); + $parsedKey = explode('[:property]', $key); + $flattenFields = []; + + if ($filter instanceof FilterInterface) { + foreach ($filter->getDescription($operation->getClass()) as $name => $value) { + $values = []; + parse_str($name, $values); + if (isset($values[$parsedKey[0]])) { + $values = $values[$parsedKey[0]]; + } + + $name = key($values); + $flattenFields[] = ['name' => $name, 'required' => $value['required'] ?? null, 'description' => $value['description'] ?? null, 'leafs' => $values[$name], 'type' => $value['type'] ?? 'string']; + } + + $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0]); + } + + if ($filter instanceof OpenApiParameterFilterInterface) { + foreach ($filter->getOpenApiParameters($parameter) as $value) { + $values = []; + parse_str($value->getName(), $values); + if (isset($values[$parsedKey[0]])) { + $values = $values[$parsedKey[0]]; + } + + $name = key($values); + $flattenFields[] = ['name' => $name, 'required' => $value->getRequired(), 'description' => $value->getDescription(), 'leafs' => $values[$name], 'type' => $value->getSchema()['type'] ?? 'string']; + } + + $args[$parsedKey[0]] = $this->parameterToObjectType($flattenFields, $parsedKey[0].$operation->getShortName().$operation->getName()); + } + } + + return $args; + } + private function getGraphQlPaginationArgs(Operation $queryOperation): array { $paginationType = $this->pagination->getGraphQlPaginationType($queryOperation); diff --git a/src/Laravel/Tests/GraphQlTest.php b/src/Laravel/Tests/GraphQlTest.php index 819e4f80d6..2c3889b84e 100644 --- a/src/Laravel/Tests/GraphQlTest.php +++ b/src/Laravel/Tests/GraphQlTest.php @@ -47,4 +47,23 @@ public function testGetBooks(): void $this->assertArrayHasKey('data', $data); $this->assertArrayNotHasKey('errors', $data); } + + public function testGetBooksWithPaginationAndOrder(): void + { + BookFactory::new()->has(AuthorFactory::new())->count(10)->create(); + $response = $this->postJson('/api/graphql', ['query' => '{ + books(first: 3, order: {name: "desc"}) { + edges { + node { + id, name, publicationDate, author { id, name } + } + } + } +}'], ['accept' => ['application/json']]); + $response->assertStatus(200); + $data = $response->json(); + $this->assertArrayHasKey('data', $data); + $this->assertCount(3, $data['data']['books']['edges']); + $this->assertArrayNotHasKey('errors', $data); + } } diff --git a/src/Laravel/workbench/app/Models/Book.php b/src/Laravel/workbench/app/Models/Book.php index c458cb68c4..33bcf09342 100644 --- a/src/Laravel/workbench/app/Models/Book.php +++ b/src/Laravel/workbench/app/Models/Book.php @@ -15,6 +15,7 @@ use ApiPlatform\Laravel\Eloquent\Filter\DateFilter; use ApiPlatform\Laravel\Eloquent\Filter\EqualsFilter; +use ApiPlatform\Laravel\Eloquent\Filter\OrderFilter; use ApiPlatform\Laravel\Eloquent\Filter\OrFilter; use ApiPlatform\Laravel\Eloquent\Filter\PartialSearchFilter; use ApiPlatform\Laravel\Eloquent\Filter\RangeFilter; @@ -22,6 +23,8 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\GraphQl\QueryCollection; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use ApiPlatform\Metadata\Put; @@ -44,6 +47,14 @@ new Post(), new Delete(), new GetCollection(), + ], + graphQlOperations: [ + new Query(), + new QueryCollection( + parameters: [ + new QueryParameter(key: 'order[:property]', filter: OrderFilter::class), + ], + ), ] )] #[QueryParameter(key: 'isbn', filter: PartialSearchFilter::class, constraints: 'min:2')]