Skip to content
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

Natural filtering #5

Open
wants to merge 3 commits into
base: 1.0
Choose a base branch
from
Open
Changes from 1 commit
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
Next Next commit
Implemented natural field filtering
For each content type connection, a 'filter' argument allows to filter based on the content type's field definitions.
Each content type is matched with a {ContentType}Filter type with each searchable field from the type.

```
{
  content {
    articles(filter: {title: "~text"}) {
      edges {
        node {
          _name
        }
      }
    }
  }
}
```
  • Loading branch information
Bertrand Dunogier committed Aug 31, 2019

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature.
commit 633c52694bba6e9ae63da6607dc0e23aa6126e31
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ public function load(array $configs, ContainerBuilder $container)
$loader->load('services/mutations.yml');
$loader->load('services/resolvers.yml');
$loader->load('services/schema.yml');
$loader->load('services/search.yml');
$loader->load('services/services.yml');
$loader->load('default_settings.yml');
}
39 changes: 39 additions & 0 deletions src/DependencyInjection/Factory/SearchFeaturesFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/**
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
namespace EzSystems\EzPlatformGraphQL\DependencyInjection\Factory;

use eZ\Bundle\EzPublishCoreBundle\ApiLoader\RepositoryConfigurationProvider;

class SearchFeaturesFactory
{
/**
* @var \eZ\Bundle\EzPublishCoreBundle\ApiLoader\RepositoryConfigurationProvider
*/
private $configurationProvider;

/**
* @var \EzSystems\EzPlatformGraphQL\Search\SearchFeatures[]
*/
private $searchFeatures = [];

public function __construct(RepositoryConfigurationProvider $configurationProvider, array $searchFeatures)
{
$this->configurationProvider = $configurationProvider;
$this->searchFeatures = $searchFeatures;
}

public function build()
{
$searchEngine = $this->configurationProvider->getRepositoryConfig()['search']['engine'];

if (isset($this->searchFeatures[$searchEngine])) {
return $this->searchFeatures[$searchEngine];
} else {
throw new \InvalidArgumentException('Search engine not found');
}
}
}
103 changes: 103 additions & 0 deletions src/GraphQL/InputMapper/FieldsQueryMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

/**
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
namespace EzSystems\EzPlatformGraphQL\GraphQL\InputMapper;

use EzSystems\EzPlatformGraphQL\GraphQL\DataLoader\ContentTypeLoader;

/**
* Pre-processes the input to change fields passed using their identifier to the Field input key.
*/
class FieldsQueryMapper implements QueryMapper
{
/**
* @var QueryMapper
*/
private $innerMapper;
/**
* @var ContentTypeLoader
*/
private $contentTypeLoader;

public function __construct(ContentTypeLoader $contentTypeLoader, QueryMapper $innerMapper)
{
$this->innerMapper = $innerMapper;
$this->contentTypeLoader = $contentTypeLoader;
}

/**
* @param array $inputArray
*
* @return \eZ\Publish\API\Repository\Values\Content\Query
*/
public function mapInputToQuery(array $inputArray)
{
if (isset($inputArray['ContentTypeIdentifier']) && isset($inputArray['fieldsFilters'])) {
$contentType = $this->contentTypeLoader->loadByIdentifier($inputArray['ContentTypeIdentifier']);
$fieldsArgument = [];

foreach ($inputArray['fieldsFilters'] as $fieldDefinitionIdentifier => $value) {
if (($fieldDefinition = $contentType->getFieldDefinition($fieldDefinitionIdentifier)) === null) {
continue;
}

if (!$fieldDefinition->isSearchable) {
continue;
}

$fieldFilter = $this->buildFieldFilter($fieldDefinitionIdentifier, $value);
if ($fieldFilter !== null) {
$fieldsArgument[] = $fieldFilter;
}
}

$inputArray['Fields'] = $fieldsArgument;
}

return $this->innerMapper->mapInputToQuery($inputArray);
}

private function buildFieldFilter($fieldDefinitionIdentifier, $value)
{
if (is_array($value) && count($value) === 1) {
$value = $value[0];
}
$operator = 'eq';

// @todo if 3 items, and first item is 'between', use next two items as value
if (is_array($value)) {
$operator = 'in';
} elseif (is_string($value)) {
if ($value[0] === '~') {
$operator = 'like';
$value = substr($value, 1);
if (strpos($value, '%') === false) {
$value = "%$value%";
}
} elseif ($value[0] === '<') {
$value = substr($value, 1);
if ($value[0] === '=') {
$operator = 'lte';
$value = substr($value, 2);
} else {
$operator = 'lt';
$value = substr($value, 1);
}
} elseif ($value[0] === '<') {
$value = substr($value, 1);
if ($value[0] === '=') {
$operator = 'gte';
$value = substr($value, 2);
} else {
$operator = 'gt';
$value = substr($value, 1);
}
}
}

return ['target' => $fieldDefinitionIdentifier, $operator => trim($value)];
}
}
17 changes: 17 additions & 0 deletions src/GraphQL/InputMapper/QueryMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/**
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
namespace EzSystems\EzPlatformGraphQL\GraphQL\InputMapper;

interface QueryMapper
{
/**
* @param array $inputArray
*
* @return \eZ\Publish\API\Repository\Values\Content\Query
*/
public function mapInputToQuery(array $inputArray);
}
28 changes: 14 additions & 14 deletions src/GraphQL/InputMapper/SearchQueryMapper.php
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@
use eZ\Publish\API\Repository\Values\Content\Query;
use InvalidArgumentException;

