Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
258 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
Feature: GraphQL query support | ||
|
||
@elasticsearch | ||
Scenario: Execute a GraphQL query on an ElasticSearch model with SubResources | ||
When I send the following GraphQL request: | ||
""" | ||
query { | ||
users { | ||
edges { | ||
node { | ||
id, | ||
gender, | ||
tweets { | ||
edges { | ||
node { | ||
id, | ||
message, | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
""" | ||
Then print last response | ||
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": { | ||
"users": { | ||
"edges": [ | ||
{ | ||
"node": { | ||
"id": "/users/116b83f8-6c32-48d8-8e28-c5c247532d3f", | ||
"gender": "male", | ||
"tweets": { | ||
"edges": [ | ||
{ | ||
"node": { | ||
"id": "/tweets/89601e1c-3ef2-4ef7-bca2-7511d38611c6", | ||
"message": "Great day in any endur... During the Himalayas were very talented skimo racer junior podiums, top 10." | ||
} | ||
}, | ||
{ | ||
"node": { | ||
"id": "/tweets/9da70727-d656-42d9-876a-1be6321f171b", | ||
"message": "During the path and his Summits Of My Life project. Next Wednesday, Kilian Jornet..." | ||
} | ||
}, | ||
{ | ||
"node": { | ||
"id": "/tweets/f36a0026-0635-4865-86a6-5adb21d94d64", | ||
"message": "The north summit, Store Vengetind Thanks for t... These Top 10 Women of a fk... Francois is the field which." | ||
} | ||
} | ||
] | ||
} | ||
} | ||
}, | ||
{ | ||
"node": { | ||
"id": "/users/15fce6f1-18fd-4ef6-acab-7e6a3333ec7f", | ||
"gender": "male", | ||
"tweets": { | ||
"edges": [ | ||
{ | ||
"node": { | ||
"id": "/tweets/0cfe3d33-6116-416b-8c50-3b8319331998", | ||
"message": "Thanks! Fun day with Next up one of our 2018 cover: One look into what races we'll be running that they!" | ||
} | ||
} | ||
] | ||
} | ||
} | ||
}, | ||
{ | ||
"node": { | ||
"id": "/users/6a457188-d1ba-45e3-8509-81e5c66a5297", | ||
"gender": "female", | ||
"tweets": { | ||
"edges": [ | ||
{ | ||
"node": { | ||
"id": "/tweets/9de3308c-6f82-4a57-a33c-4e3cd5d5a3f6", | ||
"message": "In case you do! A humble beginning to traverse... Im so now until you can't tell how strong she run at!" | ||
} | ||
} | ||
] | ||
} | ||
} | ||
} | ||
] | ||
} | ||
} | ||
} | ||
""" |
141 changes: 141 additions & 0 deletions
141
src/Bridge/Elasticsearch/DataProvider/SubresourceDataProvider.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the API Platform project. | ||
* | ||
* (c) Kévin Dunglas <dunglas@gmail.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
declare(strict_types=1); | ||
|
||
namespace ApiPlatform\Core\Bridge\Elasticsearch\DataProvider; | ||
|
||
use ApiPlatform\Core\Bridge\Elasticsearch\Api\IdentifierExtractorInterface; | ||
use ApiPlatform\Core\Bridge\Elasticsearch\Exception\IndexNotFoundException; | ||
use ApiPlatform\Core\Bridge\Elasticsearch\Exception\NonUniqueIdentifierException; | ||
use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\Factory\DocumentMetadataFactoryInterface; | ||
use ApiPlatform\Core\DataProvider\Pagination; | ||
use ApiPlatform\Core\DataProvider\RestrictedDataProviderInterface; | ||
use ApiPlatform\Core\DataProvider\SubresourceDataProviderInterface; | ||
use ApiPlatform\Core\Exception\ResourceClassNotFoundException; | ||
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; | ||
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; | ||
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; | ||
use Elasticsearch\Client; | ||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; | ||
|
||
final class SubresourceDataProvider implements SubresourceDataProviderInterface, RestrictedDataProviderInterface | ||
{ | ||
private $client; | ||
private $documentMetadataFactory; | ||
private $identifierExtractor; | ||
private $denormalizer; | ||
private $pagination; | ||
private $resourceMetadataFactory; | ||
private $propertyNameCollectionFactory; | ||
private $propertyMetadataFactory; | ||
private $collectionExtensions; | ||
|
||
public function __construct(Client $client, DocumentMetadataFactoryInterface $documentMetadataFactory, IdentifierExtractorInterface $identifierExtractor, DenormalizerInterface $denormalizer, Pagination $pagination, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, iterable $collectionExtensions = []) | ||
{ | ||
$this->client = $client; | ||
$this->documentMetadataFactory = $documentMetadataFactory; | ||
$this->identifierExtractor = $identifierExtractor; | ||
$this->denormalizer = $denormalizer; | ||
$this->pagination = $pagination; | ||
$this->resourceMetadataFactory = $resourceMetadataFactory; | ||
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory; | ||
$this->propertyMetadataFactory = $propertyMetadataFactory; | ||
$this->collectionExtensions = $collectionExtensions; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function supports(string $resourceClass, ?string $operationName = null, array $context = []): bool | ||
{ | ||
try { | ||
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); | ||
if (false === $resourceMetadata->getCollectionOperationAttribute($operationName, 'elasticsearch', true, true)) { | ||
return false; | ||
} | ||
} catch (ResourceClassNotFoundException $e) { | ||
return false; | ||
} | ||
|
||
try { | ||
$this->documentMetadataFactory->create($resourceClass); | ||
} catch (IndexNotFoundException $e) { | ||
return false; | ||
} | ||
|
||
// Elasticsearch does not support subresources as items (yet) | ||
if (false === ($context['collection'] ?? false)) { | ||
return false; | ||
} | ||
|
||
try { | ||
$this->identifierExtractor->getIdentifierFromResourceClass($resourceClass); | ||
} catch (NonUniqueIdentifierException $e) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getSubresource(string $resourceClass, array $identifiers, array $context, string $operationName = null) | ||
{ | ||
$documentMetadata = $this->documentMetadataFactory->create($resourceClass); | ||
$body = []; | ||
|
||
foreach ($this->collectionExtensions as $collectionExtension) { | ||
$body = $collectionExtension->applyToCollection($body, $resourceClass, $operationName, $context); | ||
} | ||
|
||
[$relationClass, $identiferProperty] = $context['identifiers']['id']; | ||
|
||
$propertyName = null; | ||
|
||
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) { | ||
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property); | ||
$type = $propertyMetadata->getType(); | ||
|
||
if ($type && $type->getClassName() === $relationClass) { | ||
$propertyName = $property; | ||
break; | ||
} | ||
} | ||
|
||
$relationQuery = ['match' => [$propertyName.'.'.$identiferProperty => $identifiers['id']]]; | ||
|
||
if (!isset($body['query']) && !isset($body['aggs'])) { | ||
$body['query'] = $relationQuery; | ||
} else { | ||
$body['query']['constant_score']['filter']['bool']['must'][] = $relationQuery; | ||
} | ||
|
||
$limit = $body['size'] = $body['size'] ?? $this->pagination->getLimit($resourceClass, $operationName, $context); | ||
$offset = $body['from'] = $body['from'] ?? $this->pagination->getOffset($resourceClass, $operationName, $context); | ||
|
||
$documents = $this->client->search([ | ||
'index' => $documentMetadata->getIndex(), | ||
'type' => $documentMetadata->getType(), | ||
'body' => $body, | ||
]); | ||
|
||
return new Paginator( | ||
$this->denormalizer, | ||
$documents, | ||
$resourceClass, | ||
$limit, | ||
$offset, | ||
$context | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters