Skip to content

Commit

Permalink
Merge pull request #89 from TomHAnderson/feature/filters-by-field-type
Browse files Browse the repository at this point in the history
Feature/filters by field type
  • Loading branch information
TomHAnderson authored Mar 29, 2024
2 parents 3960e7d + b9a156b commit 1ba2c24
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 30 deletions.
115 changes: 102 additions & 13 deletions src/Filter/FilterFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use ApiSkeletons\Doctrine\ORM\GraphQL\Filter\InputObjectType\Field;
use ApiSkeletons\Doctrine\ORM\GraphQL\Type\Entity\Entity;
use ApiSkeletons\Doctrine\ORM\GraphQL\Type\TypeManager;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use GraphQL\Type\Definition\InputObjectType as GraphQLInputObjectType;
Expand Down Expand Up @@ -118,10 +119,16 @@ protected function addFields(Entity $targetEntity, string $typeName, array $allo
continue;
}

$graphQLType = $this->typeManager
$type = $this->typeManager
->get($entityMetadata['fields'][$fieldName]['type']);

if (! $graphQLType instanceof ScalarType) {
// Custom types may hit this condition
if (! $type instanceof ScalarType) {
continue;
}

// Skip Blob fields
if ($type->name() === 'Blob') {
continue;
}

Expand All @@ -139,21 +146,24 @@ static function ($value) use ($fieldExcludeFilters) {
);
}

// Remove filters that are not allowed for this field type
$filteredFilters = $this->filterFiltersByType($allowedFilters, $type);

// ScalarType field filters are named by their field type
// and a hash of the allowed filters
$filterTypeName = 'Filters_' . $graphQLType->name . '_' . md5(serialize($allowedFilters));
$filterTypeName = 'Filters_' . $type->name() . '_' . md5(serialize($filteredFilters));

if ($this->typeManager->has($filterTypeName)) {
$type = $this->typeManager->get($filterTypeName);
$fieldType = $this->typeManager->get($filterTypeName);
} else {
$type = new Field($this->typeManager, $typeName, $fieldName, $graphQLType, $allowedFilters);
$this->typeManager->set($filterTypeName, $type);
$fieldType = new Field($this->typeManager, $type, $filteredFilters);
$this->typeManager->set($filterTypeName, $fieldType);
}

$fields[$fieldName] = [
'name' => $fieldName,
'type' => $type,
'description' => 'Filters for ' . $fieldName,
'type' => $fieldType,
'description' => $type->name() . ' Filters',
];
}

Expand Down Expand Up @@ -198,20 +208,99 @@ protected function addAssociations(Entity $targetEntity, string $typeName, array
$filterTypeName = 'Filters_ID_' . md5(serialize($allowedFilters));

if ($this->typeManager->has($filterTypeName)) {
$type = $this->typeManager->get($filterTypeName);
$associationType = $this->typeManager->get($filterTypeName);
} else {
$type = new Association($this->typeManager, $typeName, $associationName, Type::id(), [Filters::EQ]);
$this->typeManager->set($filterTypeName, $type);
$associationType = new Association($this->typeManager, Type::id(), [Filters::EQ]);
$this->typeManager->set($filterTypeName, $associationType);
}

// eq filter is for association id from parent entity
$fields[$associationName] = [
'name' => $associationName,
'type' => $type,
'description' => 'Filters for ' . $associationName,
'type' => $associationType,
'description' => 'Association Filters',
];
}

return $fields;
}

