Skip to content

feat(graphql): added support for graphql subscriptions to work for actions #6904

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
43 changes: 43 additions & 0 deletions src/GraphQl/Serializer/ItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use ApiPlatform\GraphQl\State\Provider\NoopProvider;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\GraphQl\Query;
use ApiPlatform\Metadata\GraphQl\QueryCollection;
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
use ApiPlatform\Metadata\IriConverterInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
Expand All @@ -26,6 +27,7 @@
use ApiPlatform\Metadata\Util\ClassInfoTrait;
use ApiPlatform\Serializer\CacheKeyTrait;
use ApiPlatform\Serializer\ItemNormalizer as BaseItemNormalizer;
use Doctrine\Common\Collections\Collection;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
Expand Down Expand Up @@ -106,6 +108,11 @@
$data[self::ITEM_IDENTIFIERS_KEY] = $this->identifiersExtractor->getIdentifiersFromItem($object, $context['operation'] ?? null);
}

if (isset($context['graphql_operation_name']) && 'mercure_subscription' === $context['graphql_operation_name'] && \is_object($object) && isset($data['id']) && !isset($data['_id'])) {
$data['_id'] = $data['id'];
$data['id'] = $this->iriConverter->getIriFromResource($object);

Check warning on line 113 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L112-L113

Added lines #L112 - L113 were not covered by tests
}

return $data;
}

Expand All @@ -120,10 +127,46 @@
return [...$attributeValue];
}

// Handle relationships for mercure subscriptions
if ($operation instanceof QueryCollection && 'mercure_subscription' === $context['graphql_operation_name'] && $attributeValue instanceof Collection && !$attributeValue->isEmpty()) {
$relationContext = $context;

Check warning on line 132 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L131-L132

Added lines #L131 - L132 were not covered by tests
// Grab collection attributes
$relationContext['attributes'] = $context['attributes']['collection'];

Check warning on line 134 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L134

Added line #L134 was not covered by tests
// Iterate over the collection and normalize each item
$data['collection'] = $attributeValue
->map(fn ($item) => $this->normalize($item, $format, $relationContext))
// Convert the collection to an array
->toArray();

Check warning on line 139 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L136-L139

Added lines #L136 - L139 were not covered by tests

// Handle pagination if it's enabled in the query
return $this->addPagination($attributeValue, $data, $context);

Check warning on line 142 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L142

Added line #L142 was not covered by tests
}

// to-many are handled directly by the GraphQL resolver
return [];
}

private function addPagination(Collection $collection, array $data, array $context): array

Check warning on line 149 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L149

Added line #L149 was not covered by tests
{
if ($context['attributes']['paginationInfo'] ?? false) {
$data['paginationInfo'] = [];
if (\array_key_exists('hasNextPage', $context['attributes']['paginationInfo'])) {
$data['paginationInfo']['hasNextPage'] = $collection->count() > ($context['pagination']['itemsPerPage'] ?? 10);

Check warning on line 154 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L151-L154

Added lines #L151 - L154 were not covered by tests
}
if (\array_key_exists('itemsPerPage', $context['attributes']['paginationInfo'])) {
$data['paginationInfo']['itemsPerPage'] = $context['pagination']['itemsPerPage'] ?? 10;

Check warning on line 157 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L156-L157

Added lines #L156 - L157 were not covered by tests
}
if (\array_key_exists('lastPage', $context['attributes']['paginationInfo'])) {
$data['paginationInfo']['lastPage'] = (int) ceil($collection->count() / ($context['pagination']['itemsPerPage'] ?? 10));

Check warning on line 160 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L159-L160

Added lines #L159 - L160 were not covered by tests
}
if (\array_key_exists('totalCount', $context['attributes']['paginationInfo'])) {
$data['paginationInfo']['totalCount'] = $collection->count();

Check warning on line 163 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L162-L163

Added lines #L162 - L163 were not covered by tests
}
}

return $data;

Check warning on line 167 in src/GraphQl/Serializer/ItemNormalizer.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Serializer/ItemNormalizer.php#L167

Added line #L167 was not covered by tests
}

/**
* {@inheritdoc}
*/
Expand Down
6 changes: 5 additions & 1 deletion src/GraphQl/State/Processor/SubscriptionProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\GraphQl\Subscription\OperationAwareSubscriptionManagerInterface;
use ApiPlatform\GraphQl\Subscription\SubscriptionManagerInterface;
use ApiPlatform\Metadata\GraphQl\Operation as GraphQlOperation;
use ApiPlatform\Metadata\GraphQl\Subscription;
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;

Expand All @@ -32,7 +33,7 @@
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
{
$data = $this->decorated->process($data, $operation, $uriVariables, $context);
if (!$operation instanceof GraphQlOperation || !($mercure = $operation->getMercure())) {
if (!$operation instanceof Subscription || !($mercure = $operation->getMercure())) {
return $data;
}

Expand All @@ -49,6 +50,9 @@

$hub = \is_array($mercure) ? ($mercure['hub'] ?? null) : null;
$data['mercureUrl'] = $this->mercureSubscriptionIriGenerator->generateMercureUrl($subscriptionId, $hub);
if ($operation instanceof Subscription) {
$data['isCollection'] = $operation->isCollection();

Check warning on line 54 in src/GraphQl/State/Processor/SubscriptionProcessor.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/State/Processor/SubscriptionProcessor.php#L53-L54

Added lines #L53 - L54 were not covered by tests
}
}

return $data;
Expand Down
14 changes: 14 additions & 0 deletions src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,21 @@
public function generateSubscriptionIdentifier(array $fields): string
{
unset($fields['mercureUrl'], $fields['clientSubscriptionId']);
$fields = $this->removeTypename($fields);

Check warning on line 26 in src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php#L26

Added line #L26 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this needed?

Copy link
Contributor Author

@psihius psihius Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When executing a subscription GraphQL query, the __typename key is not present in fields in the SubscriptionProcessor, but when you get fields in doctrine subscriber to publish updates, the __typename is present in the fields. That results in different sha256 hashes as subscription id, which breaks publishing.


return hash('sha256', print_r($fields, true));
}

private function removeTypename(array $data): array

Check warning on line 31 in src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php#L31

Added line #L31 was not covered by tests
{
foreach ($data as $key => $value) {
if ('__typename' === $key) {
unset($data[$key]);
} elseif (\is_array($value)) {
$data[$key] = $this->removeTypename($value);

Check warning on line 37 in src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php#L33-L37

Added lines #L33 - L37 were not covered by tests
}
}

return $data;

Check warning on line 41 in src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php

View check run for this annotation

Codecov / codecov/patch

src/GraphQl/Subscription/SubscriptionIdentifierGenerator.php#L41

Added line #L41 was not covered by tests
}
}
Loading
Loading