class SearchQueryMapper
class SearchQueryMapper implements QueryMapper
{
/**
* @param array $inputArray
@@ -36,19 +36,19 @@ public function mapInputToQuery(array $inputArray)
}

if (isset($inputArray['Field'])) {
if (isset($inputArray['Field']['target'])) {
$criteria[] = $this->mapInputToFieldCriterion($inputArray['Field']);
} else {
$criteria = array_merge(
$criteria,
array_map(
function ($input) {
return $this->mapInputToFieldCriterion($input);
},
$inputArray['Field']
)
);
}
$inputArray['Fields'] = [$inputArray['Field']];
}

if (isset($inputArray['Fields'])) {
$criteria = array_merge(
$criteria,
array_map(
function ($input) {
return $this->mapInputToFieldCriterion($input);
},
$inputArray['Fields']
)
);
}

if (isset($inputArray['ParentLocationId'])) {
7 changes: 4 additions & 3 deletions src/GraphQL/Resolver/SearchResolver.php
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@
namespace EzSystems\EzPlatformGraphQL\GraphQL\Resolver;

use EzSystems\EzPlatformGraphQL\GraphQL\DataLoader\ContentLoader;
use EzSystems\EzPlatformGraphQL\GraphQL\InputMapper\SearchQueryMapper;
use EzSystems\EzPlatformGraphQL\GraphQL\InputMapper\QueryMapper;
use eZ\Publish\API\Repository\SearchService;
use Overblog\GraphQLBundle\Relay\Connection\Paginator;

@@ -22,7 +22,7 @@ class SearchResolver
private $searchService;

/**
* @var SearchQueryMapper
* @var QueryMapper
*/
private $queryMapper;

@@ -31,7 +31,7 @@ class SearchResolver
*/
private $contentLoader;

public function __construct(ContentLoader $contentLoader, SearchService $searchService, SearchQueryMapper $queryMapper)
public function __construct(ContentLoader $contentLoader, SearchService $searchService, QueryMapper $queryMapper)
{
$this->contentLoader = $contentLoader;
$this->searchService = $searchService;
@@ -48,6 +48,7 @@ public function searchContent($args)
public function searchContentOfTypeAsConnection($contentTypeIdentifier, $args)
{
$query = $args['query'] ?: [];
$query['fieldsFilters'] = $args['filter'] ?: [];
$query['ContentTypeIdentifier'] = $contentTypeIdentifier;
$query['sortBy'] = $args['sortBy'];
$query = $this->queryMapper->mapInputToQuery($query);
31 changes: 31 additions & 0 deletions src/Resources/config/services/search.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
services:
_defaults:
autoconfigure: true
autowire: true
public: false

EzSystems\EzPlatformGraphQL\DependencyInjection\Factory\SearchFeaturesFactory:
arguments:
$configurationProvider: '@ezpublish.api.repository_configuration_provider'
$searchFeatures:
solr: '@EzSystems\EzPlatformGraphQL\Search\SolrSearchFeatures'
legacy: '@EzSystems\EzPlatformGraphQL\Search\LegacySearchFeatures'


EzSystems\EzPlatformGraphQL\Search\SearchFeatures:
factory: ['@EzSystems\EzPlatformGraphQL\DependencyInjection\Factory\SearchFeaturesFactory', build]

EzSystems\EzPlatformGraphQL\Search\SolrSearchFeatures: ~

EzSystems\EzPlatformGraphQL\Search\LegacySearchFeatures:
arguments:
$converterRegistry: '@ezpublish.persistence.legacy.field_value_converter.registry'

EzSystems\EzPlatformGraphQL\GraphQL\InputMapper\QueryMapper: '@EzSystems\EzPlatformGraphQL\GraphQL\InputMapper\SearchQueryMapper'

EzSystems\EzPlatformGraphQL\GraphQL\InputMapper\SearchQueryMapper: ~

EzSystems\EzPlatformGraphQL\GraphQL\InputMapper\FieldsQueryMapper:
decorates: EzSystems\EzPlatformGraphQL\GraphQL\InputMapper\SearchQueryMapper
arguments:
$innerMapper: '@EzSystems\EzPlatformGraphQL\GraphQL\InputMapper\FieldsQueryMapper.inner'
2 changes: 0 additions & 2 deletions src/Resources/config/services/services.yml
Original file line number Diff line number Diff line change
@@ -11,5 +11,3 @@ services:
- { name: console.command }

EzSystems\EzPlatformGraphQL\GraphQL\TypeDefinition\ContentTypeMapper: ~

EzSystems\EzPlatformGraphQL\GraphQL\InputMapper\SearchQueryMapper: ~
5 changes: 5 additions & 0 deletions src/Schema/Domain/Content/NameHelper.php
Original file line number Diff line number Diff line change
@@ -93,6 +93,11 @@ public function fieldDefinitionField(FieldDefinition $fieldDefinition)
return lcfirst($this->toCamelCase($fieldDefinition->identifier));
}

public function filterType(ContentType $contentType)
{
return $this->domainContentName($contentType) . 'Filter';
}

private function toCamelCase($string)
{
return $this->caseConverter->denormalize($string);
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

/**
* @copyright Copyright (C) eZ Systems AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
namespace EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Worker\ContentType;

use eZ\Publish\API\Repository\Values\ContentType\ContentTypeGroup;
use EzSystems\EzPlatformGraphQL\Schema\Domain\Content\Worker\BaseWorker;
use EzSystems\EzPlatformGraphQL\Schema\Worker;
use EzSystems\EzPlatformGraphQL\Schema\Builder;
use EzSystems\EzPlatformGraphQL\Schema\Builder\Input;
use eZ\Publish\API\Repository\Values\ContentType\ContentType;

class DefineDomainContentFilter extends BaseWorker implements Worker
{
public function work(Builder $schema, array $args)
{
$schema->addType(new Input\Type($this->filterType($args), 'input-object'));
$schema->addArgToField(
$this->groupType($args),
$this->connectionField($args),
new Input\Arg('filter', $this->filterType($args))
);
}

public function canWork(Builder $schema, array $args)
{
return isset($args['ContentTypeGroup']) && $args['ContentTypeGroup'] instanceof ContentTypeGroup
&& isset($args['ContentType']) && $args['ContentType'] instanceof ContentType
&& !$schema->hasType($this->filterType($args));
}

protected function filterType(array $args): string
{
return $this->getNameHelper()->filterType($args['ContentType']);
}

protected function groupType(array $args): string
{
return $this->getNameHelper()->domainGroupName($args['ContentTypeGroup']);
}

protected function connectionField(array $args): string
{
return $this->getNameHelper()->domainContentCollectionField($args['ContentType']);
}
}
Loading