Skip to content

Commit

Permalink
Merge pull request #3 from itk-dev/feature/418-filter-and-pagination
Browse files Browse the repository at this point in the history
418: Added paginator to search results
  • Loading branch information
cableman authored Jan 15, 2024
2 parents b9df9df + 3d148d0 commit 4256023
Show file tree
Hide file tree
Showing 14 changed files with 292 additions and 70 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ See [keep a changelog] for information about writing changes to this log.
- Added basic index service.
- Basic events DTO added.
- Added tags filter
- Added pagination and match filter

[keep a changelog]: https://keepachangelog.com/en/1.1.0/
[unreleased]: https://github.com/itk-dev/event-database-imports/compare/main...develop
11 changes: 8 additions & 3 deletions baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,14 @@
</file>
<file src="src/Api/State/EventRepresentationProvider.php">
<InvalidReturnStatement>
<code><![CDATA[$this->index->getAll(IndexNames::Events->value, $filters, $from, self::PAGE_SIZE)]]></code>
<code><![CDATA[[$this->index->get(IndexNames::Events->value, $uriVariables['id'])['_source']]]]></code>
</InvalidReturnStatement>
<MissingTemplateParam>
<code>ProviderInterface</code>
</MissingTemplateParam>
</file>
<file src="src/Api/State/OrganizationRepresentationProvider.php">
<InvalidReturnStatement>
<code><![CDATA[$this->index->getAll(IndexNames::Organization->value, $filters, $from, self::PAGE_SIZE)]]></code>
<code><![CDATA[[$this->index->get(IndexNames::Organization->value, $uriVariables['id'])['_source']]]]></code>
</InvalidReturnStatement>
<MissingTemplateParam>
Expand Down Expand Up @@ -51,10 +50,16 @@
<code>\HttpException</code>
</UndefinedDocblockClass>
</file>
<file src="src/Service/ElasticSearchIndex.php">
<file src="src/Service/ElasticSearch/ElasticSearchIndex.php">
<InvalidArgument>
<code>$params</code>
<code><![CDATA[['name' => $indexName]]]></code>
</InvalidArgument>
</file>
<file src="src/Service/ElasticSearch/ElasticSearchPaginator.php">
<MissingTemplateParam>
<code>PaginatorInterface</code>
<code>\IteratorAggregate</code>
</MissingTemplateParam>
</file>
</files>
1 change: 0 additions & 1 deletion config/packages/api_platform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ api_platform:

docs_formats:
jsonld: ['application/ld+json']
jsonopenapi: ['application/vnd.openapi+json']
html: ['text/html']

mapping:
Expand Down
44 changes: 43 additions & 1 deletion public/spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ paths:
style: form
explode: false
allowReserved: false
-
name: itemsPerPage
in: query
description: 'The number of items per page'
required: false
deprecated: false
allowEmptyValue: true
schema:
type: integer
default: 10
minimum: 0
maximum: 50
style: form
explode: false
allowReserved: false
-
name: tags
in: query
Expand Down Expand Up @@ -119,6 +134,33 @@ paths:
style: form
explode: false
allowReserved: false
-
name: itemsPerPage
in: query
description: 'The number of items per page'
required: false
deprecated: false
allowEmptyValue: true
schema:
type: integer
default: 20
minimum: 0
maximum: 100
style: form
explode: false
allowReserved: false
-
name: name
in: query
description: 'Search field based on value given'
required: false
deprecated: false
allowEmptyValue: true
schema:
type: string
style: form
explode: false
allowReserved: false
deprecated: false
parameters: []
'/api/v2/organizations/{id}':
Expand All @@ -129,7 +171,7 @@ paths:
responses:
200:
description: 'Single organization'
summary: 'Get single organization base on identifier'
summary: 'Get single organization based on identifier'
description: 'Retrieves a Organization resource.'
parameters:
-
Expand Down
51 changes: 28 additions & 23 deletions src/Api/Dto/Event.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,33 +10,38 @@
use App\Api\Filter\EventTagFilter;
use App\Api\State\EventRepresentationProvider;

#[ApiResource(operations: [
new Get(
openapiContext: [
'parameters' => [
[
'name' => 'id',
'in' => 'path',
'required' => true,
'schema' => [
'type' => 'integer',
#[ApiResource(
operations: [
new Get(
openapiContext: [
'parameters' => [
[
'name' => 'id',
'in' => 'path',
'required' => true,
'schema' => [
'type' => 'integer',
],
],
],
],
'responses' => [
'200' => [
'description' => 'Single event',
'responses' => [
'200' => [
'description' => 'Single event',
],
],
],
],
output: EventRepresentationProvider::class,
provider: EventRepresentationProvider::class,
),
new GetCollection(
output: EventRepresentationProvider::class,
provider: EventRepresentationProvider::class,
),
])]
output: EventRepresentationProvider::class,
provider: EventRepresentationProvider::class,
),
new GetCollection(
output: EventRepresentationProvider::class,
provider: EventRepresentationProvider::class,
),
],
paginationClientItemsPerPage: true,
paginationItemsPerPage: 10,
paginationMaximumItemsPerPage: 50
)]
#[ApiFilter(
EventTagFilter::class,
properties: ['tags']
Expand Down
59 changes: 35 additions & 24 deletions src/Api/Dto/Organization.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,51 @@

namespace App\Api\Dto;

use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Api\Filter\MatchFilter;
use App\Api\State\OrganizationRepresentationProvider;

