diff --git a/CHANGELOG.md b/CHANGELOG.md index 62f84437384..0c578b68cca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.7.0 +* GraphQL: Add ability to use different pagination types for the queries of a resource (#4453) * Security: **BC** Fix `ApiProperty` `security` attribute expression being passed a class string for the `object` variable on updates/creates - null is now passed instead if the object is not available (#4184) * Security: `ApiProperty` now supports a `security_post_denormalize` attribute, which provides access to the `object` variable for the object being updated/created and `previous_object` for the object before it was updated (#4184) * Maker: Add `make:data-provider` and `make :data-persister` commands to generate a data provider / persister (#3850) @@ -231,7 +232,7 @@ For compatibility reasons with Symfony 5.2 and PHP 8, we do not test anymore the * Doctrine: Order filter doesn't throw anymore with numeric key (#3673 and #3687) * Doctrine: Fix ODM check change tracking deferred (#3629) * Doctrine: Allow 2inflector version 2.0 (#3607) -* OpenAPI: Allow subresources context to be added (#3685) +* OpenAPI: Allow subresources context to be added (#3685) * OpenAPI: Fix pagination documentation on subresources (#3678) * Subresource: Fix query when using a custom identifier (#3529 and #3671) * GraphQL: Fix relation types without Doctrine (#3591) @@ -337,7 +338,7 @@ For compatibility reasons with Symfony 5.2 and PHP 8, we do not test anymore the ## 2.5.0 beta 1 * Add an HTTP client dedicated to functional API testing (#2608) -* Add PATCH support (#2895) +* Add PATCH support (#2895) Note: with JSON Merge Patch, responses will skip null values. As this may break on some endpoints, you need to manually [add the `merge-patch+json` format](https://api-platform.com/docs/core/content-negotiation/#configuring-patch-formats) to enable PATCH support. This will be the default behavior in API Platform 3. * Add a command to generate json schemas `api:json-schema:generate` (#2996) * Add infrastructure to generate a JSON Schema from a Resource `ApiPlatform\Core\JsonSchema\SchemaFactoryInterface` (#2983) diff --git a/features/graphql/collection.feature b/features/graphql/collection.feature index cf5b7a1fbc7..5148acddfdf 100644 --- a/features/graphql/collection.feature +++ b/features/graphql/collection.feature @@ -9,7 +9,7 @@ Feature: GraphQL collection support ...dummyFields } } - fragment dummyFields on DummyConnection { + fragment dummyFields on DummyCursorConnection { edges { node { id diff --git a/features/graphql/introspection.feature b/features/graphql/introspection.feature index d1923c738c2..e60211419c6 100644 --- a/features/graphql/introspection.feature +++ b/features/graphql/introspection.feature @@ -37,7 +37,7 @@ Feature: GraphQL introspection support } } } - type2: __type(name: "DummyAggregateOfferConnection") { + type2: __type(name: "DummyAggregateOfferCursorConnection") { description, fields { name @@ -76,7 +76,7 @@ Feature: GraphQL introspection support { "name":"offers", "type":{ - "name":"DummyAggregateOfferConnection", + "name":"DummyAggregateOfferCursorConnection", "kind":"OBJECT", "ofType":null } @@ -546,7 +546,7 @@ Feature: GraphQL introspection support When I send the following GraphQL request: """ { - typeNotAvailable: __type(name: "VoDummyInspectionConnection") { + typeNotAvailable: __type(name: "VoDummyInspectionCursorConnection") { description } typeOwner: __type(name: "VoDummyCar") { @@ -563,6 +563,6 @@ Feature: GraphQL introspection support Then the response status code should be 200 And the response should be in JSON And the header "Content-Type" should be equal to "application/json" - And the JSON node "errors[0].debugMessage" should be equal to 'Type with id "VoDummyInspectionConnection" is not present in the types container' + And the JSON node "errors[0].debugMessage" should be equal to 'Type with id "VoDummyInspectionCursorConnection" is not present in the types container' And the JSON node "data.typeNotAvailable" should be null - And the JSON node "data.typeOwner.fields[3].type.name" should be equal to "VoDummyInspectionConnection" + And the JSON node "data.typeOwner.fields[3].type.name" should be equal to "VoDummyInspectionCursorConnection" diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature index 0bb5caf44eb..e67e55554b4 100644 --- a/features/graphql/mutation.feature +++ b/features/graphql/mutation.feature @@ -709,7 +709,7 @@ Feature: GraphQL mutation support And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.__typename" should be equal to "updateThirdLevelNestedPayload" And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.fourthLevel.id" should be equal to "/fourth_levels/1" And the JSON node "data.updateRelatedDummy.relatedDummy.thirdLevel.fourthLevel.__typename" should be equal to "updateFourthLevelNestedPayload" - And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.__typename" should be equal to "updateRelatedToDummyFriendNestedPayloadConnection" + And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.__typename" should be equal to "updateRelatedToDummyFriendNestedPayloadCursorConnection" And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.edges[0].node.name" should be equal to "Relation-1" And the JSON node "data.updateRelatedDummy.relatedDummy.relatedToDummyFriend.edges[1].node.name" should be equal to "Relation-2" diff --git a/features/graphql/schema.feature b/features/graphql/schema.feature index 75f63b9a59a..765905fce98 100644 --- a/features/graphql/schema.feature +++ b/features/graphql/schema.feature @@ -16,8 +16,8 @@ Feature: GraphQL schema-related features name: String! } - ###Connection for DummyFriend.### - type DummyFriendConnection { + ###Cursor connection for DummyFriend.### + type DummyFriendCursorConnection { edges: [DummyFriendEdge] pageInfo: DummyFriendPageInfo! totalCount: Int! @@ -54,8 +54,8 @@ Feature: GraphQL schema-related features name: String! } - # Connection for DummyFriend. - type DummyFriendConnection { + # Cursor connection for DummyFriend. + type DummyFriendCursorConnection { edges: [DummyFriendEdge] pageInfo: DummyFriendPageInfo! totalCount: Int! diff --git a/src/GraphQl/Type/TypeBuilder.php b/src/GraphQl/Type/TypeBuilder.php index 68bc15b736c..2021f68cbfe 100644 --- a/src/GraphQl/Type/TypeBuilder.php +++ b/src/GraphQl/Type/TypeBuilder.php @@ -224,25 +224,25 @@ public function getNodeInterface(): InterfaceType public function getResourcePaginatedCollectionType(GraphQLType $resourceType, string $resourceClass, string $operationName): GraphQLType { $shortName = $resourceType->name; + $paginationType = $this->pagination->getGraphQlPaginationType($resourceClass, $operationName); - if ($this->typesContainer->has("{$shortName}Connection")) { - return $this->typesContainer->get("{$shortName}Connection"); + $connectionTypeKey = sprintf('%s%sConnection', $shortName, ucfirst($paginationType)); + if ($this->typesContainer->has($connectionTypeKey)) { + return $this->typesContainer->get($connectionTypeKey); } - $paginationType = $this->pagination->getGraphQlPaginationType($resourceClass, $operationName); - $fields = 'cursor' === $paginationType ? $this->getCursorBasedPaginationFields($resourceType) : $this->getPageBasedPaginationFields($resourceType); $configuration = [ - 'name' => "{$shortName}Connection", - 'description' => "Connection for $shortName.", + 'name' => $connectionTypeKey, + 'description' => sprintf("%s connection for $shortName.", ucfirst($paginationType)), 'fields' => $fields, ]; $resourcePaginatedCollectionType = new ObjectType($configuration); - $this->typesContainer->set("{$shortName}Connection", $resourcePaginatedCollectionType); + $this->typesContainer->set($connectionTypeKey, $resourcePaginatedCollectionType); return $resourcePaginatedCollectionType; } diff --git a/tests/GraphQl/Type/TypeBuilderTest.php b/tests/GraphQl/Type/TypeBuilderTest.php index 19b0feaaaaa..11c6f8aeb1a 100644 --- a/tests/GraphQl/Type/TypeBuilderTest.php +++ b/tests/GraphQl/Type/TypeBuilderTest.php @@ -472,8 +472,8 @@ public function testGetNodeInterface(): void public function testCursorBasedGetResourcePaginatedCollectionType(): void { - $this->typesContainerProphecy->has('StringConnection')->shouldBeCalled()->willReturn(false); - $this->typesContainerProphecy->set('StringConnection', Argument::type(ObjectType::class))->shouldBeCalled(); + $this->typesContainerProphecy->has('StringCursorConnection')->shouldBeCalled()->willReturn(false); + $this->typesContainerProphecy->set('StringCursorConnection', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->set('StringEdge', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->set('StringPageInfo', Argument::type(ObjectType::class))->shouldBeCalled(); $this->resourceMetadataCollectionFactoryProphecy->create('StringResourceClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('StringResourceClass', [ @@ -484,8 +484,8 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void /** @var ObjectType $resourcePaginatedCollectionType */ $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', 'operationName'); - $this->assertSame('StringConnection', $resourcePaginatedCollectionType->name); - $this->assertSame('Connection for String.', $resourcePaginatedCollectionType->description); + $this->assertSame('StringCursorConnection', $resourcePaginatedCollectionType->name); + $this->assertSame('Cursor connection for String.', $resourcePaginatedCollectionType->description); $resourcePaginatedCollectionTypeFields = $resourcePaginatedCollectionType->getFields(); $this->assertArrayHasKey('edges', $resourcePaginatedCollectionTypeFields); @@ -531,8 +531,8 @@ public function testCursorBasedGetResourcePaginatedCollectionType(): void public function testPageBasedGetResourcePaginatedCollectionType(): void { - $this->typesContainerProphecy->has('StringConnection')->shouldBeCalled()->willReturn(false); - $this->typesContainerProphecy->set('StringConnection', Argument::type(ObjectType::class))->shouldBeCalled(); + $this->typesContainerProphecy->has('StringPageConnection')->shouldBeCalled()->willReturn(false); + $this->typesContainerProphecy->set('StringPageConnection', Argument::type(ObjectType::class))->shouldBeCalled(); $this->typesContainerProphecy->set('StringPaginationInfo', Argument::type(ObjectType::class))->shouldBeCalled(); $this->resourceMetadataCollectionFactoryProphecy->create('StringResourceClass')->shouldBeCalled()->willReturn(new ResourceMetadataCollection('StringResourceClass', [ @@ -543,8 +543,8 @@ public function testPageBasedGetResourcePaginatedCollectionType(): void /** @var ObjectType $resourcePaginatedCollectionType */ $resourcePaginatedCollectionType = $this->typeBuilder->getResourcePaginatedCollectionType(GraphQLType::string(), 'StringResourceClass', 'operationName'); - $this->assertSame('StringConnection', $resourcePaginatedCollectionType->name); - $this->assertSame('Connection for String.', $resourcePaginatedCollectionType->description); + $this->assertSame('StringPageConnection', $resourcePaginatedCollectionType->name); + $this->assertSame('Page connection for String.', $resourcePaginatedCollectionType->description); $resourcePaginatedCollectionTypeFields = $resourcePaginatedCollectionType->getFields(); $this->assertArrayHasKey('collection', $resourcePaginatedCollectionTypeFields);