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 => []
+ };
+ }
}