/**
* Filter the allowed filters based on the field type
*
* @param Filters[] $filters
*
* @return Filters[]
*/
protected function filterFiltersByType(array $filters, ScalarType $type): array
{
$filterCollection = new ArrayCollection($filters);

// Numbers
if (
in_array($type->name(), [
'Float',
'ID',
'Int',
'Integer',
])
) {
$filterCollection->removeElement(Filters::CONTAINS);
$filterCollection->removeElement(Filters::STARTSWITH);
$filterCollection->removeElement(Filters::ENDSWITH);

return $filterCollection->toArray();
}

// Booleans
if ($type->name() === 'Boolean') {
$filterCollection->removeElement(Filters::LT);
$filterCollection->removeElement(Filters::LTE);
$filterCollection->removeElement(Filters::GT);
$filterCollection->removeElement(Filters::GTE);
$filterCollection->removeElement(Filters::BETWEEN);
$filterCollection->removeElement(Filters::CONTAINS);
$filterCollection->removeElement(Filters::STARTSWITH);
$filterCollection->removeElement(Filters::ENDSWITH);

return $filterCollection->toArray();
}

// Strings
if (
in_array($type->name(), [
'String',
'Text',
])
) {
$filterCollection->removeElement(Filters::LT);
$filterCollection->removeElement(Filters::LTE);
$filterCollection->removeElement(Filters::GT);
$filterCollection->removeElement(Filters::GTE);
$filterCollection->removeElement(Filters::BETWEEN);

return $filterCollection->toArray();
}

// Dates and times
if (
in_array($type->name(), [
'Date',
'DateTime',
'DateTimeImmutable',
'DateTimeTZ',
'DateTimeTZImmutable',
'Time',
'TimeImmutable',
])
) {
$filterCollection->removeElement(Filters::CONTAINS);
$filterCollection->removeElement(Filters::STARTSWITH);
$filterCollection->removeElement(Filters::ENDSWITH);

return $filterCollection->toArray();
}

return $filterCollection->toArray();
}
}
17 changes: 8 additions & 9 deletions src/Filter/InputObjectType/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ class Field extends InputObjectType
/** @param Filters[] $allowedFilters */
public function __construct(
TypeManager $typeManager,
string $typeName,
string $fieldName,
ScalarType|ListOfType $type,
array $allowedFilters,
) {
Expand All @@ -38,30 +36,31 @@ public function __construct(
'description' => $filter->description(),
];

// Custom types may hit this condition
// @codeCoverageIgnoreStart
if (! $type instanceof ScalarType) {
continue;
}

// @codeCoverageIgnoreEnd

// Between is a special case filter.
// To avoid creating a new Between type for each field,
// check if the Between type exists and reuse it.
if (! ($fields[$filter->value]['type'] instanceof Between)) {
continue;
}

// Between is a special case filter.
// To avoid creating a new Between type for each field,
// check if the Between type exists and reuse it.
if ($typeManager->has('Between_' . $type->name)) {
$fields[$filter->value]['type'] = $typeManager->get('Between_' . $type->name);
if ($typeManager->has('Between_' . $type->name())) {
$fields[$filter->value]['type'] = $typeManager->get('Between_' . $type->name());
} else {
$betweenType = new Between($type);
$typeManager->set('Between_' . $type->name, $betweenType);
$typeManager->set('Between_' . $type->name(), $betweenType);
$fields[$filter->value]['type'] = $betweenType;
}
}

$typeName = $type instanceof ScalarType ? $type->name : uniqid();
$typeName = $type instanceof ScalarType ? $type->name() : uniqid();

// ScalarType field filters are named by their field type
// and a hash of the allowed filters
Expand Down
8 changes: 4 additions & 4 deletions test/Feature/Filter/ConfigExcludeFiltersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,28 +47,28 @@ public function testConfigExcludeFilters(): void
$result = GraphQL::executeQuery($schema, $query);

foreach ($result->errors as $error) {
$this->assertEquals('Field "eq" is not defined by type "Filters_String_b26f464f97a76891491eafa1573acb24".', $error->getMessage());
$this->assertEquals('Field "eq" is not defined by type "Filters_String_0812311810b0ba1d34247150620b78b0".', $error->getMessage());
}

$query = '{ artists (filter: { name: { neq: "Grateful Dead" } } ) { edges { node { name } } } }';
$result = GraphQL::executeQuery($schema, $query);

foreach ($result->errors as $error) {
$this->assertEquals('Field "neq" is not defined by type "Filters_String_b26f464f97a76891491eafa1573acb24".', $error->getMessage());
$this->assertEquals('Field "neq" is not defined by type "Filters_String_0812311810b0ba1d34247150620b78b0".', $error->getMessage());
}

$query = '{ artists { edges { node { performances ( filter: {venue: { neq: "test"} } ) { edges { node { venue } } } } } } }';
$result = GraphQL::executeQuery($schema, $query);

foreach ($result->errors as $error) {
$this->assertEquals('Field "neq" is not defined by type "Filters_String_b26f464f97a76891491eafa1573acb24".', $error->getMessage());
$this->assertEquals('Field "neq" is not defined by type "Filters_String_0812311810b0ba1d34247150620b78b0".', $error->getMessage());
}

$query = '{ artists { edges { node { performances ( filter: {venue: { contains: "test" } } ) { edges { node { venue } } } } } } }';
$result = GraphQL::executeQuery($schema, $query);

foreach ($result->errors as $error) {
$this->assertEquals('Field "contains" is not defined by type "Filters_String_b26f464f97a76891491eafa1573acb24". Did you mean "notin"?', $error->getMessage());
$this->assertEquals('Field "contains" is not defined by type "Filters_String_0812311810b0ba1d34247150620b78b0". Did you mean "notin"?', $error->getMessage());
}
}
}
8 changes: 4 additions & 4 deletions test/Feature/Filter/ExcludeFiltersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,28 +39,28 @@ public function testExcludeCriteria(): void
$result = GraphQL::executeQuery($schema, $query);

foreach ($result->errors as $error) {
$this->assertEquals('Field "eq" is not defined by type "Filters_String_436fb9911a1f07ad8eb7057c1a8e3d2b".', $error->getMessage());
$this->assertEquals('Field "eq" is not defined by type "Filters_String_a03586330c4e7326edac556450d913ee".', $error->getMessage());
}

$query = '{ artists (filter: { name: { neq: "Grateful Dead" } } ) { edges { node { name } } } }';
$result = GraphQL::executeQuery($schema, $query);

foreach ($result->errors as $error) {
$this->assertEquals('Field "neq" is not defined by type "Filters_String_436fb9911a1f07ad8eb7057c1a8e3d2b".', $error->getMessage());
$this->assertEquals('Field "neq" is not defined by type "Filters_String_a03586330c4e7326edac556450d913ee".', $error->getMessage());
}

$query = '{ artists { edges { node { performances ( filter: {venue: { neq: "test"} } ) { edges { node { venue } } } } } } }';
$result = GraphQL::executeQuery($schema, $query);

foreach ($result->errors as $error) {
$this->assertEquals('Field "neq" is not defined by type "Filters_String_bef569e688f8bb56acb1e0e4e430b055". Did you mean "eq"?', $error->getMessage());
$this->assertEquals('Field "neq" is not defined by type "Filters_String_e55a7b533af3c46236f06d0fb99f08c6". Did you mean "eq"?', $error->getMessage());
}

$query = '{ artists { edges { node { performances ( filter: {venue: { contains: "test" } } ) { edges { node { venue } } } } } } }';
$result = GraphQL::executeQuery($schema, $query);

foreach ($result->errors as $error) {
$this->assertEquals('Field "contains" is not defined by type "Filters_String_bef569e688f8bb56acb1e0e4e430b055". Did you mean "notin"?', $error->getMessage());
$this->assertEquals('Field "contains" is not defined by type "Filters_String_e55a7b533af3c46236f06d0fb99f08c6". Did you mean "notin"?', $error->getMessage());
}
}
}

0 comments on commit 1ba2c24

Please sign in to comment.