Skip to content

[GraphQL] Resolver Stages #2959

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 31 additions & 27 deletions features/graphql/input_output.feature
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,37 @@ Feature: GraphQL DTO input and output
}
"""

Scenario: Create an item with custom input and output
When I send the following GraphQL request:
"""
mutation {
createDummyDtoInputOutput(input: {foo: "A foo", bar: 4, clientMutationId: "myId"}) {
dummyDtoInputOutput {
baz,
bat
}
clientMutationId
}
}
"""
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 should be equal to:
"""
{
"data": {
"createDummyDtoInputOutput": {
"dummyDtoInputOutput": {
"baz": 4,
"bat": "A foo"
},
"clientMutationId": "myId"
}
}
}
"""

Scenario: Create an item using custom inputClass & disabled outputClass
Given there are 2 dummyDtoNoOutput objects
When I send the following GraphQL request:
Expand Down Expand Up @@ -135,33 +166,6 @@ Feature: GraphQL DTO input and output
}
"""

Scenario: Create an item with empty input fields using disabled inputClass (no persist done)
When I send the following GraphQL request:
"""
mutation {
createDummyDtoNoInput(input: {clientMutationId: "myId"}) {
dummyDtoNoInput {
id
}
clientMutationId
}
}
"""
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 should be equal to:
"""
{
"data": {
"createDummyDtoNoInput": {
"dummyDtoNoInput": null,
"clientMutationId": "myId"
}
}
}
"""

