diff --git a/CHANGELOG.md b/CHANGELOG.md index fd68f8b..e5dde1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ See [keep a changelog] for information about writing changes to this log. - Added pagination and match filter - Added date filters - Ensure combined filters are possible -- -Added api endpoinst for occurrences, location, tags, vocabularies and filters +- Added api endpoinst for occurrences, location, tags, vocabularies and filters +- Sort response [keep a changelog]: https://keepachangelog.com/en/1.1.0/ [unreleased]: https://github.com/itk-dev/event-database-imports/compare/main...develop diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 3c02812..a4eeba0 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,5 +1,5 @@ # itk-version: 3.2.0 -version: "3.8" + services: phpfpm: environment: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index c4ca834..9526ed6 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -1,5 +1,3 @@ -version: "3" - services: phpfpm: networks: @@ -20,20 +18,20 @@ services: - RABBITMQ_DEFAULT_PASS=password - RABBITMQ_ERLANG_COOKIE='d53f319cd7376f8f840aaf9889f315ab - elasticsearch: - image: elasticsearch:8.10.2 - networks: - - app - - frontend - ports: - - "9200" - deploy: - resources: - limits: - memory: 1096M - reservations: - memory: 1096M - environment: - - discovery.type=single-node - - xpack.security.enabled=false +# elasticsearch: +# image: elasticsearch:8.10.2 +# networks: +# - app +# - frontend +# ports: +# - "9200" +# deploy: +# resources: +# limits: +# memory: 1096M +# reservations: +# memory: 1096M +# environment: +# - discovery.type=single-node +# - xpack.security.enabled=false diff --git a/docker-compose.redirect.yml b/docker-compose.redirect.yml index 0fafb31..3b33b5a 100644 --- a/docker-compose.redirect.yml +++ b/docker-compose.redirect.yml @@ -1,5 +1,4 @@ # itk-version: 3.2.0 -version: "3.8" services: nginx: diff --git a/docker-compose.server.yml b/docker-compose.server.yml index cfe68c4..a61a973 100644 --- a/docker-compose.server.yml +++ b/docker-compose.server.yml @@ -1,5 +1,4 @@ # itk-version: 3.2.0 -version: "3.8" networks: frontend: diff --git a/docker-compose.yml b/docker-compose.yml index b47e495..4ba4c09 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,4 @@ # itk-version: 3.2.0 -version: "3.8" networks: frontend: diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 9dfc66a..dcd3a70 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -64,29 +64,11 @@ ProviderInterface - - - - - - index->get(IndexNames::Tags->value, $uriVariables['name'], 'name')['_source']]]> - ProviderInterface - - - - - - - - - - index->get(IndexNames::Vocabularies->value, $uriVariables['name'], 'name')['_source']]]> - diff --git a/public/spec.yaml b/public/spec.yaml index 224c70b..678d411 100644 --- a/public/spec.yaml +++ b/public/spec.yaml @@ -178,6 +178,8 @@ paths: responses: 200: description: 'Single daily occurrence' + 404: + description: 'Resource not found' summary: 'Retrieves a DailyOccurrence resource.' description: 'Retrieves a DailyOccurrence resource.' parameters: @@ -365,6 +367,8 @@ paths: responses: 200: description: 'Single event' + 404: + description: 'Resource not found' summary: 'Retrieves a Event resource.' description: 'Retrieves a Event resource.' parameters: @@ -466,6 +470,8 @@ paths: responses: 200: description: 'Single location' + 404: + description: 'Resource not found' summary: 'Get single location based on identifier' description: 'Retrieves a Location resource.' parameters: @@ -653,7 +659,9 @@ paths: responses: 200: description: 'Single occurrence' - summary: 'Retrieves a Occurrence resource.' + 404: + description: 'Resource not found' + summary: 'Get single occurrence based on identifier' description: 'Retrieves a Occurrence resource.' parameters: - @@ -742,6 +750,8 @@ paths: responses: 200: description: 'Single organization' + 404: + description: 'Resource not found' summary: 'Get single organization based on identifier' description: 'Retrieves a Organization resource.' parameters: @@ -823,19 +833,21 @@ paths: allowReserved: false deprecated: false parameters: [] - '/api/v2/tags/{name}': + '/api/v2/tags/{slug}': get: - operationId: api_tags_name_get + operationId: api_tags_slug_get tags: - Tag responses: 200: - description: 'Single tag' + description: 'Get single tag' + 404: + description: 'Resource not found' summary: 'Get single tag' description: 'Retrieves a Tag resource.' parameters: - - name: name + name: slug in: path description: '' required: true @@ -912,19 +924,21 @@ paths: allowReserved: false deprecated: false parameters: [] - '/api/v2/vocabularies/{name}': + '/api/v2/vocabularies/{slug}': get: - operationId: api_vocabularies_name_get + operationId: api_vocabularies_slug_get tags: - Vocabulary responses: 200: - description: 'Single vocabulary' - summary: 'Get a vocabulary based on name' + description: 'Get single vocabulary' + 404: + description: 'Resource not found' + summary: 'Get a vocabulary based on slug' description: 'Retrieves a Vocabulary resource.' parameters: - - name: name + name: slug in: path description: '' required: true @@ -1107,6 +1121,8 @@ components: - '@vocab' - hydra additionalProperties: true + slug: + type: string name: type: string Vocabulary.jsonld: @@ -1137,6 +1153,8 @@ components: - '@vocab' - hydra additionalProperties: true + slug: + type: string name: type: string description: diff --git a/src/Api/Dto/DailyOccurrence.php b/src/Api/Dto/DailyOccurrence.php index 503466b..99caf72 100644 --- a/src/Api/Dto/DailyOccurrence.php +++ b/src/Api/Dto/DailyOccurrence.php @@ -7,6 +7,9 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\Parameter; +use ApiPlatform\OpenApi\Model\Response; use App\Api\Filter\ElasticSearch\BooleanFilter; use App\Api\Filter\ElasticSearch\DateFilter; use App\Api\Filter\ElasticSearch\EventTagFilter; @@ -17,23 +20,23 @@ #[ApiResource( operations: [ new Get( - openapiContext: [ - 'parameters' => [ - [ - 'name' => 'id', - 'in' => 'path', - 'required' => true, - 'schema' => [ + openapi: new Operation( + responses: [ + '200' => new Response( + description: 'Single daily occurrence' + ), + ], + parameters: [ + new Parameter( + name: 'id', + in: 'path', + required: true, + schema: [ 'type' => 'integer', ], - ], - ], - 'responses' => [ - '200' => [ - 'description' => 'Single daily occurrence', - ], + ), ], - ], + ), output: DailyOccurrenceRepresentationProvider::class, provider: DailyOccurrenceRepresentationProvider::class, ), diff --git a/src/Api/Dto/Event.php b/src/Api/Dto/Event.php index 0fd5c61..c24f5f7 100644 --- a/src/Api/Dto/Event.php +++ b/src/Api/Dto/Event.php @@ -7,6 +7,9 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\Parameter; +use ApiPlatform\OpenApi\Model\Response; use App\Api\Filter\ElasticSearch\BooleanFilter; use App\Api\Filter\ElasticSearch\DateFilter; use App\Api\Filter\ElasticSearch\EventTagFilter; @@ -17,23 +20,23 @@ #[ApiResource( operations: [ new Get( - openapiContext: [ - 'parameters' => [ - [ - 'name' => 'id', - 'in' => 'path', - 'required' => true, - 'schema' => [ + openapi: new Operation( + responses: [ + '200' => new Response( + description: 'Single event' + ), + ], + parameters: [ + new Parameter( + name: 'id', + in: 'path', + required: true, + schema: [ 'type' => 'integer', ], - ], - ], - 'responses' => [ - '200' => [ - 'description' => 'Single event', - ], - ], - ], + ), + ] + ), output: EventRepresentationProvider::class, provider: EventRepresentationProvider::class, ), diff --git a/src/Api/Dto/Location.php b/src/Api/Dto/Location.php index 7786344..c91a2f1 100644 --- a/src/Api/Dto/Location.php +++ b/src/Api/Dto/Location.php @@ -7,30 +7,33 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\Parameter; +use ApiPlatform\OpenApi\Model\Response; use App\Api\Filter\ElasticSearch\MatchFilter; use App\Api\State\LocationRepresentationProvider; #[ApiResource( operations: [ new Get( - openapiContext: [ - 'summary' => 'Get single location based on identifier', - 'parameters' => [ - [ - 'name' => 'id', - 'in' => 'path', - 'required' => true, - 'schema' => [ + openapi: new Operation( + responses: [ + '200' => new Response( + description: 'Single location' + ), + ], + summary: 'Get single location based on identifier', + parameters: [ + new Parameter( + name: 'id', + in: 'path', + required: true, + schema: [ 'type' => 'integer', ], - ], - ], - 'responses' => [ - '200' => [ - 'description' => 'Single location', - ], - ], - ], + ), + ] + ), output: LocationRepresentationProvider::class, provider: LocationRepresentationProvider::class, ), diff --git a/src/Api/Dto/Occurrence.php b/src/Api/Dto/Occurrence.php index 0cb8b58..4a7a202 100644 --- a/src/Api/Dto/Occurrence.php +++ b/src/Api/Dto/Occurrence.php @@ -7,6 +7,9 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\Parameter; +use ApiPlatform\OpenApi\Model\Response; use App\Api\Filter\ElasticSearch\BooleanFilter; use App\Api\Filter\ElasticSearch\DateFilter; use App\Api\Filter\ElasticSearch\EventTagFilter; @@ -17,23 +20,24 @@ #[ApiResource( operations: [ new Get( - openapiContext: [ - 'parameters' => [ - [ - 'name' => 'id', - 'in' => 'path', - 'required' => true, - 'schema' => [ + openapi: new Operation( + responses: [ + '200' => new Response( + description: 'Single occurrence' + ), + ], + summary: 'Get single occurrence based on identifier', + parameters: [ + new Parameter( + name: 'id', + in: 'path', + required: true, + schema: [ 'type' => 'integer', ], - ], - ], - 'responses' => [ - '200' => [ - 'description' => 'Single occurrence', - ], - ], - ], + ), + ] + ), output: OccurrenceRepresentationProvider::class, provider: OccurrenceRepresentationProvider::class, ), diff --git a/src/Api/Dto/Organization.php b/src/Api/Dto/Organization.php index 1c6e084..0ea1464 100644 --- a/src/Api/Dto/Organization.php +++ b/src/Api/Dto/Organization.php @@ -7,30 +7,33 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\Parameter; +use ApiPlatform\OpenApi\Model\Response; use App\Api\Filter\ElasticSearch\MatchFilter; use App\Api\State\OrganizationRepresentationProvider; #[ApiResource( operations: [ new Get( - openapiContext: [ - 'summary' => 'Get single organization based on identifier', - 'parameters' => [ - [ - 'name' => 'id', - 'in' => 'path', - 'required' => true, - 'schema' => [ + openapi: new Operation( + responses: [ + '200' => new Response( + description: 'Single organization' + ), + ], + summary: 'Get single organization based on identifier', + parameters: [ + new Parameter( + name: 'id', + in: 'path', + required: true, + schema: [ 'type' => 'integer', ], - ], - ], - 'responses' => [ - '200' => [ - 'description' => 'Single organization', - ], - ], - ], + ), + ] + ), output: OrganizationRepresentationProvider::class, provider: OrganizationRepresentationProvider::class, ), diff --git a/src/Api/Dto/Tag.php b/src/Api/Dto/Tag.php index cfee74f..5209682 100644 --- a/src/Api/Dto/Tag.php +++ b/src/Api/Dto/Tag.php @@ -7,30 +7,33 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\Parameter; +use ApiPlatform\OpenApi\Model\Response; use App\Api\Filter\ElasticSearch\MatchFilter; use App\Api\State\TagRepresentationProvider; #[ApiResource( operations: [ new Get( - openapiContext: [ - 'summary' => 'Get single tag', - 'parameters' => [ - [ - 'name' => 'name', - 'in' => 'path', - 'required' => true, - 'schema' => [ + openapi: new Operation( + responses: [ + '200' => new Response( + description: 'Get single tag' + ), + ], + summary: 'Get single tag', + parameters: [ + new Parameter( + name: 'slug', + in: 'path', + required: true, + schema: [ 'type' => 'string', ], - ], - ], - 'responses' => [ - '200' => [ - 'description' => 'Single tag', - ], - ], - ], + ), + ] + ), output: Tag::class, provider: TagRepresentationProvider::class, ), @@ -56,13 +59,14 @@ #[ApiProperty(identifier: false)] private ?int $id; - #[ApiProperty( - identifier: true, - )] + #[ApiProperty(identifier: true)] + public string $slug; + public string $name; - public function __construct(string $name) + public function __construct(string $name, string $slug) { $this->name = $name; + $this->slug = $slug; } } diff --git a/src/Api/Dto/Vocabulary.php b/src/Api/Dto/Vocabulary.php index 3984447..0de9e48 100644 --- a/src/Api/Dto/Vocabulary.php +++ b/src/Api/Dto/Vocabulary.php @@ -7,30 +7,33 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\OpenApi\Model\Operation; +use ApiPlatform\OpenApi\Model\Parameter; +use ApiPlatform\OpenApi\Model\Response; use App\Api\Filter\ElasticSearch\MatchFilter; use App\Api\State\VocabularyRepresentationProvider; #[ApiResource( operations: [ new Get( - openapiContext: [ - 'summary' => 'Get a vocabulary based on name', - 'parameters' => [ - [ - 'name' => 'name', - 'in' => 'path', - 'required' => true, - 'schema' => [ + openapi: new Operation( + responses: [ + '200' => new Response( + description: 'Get single vocabulary' + ), + ], + summary: 'Get a vocabulary based on slug', + parameters: [ + new Parameter( + name: 'slug', + in: 'path', + required: true, + schema: [ 'type' => 'string', ], - ], - ], - 'responses' => [ - '200' => [ - 'description' => 'Single vocabulary', - ], - ], - ], + ), + ] + ), output: Vocabulary::class, provider: VocabularyRepresentationProvider::class, ), @@ -56,15 +59,18 @@ #[ApiProperty( identifier: true, )] + public string $slug; + public string $name; public string $description; public array $tags; - public function __construct(string $name, string $description, array $tags) + public function __construct(string $name, string $slug, string $description, array $tags) { $this->name = $name; + $this->slug = $slug; $this->description = $description; $this->tags = $tags; } diff --git a/src/Api/State/TagRepresentationProvider.php b/src/Api/State/TagRepresentationProvider.php index c1c310d..98ede74 100644 --- a/src/Api/State/TagRepresentationProvider.php +++ b/src/Api/State/TagRepresentationProvider.php @@ -30,7 +30,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $tags = []; foreach ($results->hits as $hit) { - $tags[] = new Tag(name: $hit['name']); + $tags[] = new Tag(name: $hit['name'], slug: $hit['slug']); } $results = new SearchResults(hits: $tags, total: $results->total); @@ -39,7 +39,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c } try { - $hit = $this->index->get(IndexNames::Tags->value, $uriVariables['name'], 'name')['_source']; + $data = $this->index->get(IndexNames::Tags->value, $uriVariables['slug'], 'slug'); + $hit = $data['_source'] ?? null; } catch (IndexException $e) { if (404 === $e->getCode()) { return null; @@ -48,6 +49,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c throw $e; } - return new Tag(name: $hit['name']); + return is_null($hit) ? $hit : new Tag(name: $hit['name'], slug: $hit['slug']); } } diff --git a/src/Api/State/VocabularyRepresentationProvider.php b/src/Api/State/VocabularyRepresentationProvider.php index 4050d37..d1fb93a 100644 --- a/src/Api/State/VocabularyRepresentationProvider.php +++ b/src/Api/State/VocabularyRepresentationProvider.php @@ -30,7 +30,7 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $vocabularies = []; foreach ($results->hits as $hit) { - $vocabularies[] = new Vocabulary(name: $hit['name'], description: $hit['description'], tags: $hit['tags']); + $vocabularies[] = new Vocabulary(name: $hit['name'], slug: $hit['slug'], description: $hit['description'], tags: $hit['tags']); } $results = new SearchResults(hits: $vocabularies, total: $results->total); @@ -39,7 +39,8 @@ public function provide(Operation $operation, array $uriVariables = [], array $c } try { - $hit = $this->index->get(IndexNames::Vocabularies->value, $uriVariables['name'], 'name')['_source']; + $data = $this->index->get(IndexNames::Vocabularies->value, $uriVariables['slug'], 'slug'); + $hit = $data['_source'] ?? null; } catch (IndexException $e) { if (404 === $e->getCode()) { return null; @@ -48,6 +49,6 @@ public function provide(Operation $operation, array $uriVariables = [], array $c throw $e; } - return new Vocabulary(name: $hit['name'], description: $hit['description'], tags: $hit['tags']); + return is_null($hit) ? $hit : new Vocabulary(name: $hit['name'], slug: $hit['slug'], description: $hit['description'], tags: $hit['tags']); } } diff --git a/src/Service/ElasticSearch/ElasticSearchIndex.php b/src/Service/ElasticSearch/ElasticSearchIndex.php index f0174f2..65ce030 100644 --- a/src/Service/ElasticSearch/ElasticSearchIndex.php +++ b/src/Service/ElasticSearch/ElasticSearchIndex.php @@ -4,6 +4,7 @@ use App\Exception\IndexException; use App\Model\FilterTypes; +use App\Model\IndexNames; use App\Model\SearchResults; use App\Service\IndexInterface; use Elastic\Elasticsearch\Client; @@ -156,8 +157,8 @@ private function buildParams(string $indexName, array $filters, int $from, int $ ], 'size' => $size, 'from' => $from, - // @TODO: add order filters to sort results - 'sort' => [], + // @TODO: make a proper sort filter to allow client to set sort direction + 'sort' => $this->getSort($indexName), ], ]; @@ -251,4 +252,45 @@ private function getTotalHits(array $data): int { return $data['hits']['total']['value'] ?? 0; } + + /** + * Get the sorting configuration for a specific index. + * + * This method returns an array containing the sorting configuration based on the given index name. + * If the index name matches one of the predefined index names, a specific sorting configuration will be returned. + * Otherwise, an empty array will be returned indicating no sorting is required. + * + * @param string $indexName the name of the index + * + * @return array the sorting configuration + */ + private function getSort(string $indexName): array + { + $indexName = IndexNames::tryFrom($indexName); + + return match ($indexName) { + IndexNames::Events => [ + 'title.keyword' => [ + 'order' => 'asc', + ], + ], + IndexNames::DailyOccurrences, IndexNames::Occurrences => [ + 'start' => [ + 'order' => 'asc', + 'format' => 'strict_date_optional_time_nanos', + ], + ], + IndexNames::Locations, IndexNames::Organizations => [ + 'name.keyword' => [ + 'order' => 'asc', + ], + ], + IndexNames::Tags, IndexNames::Vocabularies => [ + 'name' => [ + 'order' => 'asc', + ], + ], + default => [] + }; + } }