diff --git a/features/graphql/mutation.feature b/features/graphql/mutation.feature index 5004556ef19..f42c668c709 100644 --- a/features/graphql/mutation.feature +++ b/features/graphql/mutation.feature @@ -54,11 +54,36 @@ Feature: GraphQL mutation support And the JSON node "data.createFoo.bar" should be equal to "new" And the JSON node "data.createFoo.clientMutationId" should be equal to "myId" + Scenario: Create an item with a subresource + Given there are 1 dummy objects with relatedDummy + When I send the following GraphQL request: + """ + mutation { + createDummy(input: {_id: 1, name: "A dummy", foo: [], relatedDummy: "/related_dummies/1", clientMutationId: "myId"}) { + id + name + foo + relatedDummy { + name + } + 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 node "data.createDummy.id" should be equal to "/dummies/2" + And the JSON node "data.createDummy.name" should be equal to "A dummy" + And the JSON node "data.createDummy.foo" should have 0 elements + And the JSON node "data.createDummy.relatedDummy.name" should be equal to "RelatedDummy #1" + And the JSON node "data.createDummy.clientMutationId" should be equal to "myId" + Scenario: Create an item with an iterable field When I send the following GraphQL request: """ mutation { - createDummy(input: {_id: 1, name: "A dummy", foo: [], jsonData: {bar:{baz:3,qux:[7.6,false,null]}}, clientMutationId: "myId"}) { + createDummy(input: {_id: 2, name: "A dummy", foo: [], jsonData: {bar:{baz:3,qux:[7.6,false,null]}}, clientMutationId: "myId"}) { id name foo @@ -70,7 +95,7 @@ Feature: GraphQL mutation 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 "data.createDummy.id" should be equal to "/dummies/1" + And the JSON node "data.createDummy.id" should be equal to "/dummies/3" And the JSON node "data.createDummy.name" should be equal to "A dummy" And the JSON node "data.createDummy.foo" should have 0 elements And the JSON node "data.createDummy.jsonData.bar.baz" should be equal to the number 3 diff --git a/src/GraphQl/Type/SchemaBuilder.php b/src/GraphQl/Type/SchemaBuilder.php index 4267f1ddcc5..fce18d93efa 100644 --- a/src/GraphQl/Type/SchemaBuilder.php +++ b/src/GraphQl/Type/SchemaBuilder.php @@ -189,10 +189,10 @@ private function getMutationFields(string $resourceClass, ResourceMetadata $reso * * @return array|null */ - private function getResourceFieldConfiguration(string $resourceClass, ResourceMetadata $resourceMetadata, string $fieldDescription = null, Type $type, string $rootResource, bool $input = false, string $mutationName = null) + private function getResourceFieldConfiguration(string $resourceClass, ResourceMetadata $resourceMetadata, string $fieldDescription = null, Type $type, string $rootResource, bool $input = false, string $mutationName = null, int $depth = 0) { try { - if (null === $graphqlType = $this->convertType($type, $input, $mutationName)) { + if (null === $graphqlType = $this->convertType($type, $input, $mutationName, $depth)) { return null; } @@ -227,7 +227,7 @@ private function getResourceFieldConfiguration(string $resourceClass, ResourceMe foreach ($this->filterLocator->get($filterId)->getDescription($resourceClass) as $key => $value) { $nullable = isset($value['required']) ? !$value['required'] : true; $filterType = \in_array($value['type'], Type::$builtinTypes, true) ? new Type($value['type'], $nullable) : new Type('object', $nullable, $value['type']); - $graphqlFilterType = $this->convertType($filterType); + $graphqlFilterType = $this->convertType($filterType, false, null, $depth); if ('[]' === $newKey = substr($key, -2)) { $key = $newKey; @@ -317,7 +317,7 @@ private function convertFilterArgsToTypes(array $args): array * * @throws InvalidTypeException */ - private function convertType(Type $type, bool $input = false, string $mutationName = null) + private function convertType(Type $type, bool $input = false, string $mutationName = null, int $depth = 0) { $resourceClass = null; switch ($builtinType = $type->getBuiltinType()) { @@ -341,7 +341,7 @@ private function convertType(Type $type, bool $input = false, string $mutationNa $graphqlType = $this->graphqlTypes['#iterable']; break; case Type::BUILTIN_TYPE_OBJECT: - if (is_a($type->getClassName(), \DateTimeInterface::class, true)) { + if (($input && $depth > 0) || is_a($type->getClassName(), \DateTimeInterface::class, true)) { $graphqlType = GraphQLType::string(); break; } @@ -357,14 +357,14 @@ private function convertType(Type $type, bool $input = false, string $mutationNa return null; } - $graphqlType = $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $mutationName); + $graphqlType = $this->getResourceObjectType($resourceClass, $resourceMetadata, $input, $mutationName, $depth); break; default: throw new InvalidTypeException(sprintf('The type "%s" is not supported.', $builtinType)); } if ($this->isCollection($type)) { - return $this->paginationEnabled ? $this->getResourcePaginatedCollectionType($resourceClass, $graphqlType, $input) : GraphQLType::listOf($graphqlType); + return $this->paginationEnabled && !$input ? $this->getResourcePaginatedCollectionType($resourceClass, $graphqlType) : GraphQLType::listOf($graphqlType); } return $type->isNullable() || (null !== $mutationName && 'update' === $mutationName) ? $graphqlType : GraphQLType::nonNull($graphqlType); @@ -375,7 +375,7 @@ private function convertType(Type $type, bool $input = false, string $mutationNa * * @return ObjectType|InputObjectType */ - private function getResourceObjectType(string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null): GraphQLType + private function getResourceObjectType(string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0): GraphQLType { if (isset($this->graphqlTypes[$resourceClass][$mutationName][$input])) { return $this->graphqlTypes[$resourceClass][$mutationName][$input]; @@ -395,8 +395,8 @@ private function getResourceObjectType(string $resourceClass, ResourceMetadata $ 'name' => $shortName, 'description' => $resourceMetadata->getDescription(), 'resolveField' => $this->defaultFieldResolver, - 'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName) { - return $this->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $mutationName); + 'fields' => function () use ($resourceClass, $resourceMetadata, $input, $mutationName, $depth) { + return $this->getResourceObjectTypeFields($resourceClass, $resourceMetadata, $input, $mutationName, $depth); }, 'interfaces' => [$this->getNodeInterface()], ]; @@ -407,7 +407,7 @@ private function getResourceObjectType(string $resourceClass, ResourceMetadata $ /** * Gets the fields of the type of the given resource. */ - private function getResourceObjectTypeFields(string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null): array + private function getResourceObjectTypeFields(string $resourceClass, ResourceMetadata $resourceMetadata, bool $input = false, string $mutationName = null, int $depth = 0): array { $fields = []; $idField = ['type' => GraphQLType::nonNull(GraphQLType::id())]; @@ -434,7 +434,7 @@ private function getResourceObjectTypeFields(string $resourceClass, ResourceMeta continue; } - if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, $propertyMetadata->getDescription(), $propertyType, $resourceClass, $input, $mutationName)) { + if ($fieldConfiguration = $this->getResourceFieldConfiguration($resourceClass, $resourceMetadata, $propertyMetadata->getDescription(), $propertyType, $resourceClass, $input, $mutationName, ++$depth)) { $fields['id' === $property ? '_id' : $property] = $fieldConfiguration; } } @@ -449,19 +449,16 @@ private function getResourceObjectTypeFields(string $resourceClass, ResourceMeta /** * Gets the type of a paginated collection of the given resource type. * - * @param ObjectType|InputObjectType $resourceType + * @param ObjectType $resourceType * - * @return ObjectType|InputObjectType + * @return ObjectType */ - private function getResourcePaginatedCollectionType(string $resourceClass, GraphQLType $resourceType, bool $input = false): GraphQLType + private function getResourcePaginatedCollectionType(string $resourceClass, GraphQLType $resourceType): GraphQLType { $shortName = $resourceType->name; - if ($input) { - $shortName .= 'Input'; - } - if (isset($this->graphqlTypes[$resourceClass]['connection'][$input])) { - return $this->graphqlTypes[$resourceClass]['connection'][$input]; + if (isset($this->graphqlTypes[$resourceClass]['connection'])) { + return $this->graphqlTypes[$resourceClass]['connection']; } $edgeObjectTypeConfiguration = [ @@ -472,7 +469,7 @@ private function getResourcePaginatedCollectionType(string $resourceClass, Graph 'cursor' => GraphQLType::nonNull(GraphQLType::string()), ], ]; - $edgeObjectType = $input ? new InputObjectType($edgeObjectTypeConfiguration) : new ObjectType($edgeObjectTypeConfiguration); + $edgeObjectType = new ObjectType($edgeObjectTypeConfiguration); $pageInfoObjectTypeConfiguration = [ 'name' => "{$shortName}PageInfo", 'description' => 'Information about the current page.', @@ -481,7 +478,7 @@ private function getResourcePaginatedCollectionType(string $resourceClass, Graph 'hasNextPage' => GraphQLType::nonNull(GraphQLType::boolean()), ], ]; - $pageInfoObjectType = $input ? new InputObjectType($pageInfoObjectTypeConfiguration) : new ObjectType($pageInfoObjectTypeConfiguration); + $pageInfoObjectType = new ObjectType($pageInfoObjectTypeConfiguration); $configuration = [ 'name' => "{$shortName}Connection", @@ -492,7 +489,7 @@ private function getResourcePaginatedCollectionType(string $resourceClass, Graph ], ]; - return $this->graphqlTypes[$resourceClass]['connection'][$input] = $input ? new InputObjectType($configuration) : new ObjectType($configuration); + return $this->graphqlTypes[$resourceClass]['connection'] = new ObjectType($configuration); } private function isCollection(Type $type): bool