Scenario: Use messenger with GraphQL and an input where the handler gives a synchronous result
When I send the following GraphQL request:
"""
Expand Down
107 changes: 0 additions & 107 deletions features/graphql/mutation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -334,113 +334,6 @@ Feature: GraphQL mutation support
And the header "Content-Type" should be equal to "application/json"
And the JSON node "errors[0].message" should be equal to "name: This value should not be blank."

Scenario: Create an item using custom inputClass & disabled outputClass
Given there are 2 dummyDtoNoOutput objects
When I send the following GraphQL request:
"""
mutation {
createDummyDtoNoOutput(input: {foo: "A new one", bar: 3, clientMutationId: "myId"}) {
dummyDtoNoOutput {
id
}
clientMutationId
}
}
"""
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 should be equal to:
"""
{
"errors": [
{
"message": "Cannot query field \"id\" on type \"DummyDtoNoOutput\".",
"extensions": {
"category": "graphql"
},
"locations": [
{
"line": 4,
"column": 7
}
]
}
]
}
"""

Scenario: Cannot create an item with input fields using disabled inputClass
When I send the following GraphQL request:
"""
mutation {
createDummyDtoNoInput(input: {lorem: "A new one", ipsum: 3, clientMutationId: "myId"}) {
clientMutationId
}
}
"""
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 should be equal to:
"""
{
"errors": [
{
"message": "Field \"lorem\" is not defined by type createDummyDtoNoInputInput.",
"extensions": {
"category": "graphql"
},
"locations": [
{
"line": 2,
"column": 33
}
]
},
{
"message": "Field \"ipsum\" is not defined by type createDummyDtoNoInputInput.",
"extensions": {
"category": "graphql"
},
"locations": [
{
"line": 2,
"column": 53
}
]
}
]
}
"""

Scenario: Create an item with empty input fields using disabled inputClass (no persist done)
When I send the following GraphQL request:
"""
mutation {
createDummyDtoNoInput(input: {clientMutationId: "myId"}) {
dummyDtoNoInput {
id
}
clientMutationId
}
}
"""
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 should be equal to:
"""
{
"data": {
"createDummyDtoNoInput": {
"dummyDtoNoInput": null,
"clientMutationId": "myId"
}
}
}
"""

Scenario: Execute a custom mutation
Given there are 1 dummyCustomMutation objects
When I send the following GraphQL request:
Expand Down
5 changes: 1 addition & 4 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ parameters:
path: %currentWorkingDirectory%/src/Util/ClassInfoTrait.php
-
message: '#Cannot assign offset .+ to bool\.#'
path: %currentWorkingDirectory%/src/GraphQl/Resolver/FieldsToAttributesTrait.php
path: %currentWorkingDirectory%/src/GraphQl/Serializer/SerializerContextBuilder.php
- '#Access to an undefined property Prophecy\\Prophecy\\ObjectProphecy<(\\?[a-zA-Z0-9_]+)+>::\$[a-zA-Z0-9_]+#'
- '#Call to an undefined method Doctrine\\Common\\Persistence\\ObjectManager::getConnection\(\)#'
# https://github.com/willdurand/Negotiation/issues/89#issuecomment-513283286
Expand All @@ -57,9 +57,6 @@ parameters:
-
message: '#Parameter \#1 \$resource of method ApiPlatform\\Core\\Metadata\\Extractor\\XmlExtractor::getAttributes\(\) expects SimpleXMLElement, object given\.#'
path: %currentWorkingDirectory%/src/Metadata/Extractor/XmlExtractor.php
-
message: '#Parameter \#1 \$collection of method ApiPlatform\\Core\\Tests\\GraphQl\\Resolver\\Factory\\CollectionResolverFactoryTest::createCollectionResolverFactory\(\) expects array\|Iterator, object given\.#'
path: %currentWorkingDirectory%/tests/GraphQl/Resolver/Factory/CollectionResolverFactoryTest.php
-
message: '#Parameter \#1 \$docblock of method phpDocumentor\\Reflection\\DocBlockFactoryInterface::create\(\) expects string, ReflectionClass given\.#'
path: %currentWorkingDirectory%/src/Metadata/Resource/Factory/PhpDocResourceMetadataFactory.php
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ public function getResult(Builder $aggregationBuilder, string $resourceClass, st

private function addCountToContext(Builder $aggregationBuilder, array $context): array
{
if (!($context['graphql'] ?? false)) {
if (!($context['graphql_operation_name'] ?? false)) {
return $context;
}

Expand Down
2 changes: 1 addition & 1 deletion src/Bridge/Doctrine/Orm/Extension/PaginationExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ private function getPaginationParameter(Request $request, string $parameterName,

private function addCountToContext(QueryBuilder $queryBuilder, array $context): array
{
if (!($context['graphql'] ?? false)) {
if (!($context['graphql_operation_name'] ?? false)) {
return $context;
}

Expand Down
67 changes: 55 additions & 12 deletions src/Bridge/Symfony/Bundle/Resources/config/graphql.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,70 @@
<!-- Resolvers -->

<service id="api_platform.graphql.resolver.factory.item" class="ApiPlatform\Core\GraphQl\Resolver\Factory\ItemResolverFactory" public="false">
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="api_platform.graphql.resolver.stage.read" />
<argument type="service" id="api_platform.graphql.resolver.stage.deny_access" />
<argument type="service" id="api_platform.graphql.resolver.stage.serialize" />
<argument type="service" id="api_platform.graphql.query_resolver_locator" />
<argument type="service" id="serializer" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="ignore" />
</service>

<service id="api_platform.graphql.resolver.factory.collection" class="ApiPlatform\Core\GraphQl\Resolver\Factory\CollectionResolverFactory" public="false">
<argument type="service" id="api_platform.collection_data_provider" />
<argument type="service" id="api_platform.subresource_data_provider" />
<argument type="service" id="api_platform.graphql.resolver.stage.read" />
<argument type="service" id="api_platform.graphql.resolver.stage.deny_access" />
<argument type="service" id="api_platform.graphql.resolver.stage.serialize" />
<argument type="service" id="api_platform.graphql.query_resolver_locator" />
<argument type="service" id="serializer" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="null" />
<argument type="service" id="request_stack" />
<argument>%api_platform.collection.pagination.enabled%</argument>
</service>

<service id="api_platform.graphql.resolver.factory.item_mutation" class="ApiPlatform\Core\GraphQl\Resolver\Factory\ItemMutationResolverFactory" public="false">
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="api_platform.data_persister" />
<argument type="service" id="api_platform.graphql.resolver.stage.read" />
<argument type="service" id="api_platform.graphql.resolver.stage.deny_access" />
<argument type="service" id="api_platform.graphql.resolver.stage.serialize" />
<argument type="service" id="api_platform.graphql.resolver.stage.deserialize" />
<argument type="service" id="api_platform.graphql.resolver.stage.write" />
<argument type="service" id="api_platform.graphql.resolver.stage.validate" />
<argument type="service" id="api_platform.graphql.mutation_resolver_locator" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
</service>

<!-- Resolver Stages -->

<service id="api_platform.graphql.resolver.stage.read" class="ApiPlatform\Core\GraphQl\Resolver\Stage\ReadStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.iri_converter" />
<argument type="service" id="api_platform.collection_data_provider" />
<argument type="service" id="api_platform.subresource_data_provider" />
<argument type="service" id="api_platform.graphql.serializer.context_builder" />
</service>

<service id="api_platform.graphql.resolver.stage.deny_access" class="ApiPlatform\Core\GraphQl\Resolver\Stage\DenyAccessStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.security.resource_access_checker" />
</service>

<service id="api_platform.graphql.resolver.stage.serialize" class="ApiPlatform\Core\GraphQl\Resolver\Stage\SerializeStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="serializer" />
<argument type="service" id="api_platform.graphql.serializer.context_builder" />
<argument>%api_platform.collection.pagination.enabled%</argument>
</service>

<service id="api_platform.graphql.resolver.stage.deserialize" class="ApiPlatform\Core\GraphQl\Resolver\Stage\DeserializeStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="serializer" />
<argument type="service" id="api_platform.graphql.serializer.context_builder" />
</service>

<service id="api_platform.graphql.resolver.stage.write" class="ApiPlatform\Core\GraphQl\Resolver\Stage\WriteStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.security.resource_access_checker" on-invalid="null" />
<argument type="service" id="api_platform.validator" on-invalid="null" />
<argument type="service" id="api_platform.data_persister" />
<argument type="service" id="api_platform.graphql.serializer.context_builder" />
</service>

<service id="api_platform.graphql.resolver.stage.validate" class="ApiPlatform\Core\GraphQl\Resolver\Stage\ValidateStage" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
<argument type="service" id="api_platform.validator" />
</service>

<service id="api_platform.graphql.resolver.resource_field" class="ApiPlatform\Core\GraphQl\Resolver\ResourceFieldResolver" public="false">
Expand Down Expand Up @@ -164,6 +202,11 @@
<tag name="serializer.normalizer" priority="-995" />
</service>

<service id="api_platform.graphql.serializer.context_builder" class="ApiPlatform\Core\GraphQl\Serializer\SerializerContextBuilder" public="false">
<argument type="service" id="api_platform.metadata.resource.metadata_factory" />
</service>
<service id="ApiPlatform\Core\GraphQl\Serializer\SerializerContextBuilderInterface" alias="api_platform.graphql.serializer.context_builder" />

<!-- Command -->

<service id="api_platform.graphql.command.export_command" class="ApiPlatform\Core\Bridge\Symfony\Bundle\Command\GraphQlExportCommand">
Expand Down
4 changes: 2 additions & 2 deletions src/DataProvider/Pagination.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public function getPage(array $context = []): int
*/
public function getOffset(string $resourceClass = null, string $operationName = null, array $context = []): int
{
$graphql = $context['graphql'] ?? false;
$graphql = (bool) ($context['graphql_operation_name'] ?? false);

$limit = $this->getLimit($resourceClass, $operationName, $context);

Expand All @@ -96,7 +96,7 @@ public function getOffset(string $resourceClass = null, string $operationName =
*/
public function getLimit(string $resourceClass = null, string $operationName = null, array $context = []): int
{
$graphql = $context['graphql'] ?? false;
$graphql = (bool) ($context['graphql_operation_name'] ?? false);

$limit = $this->options['items_per_page'];
$clientLimit = $this->options['client_items_per_page'];
Expand Down
Loading