#[ApiResource(operations: [
new Get(
openapiContext: [
'summary' => 'Get single organization base on identifier',
'parameters' => [
[
'name' => 'id',
'in' => 'path',
'required' => true,
'schema' => [
'type' => 'integer',
#[ApiResource(
operations: [
new Get(
openapiContext: [
'summary' => 'Get single organization based on identifier',
'parameters' => [
[
'name' => 'id',
'in' => 'path',
'required' => true,
'schema' => [
'type' => 'integer',
],
],
],
],
'responses' => [
'200' => [
'description' => 'Single organization',
'responses' => [
'200' => [
'description' => 'Single organization',
],
],
],
],
output: OrganizationRepresentationProvider::class,
provider: OrganizationRepresentationProvider::class,
),
new GetCollection(
output: OrganizationRepresentationProvider::class,
provider: OrganizationRepresentationProvider::class,
),
])]
output: OrganizationRepresentationProvider::class,
provider: OrganizationRepresentationProvider::class,
),
new GetCollection(
output: OrganizationRepresentationProvider::class,
provider: OrganizationRepresentationProvider::class,
),
],
paginationClientItemsPerPage: true,
paginationItemsPerPage: 20,
paginationMaximumItemsPerPage: 100
)]
#[ApiFilter(
MatchFilter::class,
properties: ['name']
)]
class Organization
{
#[ApiProperty(
Expand Down
50 changes: 50 additions & 0 deletions src/Api/Filter/MatchFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace App\Api\Filter;

use ApiPlatform\Elasticsearch\Filter\AbstractFilter;
use ApiPlatform\Metadata\Operation;
use Symfony\Component\PropertyInfo\Type;

/**
* This class represents a filter that performs a search based on matching properties in a given resource.
*/
final class MatchFilter extends AbstractFilter
{
public function apply(array $clauseBody, string $resourceClass, Operation $operation = null, array $context = []): array
{
$properties = $this->getProperties($resourceClass);
$matches = [];

/** @var string $property */
foreach ($properties as $property) {
$matches[] = ['match' => [$property => $context['filters'][$property]]];
}

return isset($matches[1]) ? ['bool' => ['should' => $matches]] : $matches[0];
}

public function getDescription(string $resourceClass): array
{
if (!$this->properties) {
return [];
}

$description = [];
foreach ($this->properties as $filterParameterName => $value) {
$description[$filterParameterName] = [
'property' => $filterParameterName,
'type' => Type::BUILTIN_TYPE_STRING,
'required' => false,
'description' => 'Search field based on value given',
'openapi' => [
'allowReserved' => false,
'allowEmptyValue' => true,
'explode' => false,
],
];
}

return $description;
}
}
20 changes: 17 additions & 3 deletions src/Api/State/AbstractProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/
abstract class AbstractProvider
{
protected const PAGE_SIZE = 10;
protected const PAGE_SIZE_FALLBACK = 10;

public function __construct(
protected readonly IndexInterface $index,
Expand Down Expand Up @@ -70,14 +70,28 @@ protected function getFilters(Operation $operation, array $context = []): array
* Calculates the offset for a paginated result based on the provided context.
*
* @param array $context
* The context containing the pagination filters
* The context containing the pagination information
*
* @return int
* The calculated offset value
*/
protected function calculatePageOffset(array $context): int
{
return (($context['filters']['page'] ?? 1) - 1) * self::PAGE_SIZE;
return (($context['filters']['page'] ?? 1) - 1) * $this->getImagesPerPage($context);
}

/**
* Retrieves the number of items per page.
*
* @param array $context
* The context containing the pagination information
*
* @return int
* The number of items per page as determined by the context
*/
protected function getImagesPerPage(array $context): int
{
return $context['filters']['itemsPerPage'] ?? self::PAGE_SIZE_FALLBACK;
}

/**
Expand Down
14 changes: 7 additions & 7 deletions src/Api/State/EventRepresentationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Model\IndexNames;
use App\Service\ElasticSearch\ElasticSearchPaginator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;

Expand All @@ -16,18 +17,17 @@ final class EventRepresentationProvider extends AbstractProvider implements Prov
* @throws NotFoundExceptionInterface
* @throws \App\Exception\IndexException
*/
public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ElasticSearchPaginator|array|null
{
// @TODO: should we create enum with 5,10,15,20
// Get page size from context.

if ($operation instanceof CollectionOperationInterface) {
$filters = $this->getFilters($operation, $context);
$from = $this->calculatePageOffset($context);
$offset = $this->calculatePageOffset($context);
$limit = $this->getImagesPerPage($context);
$results = $this->index->getAll(IndexNames::Events->value, $filters, $offset, $limit);

return $this->index->getAll(IndexNames::Events->value, $filters, $from, self::PAGE_SIZE);
return new ElasticSearchPaginator($results, $limit, $offset);
}

return (object) $this->index->get(IndexNames::Events->value, $uriVariables['id'])['_source'];
return [$this->index->get(IndexNames::Events->value, $uriVariables['id'])['_source']];
}
}
7 changes: 5 additions & 2 deletions src/Api/State/OrganizationRepresentationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProviderInterface;
use App\Model\IndexNames;
use App\Service\ElasticSearch\ElasticSearchPaginator;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;

Expand All @@ -20,9 +21,11 @@ public function provide(Operation $operation, array $uriVariables = [], array $c
{
if ($operation instanceof CollectionOperationInterface) {
$filters = $this->getFilters($operation, $context);
$from = $this->calculatePageOffset($context);
$offset = $this->calculatePageOffset($context);
$limit = $this->getImagesPerPage($context);
$results = $this->index->getAll(IndexNames::Organization->value, $filters, $offset, $limit);

return $this->index->getAll(IndexNames::Organization->value, $filters, $from, self::PAGE_SIZE);
return new ElasticSearchPaginator($results, $limit, $offset);
}

return [$this->index->get(IndexNames::Organization->value, $uriVariables['id'])['_source']];
Expand Down
Loading

0 comments on commit 4256023

Please sign in to comment.