From cb6f7f0d4b097961d0b686af820750bd00a2f887 Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 7 Mar 2024 10:59:15 +0100 Subject: [PATCH 1/5] #687: Updated docker development setup --- .docker/data/README.md | 26 ++++++++++++++++ .docker/nginx.conf | 6 ++-- .docker/templates/default.conf.template | 40 +++++++++++++++++++++++++ docker-compose.dev.yml | 5 ++-- docker-compose.override.yml | 4 ++- docker-compose.redirect.yml | 4 +-- docker-compose.server.yml | 16 +++++----- docker-compose.yml | 14 +++++---- 8 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 .docker/data/README.md create mode 100644 .docker/templates/default.conf.template diff --git a/.docker/data/README.md b/.docker/data/README.md new file mode 100644 index 0000000..8895d7b --- /dev/null +++ b/.docker/data/README.md @@ -0,0 +1,26 @@ +# .docker/data + +Please map persistent volumes to this directory on the servers. + +If a container needs to persist data between restarts you can map the relevant files in the container to ``docker/data/`. + +## RabbitMQ example +If you are using RabbitMQ running in a container as a message broker you need to configure a persistent volume for RabbitMQs data directory to avoid losing message on container restarts. + +```yaml +# docker-compose.server.override.yml + +services: + rabbit: + image: rabbitmq:3.9-management-alpine + hostname: "${COMPOSE_PROJECT_NAME}" + networks: + - app + - frontend + environment: + - "RABBITMQ_DEFAULT_USER=${RABBITMQ_USER}" + - "RABBITMQ_DEFAULT_PASS=${RABBITMQ_PASSWORD}" + - "RABBITMQ_ERLANG_COOKIE=${RABBITMQ_ERLANG_COOKIE}" + volumes: + - ".docker/data/rabbitmq:/var/lib/rabbitmq/mnesia/" +``` diff --git a/.docker/nginx.conf b/.docker/nginx.conf index 43dbd19..d0a557b 100644 --- a/.docker/nginx.conf +++ b/.docker/nginx.conf @@ -1,6 +1,6 @@ worker_processes auto; -error_log /var/log/nginx/error.log notice; +error_log /dev/stderr notice; pid /tmp/nginx.pid; events { @@ -26,11 +26,9 @@ http { '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; - access_log /var/log/nginx/access.log main; + access_log /dev/stdout main; sendfile on; - #tcp_nopush on; - keepalive_timeout 65; gzip on; diff --git a/.docker/templates/default.conf.template b/.docker/templates/default.conf.template new file mode 100644 index 0000000..df4744c --- /dev/null +++ b/.docker/templates/default.conf.template @@ -0,0 +1,40 @@ +server { + listen ${NGINX_PORT}; + server_name localhost; + + root ${NGINX_WEB_ROOT}; + + location / { + # try to serve file directly, fallback to index.php + try_files $uri /index.php$is_args$args; + } + + # Protect files and directories from prying eyes. + location ~* \.(engine|inc|install|make|module|profile|po|sh|.*sql|.tar|.gz|.bz2|theme|twig|tpl(\.php)?|xtmpl|yml)(~|\.sw[op]|\.bak|\.orig|\.save)?$|^(\.(?!well-known).*|Entries.*|Repository|Root|Tag|Template|composer\.(json|lock)|web\.config)$|^#.*#$|\.php(~|\.sw[op]|\.bak|\.orig|\.save)$ { + deny all; + return 404; + } + + location ~ ^/index\.php(/|$) { + fastcgi_buffers 16 32k; + fastcgi_buffer_size 64k; + fastcgi_busy_buffers_size 64k; + + fastcgi_pass ${NGINX_FPM_SERVICE}; + fastcgi_split_path_info ^(.+\.php)(/.*)$; + include fastcgi_params; + + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param DOCUMENT_ROOT $realpath_root; + + internal; + } + + location ~ \.php$ { + return 404; + } + + # Send log message to files symlinked to stdout/stderr. + error_log /dev/stderr; + access_log /dev/stdout main; +} diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 05919f4..3c02812 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,6 +1,5 @@ -# itk-version: 3.1.1 -version: "3" - +# itk-version: 3.2.0 +version: "3.8" services: phpfpm: environment: diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 7b93c15..c4ca834 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -2,8 +2,10 @@ version: "3" services: phpfpm: + networks: + - frontend depends_on: - - elasticsearch +# - elasticsearch - rabbit rabbit: diff --git a/docker-compose.redirect.yml b/docker-compose.redirect.yml index 8347a5a..0fafb31 100644 --- a/docker-compose.redirect.yml +++ b/docker-compose.redirect.yml @@ -1,5 +1,5 @@ -# itk-version: 3.1.1 -version: "3" +# itk-version: 3.2.0 +version: "3.8" services: nginx: diff --git a/docker-compose.server.yml b/docker-compose.server.yml index 713fc01..cfe68c4 100644 --- a/docker-compose.server.yml +++ b/docker-compose.server.yml @@ -1,5 +1,5 @@ -# itk-version: 3.1.1 -version: "3" +# itk-version: 3.2.0 +version: "3.8" networks: frontend: @@ -10,7 +10,7 @@ networks: services: phpfpm: - image: itkdev/php8.2-fpm:alpine + image: itkdev/php8.3-fpm:alpine restart: unless-stopped networks: - app @@ -31,12 +31,14 @@ services: - frontend depends_on: - phpfpm - ports: - - '8080' volumes: - - ./.docker/vhost.conf:/etc/nginx/conf.d/default.conf:ro + - ./.docker/templates:/etc/nginx/templates:ro - ./.docker/nginx.conf:/etc/nginx/nginx.conf:ro - - ./:/app:rw + - .:/app + environment: + NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME}-phpfpm-1:9000 + NGINX_WEB_ROOT: /app/public + NGINX_PORT: 8080 labels: - "traefik.enable=true" - "traefik.docker.network=frontend" diff --git a/docker-compose.yml b/docker-compose.yml index 2288994..b47e495 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,5 @@ -# itk-version: 3.1.1 -version: "3" +# itk-version: 3.2.0 +version: "3.8" networks: frontend: @@ -23,7 +23,7 @@ services: #- ENCRYPT=1 # Uncomment to enable database encryption. phpfpm: - image: itkdev/php8.2-fpm:latest + image: itkdev/php8.3-fpm:latest networks: - app extra_hosts: @@ -32,7 +32,7 @@ services: - PHP_XDEBUG_MODE=${PHP_XDEBUG_MODE:-off} - PHP_MAX_EXECUTION_TIME=30 - PHP_MEMORY_LIMIT=256M - # Depending on the setup you may have to remove --read-envelope-from from msmtp (cf. https://marlam.de/msmtp/msmtp.html) or use SMTP to send mail + # Depending on the setup, you may have to remove --read-envelope-from from msmtp (cf. https://marlam.de/msmtp/msmtp.html) or use SMTP to send mail - PHP_SENDMAIL_PATH=/usr/bin/msmtp --host=mail --port=1025 --read-recipients --read-envelope-from - DOCKER_HOST_DOMAIN=${COMPOSE_DOMAIN} - COMPOSER_VERSION=2 @@ -52,8 +52,12 @@ services: ports: - '8080' volumes: - - ./.docker/vhost.conf:/etc/nginx/conf.d/default.conf:ro + - ./.docker/templates:/etc/nginx/templates:ro - .:/app + environment: + NGINX_FPM_SERVICE: ${COMPOSE_PROJECT_NAME}-phpfpm-1:9000 + NGINX_WEB_ROOT: /app/public + NGINX_PORT: 8080 labels: - "traefik.enable=true" - "traefik.docker.network=frontend" From f287086548edfb6091c7279e56ee16444ab972be Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 7 Mar 2024 11:07:04 +0100 Subject: [PATCH 2/5] #687: Added api endpoints and filters --- baseline.xml | 65 --------- composer.json | 1 + composer.lock | 3 +- config/services.yaml | 20 +++ psalm-baseline.xml | 135 ++++++++++++++++++ psalm.xml | 6 +- src/Api/Dto/DailyOccurrence.php | 86 +++++++++++ src/Api/Dto/Event.php | 10 ++ src/Api/Dto/Location.php | 56 ++++++++ src/Api/Dto/Occurrence.php | 86 +++++++++++ src/Api/Dto/Tag.php | 68 +++++++++ src/Api/Dto/Vocabulary.php | 71 +++++++++ .../Filter/ElasticSearch/BooleanFilter.php | 52 +++++++ .../Filter/ElasticSearch/EventTagFilter.php | 2 +- .../DailyOccurrenceRepresentationProvider.php | 42 ++++++ src/Api/State/EventRepresentationProvider.php | 13 +- .../State/LocationRepresentationProvider.php | 42 ++++++ .../OccurrenceRepresentationProvider.php | 42 ++++++ .../OrganizationRepresentationProvider.php | 15 +- src/Api/State/TagRepresentationProvider.php | 53 +++++++ .../VocabularyRepresentationProvider.php | 53 +++++++ src/Command/FixturesLoadCommand.php | 1 - src/Controller/RootController.php | 16 +++ src/Fixtures/FixtureLoader.php | 1 - src/Model/IndexNames.php | 12 +- .../ElasticSearch/ElasticSearchIndex.php | 55 ++++++- src/Service/IndexInterface.php | 10 +- 27 files changed, 931 insertions(+), 85 deletions(-) delete mode 100644 baseline.xml create mode 100644 psalm-baseline.xml create mode 100644 src/Api/Dto/DailyOccurrence.php create mode 100644 src/Api/Dto/Location.php create mode 100644 src/Api/Dto/Occurrence.php create mode 100644 src/Api/Dto/Tag.php create mode 100644 src/Api/Dto/Vocabulary.php create mode 100644 src/Api/Filter/ElasticSearch/BooleanFilter.php create mode 100644 src/Api/State/DailyOccurrenceRepresentationProvider.php create mode 100644 src/Api/State/LocationRepresentationProvider.php create mode 100644 src/Api/State/OccurrenceRepresentationProvider.php create mode 100644 src/Api/State/TagRepresentationProvider.php create mode 100644 src/Api/State/VocabularyRepresentationProvider.php create mode 100644 src/Controller/RootController.php diff --git a/baseline.xml b/baseline.xml deleted file mode 100644 index 478f233..0000000 --- a/baseline.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - getFilters - - - - - index->get(IndexNames::Events->value, $uriVariables['id'])['_source']]]]> - - - ProviderInterface - - - - - index->get(IndexNames::Organization->value, $uriVariables['id'])['_source']]]]> - - - ProviderInterface - - - - - \HttpException - - - - - $indexName, - 'body' => [ - 'settings' => [ - 'number_of_shards' => 5, - 'number_of_replicas' => 0, - ], - ], - ]]]> - - - getStatusCode - getStatusCode - - - \HttpException - - - \HttpException - \HttpException - - - - - $params - $indexName]]]> - - - - - PaginatorInterface - \IteratorAggregate - - - diff --git a/composer.json b/composer.json index 4aa85db..aacf1ec 100644 --- a/composer.json +++ b/composer.json @@ -8,6 +8,7 @@ "require": { "php": ">=8.2", "ext-ctype": "*", + "ext-http": "*", "ext-iconv": "*", "api-platform/core": "^3.2", "doctrine/doctrine-bundle": "^2.11", diff --git a/composer.lock b/composer.lock index 175cd38..5adcf5a 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f21afd798760a2d159752aa257e36267", + "content-hash": "2eac31f0adfb242dbc2f78d7d4838cc8", "packages": [ { "name": "api-platform/core", @@ -9944,6 +9944,7 @@ "platform": { "php": ">=8.2", "ext-ctype": "*", + "ext-http": "*", "ext-iconv": "*" }, "platform-dev": [], diff --git a/config/services.yaml b/config/services.yaml index 0aca490..5ed40c3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -29,14 +29,34 @@ services: arguments: $filterLocator: '@api_platform.filter_locator' + App\Api\State\DailyOccurrenceRepresentationProvider: + arguments: + $filterLocator: '@api_platform.filter_locator' + App\Api\State\EventRepresentationProvider: arguments: $filterLocator: '@api_platform.filter_locator' + App\Api\State\LocationRepresentationProvider: + arguments: + $filterLocator: '@api_platform.filter_locator' + + App\Api\State\OccurrenceRepresentationProvider: + arguments: + $filterLocator: '@api_platform.filter_locator' + App\Api\State\OrganizationRepresentationProvider: arguments: $filterLocator: '@api_platform.filter_locator' + App\Api\State\TagRepresentationProvider: + arguments: + $filterLocator: '@api_platform.filter_locator' + + App\Api\State\VocabularyRepresentationProvider: + arguments: + $filterLocator: '@api_platform.filter_locator' + App\Command\FixturesLoadCommand: arguments: $appEnv: '%env(string:APP_ENV)%' diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..9dfc66a --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,135 @@ + + + + + getFilters + + + + + index->get(IndexNames::DailyOccurrences->value, $uriVariables['id'])['_source']]]]> + + + ProviderInterface + + + index->get(IndexNames::DailyOccurrences->value, $uriVariables['id'])['_source']]]> + + + + + index->get(IndexNames::Events->value, $uriVariables['id'])['_source']]]]> + + + ProviderInterface + + + index->get(IndexNames::Events->value, $uriVariables['id'])['_source']]]> + + + + + index->get(IndexNames::Locations->value, $uriVariables['id'])['_source']]]]> + + + ProviderInterface + + + index->get(IndexNames::Locations->value, $uriVariables['id'])['_source']]]> + + + + + index->get(IndexNames::Occurrences->value, $uriVariables['id'])['_source']]]]> + + + ProviderInterface + + + index->get(IndexNames::Occurrences->value, $uriVariables['id'])['_source']]]> + + + + + index->get(IndexNames::Organizations->value, $uriVariables['id'])['_source']]]]> + + + ProviderInterface + + + index->get(IndexNames::Organizations->value, $uriVariables['id'])['_source']]]> + + + + + ProviderInterface + + + + + + + index->get(IndexNames::Tags->value, $uriVariables['name'], 'name')['_source']]]> + + + + + ProviderInterface + + + + + + + + + + + index->get(IndexNames::Vocabularies->value, $uriVariables['name'], 'name')['_source']]]> + + + + + \HttpException + + + + + $indexName, + 'body' => [ + 'settings' => [ + 'number_of_shards' => 5, + 'number_of_replicas' => 0, + ], + ], + ]]]> + + + getStatusCode + getStatusCode + + + \HttpException + + + \HttpException + \HttpException + + + + + $params + + + $params + $indexName]]]> + + + + + PaginatorInterface + \IteratorAggregate + + + diff --git a/psalm.xml b/psalm.xml index 33dd527..e7fa19e 100644 --- a/psalm.xml +++ b/psalm.xml @@ -5,7 +5,9 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" - errorBaseline="baseline.xml" + errorBaseline="psalm-baseline.xml" + findUnusedBaselineEntry="true" + findUnusedCode="false" > @@ -18,6 +20,6 @@ var/cache/dev/App_KernelDevDebugContainer.xml - + diff --git a/src/Api/Dto/DailyOccurrence.php b/src/Api/Dto/DailyOccurrence.php new file mode 100644 index 0000000..503466b --- /dev/null +++ b/src/Api/Dto/DailyOccurrence.php @@ -0,0 +1,86 @@ + [ + [ + 'name' => 'id', + 'in' => 'path', + 'required' => true, + 'schema' => [ + 'type' => 'integer', + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => 'Single daily occurrence', + ], + ], + ], + output: DailyOccurrenceRepresentationProvider::class, + provider: DailyOccurrenceRepresentationProvider::class, + ), + new GetCollection( + output: DailyOccurrenceRepresentationProvider::class, + provider: DailyOccurrenceRepresentationProvider::class, + ), + ], + paginationClientItemsPerPage: true, + paginationItemsPerPage: 10, + paginationMaximumItemsPerPage: 50 +)] +#[ApiFilter( + MatchFilter::class, + properties: ['event.title', 'event.organizer.name', 'event.organizer.entityId', 'event.location.name', 'event.location.entityId'] +)] +#[ApiFilter( + BooleanFilter::class, + properties: ['event.publicAccess'] +)] +#[ApiFilter( + EventTagFilter::class, + properties: ['event.tags'] +)] +#[ApiFilter( + DateFilter::class, + properties: [ + 'start' => 'start', + 'end' => 'end', + ], + arguments: [ + 'config' => [ + 'start' => [ + 'limit' => DateLimits::GreaterThanOrEqual, + 'throwOnInvalid' => true, + ], + 'end' => [ + 'limit' => DateLimits::LessThanOrEqual, + 'throwOnInvalid' => true, + ], + ], + ] +)] +class DailyOccurrence +{ + #[ApiProperty( + identifier: true, + )] + private int $id; +} diff --git a/src/Api/Dto/Event.php b/src/Api/Dto/Event.php index 804980d..0fd5c61 100644 --- a/src/Api/Dto/Event.php +++ b/src/Api/Dto/Event.php @@ -7,8 +7,10 @@ use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use App\Api\Filter\ElasticSearch\BooleanFilter; use App\Api\Filter\ElasticSearch\DateFilter; use App\Api\Filter\ElasticSearch\EventTagFilter; +use App\Api\Filter\ElasticSearch\MatchFilter; use App\Api\State\EventRepresentationProvider; use App\Model\DateLimits; @@ -44,6 +46,14 @@ paginationItemsPerPage: 10, paginationMaximumItemsPerPage: 50 )] +#[ApiFilter( + MatchFilter::class, + properties: ['title', 'organizer.name', 'organizer.entityId', 'location.name', 'location.entityId'] +)] +#[ApiFilter( + BooleanFilter::class, + properties: ['publicAccess'] +)] #[ApiFilter( EventTagFilter::class, properties: ['tags'] diff --git a/src/Api/Dto/Location.php b/src/Api/Dto/Location.php new file mode 100644 index 0000000..7786344 --- /dev/null +++ b/src/Api/Dto/Location.php @@ -0,0 +1,56 @@ + 'Get single location based on identifier', + 'parameters' => [ + [ + 'name' => 'id', + 'in' => 'path', + 'required' => true, + 'schema' => [ + 'type' => 'integer', + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => 'Single location', + ], + ], + ], + output: LocationRepresentationProvider::class, + provider: LocationRepresentationProvider::class, + ), + new GetCollection( + output: LocationRepresentationProvider::class, + provider: LocationRepresentationProvider::class, + ), + ], + paginationClientItemsPerPage: true, + paginationItemsPerPage: 20, + paginationMaximumItemsPerPage: 100 +)] +#[ApiFilter( + MatchFilter::class, + properties: ['name', 'postalCode'] +)] +class Location +{ + #[ApiProperty( + identifier: true, + )] + private int $id; +} diff --git a/src/Api/Dto/Occurrence.php b/src/Api/Dto/Occurrence.php new file mode 100644 index 0000000..0cb8b58 --- /dev/null +++ b/src/Api/Dto/Occurrence.php @@ -0,0 +1,86 @@ + [ + [ + 'name' => 'id', + 'in' => 'path', + 'required' => true, + 'schema' => [ + 'type' => 'integer', + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => 'Single occurrence', + ], + ], + ], + output: OccurrenceRepresentationProvider::class, + provider: OccurrenceRepresentationProvider::class, + ), + new GetCollection( + output: OccurrenceRepresentationProvider::class, + provider: OccurrenceRepresentationProvider::class, + ), + ], + paginationClientItemsPerPage: true, + paginationItemsPerPage: 10, + paginationMaximumItemsPerPage: 50 +)] +#[ApiFilter( + MatchFilter::class, + properties: ['event.title', 'event.organizer.name', 'event.organizer.entityId', 'event.location.name', 'event.location.entityId'] +)] +#[ApiFilter( + BooleanFilter::class, + properties: ['event.publicAccess'] +)] +#[ApiFilter( + EventTagFilter::class, + properties: ['event.tags'] +)] +#[ApiFilter( + DateFilter::class, + properties: [ + 'start' => 'start', + 'end' => 'end', + ], + arguments: [ + 'config' => [ + 'start' => [ + 'limit' => DateLimits::GreaterThanOrEqual, + 'throwOnInvalid' => true, + ], + 'end' => [ + 'limit' => DateLimits::LessThanOrEqual, + 'throwOnInvalid' => true, + ], + ], + ] +)] +class Occurrence +{ + #[ApiProperty( + identifier: true, + )] + private int $id; +} diff --git a/src/Api/Dto/Tag.php b/src/Api/Dto/Tag.php new file mode 100644 index 0000000..cfee74f --- /dev/null +++ b/src/Api/Dto/Tag.php @@ -0,0 +1,68 @@ + 'Get single tag', + 'parameters' => [ + [ + 'name' => 'name', + 'in' => 'path', + 'required' => true, + 'schema' => [ + 'type' => 'string', + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => 'Single tag', + ], + ], + ], + output: Tag::class, + provider: TagRepresentationProvider::class, + ), + new GetCollection( + output: Tag::class, + provider: TagRepresentationProvider::class, + ), + ], + paginationClientItemsPerPage: true, + paginationItemsPerPage: 20, + paginationMaximumItemsPerPage: 100 +)] +#[ApiFilter( + MatchFilter::class, + properties: ['name'] +)] +#[ApiFilter( + MatchFilter::class, + properties: ['vocabulary'] +)] +readonly class Tag +{ + #[ApiProperty(identifier: false)] + private ?int $id; + + #[ApiProperty( + identifier: true, + )] + public string $name; + + public function __construct(string $name) + { + $this->name = $name; + } +} diff --git a/src/Api/Dto/Vocabulary.php b/src/Api/Dto/Vocabulary.php new file mode 100644 index 0000000..3984447 --- /dev/null +++ b/src/Api/Dto/Vocabulary.php @@ -0,0 +1,71 @@ + 'Get a vocabulary based on name', + 'parameters' => [ + [ + 'name' => 'name', + 'in' => 'path', + 'required' => true, + 'schema' => [ + 'type' => 'string', + ], + ], + ], + 'responses' => [ + '200' => [ + 'description' => 'Single vocabulary', + ], + ], + ], + output: Vocabulary::class, + provider: VocabularyRepresentationProvider::class, + ), + new GetCollection( + output: Vocabulary::class, + provider: VocabularyRepresentationProvider::class, + ), + ], + paginationClientItemsPerPage: true, + paginationItemsPerPage: 20, + paginationMaximumItemsPerPage: 100 +)] +#[ApiFilter( + MatchFilter::class, + properties: ['name'] +)] +#[ApiFilter( + MatchFilter::class, + properties: ['tags'] +)] +readonly class Vocabulary +{ + #[ApiProperty( + identifier: true, + )] + public string $name; + + public string $description; + + public array $tags; + + public function __construct(string $name, string $description, array $tags) + { + $this->name = $name; + $this->description = $description; + $this->tags = $tags; + } +} diff --git a/src/Api/Filter/ElasticSearch/BooleanFilter.php b/src/Api/Filter/ElasticSearch/BooleanFilter.php new file mode 100644 index 0000000..ba00df3 --- /dev/null +++ b/src/Api/Filter/ElasticSearch/BooleanFilter.php @@ -0,0 +1,52 @@ +getProperties($resourceClass); + $terms = []; + + /** @var string $property */ + foreach ($properties as $property) { + if (empty($context['filters'][$property])) { + // If no value or empty value is set, skip it. + continue; + } + $terms[$property] = explode(',', $context['filters'][$property]); + } + + return empty($terms) ? $terms : ['terms' => $terms + ['boost' => 1.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_BOOL, + 'required' => false, + 'description' => 'Is this a public event', + 'is_collection' => false, + 'openapi' => [ + 'allowReserved' => false, + 'allowEmptyValue' => true, + 'explode' => false, + ], + ]; + } + + return $description; + } +} diff --git a/src/Api/Filter/ElasticSearch/EventTagFilter.php b/src/Api/Filter/ElasticSearch/EventTagFilter.php index 0f75f58..a12a34a 100644 --- a/src/Api/Filter/ElasticSearch/EventTagFilter.php +++ b/src/Api/Filter/ElasticSearch/EventTagFilter.php @@ -37,7 +37,7 @@ public function getDescription(string $resourceClass): array 'property' => $filterParameterName, 'type' => Type::BUILTIN_TYPE_ARRAY, 'required' => false, - 'description' => 'Filter base on values given', + 'description' => 'Filter based on given tags', 'is_collection' => true, 'openapi' => [ 'allowReserved' => false, diff --git a/src/Api/State/DailyOccurrenceRepresentationProvider.php b/src/Api/State/DailyOccurrenceRepresentationProvider.php new file mode 100644 index 0000000..1322df9 --- /dev/null +++ b/src/Api/State/DailyOccurrenceRepresentationProvider.php @@ -0,0 +1,42 @@ +getFilters($operation, $context); + $offset = $this->calculatePageOffset($context); + $limit = $this->getImagesPerPage($context); + $results = $this->index->getAll(IndexNames::DailyOccurrences->value, $filters, $offset, $limit); + + return new ElasticSearchPaginator($results, $limit, $offset); + } + + try { + return [$this->index->get(IndexNames::DailyOccurrences->value, $uriVariables['id'])['_source']]; + } catch (IndexException $e) { + if (404 === $e->getCode()) { + return null; + } + + throw $e; + } + } +} diff --git a/src/Api/State/EventRepresentationProvider.php b/src/Api/State/EventRepresentationProvider.php index 7097dc0..ce70de3 100644 --- a/src/Api/State/EventRepresentationProvider.php +++ b/src/Api/State/EventRepresentationProvider.php @@ -5,6 +5,7 @@ use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; +use App\Exception\IndexException; use App\Model\IndexNames; use App\Service\ElasticSearch\ElasticSearchPaginator; use Psr\Container\ContainerExceptionInterface; @@ -15,7 +16,7 @@ final class EventRepresentationProvider extends AbstractProvider implements Prov /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface - * @throws \App\Exception\IndexException + * @throws IndexException */ public function provide(Operation $operation, array $uriVariables = [], array $context = []): ElasticSearchPaginator|array|null { @@ -28,6 +29,14 @@ public function provide(Operation $operation, array $uriVariables = [], array $c return new ElasticSearchPaginator($results, $limit, $offset); } - return [$this->index->get(IndexNames::Events->value, $uriVariables['id'])['_source']]; + try { + return [$this->index->get(IndexNames::Events->value, $uriVariables['id'])['_source']]; + } catch (IndexException $e) { + if (404 === $e->getCode()) { + return null; + } + + throw $e; + } } } diff --git a/src/Api/State/LocationRepresentationProvider.php b/src/Api/State/LocationRepresentationProvider.php new file mode 100644 index 0000000..7f98bcb --- /dev/null +++ b/src/Api/State/LocationRepresentationProvider.php @@ -0,0 +1,42 @@ +getFilters($operation, $context); + $offset = $this->calculatePageOffset($context); + $limit = $this->getImagesPerPage($context); + $results = $this->index->getAll(IndexNames::Locations->value, $filters, $offset, $limit); + + return new ElasticSearchPaginator($results, $limit, $offset); + } + + try { + return [$this->index->get(IndexNames::Locations->value, $uriVariables['id'])['_source']]; + } catch (IndexException $e) { + if (404 === $e->getCode()) { + return null; + } + + throw $e; + } + } +} diff --git a/src/Api/State/OccurrenceRepresentationProvider.php b/src/Api/State/OccurrenceRepresentationProvider.php new file mode 100644 index 0000000..c5c72c7 --- /dev/null +++ b/src/Api/State/OccurrenceRepresentationProvider.php @@ -0,0 +1,42 @@ +getFilters($operation, $context); + $offset = $this->calculatePageOffset($context); + $limit = $this->getImagesPerPage($context); + $results = $this->index->getAll(IndexNames::Occurrences->value, $filters, $offset, $limit); + + return new ElasticSearchPaginator($results, $limit, $offset); + } + + try { + return [$this->index->get(IndexNames::Occurrences->value, $uriVariables['id'])['_source']]; + } catch (IndexException $e) { + if (404 === $e->getCode()) { + return null; + } + + throw $e; + } + } +} diff --git a/src/Api/State/OrganizationRepresentationProvider.php b/src/Api/State/OrganizationRepresentationProvider.php index 2da657e..845f96e 100644 --- a/src/Api/State/OrganizationRepresentationProvider.php +++ b/src/Api/State/OrganizationRepresentationProvider.php @@ -5,6 +5,7 @@ use ApiPlatform\Metadata\CollectionOperationInterface; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; +use App\Exception\IndexException; use App\Model\IndexNames; use App\Service\ElasticSearch\ElasticSearchPaginator; use Psr\Container\ContainerExceptionInterface; @@ -15,7 +16,7 @@ final class OrganizationRepresentationProvider extends AbstractProvider implemen /** * @throws ContainerExceptionInterface * @throws NotFoundExceptionInterface - * @throws \App\Exception\IndexException + * @throws IndexException */ public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null { @@ -23,11 +24,19 @@ public function provide(Operation $operation, array $uriVariables = [], array $c $filters = $this->getFilters($operation, $context); $offset = $this->calculatePageOffset($context); $limit = $this->getImagesPerPage($context); - $results = $this->index->getAll(IndexNames::Organization->value, $filters, $offset, $limit); + $results = $this->index->getAll(IndexNames::Organizations->value, $filters, $offset, $limit); return new ElasticSearchPaginator($results, $limit, $offset); } - return [$this->index->get(IndexNames::Organization->value, $uriVariables['id'])['_source']]; + try { + return [$this->index->get(IndexNames::Organizations->value, $uriVariables['id'])['_source']]; + } catch (IndexException $e) { + if (404 === $e->getCode()) { + return null; + } + + throw $e; + } } } diff --git a/src/Api/State/TagRepresentationProvider.php b/src/Api/State/TagRepresentationProvider.php new file mode 100644 index 0000000..c1c310d --- /dev/null +++ b/src/Api/State/TagRepresentationProvider.php @@ -0,0 +1,53 @@ +getFilters($operation, $context); + $offset = $this->calculatePageOffset($context); + $limit = $this->getImagesPerPage($context); + $results = $this->index->getAll(IndexNames::Tags->value, $filters, $offset, $limit); + + $tags = []; + foreach ($results->hits as $hit) { + $tags[] = new Tag(name: $hit['name']); + } + + $results = new SearchResults(hits: $tags, total: $results->total); + + return new ElasticSearchPaginator($results, $limit, $offset); + } + + try { + $hit = $this->index->get(IndexNames::Tags->value, $uriVariables['name'], 'name')['_source']; + } catch (IndexException $e) { + if (404 === $e->getCode()) { + return null; + } + + throw $e; + } + + return new Tag(name: $hit['name']); + } +} diff --git a/src/Api/State/VocabularyRepresentationProvider.php b/src/Api/State/VocabularyRepresentationProvider.php new file mode 100644 index 0000000..4050d37 --- /dev/null +++ b/src/Api/State/VocabularyRepresentationProvider.php @@ -0,0 +1,53 @@ +getFilters($operation, $context); + $offset = $this->calculatePageOffset($context); + $limit = $this->getImagesPerPage($context); + $results = $this->index->getAll(IndexNames::Vocabularies->value, $filters, $offset, $limit); + + $vocabularies = []; + foreach ($results->hits as $hit) { + $vocabularies[] = new Vocabulary(name: $hit['name'], description: $hit['description'], tags: $hit['tags']); + } + + $results = new SearchResults(hits: $vocabularies, total: $results->total); + + return new ElasticSearchPaginator($results, $limit, $offset); + } + + try { + $hit = $this->index->get(IndexNames::Vocabularies->value, $uriVariables['name'], 'name')['_source']; + } catch (IndexException $e) { + if (404 === $e->getCode()) { + return null; + } + + throw $e; + } + + return new Vocabulary(name: $hit['name'], description: $hit['description'], tags: $hit['tags']); + } +} diff --git a/src/Command/FixturesLoadCommand.php b/src/Command/FixturesLoadCommand.php index c4231d5..b5b62c7 100644 --- a/src/Command/FixturesLoadCommand.php +++ b/src/Command/FixturesLoadCommand.php @@ -50,7 +50,6 @@ function (CompletionInput $input): array { /** * @throws RedirectionExceptionInterface * @throws ClientExceptionInterface - * @throws \JsonException * @throws TransportExceptionInterface * @throws ClientResponseException * @throws ServerExceptionInterface diff --git a/src/Controller/RootController.php b/src/Controller/RootController.php new file mode 100644 index 0000000..7642cfc --- /dev/null +++ b/src/Controller/RootController.php @@ -0,0 +1,16 @@ +redirectToRoute('api_doc'); + } +} diff --git a/src/Fixtures/FixtureLoader.php b/src/Fixtures/FixtureLoader.php index d5089b3..edd0c8b 100644 --- a/src/Fixtures/FixtureLoader.php +++ b/src/Fixtures/FixtureLoader.php @@ -27,7 +27,6 @@ public function __construct( /** * @throws RedirectionExceptionInterface * @throws ClientExceptionInterface - * @throws \JsonException * @throws TransportExceptionInterface * @throws ClientResponseException * @throws ServerExceptionInterface diff --git a/src/Model/IndexNames.php b/src/Model/IndexNames.php index bf3bbe5..68283e8 100644 --- a/src/Model/IndexNames.php +++ b/src/Model/IndexNames.php @@ -2,13 +2,17 @@ namespace App\Model; -/** - * Represents an enumeration of index names. - */ enum IndexNames: string { case Events = 'events'; - case Organization = 'organization'; + case Organizations = 'organizations'; + case Occurrences = 'occurrences'; + case DailyOccurrences = 'daily_occurrences'; + case Tags = 'tags'; + case Vocabularies = 'vocabularies'; + case Locations = 'locations'; + // @todo add apikeys index + // case ApiKeys = 'api_keys'; public static function values(): array { diff --git a/src/Service/ElasticSearch/ElasticSearchIndex.php b/src/Service/ElasticSearch/ElasticSearchIndex.php index 90ef992..f0174f2 100644 --- a/src/Service/ElasticSearch/ElasticSearchIndex.php +++ b/src/Service/ElasticSearch/ElasticSearchIndex.php @@ -36,7 +36,16 @@ public function indexExists($indexName): bool } } - public function get(string $indexName, int $id): array + public function get(string $indexName, int|string $id, string $indexField = 'id'): array + { + if ('id' === $indexField) { + return $this->getById($indexName, $id); + } else { + return $this->getByCustomIdField($indexName, $id, $indexField); + } + } + + private function getById(string $indexName, int|string $id): array { $params = [ 'index' => $indexName, @@ -57,6 +66,50 @@ public function get(string $indexName, int $id): array return $result; } + /** + * Simulate get for indexes where the "id" field has a different name. + * + * The ES client get() requires a 'id' field. To get items from indexes where + * we have to use search() with a term query. + * + * @throws IndexException + */ + private function getByCustomIdField(string $indexName, int|string $id, string $indexField = 'id'): array + { + $params = [ + 'index' => $indexName, + 'body' => [ + 'query' => [ + 'term' => [ + $indexField => $id, + ], + ], + ], + ]; + + try { + /** @var Elasticsearch $response */ + $response = $this->client->search($params); + if (Response::HTTP_OK !== $response->getStatusCode()) { + throw new IndexException('Failed to get document from Elasticsearch', $response->getStatusCode()); + } + $result = $this->parseResponse($response); + } catch (ClientResponseException|ServerResponseException|\JsonException $e) { + throw new IndexException($e->getMessage(), $e->getCode(), $e); + } + + if (0 === $result['hits']['total']['value']) { + // The ES client get() will throw a 404 exception when no document was found. + throw new IndexException('Not found', 404); + } + + if (1 < $result['hits']['total']['value']) { + throw new IndexException('ID search returned multiple hits', 500); + } + + return $result['hits']['hits'][0]; + } + public function getAll(string $indexName, array $filters = [], int $from = 0, int $size = 10): SearchResults { $params = $this->buildParams($indexName, $filters, $from, $size); diff --git a/src/Service/IndexInterface.php b/src/Service/IndexInterface.php index 675c1af..6d3e01d 100644 --- a/src/Service/IndexInterface.php +++ b/src/Service/IndexInterface.php @@ -25,15 +25,17 @@ public function indexExists(string $indexName): bool; * * @param string $indexName * The name of the index to retrieve information from - * @param int $id + * @param int|string $id * The ID of the document to retrieve + * @param string $indexField + * The id field in the index * - * @return array - * The retrieved document as an array + * @return ?array + * The retrieved document as an array or null if id not found * * @throws IndexException */ - public function get(string $indexName, int $id): array; + public function get(string $indexName, int|string $id, string $indexField = 'id'): ?array; /** * Retrieves documents from the specified index with optional filters and pagination. From cfe256da3f6a37de0c4498d7818939bb01bcf9d2 Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 7 Mar 2024 11:14:07 +0100 Subject: [PATCH 3/5] #687: Actions fixes --- .github/workflows/pr.yaml | 18 ++++++++++-------- CHANGELOG.md | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 5b4fd0f..a41fe2f 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -6,7 +6,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.2' ] + php: [ '8.3' ] name: Validate composer (${{ matrix.php}}) steps: - uses: actions/checkout@master @@ -15,6 +15,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php}} + extensions: http, ctype, iconv coverage: none - name: Get composer cache directory @@ -42,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.2' ] + php: [ '8.3' ] steps: - uses: actions/checkout@master @@ -50,6 +51,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php}} + extensions: http, ctype, iconv coverage: xdebug - name: Get composer cache directory @@ -80,7 +82,7 @@ jobs: strategy: fail-fast: false matrix: - php: ["8.2"] + php: ["8.3"] name: PHP Coding Standards Fixer (PHP ${{ matrix.php }}) steps: - name: Checkout @@ -90,7 +92,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php}} - extensions: apcu, ctype, iconv, imagick, json, pdo_sqlsrv, redis, soap, sqlsrv, xmlreader, zip + extensions: http, ctype, iconv coverage: none - name: Get composer cache directory @@ -115,7 +117,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.2'] + php: ['8.3'] name: Psalm static analysis (${{ matrix.php}}) steps: - uses: actions/checkout@master @@ -124,7 +126,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php}} - extensions: ctype, iconv, imagick, json, redis, soap, xmlreader, zip + extensions: http, ctype, iconv coverage: none - name: Get composer cache directory @@ -176,7 +178,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ '8.2' ] + php: [ '8.3' ] steps: - name: Checkout uses: actions/checkout@v3 @@ -187,7 +189,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php}} - extensions: ctype, iconv, imagick, json, redis, soap, xmlreader, zip + extensions: http, ctype, iconv coverage: none - name: Get composer cache directory diff --git a/CHANGELOG.md b/CHANGELOG.md index a7cbf53..fd68f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ 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 [keep a changelog]: https://keepachangelog.com/en/1.1.0/ [unreleased]: https://github.com/itk-dev/event-database-imports/compare/main...develop From b3aa6337cf7eb025c0102cf6ab5e18ae72f0b50b Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 7 Mar 2024 11:18:02 +0100 Subject: [PATCH 4/5] #687: Update github actions to latest version --- .github/workflows/pr.yaml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index a41fe2f..12fb02b 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -9,7 +9,7 @@ jobs: php: [ '8.3' ] name: Validate composer (${{ matrix.php}}) steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -23,7 +23,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ matrix.dependency-version }}- @@ -45,7 +45,7 @@ jobs: matrix: php: [ '8.3' ] steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -59,7 +59,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ matrix.dependency-version }}- @@ -86,7 +86,7 @@ jobs: name: PHP Coding Standards Fixer (PHP ${{ matrix.php }}) steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -100,7 +100,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache composer dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} @@ -120,7 +120,7 @@ jobs: php: ['8.3'] name: Psalm static analysis (${{ matrix.php}}) steps: - - uses: actions/checkout@master + - uses: actions/checkout@v4 - name: Setup PHP, with composer and extensions uses: shivammathur/setup-php@v2 @@ -134,7 +134,7 @@ jobs: run: echo "::set-output name=dir::$(composer config cache-files-dir)" - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}-${{ matrix.dependency-version }}- @@ -152,12 +152,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Get yarn cache directory path id: yarn-cache-dir-path run: echo "::set-output name=dir::$(yarn cache dir)" - name: Cache yarn packages - uses: actions/cache@v2 + uses: actions/cache@v4 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -181,7 +181,7 @@ jobs: php: [ '8.3' ] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 2 @@ -197,7 +197,7 @@ jobs: run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT - name: Cache dependencies - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ steps.composer-cache.outputs.dir }} key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} @@ -217,7 +217,7 @@ jobs: name: Changelog should be updated steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v4 with: fetch-depth: 2 From f1570925c6fcaa9cb1d957b410ec3a1c86bee040 Mon Sep 17 00:00:00 2001 From: turegjorup Date: Thu, 7 Mar 2024 11:20:29 +0100 Subject: [PATCH 5/5] #687: Update api spec --- public/spec.yaml | 957 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 916 insertions(+), 41 deletions(-) diff --git a/public/spec.yaml b/public/spec.yaml index d350be3..224c70b 100644 --- a/public/spec.yaml +++ b/public/spec.yaml @@ -8,6 +8,193 @@ servers: url: / description: '' paths: + /api/v2/daily_occurrences: + get: + operationId: api_daily_occurrences_get_collection + tags: + - DailyOccurrence + responses: + 200: + description: 'DailyOccurrence collection' + content: + application/ld+json: + schema: + type: object + properties: + 'hydra:member': { type: array, items: { $ref: '#/components/schemas/DailyOccurrence.DailyOccurrenceRepresentationProvider.jsonld' } } + 'hydra:totalItems': { type: integer, minimum: 0 } + 'hydra:view': { type: object, properties: { '@id': { type: string, format: iri-reference }, '@type': { type: string }, 'hydra:first': { type: string, format: iri-reference }, 'hydra:last': { type: string, format: iri-reference }, 'hydra:previous': { type: string, format: iri-reference }, 'hydra:next': { type: string, format: iri-reference } }, example: { '@id': string, type: string, 'hydra:first': string, 'hydra:last': string, 'hydra:previous': string, 'hydra:next': string } } + 'hydra:search': { type: object, properties: { '@type': { type: string }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string }, 'hydra:mapping': { type: array, items: { type: object, properties: { '@type': { type: string }, variable: { type: string }, property: { type: [string, 'null'] }, required: { type: boolean } } } } } } + required: + - 'hydra:member' + summary: 'Retrieves the collection of DailyOccurrence resources.' + description: 'Retrieves the collection of DailyOccurrence resources.' + parameters: + - + name: page + in: query + description: 'The collection page number' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: integer + default: 1 + 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: event.title + in: query + description: 'Search field based on value given' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: event.organizer.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 + - + name: event.organizer.entityId + in: query + description: 'Search field based on value given' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: event.location.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 + - + name: event.location.entityId + in: query + description: 'Search field based on value given' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: event.publicAccess + in: query + description: 'Is this a public event' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: boolean + style: form + explode: false + allowReserved: false + - + name: event.tags + in: query + description: 'Filter based on given tags' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: array + items: + type: string + style: deepObject + explode: false + allowReserved: false + - + name: start + in: query + description: 'Filter base on date (greater then or equal to)' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: end + in: query + description: 'Filter base on date (less then or equal to)' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + deprecated: false + parameters: [] + '/api/v2/daily_occurrences/{id}': + get: + operationId: api_daily_occurrences_id_get + tags: + - DailyOccurrence + responses: + 200: + description: 'Single daily occurrence' + summary: 'Retrieves a DailyOccurrence resource.' + description: 'Retrieves a DailyOccurrence resource.' + parameters: + - + name: id + in: path + description: '' + required: true + deprecated: false + allowEmptyValue: false + schema: + type: integer + style: simple + explode: false + allowReserved: false + deprecated: false + parameters: [] /api/v2/events: get: operationId: api_events_get_collection @@ -15,20 +202,495 @@ paths: - Event responses: 200: - description: 'Event collection' + description: 'Event collection' + content: + application/ld+json: + schema: + type: object + properties: + 'hydra:member': { type: array, items: { $ref: '#/components/schemas/Event.EventRepresentationProvider.jsonld' } } + 'hydra:totalItems': { type: integer, minimum: 0 } + 'hydra:view': { type: object, properties: { '@id': { type: string, format: iri-reference }, '@type': { type: string }, 'hydra:first': { type: string, format: iri-reference }, 'hydra:last': { type: string, format: iri-reference }, 'hydra:previous': { type: string, format: iri-reference }, 'hydra:next': { type: string, format: iri-reference } }, example: { '@id': string, type: string, 'hydra:first': string, 'hydra:last': string, 'hydra:previous': string, 'hydra:next': string } } + 'hydra:search': { type: object, properties: { '@type': { type: string }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string }, 'hydra:mapping': { type: array, items: { type: object, properties: { '@type': { type: string }, variable: { type: string }, property: { type: [string, 'null'] }, required: { type: boolean } } } } } } + required: + - 'hydra:member' + summary: 'Retrieves the collection of Event resources.' + description: 'Retrieves the collection of Event resources.' + parameters: + - + name: page + in: query + description: 'The collection page number' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: integer + default: 1 + 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: title + in: query + description: 'Search field based on value given' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: organizer.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 + - + name: organizer.entityId + in: query + description: 'Search field based on value given' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: location.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 + - + name: location.entityId + in: query + description: 'Search field based on value given' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: publicAccess + in: query + description: 'Is this a public event' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: boolean + style: form + explode: false + allowReserved: false + - + name: tags + in: query + description: 'Filter based on given tags' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: array + items: + type: string + style: deepObject + explode: false + allowReserved: false + - + name: occurrences.start + in: query + description: 'Filter base on date (greater then or equal to)' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: occurrences.end + in: query + description: 'Filter base on date (less then or equal to)' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + deprecated: false + parameters: [] + '/api/v2/events/{id}': + get: + operationId: api_events_id_get + tags: + - Event + responses: + 200: + description: 'Single event' + summary: 'Retrieves a Event resource.' + description: 'Retrieves a Event resource.' + parameters: + - + name: id + in: path + description: '' + required: true + deprecated: false + allowEmptyValue: false + schema: + type: integer + style: simple + explode: false + allowReserved: false + deprecated: false + parameters: [] + /api/v2/locations: + get: + operationId: api_locations_get_collection + tags: + - Location + responses: + 200: + description: 'Location collection' + content: + application/ld+json: + schema: + type: object + properties: + 'hydra:member': { type: array, items: { $ref: '#/components/schemas/Location.LocationRepresentationProvider.jsonld' } } + 'hydra:totalItems': { type: integer, minimum: 0 } + 'hydra:view': { type: object, properties: { '@id': { type: string, format: iri-reference }, '@type': { type: string }, 'hydra:first': { type: string, format: iri-reference }, 'hydra:last': { type: string, format: iri-reference }, 'hydra:previous': { type: string, format: iri-reference }, 'hydra:next': { type: string, format: iri-reference } }, example: { '@id': string, type: string, 'hydra:first': string, 'hydra:last': string, 'hydra:previous': string, 'hydra:next': string } } + 'hydra:search': { type: object, properties: { '@type': { type: string }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string }, 'hydra:mapping': { type: array, items: { type: object, properties: { '@type': { type: string }, variable: { type: string }, property: { type: [string, 'null'] }, required: { type: boolean } } } } } } + required: + - 'hydra:member' + summary: 'Retrieves the collection of Location resources.' + description: 'Retrieves the collection of Location resources.' + parameters: + - + name: page + in: query + description: 'The collection page number' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: integer + default: 1 + 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 + - + name: postalCode + 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/locations/{id}': + get: + operationId: api_locations_id_get + tags: + - Location + responses: + 200: + description: 'Single location' + summary: 'Get single location based on identifier' + description: 'Retrieves a Location resource.' + parameters: + - + name: id + in: path + description: '' + required: true + deprecated: false + allowEmptyValue: false + schema: + type: integer + style: simple + explode: false + allowReserved: false + deprecated: false + parameters: [] + /api/v2/occurrences: + get: + operationId: api_occurrences_get_collection + tags: + - Occurrence + responses: + 200: + description: 'Occurrence collection' + content: + application/ld+json: + schema: + type: object + properties: + 'hydra:member': { type: array, items: { $ref: '#/components/schemas/Occurrence.OccurrenceRepresentationProvider.jsonld' } } + 'hydra:totalItems': { type: integer, minimum: 0 } + 'hydra:view': { type: object, properties: { '@id': { type: string, format: iri-reference }, '@type': { type: string }, 'hydra:first': { type: string, format: iri-reference }, 'hydra:last': { type: string, format: iri-reference }, 'hydra:previous': { type: string, format: iri-reference }, 'hydra:next': { type: string, format: iri-reference } }, example: { '@id': string, type: string, 'hydra:first': string, 'hydra:last': string, 'hydra:previous': string, 'hydra:next': string } } + 'hydra:search': { type: object, properties: { '@type': { type: string }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string }, 'hydra:mapping': { type: array, items: { type: object, properties: { '@type': { type: string }, variable: { type: string }, property: { type: [string, 'null'] }, required: { type: boolean } } } } } } + required: + - 'hydra:member' + summary: 'Retrieves the collection of Occurrence resources.' + description: 'Retrieves the collection of Occurrence resources.' + parameters: + - + name: page + in: query + description: 'The collection page number' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: integer + default: 1 + 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: event.title + in: query + description: 'Search field based on value given' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: event.organizer.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 + - + name: event.organizer.entityId + in: query + description: 'Search field based on value given' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: event.location.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 + - + name: event.location.entityId + in: query + description: 'Search field based on value given' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: event.publicAccess + in: query + description: 'Is this a public event' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: boolean + style: form + explode: false + allowReserved: false + - + name: event.tags + in: query + description: 'Filter based on given tags' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: array + items: + type: string + style: deepObject + explode: false + allowReserved: false + - + name: start + in: query + description: 'Filter base on date (greater then or equal to)' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + - + name: end + in: query + description: 'Filter base on date (less then or equal to)' + required: false + deprecated: false + allowEmptyValue: true + schema: + type: string + style: form + explode: false + allowReserved: false + deprecated: false + parameters: [] + '/api/v2/occurrences/{id}': + get: + operationId: api_occurrences_id_get + tags: + - Occurrence + responses: + 200: + description: 'Single occurrence' + summary: 'Retrieves a Occurrence resource.' + description: 'Retrieves a Occurrence resource.' + parameters: + - + name: id + in: path + description: '' + required: true + deprecated: false + allowEmptyValue: false + schema: + type: integer + style: simple + explode: false + allowReserved: false + deprecated: false + parameters: [] + /api/v2/organizations: + get: + operationId: api_organizations_get_collection + tags: + - Organization + responses: + 200: + description: 'Organization collection' content: application/ld+json: schema: type: object properties: - 'hydra:member': { type: array, items: { $ref: '#/components/schemas/Event.EventRepresentationProvider.jsonld' } } + 'hydra:member': { type: array, items: { $ref: '#/components/schemas/Organization.OrganizationRepresentationProvider.jsonld' } } 'hydra:totalItems': { type: integer, minimum: 0 } 'hydra:view': { type: object, properties: { '@id': { type: string, format: iri-reference }, '@type': { type: string }, 'hydra:first': { type: string, format: iri-reference }, 'hydra:last': { type: string, format: iri-reference }, 'hydra:previous': { type: string, format: iri-reference }, 'hydra:next': { type: string, format: iri-reference } }, example: { '@id': string, type: string, 'hydra:first': string, 'hydra:last': string, 'hydra:previous': string, 'hydra:next': string } } 'hydra:search': { type: object, properties: { '@type': { type: string }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string }, 'hydra:mapping': { type: array, items: { type: object, properties: { '@type': { type: string }, variable: { type: string }, property: { type: [string, 'null'] }, required: { type: boolean } } } } } } required: - 'hydra:member' - summary: 'Retrieves the collection of Event resources.' - description: 'Retrieves the collection of Event resources.' + summary: 'Retrieves the collection of Organization resources.' + description: 'Retrieves the collection of Organization resources.' parameters: - name: page @@ -52,42 +714,105 @@ paths: allowEmptyValue: true schema: type: integer - default: 10 + default: 20 minimum: 0 - maximum: 50 + maximum: 100 style: form explode: false allowReserved: false - - name: tags + name: name in: query - description: 'Filter base on values given' + description: 'Search field based on value given' required: false deprecated: false allowEmptyValue: true schema: - type: array - items: - type: string - style: deepObject + type: string + style: form + explode: false + allowReserved: false + deprecated: false + parameters: [] + '/api/v2/organizations/{id}': + get: + operationId: api_organizations_id_get + tags: + - Organization + responses: + 200: + description: 'Single organization' + summary: 'Get single organization based on identifier' + description: 'Retrieves a Organization resource.' + parameters: + - + name: id + in: path + description: '' + required: true + deprecated: false + allowEmptyValue: false + schema: + type: integer + style: simple explode: false allowReserved: false + deprecated: false + parameters: [] + /api/v2/tags: + get: + operationId: api_tags_get_collection + tags: + - Tag + responses: + 200: + description: 'Tag collection' + content: + application/ld+json: + schema: + type: object + properties: + 'hydra:member': { type: array, items: { $ref: '#/components/schemas/Tag.jsonld' } } + 'hydra:totalItems': { type: integer, minimum: 0 } + 'hydra:view': { type: object, properties: { '@id': { type: string, format: iri-reference }, '@type': { type: string }, 'hydra:first': { type: string, format: iri-reference }, 'hydra:last': { type: string, format: iri-reference }, 'hydra:previous': { type: string, format: iri-reference }, 'hydra:next': { type: string, format: iri-reference } }, example: { '@id': string, type: string, 'hydra:first': string, 'hydra:last': string, 'hydra:previous': string, 'hydra:next': string } } + 'hydra:search': { type: object, properties: { '@type': { type: string }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string }, 'hydra:mapping': { type: array, items: { type: object, properties: { '@type': { type: string }, variable: { type: string }, property: { type: [string, 'null'] }, required: { type: boolean } } } } } } + required: + - 'hydra:member' + summary: 'Retrieves the collection of Tag resources.' + description: 'Retrieves the collection of Tag resources.' + parameters: - - name: occurrences.start + name: page in: query - description: 'Filter base on date (greater then or equal to)' + description: 'The collection page number' required: false deprecated: false allowEmptyValue: true schema: - type: string + type: integer + default: 1 style: form explode: false allowReserved: false - - name: occurrences.end + name: itemsPerPage in: query - description: 'Filter base on date (less then or equal to)' + 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: vocabulary + in: query + description: 'Search field based on value given' required: false deprecated: false allowEmptyValue: true @@ -98,52 +823,52 @@ paths: allowReserved: false deprecated: false parameters: [] - '/api/v2/events/{id}': + '/api/v2/tags/{name}': get: - operationId: api_events_id_get + operationId: api_tags_name_get tags: - - Event + - Tag responses: 200: - description: 'Single event' - summary: 'Retrieves a Event resource.' - description: 'Retrieves a Event resource.' + description: 'Single tag' + summary: 'Get single tag' + description: 'Retrieves a Tag resource.' parameters: - - name: id + name: name in: path description: '' required: true deprecated: false allowEmptyValue: false schema: - type: integer + type: string style: simple explode: false allowReserved: false deprecated: false parameters: [] - /api/v2/organizations: + /api/v2/vocabularies: get: - operationId: api_organizations_get_collection + operationId: api_vocabularies_get_collection tags: - - Organization + - Vocabulary responses: 200: - description: 'Organization collection' + description: 'Vocabulary collection' content: application/ld+json: schema: type: object properties: - 'hydra:member': { type: array, items: { $ref: '#/components/schemas/Organization.OrganizationRepresentationProvider.jsonld' } } + 'hydra:member': { type: array, items: { $ref: '#/components/schemas/Vocabulary.jsonld' } } 'hydra:totalItems': { type: integer, minimum: 0 } 'hydra:view': { type: object, properties: { '@id': { type: string, format: iri-reference }, '@type': { type: string }, 'hydra:first': { type: string, format: iri-reference }, 'hydra:last': { type: string, format: iri-reference }, 'hydra:previous': { type: string, format: iri-reference }, 'hydra:next': { type: string, format: iri-reference } }, example: { '@id': string, type: string, 'hydra:first': string, 'hydra:last': string, 'hydra:previous': string, 'hydra:next': string } } 'hydra:search': { type: object, properties: { '@type': { type: string }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string }, 'hydra:mapping': { type: array, items: { type: object, properties: { '@type': { type: string }, variable: { type: string }, property: { type: [string, 'null'] }, required: { type: boolean } } } } } } required: - 'hydra:member' - summary: 'Retrieves the collection of Organization resources.' - description: 'Retrieves the collection of Organization resources.' + summary: 'Retrieves the collection of Vocabulary resources.' + description: 'Retrieves the collection of Vocabulary resources.' parameters: - name: page @@ -174,7 +899,7 @@ paths: explode: false allowReserved: false - - name: name + name: tags in: query description: 'Search field based on value given' required: false @@ -187,26 +912,26 @@ paths: allowReserved: false deprecated: false parameters: [] - '/api/v2/organizations/{id}': + '/api/v2/vocabularies/{name}': get: - operationId: api_organizations_id_get + operationId: api_vocabularies_name_get tags: - - Organization + - Vocabulary responses: 200: - description: 'Single organization' - summary: 'Get single organization based on identifier' - description: 'Retrieves a Organization resource.' + description: 'Single vocabulary' + summary: 'Get a vocabulary based on name' + description: 'Retrieves a Vocabulary resource.' parameters: - - name: id + name: name in: path description: '' required: true deprecated: false allowEmptyValue: false schema: - type: integer + type: string style: simple explode: false allowReserved: false @@ -214,6 +939,34 @@ paths: parameters: [] components: schemas: + DailyOccurrence.DailyOccurrenceRepresentationProvider.jsonld: + type: object + description: '' + deprecated: false + properties: + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + '@context': + readOnly: true + oneOf: + - + type: string + - + type: object + properties: + '@vocab': + type: string + hydra: + type: string + enum: ['http://www.w3.org/ns/hydra/core#'] + required: + - '@vocab' + - hydra + additionalProperties: true Event.EventRepresentationProvider.jsonld: type: object description: '' @@ -242,6 +995,62 @@ components: - '@vocab' - hydra additionalProperties: true + Location.LocationRepresentationProvider.jsonld: + type: object + description: '' + deprecated: false + properties: + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + '@context': + readOnly: true + oneOf: + - + type: string + - + type: object + properties: + '@vocab': + type: string + hydra: + type: string + enum: ['http://www.w3.org/ns/hydra/core#'] + required: + - '@vocab' + - hydra + additionalProperties: true + Occurrence.OccurrenceRepresentationProvider.jsonld: + type: object + description: '' + deprecated: false + properties: + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + '@context': + readOnly: true + oneOf: + - + type: string + - + type: object + properties: + '@vocab': + type: string + hydra: + type: string + enum: ['http://www.w3.org/ns/hydra/core#'] + required: + - '@vocab' + - hydra + additionalProperties: true Organization.OrganizationRepresentationProvider.jsonld: type: object description: '' @@ -270,6 +1079,72 @@ components: - '@vocab' - hydra additionalProperties: true + Tag.jsonld: + type: object + description: '' + deprecated: false + properties: + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + '@context': + readOnly: true + oneOf: + - + type: string + - + type: object + properties: + '@vocab': + type: string + hydra: + type: string + enum: ['http://www.w3.org/ns/hydra/core#'] + required: + - '@vocab' + - hydra + additionalProperties: true + name: + type: string + Vocabulary.jsonld: + type: object + description: '' + deprecated: false + properties: + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + '@context': + readOnly: true + oneOf: + - + type: string + - + type: object + properties: + '@vocab': + type: string + hydra: + type: string + enum: ['http://www.w3.org/ns/hydra/core#'] + required: + - '@vocab' + - hydra + additionalProperties: true + name: + type: string + description: + type: string + tags: + type: array + items: + type: string responses: { } parameters: { } examples: { }