From ec474830108b48e8226c01918cb27f0e593e9912 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Thu, 29 Jun 2023 19:20:39 -0400 Subject: [PATCH 01/81] initial Varnish setup --- api/config/packages/api_platform.yaml | 8 ++++- api/docker/varnish/default.vcl | 42 +++++++++++++++++++++++++++ docker-compose.yml | 12 ++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 api/docker/varnish/default.vcl diff --git a/api/config/packages/api_platform.yaml b/api/config/packages/api_platform.yaml index 946fec379d..40a48f7f8a 100644 --- a/api/config/packages/api_platform.yaml +++ b/api/config/packages/api_platform.yaml @@ -23,10 +23,16 @@ api_platform: mercure: #hub_url: '%env(resolve:MERCURE_SUBSCRIBE_URL)%' include_type: true + http_cache: + public: true + invalidation: + enabled: false defaults: stateless: true cache_headers: - vary: ['Content-Type', 'Authorization', 'Origin'] + max_age: 0 + shared_max_age: 3600 + vary: ['Content-Type', 'Authorization', 'Origin', 'Cookie'] pagination_enabled: false itemOperations: [ 'get', 'patch', 'delete' ] collection_operations: diff --git a/api/docker/varnish/default.vcl b/api/docker/varnish/default.vcl new file mode 100644 index 0000000000..4e415e0b6e --- /dev/null +++ b/api/docker/varnish/default.vcl @@ -0,0 +1,42 @@ +vcl 4.0; + +import std; + +backend default { + .host = "caddy"; + .port = "3000"; +} + +sub vcl_recv { + # exclude other services (frontend, print, etc.) + if (req.url !~ "^/api") { + return(pass); + } + + # exclude API documentation, profiler and graphql endpoint + if (req.url ~ "^/api/docs" + || req.url ~ "^/api/graphql" + || req.url ~ "^/api/bundles" + || req.url ~ "^/api/contexts" + || req.url ~ "^/api/_profiler" + || req.url ~ "^/api/_wdt") { + return(pass); + } + + # exclude any format other than HAL + if (req.url !~ "\.jsonhal$" && req.http.Accept !~ "application/hal\+json"){ + return(pass); + } +} + +sub vcl_req_cookie { + # Varnish by default disables caching whenever the request header "Cookie" is set in the request (default safe behavior) + # this bypasses the default behaviour; this is safe because we included "Cookie" in the "Vary" header + return (hash); +} + +sub vcl_beresp_cookie { + # Varnish by default disables caching whenever the reponse header "Set-Cookie" is set in the request (default safe behavior) + # this bypasses the default behaviour + return (deliver); +} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7a47eea5a3..c073a0616e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -93,6 +93,18 @@ services: - ./api/docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - ./api/public:/srv/api/public:ro + http-cache: + image: varnish:7.3.0-alpine + container_name: 'ecamp3-http-cache' + volumes: + - ./api/docker/varnish/default.vcl:/etc/varnish/default.vcl:ro + tmpfs: + - /var/lib/varnish/varnishd:exec + ports: + - target: 80 + published: 3004 + protocol: tcp + print: image: node:18.16.1 container_name: 'ecamp3-print' From b44d9af3e346216ad85bd8b2e9ea53f202b14158 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Thu, 29 Jun 2023 19:50:43 -0400 Subject: [PATCH 02/81] enable xkey purger --- api/.env | 1 + api/config/packages/api_platform.yaml | 4 +++- api/docker/varnish/default.vcl | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/api/.env b/api/.env index b659494c16..99903e5782 100644 --- a/api/.env +++ b/api/.env @@ -18,6 +18,7 @@ TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 ADDITIONAL_TRUSTED_HOSTS=localhost COOKIE_PREFIX=localhost_ MERCURE_SUBSCRIBE_URL=https://localhost/.well-known/mercure +VARNISH_API_URL=http://http-cache:80 ###> symfony/framework-bundle ### APP_ENV=dev diff --git a/api/config/packages/api_platform.yaml b/api/config/packages/api_platform.yaml index 40a48f7f8a..b828bdb894 100644 --- a/api/config/packages/api_platform.yaml +++ b/api/config/packages/api_platform.yaml @@ -26,7 +26,9 @@ api_platform: http_cache: public: true invalidation: - enabled: false + enabled: true + purger: 'api_platform.http_cache.purger.varnish.xkey' + urls: ['%env(VARNISH_API_URL)%'] defaults: stateless: true cache_headers: diff --git a/api/docker/varnish/default.vcl b/api/docker/varnish/default.vcl index 4e415e0b6e..8b3bf394c1 100644 --- a/api/docker/varnish/default.vcl +++ b/api/docker/varnish/default.vcl @@ -1,13 +1,34 @@ vcl 4.0; import std; +import xkey; + backend default { .host = "caddy"; .port = "3000"; } +# Hosts allowed to send BAN requests +acl purgers { + "php"; +} + sub vcl_recv { + # Support xkey purge requests + # see https://raw.githubusercontent.com/varnish/varnish-modules/master/src/vmod_xkey.vcc + if (req.method == "PURGE") { + if (client.ip !~ purgers) { + return (synth(403, "Forbidden")); + } + if (req.http.xkey) { + set req.http.n-gone = xkey.purge(req.http.xkey); + return (synth(200, "Invalidated "+req.http.n-gone+" objects")); + } else { + return (purge); + } + } + # exclude other services (frontend, print, etc.) if (req.url !~ "^/api") { return(pass); From 99c372371bd93c465467ca05a186eb2b1d6d364d Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Fri, 30 Jun 2023 11:36:39 -0400 Subject: [PATCH 03/81] add resources used in security voters to cache tags; ensures cache is purged when own CampCollaboration is modified --- .../Security/Voter/CampIsPrototypeVoter.php | 17 ++++++++++++++- api/src/Security/Voter/CampRoleVoter.php | 21 +++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/api/src/Security/Voter/CampIsPrototypeVoter.php b/api/src/Security/Voter/CampIsPrototypeVoter.php index 40ff790099..368b27808c 100644 --- a/api/src/Security/Voter/CampIsPrototypeVoter.php +++ b/api/src/Security/Voter/CampIsPrototypeVoter.php @@ -2,10 +2,12 @@ namespace App\Security\Voter; +use ApiPlatform\Api\IriConverterInterface; use App\Entity\BelongsToCampInterface; use App\Entity\BelongsToContentNodeTreeInterface; use App\Util\GetCampFromContentNodeTrait; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -17,6 +19,8 @@ class CampIsPrototypeVoter extends Voter { public function __construct( private EntityManagerInterface $em, + private RequestStack $requestStack, + private IriConverterInterface $iriConverter, ) { } @@ -35,6 +39,17 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter return true; } - return $camp->isPrototype; + if ($camp->isPrototype) { + // Add Camp to cache tags + $request = $this->requestStack->getCurrentRequest(); + $resources = [ + $this->iriConverter->getIriFromResource($camp), + ]; + $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array) $resources); + + return true; + } + + return false; } } diff --git a/api/src/Security/Voter/CampRoleVoter.php b/api/src/Security/Voter/CampRoleVoter.php index defc4d2537..1b6f152600 100644 --- a/api/src/Security/Voter/CampRoleVoter.php +++ b/api/src/Security/Voter/CampRoleVoter.php @@ -2,12 +2,14 @@ namespace App\Security\Voter; +use ApiPlatform\Api\IriConverterInterface; use App\Entity\BelongsToCampInterface; use App\Entity\BelongsToContentNodeTreeInterface; use App\Entity\CampCollaboration; use App\Entity\User; use App\Util\GetCampFromContentNodeTrait; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -26,6 +28,8 @@ class CampRoleVoter extends Voter { public function __construct( private EntityManagerInterface $em, + private RequestStack $requestStack, + private IriConverterInterface $iriConverter, ) { } @@ -49,12 +53,25 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter return true; } - return $camp->collaborations + $campCollaboration = $camp->collaborations ->filter(self::withStatus(CampCollaboration::STATUS_ESTABLISHED)) ->filter(self::ofUser($user)) ->filter(self::withRole($attribute)) - ->exists(fn () => true) + ->first() ; + + if ($campCollaboration) { + // Add CampCollaboration to cache tags + $request = $this->requestStack->getCurrentRequest(); + $resources = [ + $this->iriConverter->getIriFromResource($campCollaboration), + ]; + $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array) $resources); + + return true; + } + + return false; } private static function withStatus($status) { From 6fbecb3f88b078de8159222b68d89aaa2a99a1b7 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Fri, 30 Jun 2023 12:39:31 -0400 Subject: [PATCH 04/81] copy PurgeHttpCacheListener from ApiPlatform --- api/config/services.yaml | 12 ++ .../EventListener/PurgeHttpCacheListener.php | 150 ++++++++++++++++++ 2 files changed, 162 insertions(+) create mode 100644 api/src/EventListener/PurgeHttpCacheListener.php diff --git a/api/config/services.yaml b/api/config/services.yaml index 1df8be87df..312e33f3b7 100644 --- a/api/config/services.yaml +++ b/api/config/services.yaml @@ -151,6 +151,18 @@ services: tags: - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated } + api_platform.doctrine.listener.http_cache.purge: + class: App\EventListener\PurgeHttpCacheListener + arguments: + - '@api_platform.http_cache.purger' + - '@api_platform.iri_converter' + - '@api_platform.resource_class_resolver' + - '@api_platform.property_accessor' + tags: + - { name: doctrine.event_listener, event: preUpdate } + - { name: doctrine.event_listener, event: onFlush } + - { name: doctrine.event_listener, event: postFlush } + # Entity Filter App\Doctrine\FilterByCurrentUserExtension: tags: diff --git a/api/src/EventListener/PurgeHttpCacheListener.php b/api/src/EventListener/PurgeHttpCacheListener.php new file mode 100644 index 0000000000..e07b065e5f --- /dev/null +++ b/api/src/EventListener/PurgeHttpCacheListener.php @@ -0,0 +1,150 @@ + + */ +final class PurgeHttpCacheListener { + use ClassInfoTrait; + private readonly PropertyAccessorInterface $propertyAccessor; + private array $tags = []; + + public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null) { + $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); + } + + /** + * Collects tags from the previous and the current version of the updated entities to purge related documents. + */ + public function preUpdate(PreUpdateEventArgs $eventArgs): void { + $object = $eventArgs->getObject(); + $this->gatherResourceAndItemTags($object, true); + + $changeSet = $eventArgs->getEntityChangeSet(); + $objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); + $associationMappings = $objectManager->getClassMetadata(ClassUtils::getClass($eventArgs->getObject()))->getAssociationMappings(); + + foreach ($changeSet as $key => $value) { + if (!isset($associationMappings[$key])) { + continue; + } + + $this->addTagsFor($value[0]); + $this->addTagsFor($value[1]); + } + } + + /** + * Collects tags from inserted and deleted entities, including relations. + */ + public function onFlush(OnFlushEventArgs $eventArgs): void { + $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); + $uow = $em->getUnitOfWork(); + + foreach ($uow->getScheduledEntityInsertions() as $entity) { + $this->gatherResourceAndItemTags($entity, false); + $this->gatherRelationTags($em, $entity); + } + + foreach ($uow->getScheduledEntityUpdates() as $entity) { + $this->gatherResourceAndItemTags($entity, true); + $this->gatherRelationTags($em, $entity); + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + $this->gatherResourceAndItemTags($entity, true); + $this->gatherRelationTags($em, $entity); + } + } + + /** + * Purges tags collected during this request, and clears the tag list. + */ + public function postFlush(): void { + if (empty($this->tags)) { + return; + } + + $this->purger->purge(array_values($this->tags)); + + $this->tags = []; + } + + private function gatherResourceAndItemTags(object $entity, bool $purgeItem): void { + try { + $resourceClass = $this->resourceClassResolver->getResourceClass($entity); + $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, new GetCollection()); + $this->tags[$iri] = $iri; + + if ($purgeItem) { + $this->addTagForItem($entity); + } + } catch (OperationNotFoundException|InvalidArgumentException) { + } + } + + private function gatherRelationTags(EntityManagerInterface $em, object $entity): void { + $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings(); + foreach (array_keys($associationMappings) as $property) { + if ($this->propertyAccessor->isReadable($entity, $property)) { + $this->addTagsFor($this->propertyAccessor->getValue($entity, $property)); + } + } + } + + private function addTagsFor(mixed $value): void { + if (!$value || \is_scalar($value)) { + return; + } + + if (!is_iterable($value)) { + $this->addTagForItem($value); + + return; + } + + if ($value instanceof PersistentCollection) { + $value = clone $value; + } + + foreach ($value as $v) { + $this->addTagForItem($v); + } + } + + private function addTagForItem(mixed $value): void { + if (!$this->resourceClassResolver->isResourceClass($this->getObjectClass($value))) { + return; + } + + try { + $iri = $this->iriConverter->getIriFromResource($value); + $this->tags[$iri] = $iri; + } catch (RuntimeException|InvalidArgumentException) { + } + } +} From ccced29ade450d7118e18edca4c31b552f69fb98 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Fri, 30 Jun 2023 14:14:04 -0400 Subject: [PATCH 05/81] POC: camp specific URL for category collection --- api/config/packages/api_platform.yaml | 2 +- api/docker/varnish/default.vcl | 10 ++++++++++ api/src/Entity/Category.php | 15 +++++++++++++++ .../EventListener/PurgeHttpCacheListener.php | 19 +++++++++++++++---- 4 files changed, 41 insertions(+), 5 deletions(-) diff --git a/api/config/packages/api_platform.yaml b/api/config/packages/api_platform.yaml index b828bdb894..52d8d1ab13 100644 --- a/api/config/packages/api_platform.yaml +++ b/api/config/packages/api_platform.yaml @@ -34,7 +34,7 @@ api_platform: cache_headers: max_age: 0 shared_max_age: 3600 - vary: ['Content-Type', 'Authorization', 'Origin', 'Cookie'] + vary: ['Content-Type', 'Authorization', 'Origin'] pagination_enabled: false itemOperations: [ 'get', 'patch', 'delete' ] collection_operations: diff --git a/api/docker/varnish/default.vcl b/api/docker/varnish/default.vcl index 8b3bf394c1..8cd11df6c3 100644 --- a/api/docker/varnish/default.vcl +++ b/api/docker/varnish/default.vcl @@ -2,6 +2,7 @@ vcl 4.0; import std; import xkey; +import cookie; backend default { @@ -50,6 +51,15 @@ sub vcl_recv { } } +sub vcl_hash { + if (req.http.Cookie) { + # Include JWT cookies in cache hash + cookie.parse(req.http.Cookie); + cookie.keep("localhost_jwt_hp,localhost_jwt_s"); + hash_data(cookie.get_string()); + } +} + sub vcl_req_cookie { # Varnish by default disables caching whenever the request header "Cookie" is set in the request (default safe behavior) # this bypasses the default behaviour; this is safe because we included "Cookie" in the "Vary" header diff --git a/api/src/Entity/Category.php b/api/src/Entity/Category.php index 23937d2e17..9980ec3656 100644 --- a/api/src/Entity/Category.php +++ b/api/src/Entity/Category.php @@ -9,6 +9,7 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use App\InputFilter; @@ -59,6 +60,20 @@ normalizationContext: ['groups' => ['read']], order: ['camp.id', 'short'], )] +#[ApiResource( + uriTemplate: '/camps/{campId}/categories.{_format}', + uriVariables: [ + 'campId' => new Link( + fromClass: Camp::class, + fromProperty: 'categories' + ), + ], + operations: [ + new GetCollection( + security: 'is_authenticated()', + ), + ] +)] #[ApiFilter(filterClass: SearchFilter::class, properties: ['camp'])] #[ORM\Entity(repositoryClass: CategoryRepository::class)] class Category extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface { diff --git a/api/src/EventListener/PurgeHttpCacheListener.php b/api/src/EventListener/PurgeHttpCacheListener.php index e07b065e5f..32f71ac9b4 100644 --- a/api/src/EventListener/PurgeHttpCacheListener.php +++ b/api/src/EventListener/PurgeHttpCacheListener.php @@ -14,7 +14,9 @@ use ApiPlatform\Exception\RuntimeException; use ApiPlatform\HttpCache\PurgerInterface; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; +use App\Entity\Category; use Doctrine\Common\Util\ClassUtils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; @@ -33,7 +35,7 @@ final class PurgeHttpCacheListener { private readonly PropertyAccessorInterface $propertyAccessor; private array $tags = []; - public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null) { + public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor, private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) { $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); } @@ -67,17 +69,17 @@ public function onFlush(OnFlushEventArgs $eventArgs): void { foreach ($uow->getScheduledEntityInsertions() as $entity) { $this->gatherResourceAndItemTags($entity, false); - $this->gatherRelationTags($em, $entity); + // $this->gatherRelationTags($em, $entity); } foreach ($uow->getScheduledEntityUpdates() as $entity) { $this->gatherResourceAndItemTags($entity, true); - $this->gatherRelationTags($em, $entity); + // $this->gatherRelationTags($em, $entity); } foreach ($uow->getScheduledEntityDeletions() as $entity) { $this->gatherResourceAndItemTags($entity, true); - $this->gatherRelationTags($em, $entity); + // $this->gatherRelationTags($em, $entity); } } @@ -103,6 +105,15 @@ private function gatherResourceAndItemTags(object $entity, bool $purgeItem): voi if ($purgeItem) { $this->addTagForItem($entity); } + + // purge camp specific collection + // this is only an POC/example for category; later this could filter for instances of BelongsToCampInterface + if (Category::class === $resourceClass) { + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + $operation = $resourceMetadataCollection->getOperation('_api_/camps/{campId}/categories.{_format}_get_collection'); + $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => ['campId' => $entity->getCamp()->getId()]]); + $this->tags[$iri] = $iri; + } } catch (OperationNotFoundException|InvalidArgumentException) { } } From 3dbbd5d1a98618ac452c24bc7c740f995aa2d429 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 2 Jul 2023 15:13:48 -0500 Subject: [PATCH 06/81] fix docker startup sequence --- docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index c073a0616e..61fd361cec 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -96,6 +96,8 @@ services: http-cache: image: varnish:7.3.0-alpine container_name: 'ecamp3-http-cache' + depends_on: + - caddy volumes: - ./api/docker/varnish/default.vcl:/etc/varnish/default.vcl:ro tmpfs: From 23533481bdc177bbed966153342fcf6e4f368b53 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 18 Jul 2023 10:22:54 -0500 Subject: [PATCH 07/81] 2nd example for camp specific URL (deeply nested entity) --- api/config/services.yaml | 8 +++- api/src/Doctrine/FilterByCampExtension.php | 45 +++++++++++++++++++ api/src/Entity/Category.php | 24 +++++----- api/src/Entity/ScheduleEntry.php | 11 +++++ .../Repository/CanFilterByCampInterface.php | 10 +++++ api/src/Repository/CategoryRepository.php | 11 ++++- .../Repository/ScheduleEntryRepository.php | 14 +++++- 7 files changed, 106 insertions(+), 17 deletions(-) create mode 100644 api/src/Doctrine/FilterByCampExtension.php create mode 100644 api/src/Repository/CanFilterByCampInterface.php diff --git a/api/config/services.yaml b/api/config/services.yaml index 312e33f3b7..0fb0c910ca 100644 --- a/api/config/services.yaml +++ b/api/config/services.yaml @@ -163,13 +163,19 @@ services: - { name: doctrine.event_listener, event: onFlush } - { name: doctrine.event_listener, event: postFlush } - # Entity Filter + # Entity Filters App\Doctrine\FilterByCurrentUserExtension: tags: # FilterEagerLoadingExtension has Priority -17 and breaks the DQL generated in ContentNodeRepository => Priority -20 ensures this runs after FilterEagerLoadingExtension - { name: api_platform.doctrine.orm.query_extension.collection, priority: -20 } - { name: api_platform.doctrine.orm.query_extension.item } + App\Doctrine\FilterByCampExtension: + tags: + - { name: api_platform.doctrine.orm.query_extension.collection, priority: -19 } + - { name: api_platform.doctrine.orm.query_extension.item } + + App\Metadata\Resource\Factory\UriTemplateFactory: arguments: - '@api_platform.filter_locator' diff --git a/api/src/Doctrine/FilterByCampExtension.php b/api/src/Doctrine/FilterByCampExtension.php new file mode 100644 index 0000000000..0573bd9116 --- /dev/null +++ b/api/src/Doctrine/FilterByCampExtension.php @@ -0,0 +1,45 @@ +entityManager = $entityManager; + } + + public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass = null, Operation $operation = null, array $context = []): void { + if (!isset($context['uri_variables']['campId'])) { + return; + } + + $this->addWhere($queryBuilder, $queryNameGenerator, $resourceClass, $context['uri_variables']['campId']); + } + + public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void { + if (!isset($context['uri_variables']['campId'])) { + return; + } + + $this->addWhere($queryBuilder, $queryNameGenerator, $resourceClass, $context['uri_variables']['campId']); + } + + private function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $campId): void { + $repository = $this->entityManager->getRepository($resourceClass); + + if (!($repository instanceof CanFilterByCampInterface) || null === $campId) { + return; + } + + $repository->filterByCamp($queryBuilder, $queryNameGenerator, $campId); + } +} diff --git a/api/src/Entity/Category.php b/api/src/Entity/Category.php index 9980ec3656..716fa4409b 100644 --- a/api/src/Entity/Category.php +++ b/api/src/Entity/Category.php @@ -55,25 +55,21 @@ normalizationContext: self::ITEM_NORMALIZATION_CONTEXT, securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' ), + new GetCollection( + security: 'is_authenticated()', + uriTemplate: '/camps/{campId}/categories.{_format}', + uriVariables: [ + 'campId' => new Link( + fromClass: Camp::class, + expandedValue: '{campId}' + ), + ], + ), ], denormalizationContext: ['groups' => ['write']], normalizationContext: ['groups' => ['read']], order: ['camp.id', 'short'], )] -#[ApiResource( - uriTemplate: '/camps/{campId}/categories.{_format}', - uriVariables: [ - 'campId' => new Link( - fromClass: Camp::class, - fromProperty: 'categories' - ), - ], - operations: [ - new GetCollection( - security: 'is_authenticated()', - ), - ] -)] #[ApiFilter(filterClass: SearchFilter::class, properties: ['camp'])] #[ORM\Entity(repositoryClass: CategoryRepository::class)] class Category extends BaseEntity implements BelongsToCampInterface, CopyFromPrototypeInterface { diff --git a/api/src/Entity/ScheduleEntry.php b/api/src/Entity/ScheduleEntry.php index 6fee7cd13c..21a9c7c4e4 100644 --- a/api/src/Entity/ScheduleEntry.php +++ b/api/src/Entity/ScheduleEntry.php @@ -9,6 +9,7 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use App\Doctrine\Filter\ExpressionDateTimeFilter; @@ -48,6 +49,16 @@ securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)', validationContext: ['groups' => ScheduleEntryPostGroupSequence::class] ), + new GetCollection( + security: 'is_authenticated()', + uriTemplate: '/camps/{campId}/schedule_entries.{_format}', + uriVariables: [ + 'campId' => new Link( + fromClass: Camp::class, + expandedValue: '{campId}' + ), + ], + ), ], denormalizationContext: ['groups' => ['write']], normalizationContext: ['groups' => ['read']] diff --git a/api/src/Repository/CanFilterByCampInterface.php b/api/src/Repository/CanFilterByCampInterface.php new file mode 100644 index 0000000000..ddf31aeb44 --- /dev/null +++ b/api/src/Repository/CanFilterByCampInterface.php @@ -0,0 +1,10 @@ +innerJoin("{$rootAlias}.camp", 'camp'); $this->filterByCampCollaboration($queryBuilder, $user); } + + public function filterByCamp(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $campId): void { + $rootAlias = $queryBuilder->getRootAliases()[0]; + $queryBuilder->andWhere( + $queryBuilder->expr()->eq("{$rootAlias}.camp", ':camp') + ); + $queryBuilder->setParameter('camp', $campId); + } } diff --git a/api/src/Repository/ScheduleEntryRepository.php b/api/src/Repository/ScheduleEntryRepository.php index 6b8e6e44af..367e24d376 100644 --- a/api/src/Repository/ScheduleEntryRepository.php +++ b/api/src/Repository/ScheduleEntryRepository.php @@ -2,6 +2,7 @@ namespace App\Repository; +use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use App\Entity\ScheduleEntry; use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -14,7 +15,7 @@ * @method ScheduleEntry[] findAll() * @method ScheduleEntry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class ScheduleEntryRepository extends ServiceEntityRepository implements CanFilterByUserInterface { +class ScheduleEntryRepository extends ServiceEntityRepository implements CanFilterByUserInterface, CanFilterByCampInterface { use FiltersByCampCollaboration; public function __construct(ManagerRegistry $registry) { @@ -39,4 +40,15 @@ public function filterByUser(QueryBuilder $queryBuilder, User $user): void { $queryBuilder->innerJoin('activity.camp', 'camp'); $this->filterByCampCollaboration($queryBuilder, $user); } + + public function filterByCamp(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $campId): void { + $activityJoinAlias = $queryNameGenerator->generateJoinAlias('activity'); + + $rootAlias = $queryBuilder->getRootAliases()[0]; + $queryBuilder->innerJoin("{$rootAlias}.activity", $activityJoinAlias); + $queryBuilder->andWhere( + $queryBuilder->expr()->eq("{$activityJoinAlias}.camp", ':camp') + ); + $queryBuilder->setParameter('camp', $campId); + } } From 4b1f3c685fc9f13941d75a103112443c0bf098f8 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 18 Jul 2023 11:25:13 -0500 Subject: [PATCH 08/81] adjust RelatedCollectionLinkNormalizer to use camp specific route if it exists --- api/src/Entity/Category.php | 1 + api/src/Entity/ScheduleEntry.php | 1 + .../RelatedCollectionLinkNormalizer.php | 21 +++++++++++++++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/api/src/Entity/Category.php b/api/src/Entity/Category.php index 716fa4409b..9193249e76 100644 --- a/api/src/Entity/Category.php +++ b/api/src/Entity/Category.php @@ -57,6 +57,7 @@ ), new GetCollection( security: 'is_authenticated()', + name: 'BelongsToCamp_App\Entity\Category_get_collection', uriTemplate: '/camps/{campId}/categories.{_format}', uriVariables: [ 'campId' => new Link( diff --git a/api/src/Entity/ScheduleEntry.php b/api/src/Entity/ScheduleEntry.php index 21a9c7c4e4..28ede3dda6 100644 --- a/api/src/Entity/ScheduleEntry.php +++ b/api/src/Entity/ScheduleEntry.php @@ -51,6 +51,7 @@ ), new GetCollection( security: 'is_authenticated()', + name: 'BelongsToCamp_App\Entity\ScheduleEntry_get_collection', uriTemplate: '/camps/{campId}/schedule_entries.{_format}', uriVariables: [ 'campId' => new Link( diff --git a/api/src/Serializer/Normalizer/RelatedCollectionLinkNormalizer.php b/api/src/Serializer/Normalizer/RelatedCollectionLinkNormalizer.php index ca4c03a481..bead29b4f2 100644 --- a/api/src/Serializer/Normalizer/RelatedCollectionLinkNormalizer.php +++ b/api/src/Serializer/Normalizer/RelatedCollectionLinkNormalizer.php @@ -7,10 +7,12 @@ use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Doctrine\Common\PropertyHelperTrait; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Exception\OperationNotFoundException; use ApiPlatform\Exception\ResourceClassNotFoundException; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use App\Entity\BaseEntity; +use App\Entity\BelongsToCampInterface; use App\Metadata\Resource\Factory\UriTemplateFactory; use App\Metadata\Resource\OperationHelper; use App\Util\ClassInfoTrait; @@ -166,7 +168,22 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st } $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($relatedResourceClass); - $operation = OperationHelper::findOneByType($resourceMetadataCollection, GetCollection::class); + + $operation = null; + $parameters[$relatedFilterName] = urlencode($this->iriConverter->getIriFromResource($object)); + + try { + if (is_a($relatedResourceClass, BelongsToCampInterface::class, true) && is_a($object, BelongsToCampInterface::class, true)) { + $operation = $resourceMetadataCollection->getOperation("BelongsToCamp_{$relatedResourceClass}_get_collection"); + $parameters['campId'] = $object->getCamp()->getId(); + $parameters['camp'] = null; + } + } catch (OperationNotFoundException $e) { + } finally { + if (!$operation) { + $operation = OperationHelper::findOneByType($resourceMetadataCollection, GetCollection::class); + } + } if (!$operation) { throw new UnsupportedRelationException('The resource '.$relatedResourceClass.' does not implement GetCollection() operation.'); @@ -176,7 +193,7 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st throw new UnsupportedRelationException('The resource '.$relatedResourceClass.' does not have a search filter for the relation '.$relatedFilterName.'.'); } - return $this->router->generate($operation->getName(), [$relatedFilterName => urlencode($this->iriConverter->getIriFromResource($object))], UrlGeneratorInterface::ABS_PATH); + return $this->router->generate($operation->getName(), $parameters, UrlGeneratorInterface::ABS_PATH); } protected function getRelatedCollectionLinkAnnotation(string $className, string $propertyName): ?RelatedCollectionLink { From c359c199229d32c883960f7086610966c60159c7 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 27 Aug 2023 12:54:27 +0200 Subject: [PATCH 09/81] use new TagCollector service --- api/composer.json | 10 +- api/composer.lock | 62 +++++----- api/config/services.yaml | 5 +- .../PurgeHttpCacheListener.php | 109 +++++++++++------- api/src/HttpCache/TagCollector.php | 53 +++++++++ 5 files changed, 156 insertions(+), 83 deletions(-) rename api/src/{EventListener => HttpCache}/PurgeHttpCacheListener.php (62%) create mode 100644 api/src/HttpCache/TagCollector.php diff --git a/api/composer.json b/api/composer.json index a74d5779a3..9369ed2801 100644 --- a/api/composer.json +++ b/api/composer.json @@ -5,7 +5,7 @@ "php": ">=8.1.0", "ext-ctype": "*", "ext-iconv": "*", - "api-platform/core": "3.1.12", + "api-platform/core": "dev-chore/service-for-cache-tags", "composer/package-versions-deprecated": "1.11.99", "cweagans/composer-patches": "1.7.3", "doctrine/doctrine-bundle": "2.10.0", @@ -140,5 +140,11 @@ "Allow NULL-Links": "patch/api-plattform-allow-null-links.patch" } } - } + }, + "repositories": [ + { + "type": "git", + "url": "https://github.com/usu/api-platform-core" + } + ] } diff --git a/api/composer.lock b/api/composer.lock index 94d6816e0c..d565496a6a 100644 --- a/api/composer.lock +++ b/api/composer.lock @@ -4,21 +4,15 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c9589d431daa9ab01cdf27cf1b40f5aa", + "content-hash": "c1cc1cc1b8be1c6cf5e1652db7fe7071", "packages": [ { "name": "api-platform/core", - "version": "v3.1.12", + "version": "dev-chore/service-for-cache-tags", "source": { "type": "git", - "url": "https://github.com/api-platform/core.git", - "reference": "1fe505a9d8fd235a8d7e4aa0f245f382f65578f8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/1fe505a9d8fd235a8d7e4aa0f245f382f65578f8", - "reference": "1fe505a9d8fd235a8d7e4aa0f245f382f65578f8", - "shasum": "" + "url": "https://github.com/usu/api-platform-core", + "reference": "c50339f31178b78c99bcb0369c001597466b70f6" }, "require": { "doctrine/inflector": "^1.0 || ^2.0", @@ -48,10 +42,9 @@ }, "require-dev": { "behat/behat": "^3.1", - "behat/mink": "^1.9@dev", + "behat/mink": "^1.9", "doctrine/cache": "^1.11 || ^2.1", "doctrine/common": "^3.2.2", - "doctrine/data-fixtures": "^1.2.2", "doctrine/dbal": "^3.4.0", "doctrine/doctrine-bundle": "^1.12 || ^2.0", "doctrine/mongodb-odm": "^2.2", @@ -72,9 +65,9 @@ "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-symfony": "^1.0", "psr/log": "^1.0 || ^2.0 || ^3.0", - "ramsey/uuid": "^3.7 || ^4.0", - "ramsey/uuid-doctrine": "^1.4", - "soyuka/contexts": "^3.3.6", + "ramsey/uuid": "^3.9.7 || ^4.0", + "ramsey/uuid-doctrine": "^1.4 || ^2.0", + "soyuka/contexts": "v3.3.9", "soyuka/stubs-mongodb": "^1.0", "symfony/asset": "^6.1", "symfony/browser-kit": "^6.1", @@ -82,7 +75,7 @@ "symfony/config": "^6.1", "symfony/console": "^6.1", "symfony/css-selector": "^6.1", - "symfony/dependency-injection": "^6.1", + "symfony/dependency-injection": "^6.1.12", "symfony/doctrine-bridge": "^6.1", "symfony/dom-crawler": "^6.1", "symfony/error-handler": "^6.1", @@ -140,7 +133,12 @@ "ApiPlatform\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "ApiPlatform\\Tests\\": "tests/", + "App\\": "tests/Fixtures/app/var/tmp/src/" + } + }, "license": [ "MIT" ], @@ -154,27 +152,17 @@ "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", "homepage": "https://api-platform.com", "keywords": [ + "API", + "GraphQL", + "HAL", "Hydra", "JSON-LD", - "api", - "graphql", - "hal", - "jsonapi", - "openapi", - "rest", - "swagger" + "JSONAPI", + "OpenAPI", + "REST", + "Swagger" ], - "support": { - "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/v3.1.12" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/api-platform/core", - "type": "tidelift" - } - ], - "time": "2023-05-24T19:23:57+00:00" + "time": "2023-08-27T06:25:20+00:00" }, { "name": "behat/transliterator", @@ -14209,7 +14197,9 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "api-platform/core": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { diff --git a/api/config/services.yaml b/api/config/services.yaml index 0fb0c910ca..3d37114fa9 100644 --- a/api/config/services.yaml +++ b/api/config/services.yaml @@ -152,7 +152,7 @@ services: - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated } api_platform.doctrine.listener.http_cache.purge: - class: App\EventListener\PurgeHttpCacheListener + class: App\HttpCache\PurgeHttpCacheListener arguments: - '@api_platform.http_cache.purger' - '@api_platform.iri_converter' @@ -163,6 +163,9 @@ services: - { name: doctrine.event_listener, event: onFlush } - { name: doctrine.event_listener, event: postFlush } + api_platform.http_cache.tag_collector: + class: App\HttpCache\TagCollector + # Entity Filters App\Doctrine\FilterByCurrentUserExtension: tags: diff --git a/api/src/EventListener/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php similarity index 62% rename from api/src/EventListener/PurgeHttpCacheListener.php rename to api/src/HttpCache/PurgeHttpCacheListener.php index 32f71ac9b4..e8fffcaf0a 100644 --- a/api/src/EventListener/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -1,10 +1,17 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ declare(strict_types=1); -namespace App\EventListener; +namespace App\HttpCache; use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Api\ResourceClassResolverInterface; @@ -14,9 +21,7 @@ use ApiPlatform\Exception\RuntimeException; use ApiPlatform\HttpCache\PurgerInterface; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\Util\ClassInfoTrait; -use App\Entity\Category; use Doctrine\Common\Util\ClassUtils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; @@ -30,21 +35,26 @@ * * @author Kévin Dunglas */ -final class PurgeHttpCacheListener { +final class PurgeHttpCacheListener +{ use ClassInfoTrait; private readonly PropertyAccessorInterface $propertyAccessor; private array $tags = []; - public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor, private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) { + public const IRI_RELATION_DELIMITER = '#'; + + public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null) + { $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); } /** * Collects tags from the previous and the current version of the updated entities to purge related documents. */ - public function preUpdate(PreUpdateEventArgs $eventArgs): void { + public function preUpdate(PreUpdateEventArgs $eventArgs): void + { $object = $eventArgs->getObject(); - $this->gatherResourceAndItemTags($object, true); + $this->addTagForItem($object); $changeSet = $eventArgs->getEntityChangeSet(); $objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); @@ -54,39 +64,42 @@ public function preUpdate(PreUpdateEventArgs $eventArgs): void { if (!isset($associationMappings[$key])) { continue; } + $mappings = $associationMappings[$key]; + $relatedProperty = $mappings['isOwningSide'] ? $mappings['inversedBy'] : $mappings['mappedBy']; + if (!$relatedProperty) { + continue; + } - $this->addTagsFor($value[0]); - $this->addTagsFor($value[1]); + $this->addTagsFor($value[0], $relatedProperty); + $this->addTagsFor($value[1], $relatedProperty); } } /** * Collects tags from inserted and deleted entities, including relations. */ - public function onFlush(OnFlushEventArgs $eventArgs): void { + public function onFlush(OnFlushEventArgs $eventArgs): void + { $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); $uow = $em->getUnitOfWork(); foreach ($uow->getScheduledEntityInsertions() as $entity) { - $this->gatherResourceAndItemTags($entity, false); - // $this->gatherRelationTags($em, $entity); - } - - foreach ($uow->getScheduledEntityUpdates() as $entity) { - $this->gatherResourceAndItemTags($entity, true); - // $this->gatherRelationTags($em, $entity); + $this->gatherResourceTags($entity); + $this->gatherRelationTags($em, $entity); } foreach ($uow->getScheduledEntityDeletions() as $entity) { - $this->gatherResourceAndItemTags($entity, true); - // $this->gatherRelationTags($em, $entity); + $this->addTagForItem($entity); + $this->gatherResourceTags($entity); + $this->gatherRelationTags($em, $entity); } } /** * Purges tags collected during this request, and clears the tag list. */ - public function postFlush(): void { + public function postFlush(): void + { if (empty($this->tags)) { return; } @@ -96,44 +109,48 @@ public function postFlush(): void { $this->tags = []; } - private function gatherResourceAndItemTags(object $entity, bool $purgeItem): void { + private function gatherResourceTags(object $entity): void + { try { $resourceClass = $this->resourceClassResolver->getResourceClass($entity); $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, new GetCollection()); $this->tags[$iri] = $iri; - - if ($purgeItem) { - $this->addTagForItem($entity); - } - - // purge camp specific collection - // this is only an POC/example for category; later this could filter for instances of BelongsToCampInterface - if (Category::class === $resourceClass) { - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - $operation = $resourceMetadataCollection->getOperation('_api_/camps/{campId}/categories.{_format}_get_collection'); - $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => ['campId' => $entity->getCamp()->getId()]]); - $this->tags[$iri] = $iri; - } } catch (OperationNotFoundException|InvalidArgumentException) { } } - private function gatherRelationTags(EntityManagerInterface $em, object $entity): void { + private function gatherRelationTags(EntityManagerInterface $em, object $entity): void + { $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings(); - foreach (array_keys($associationMappings) as $property) { - if ($this->propertyAccessor->isReadable($entity, $property)) { - $this->addTagsFor($this->propertyAccessor->getValue($entity, $property)); + foreach ($associationMappings as $property => $mappings) { + $relatedProperty = $mappings['isOwningSide'] ? $mappings['inversedBy'] : $mappings['mappedBy']; + if (!$relatedProperty) { + continue; + } + + if (!$this->propertyAccessor->isReadable($entity, $property)) { + continue; + } + $relatedObject = $this->propertyAccessor->getValue($entity, $property); + if ($relatedObject === $entity) { + continue; } + + $this->addTagsFor( + $relatedObject, + $relatedProperty + ); } } - private function addTagsFor(mixed $value): void { + private function addTagsFor(mixed $value, string $property = null): void + { if (!$value || \is_scalar($value)) { return; } if (!is_iterable($value)) { - $this->addTagForItem($value); + $this->addTagForItem($value, $property); return; } @@ -143,19 +160,23 @@ private function addTagsFor(mixed $value): void { } foreach ($value as $v) { - $this->addTagForItem($v); + $this->addTagForItem($v, $property); } } - private function addTagForItem(mixed $value): void { + private function addTagForItem(mixed $value, string $property = null): void + { if (!$this->resourceClassResolver->isResourceClass($this->getObjectClass($value))) { return; } try { $iri = $this->iriConverter->getIriFromResource($value); + if ($property) { + $iri .= self::IRI_RELATION_DELIMITER.$property; + } $this->tags[$iri] = $iri; } catch (RuntimeException|InvalidArgumentException) { } } -} +} \ No newline at end of file diff --git a/api/src/HttpCache/TagCollector.php b/api/src/HttpCache/TagCollector.php new file mode 100644 index 0000000000..59282aa275 --- /dev/null +++ b/api/src/HttpCache/TagCollector.php @@ -0,0 +1,53 @@ + + */ +class TagCollector implements TagCollectorInterface +{ + public const IRI_RELATION_DELIMITER = '#'; + + public function collect(mixed $object = null, string $format = null, array $context = [], string $iri = null, mixed $data = null, string $attribute = null, ApiProperty $propertyMetadata = null, Type $type = null): void + { + if($attribute){ + $this->addCacheTagsForRelation($context, $iri, $propertyMetadata); + } + elseif(is_array($data)){ + $this->addCacheTagForResource($context, $iri); + } + } + + private function addCacheTagForResource(array $context, ?string $iri): void + { + if (isset($context['resources']) && isset($iri)) { + $context['resources'][$iri] = $iri; + } + } + + private function addCacheTagsForRelation(array $context, ?string $iri, ApiProperty $propertyMetadata): void + { + if (isset($context['resources']) && isset($iri)) { + if (isset($propertyMetadata->getExtraProperties()['cacheDependencies'])) { + foreach ($propertyMetadata->getExtraProperties()['cacheDependencies'] as $dependency) { + $cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$dependency; + $context['resources'][$cacheTag] = $cacheTag; + } + } else { + $cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$context['api_attribute']; + $context['resources'][$cacheTag] = $cacheTag; + } + } + } +} \ No newline at end of file From ec7d36ad57cfffd6964f1072b1489020286f765e Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 27 Aug 2023 13:02:24 +0200 Subject: [PATCH 10/81] use id instead of IRI --- api/src/HttpCache/PurgeHttpCacheListener.php | 7 ++++++- api/src/HttpCache/TagCollector.php | 4 ++++ api/src/Security/Voter/CampIsPrototypeVoter.php | 2 +- api/src/Security/Voter/CampRoleVoter.php | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index e8fffcaf0a..c8b12278ab 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -22,6 +22,7 @@ use ApiPlatform\HttpCache\PurgerInterface; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Util\ClassInfoTrait; +use App\Entity\BaseEntity; use Doctrine\Common\Util\ClassUtils; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; @@ -171,7 +172,11 @@ private function addTagForItem(mixed $value, string $property = null): void } try { - $iri = $this->iriConverter->getIriFromResource($value); + if($value instanceof BaseEntity){ + $iri = $value->getId(); + } else { + $iri = $this->iriConverter->getIriFromResource($value); + } if ($property) { $iri .= self::IRI_RELATION_DELIMITER.$property; } diff --git a/api/src/HttpCache/TagCollector.php b/api/src/HttpCache/TagCollector.php index 59282aa275..6d5e79cb13 100644 --- a/api/src/HttpCache/TagCollector.php +++ b/api/src/HttpCache/TagCollector.php @@ -21,6 +21,10 @@ class TagCollector implements TagCollectorInterface public function collect(mixed $object = null, string $format = null, array $context = [], string $iri = null, mixed $data = null, string $attribute = null, ApiProperty $propertyMetadata = null, Type $type = null): void { + if($object instanceof BaseEntity){ + $iri = $object->getId(); + } + if($attribute){ $this->addCacheTagsForRelation($context, $iri, $propertyMetadata); } diff --git a/api/src/Security/Voter/CampIsPrototypeVoter.php b/api/src/Security/Voter/CampIsPrototypeVoter.php index 368b27808c..2f7d020e32 100644 --- a/api/src/Security/Voter/CampIsPrototypeVoter.php +++ b/api/src/Security/Voter/CampIsPrototypeVoter.php @@ -43,7 +43,7 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter // Add Camp to cache tags $request = $this->requestStack->getCurrentRequest(); $resources = [ - $this->iriConverter->getIriFromResource($camp), + $camp->getId() ]; $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array) $resources); diff --git a/api/src/Security/Voter/CampRoleVoter.php b/api/src/Security/Voter/CampRoleVoter.php index 1b6f152600..ccad4a786c 100644 --- a/api/src/Security/Voter/CampRoleVoter.php +++ b/api/src/Security/Voter/CampRoleVoter.php @@ -64,7 +64,7 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter // Add CampCollaboration to cache tags $request = $this->requestStack->getCurrentRequest(); $resources = [ - $this->iriConverter->getIriFromResource($campCollaboration), + $campCollaboration->getId() ]; $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array) $resources); From 2ea5d0ca85f057c46888a62a5de02b6e593f06fd Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 27 Aug 2023 13:28:15 +0200 Subject: [PATCH 11/81] purge subresources --- api/src/Entity/Category.php | 2 +- api/src/HttpCache/PurgeHttpCacheListener.php | 52 +++++++++++++------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/api/src/Entity/Category.php b/api/src/Entity/Category.php index 9193249e76..3da450094b 100644 --- a/api/src/Entity/Category.php +++ b/api/src/Entity/Category.php @@ -62,7 +62,7 @@ uriVariables: [ 'campId' => new Link( fromClass: Camp::class, - expandedValue: '{campId}' + fromProperty: 'categories' ), ], ), diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index c8b12278ab..c7694778b6 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -13,23 +13,25 @@ namespace App\HttpCache; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\UrlGeneratorInterface; -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Exception\OperationNotFoundException; -use ApiPlatform\Exception\RuntimeException; -use ApiPlatform\HttpCache\PurgerInterface; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use App\Entity\BaseEntity; -use Doctrine\Common\Util\ClassUtils; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\Event\OnFlushEventArgs; -use Doctrine\ORM\Event\PreUpdateEventArgs; -use Doctrine\ORM\PersistentCollection; -use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyAccess\PropertyAccess; +use Doctrine\ORM\PersistentCollection; +use Doctrine\ORM\Event\PreUpdateEventArgs; +use Doctrine\ORM\Event\OnFlushEventArgs; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Common\Util\ClassUtils; +use App\Entity\Category; +use App\Entity\BaseEntity; +use ApiPlatform\Metadata\Util\ClassInfoTrait; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\HttpCache\PurgerInterface; +use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Exception\OperationNotFoundException; +use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Api\ResourceClassResolverInterface; +use ApiPlatform\Api\IriConverterInterface; /** * Purges responses containing modified entities from the proxy cache. @@ -44,7 +46,7 @@ final class PurgeHttpCacheListener public const IRI_RELATION_DELIMITER = '#'; - public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null) + public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor, private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) { $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); } @@ -114,8 +116,20 @@ private function gatherResourceTags(object $entity): void { try { $resourceClass = $this->resourceClassResolver->getResourceClass($entity); - $iri = $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, new GetCollection()); - $this->tags[$iri] = $iri; + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + $resourceIterator = $resourceMetadataCollection->getIterator(); + while ($resourceIterator->valid()) { + /** @var ApiResource $metadata */ + $metadata = $resourceIterator->current(); + + foreach ($metadata->getOperations() ?? [] as $operation) { + if ($operation instanceof GetCollection) { + $iri = $this->iriConverter->getIriFromResource($entity, UrlGeneratorInterface::ABS_PATH, $operation); + $this->tags[$iri] = $iri; + } + } + $resourceIterator->next(); + } } catch (OperationNotFoundException|InvalidArgumentException) { } } From ece1c1833321bc190100c85d7336022707506663 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 2 Oct 2023 21:38:19 +0200 Subject: [PATCH 12/81] switch to FosHttpCacheBundle --- api/composer.json | 3 +- api/composer.lock | 186 +++++++++++++++++- api/config/bundles.php | 1 + api/config/packages/api_platform.yaml | 10 - api/config/packages/http_cache.yaml | 34 ++++ api/config/services.yaml | 14 +- api/docker/varnish/default.vcl | 23 ++- .../HttpCache/AddCollectionTagsListener.php | 55 ++++++ api/src/HttpCache/PurgeHttpCacheListener.php | 17 +- api/src/HttpCache/TagCollector.php | 15 +- .../Security/Voter/CampIsPrototypeVoter.php | 25 +-- api/src/Security/Voter/CampRoleVoter.php | 31 ++- api/symfony.lock | 3 + docker-compose.yml | 7 +- 14 files changed, 341 insertions(+), 83 deletions(-) create mode 100644 api/config/packages/http_cache.yaml create mode 100644 api/src/HttpCache/AddCollectionTagsListener.php diff --git a/api/composer.json b/api/composer.json index 9369ed2801..2b87d3b713 100644 --- a/api/composer.json +++ b/api/composer.json @@ -12,6 +12,7 @@ "doctrine/doctrine-migrations-bundle": "3.2.4", "doctrine/orm": "2.15.3", "exercise/htmlpurifier-bundle": "4.1.1", + "friendsofsymfony/http-cache-bundle": "2.16.2", "google/recaptcha": "1.3.0", "guzzlehttp/guzzle": "7.7.0", "knpuniversity/oauth2-client-bundle": "2.15.0", @@ -31,6 +32,7 @@ "symfony/expression-language": "6.3.0", "symfony/flex": "2.3.1", "symfony/framework-bundle": "6.3.0", + "symfony/http-client": "6.3.0", "symfony/intl": "6.3.0", "symfony/mailer": "6.3.0", "symfony/mercure-bundle": "0.3.7", @@ -59,7 +61,6 @@ "symfony/browser-kit": "6.3.0", "symfony/css-selector": "6.3.0", "symfony/debug-bundle": "6.3.0", - "symfony/http-client": "6.3.0", "symfony/maker-bundle": "1.49.0", "symfony/phpunit-bridge": "6.3.0", "symfony/stopwatch": "6.3.0", diff --git a/api/composer.lock b/api/composer.lock index d565496a6a..d728f1100c 100644 --- a/api/composer.lock +++ b/api/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": "c1cc1cc1b8be1c6cf5e1652db7fe7071", + "content-hash": "07126068f265f70cae1a87abe4b8f25b", "packages": [ { "name": "api-platform/core", @@ -12,7 +12,7 @@ "source": { "type": "git", "url": "https://github.com/usu/api-platform-core", - "reference": "c50339f31178b78c99bcb0369c001597466b70f6" + "reference": "c935d8d165f34983f7801f06d5dfe29ac02a0250" }, "require": { "doctrine/inflector": "^1.0 || ^2.0", @@ -162,7 +162,7 @@ "REST", "Swagger" ], - "time": "2023-08-27T06:25:20+00:00" + "time": "2023-09-29T19:13:10+00:00" }, { "name": "behat/transliterator", @@ -2034,6 +2034,184 @@ }, "time": "2022-09-18T07:06:19+00:00" }, + { + "name": "friendsofsymfony/http-cache", + "version": "2.15.1", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/FOSHttpCache.git", + "reference": "4f54d89973faabecd4f9dbb463a4d70660de6712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCache/zipball/4f54d89973faabecd4f9dbb463a4d70660de6712", + "reference": "4f54d89973faabecd4f9dbb463a4d70660de6712", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "php-http/client-common": "^1.1.0 || ^2.0", + "php-http/client-implementation": "^1.0 || ^2.0", + "php-http/discovery": "^1.12", + "php-http/message": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "symfony/event-dispatcher": "^4.3 || ^5.0 || ^6.0", + "symfony/options-resolver": "^4.3 || ^5.0 || ^6.0" + }, + "conflict": { + "toflar/psr6-symfony-http-cache-store": "<2.2.1" + }, + "require-dev": { + "mockery/mockery": "^1.3.1", + "monolog/monolog": "^1.0", + "php-http/guzzle7-adapter": "^0.1.1", + "php-http/mock-client": "^1.2", + "symfony/http-kernel": "^4.3 || ^5.0 || ^6.0", + "symfony/phpunit-bridge": "^5.0 || ^6.0", + "symfony/process": "^4.3 || ^5.0 || ^6.0" + }, + "suggest": { + "friendsofsymfony/http-cache-bundle": "For integration with the Symfony framework", + "monolog/monolog": "For logging issues while invalidating" + }, + "type": "library", + "autoload": { + "psr-4": { + "FOS\\HttpCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Liip AG", + "homepage": "http://www.liip.ch/" + }, + { + "name": "Driebit", + "email": "tech@driebit.nl", + "homepage": "http://www.driebit.nl" + }, + { + "name": "Community contributions", + "homepage": "https://github.com/friendsofsymfony/FOSHttpCache/contributors" + } + ], + "description": "Tools to manage HTTP caching proxies with PHP", + "homepage": "https://github.com/friendsofsymfony/FOSHttpCache", + "keywords": [ + "caching", + "http", + "invalidation", + "nginx", + "purge", + "varnish" + ], + "support": { + "issues": "https://github.com/FriendsOfSymfony/FOSHttpCache/issues", + "source": "https://github.com/FriendsOfSymfony/FOSHttpCache/tree/2.15.1" + }, + "time": "2023-05-17T09:08:17+00:00" + }, + { + "name": "friendsofsymfony/http-cache-bundle", + "version": "2.16.2", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle.git", + "reference": "4308b5776d3e1c16cb8257429e55395e272ee0bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCacheBundle/zipball/4308b5776d3e1c16cb8257429e55395e272ee0bb", + "reference": "4308b5776d3e1c16cb8257429e55395e272ee0bb", + "shasum": "" + }, + "require": { + "friendsofsymfony/http-cache": "^2.15", + "php": "^7.3 || ^8.0", + "symfony/framework-bundle": "^4.4.0 || ^5.0 || ^6.0", + "symfony/http-foundation": "^4.4.0 || ^5.0 || ^6.0", + "symfony/http-kernel": "^4.4.0 || ^5.0 || ^6.0" + }, + "conflict": { + "symfony/monolog-bridge": "<3.4.4", + "twig/twig": "<1.12.0" + }, + "require-dev": { + "doctrine/annotations": "^1.11", + "guzzlehttp/guzzle": "^7.2", + "matthiasnoback/symfony-dependency-injection-test": "^4.0", + "mockery/mockery": "^1.3.2", + "monolog/monolog": "*", + "php-http/discovery": "^1.13", + "php-http/guzzle7-adapter": "^0.1.1", + "php-http/httplug": "^2.2.0", + "php-http/message": "^1.0 || ^2.0", + "sebastian/exporter": "^2.0", + "sensio/framework-extra-bundle": "^4.0 || ^5.5.1 || ^6.0", + "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0", + "symfony/console": "^4.4 || ^5.0 || ^6.0", + "symfony/css-selector": "^4.4 || ^5.0 || ^6.0", + "symfony/expression-language": "^4.4 || ^5.0 || ^6.0", + "symfony/finder": "^4.4 || ^5.0 || ^6.0", + "symfony/monolog-bundle": "^3.0", + "symfony/phpunit-bridge": "v5.3.7", + "symfony/routing": "^4.4 || ^5.0 || ^6.0", + "symfony/security-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/twig-bundle": "^4.4 || ^5.0 || ^6.0", + "symfony/yaml": "^4.4 || ^5.0 || ^6.0", + "twig/twig": "^2.13" + }, + "suggest": { + "jean-beru/fos-http-cache-cloudfront": "To use CloudFront proxy", + "sensio/framework-extra-bundle": "For Tagged Cache Invalidation", + "symfony/console": "To send invalidation requests from the command line", + "symfony/expression-language": "For Tagged Cache Invalidation" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "FOS\\HttpCacheBundle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Liip AG", + "homepage": "http://www.liip.ch/" + }, + { + "name": "Driebit", + "email": "tech@driebit.nl", + "homepage": "http://www.driebit.nl" + }, + { + "name": "Community contributions", + "homepage": "https://github.com/friendsofsymfony/FOSHttpCacheBundle/contributors" + } + ], + "description": "Set path based HTTP cache headers and send invalidation requests to your HTTP cache", + "homepage": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle", + "keywords": [ + "caching", + "esi", + "http", + "invalidation", + "purge", + "varnish" + ], + "support": { + "issues": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle/issues", + "source": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle/tree/2.16.2" + }, + "time": "2023-08-03T08:52:18+00:00" + }, { "name": "gedmo/doctrine-extensions", "version": "v3.11.1", @@ -14208,5 +14386,5 @@ "ext-iconv": "*" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/api/config/bundles.php b/api/config/bundles.php index b78e671bad..56962001a0 100644 --- a/api/config/bundles.php +++ b/api/config/bundles.php @@ -22,4 +22,5 @@ KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], Sentry\SentryBundle\SentryBundle::class => ['all' => true], Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], + FOS\HttpCacheBundle\FOSHttpCacheBundle::class => ['all' => true], ]; diff --git a/api/config/packages/api_platform.yaml b/api/config/packages/api_platform.yaml index 52d8d1ab13..841684f793 100644 --- a/api/config/packages/api_platform.yaml +++ b/api/config/packages/api_platform.yaml @@ -23,18 +23,8 @@ api_platform: mercure: #hub_url: '%env(resolve:MERCURE_SUBSCRIBE_URL)%' include_type: true - http_cache: - public: true - invalidation: - enabled: true - purger: 'api_platform.http_cache.purger.varnish.xkey' - urls: ['%env(VARNISH_API_URL)%'] defaults: stateless: true - cache_headers: - max_age: 0 - shared_max_age: 3600 - vary: ['Content-Type', 'Authorization', 'Origin'] pagination_enabled: false itemOperations: [ 'get', 'patch', 'delete' ] collection_operations: diff --git a/api/config/packages/http_cache.yaml b/api/config/packages/http_cache.yaml new file mode 100644 index 0000000000..8fac0856f6 --- /dev/null +++ b/api/config/packages/http_cache.yaml @@ -0,0 +1,34 @@ +fos_http_cache: + tags: + enabled: true + annotations: + enabled: false + proxy_client: + varnish: + tag_mode: purgekeys + tags_header: xkey-purge + http: + servers: + - '%env(VARNISH_API_URL)%' + cache_control: + defaults: + overwrite: true + rules: + # matches /content_types endpoint + # matches /camps/133/categories endpoint + - + match: + path: ^/(content_types|camps/[0-9a-f]*/categories) + headers: + overwrite: true + cache_control: { public: true, max_age: 0, s_maxage: 3600 } + vary: [Accept, Content-Type, Authorization, Origin] + + # match everything else to set defaults + - + match: + path: ^/ + headers: + overwrite: true + cache_control: { no-cache: true, private: true } + vary: [Accept, Content-Type, Authorization, Origin] \ No newline at end of file diff --git a/api/config/services.yaml b/api/config/services.yaml index 3d37114fa9..4fe40c64bf 100644 --- a/api/config/services.yaml +++ b/api/config/services.yaml @@ -151,10 +151,8 @@ services: tags: - { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated } - api_platform.doctrine.listener.http_cache.purge: - class: App\HttpCache\PurgeHttpCacheListener + App\HttpCache\PurgeHttpCacheListener: arguments: - - '@api_platform.http_cache.purger' - '@api_platform.iri_converter' - '@api_platform.resource_class_resolver' - '@api_platform.property_accessor' @@ -166,6 +164,16 @@ services: api_platform.http_cache.tag_collector: class: App\HttpCache\TagCollector + api_platform.http_cache.listener.response.configure: + class: ApiPlatform\HttpCache\EventListener\AddHeadersListener + tags: + - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: 11 } + + App\HttpCache\AddCollectionTagsListener: + tags: + - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: 12 } + + # Entity Filters App\Doctrine\FilterByCurrentUserExtension: tags: diff --git a/api/docker/varnish/default.vcl b/api/docker/varnish/default.vcl index 8cd11df6c3..6fe7733deb 100644 --- a/api/docker/varnish/default.vcl +++ b/api/docker/varnish/default.vcl @@ -5,30 +5,24 @@ import xkey; import cookie; +include "./fos/fos_tags_xkey.vcl"; +include "./fos/fos_debug.vcl"; + + backend default { .host = "caddy"; .port = "3000"; } # Hosts allowed to send BAN requests -acl purgers { +acl invalidators { "php"; } sub vcl_recv { # Support xkey purge requests # see https://raw.githubusercontent.com/varnish/varnish-modules/master/src/vmod_xkey.vcc - if (req.method == "PURGE") { - if (client.ip !~ purgers) { - return (synth(403, "Forbidden")); - } - if (req.http.xkey) { - set req.http.n-gone = xkey.purge(req.http.xkey); - return (synth(200, "Invalidated "+req.http.n-gone+" objects")); - } else { - return (purge); - } - } + call fos_tags_xkey_recv; # exclude other services (frontend, print, etc.) if (req.url !~ "^/api") { @@ -70,4 +64,9 @@ sub vcl_beresp_cookie { # Varnish by default disables caching whenever the reponse header "Set-Cookie" is set in the request (default safe behavior) # this bypasses the default behaviour return (deliver); +} + +sub vcl_deliver { + call fos_tags_xkey_deliver; + call fos_debug_deliver; } \ No newline at end of file diff --git a/api/src/HttpCache/AddCollectionTagsListener.php b/api/src/HttpCache/AddCollectionTagsListener.php new file mode 100644 index 0000000000..0ca8479b76 --- /dev/null +++ b/api/src/HttpCache/AddCollectionTagsListener.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\HttpCache; + +use Symfony\Component\HttpKernel\Event\ResponseEvent; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; +use ApiPlatform\Util\RequestAttributesExtractor; +use ApiPlatform\Util\OperationRequestInitiatorTrait; +use ApiPlatform\State\UriVariablesResolverTrait; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Api\IriConverterInterface; + +final class AddCollectionTagsListener +{ + use OperationRequestInitiatorTrait; + use UriVariablesResolverTrait; + + public function __construct(private readonly IriConverterInterface $iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private SymfonyResponseTagger $responseTagger) + { + $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; + } + + public function onKernelResponse(ResponseEvent $event): void + { + $request = $event->getRequest(); + $operation = $this->initializeOperation($request); + + if ( !$attributes = RequestAttributesExtractor::extractAttributes($request)) { + return; + } + + if ($operation instanceof CollectionOperationInterface) { + // Allows to purge collections + $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $attributes['resource_class']); + $iri = $this->iriConverter->getIriFromResource($attributes['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => $uriVariables]); + + $this->responseTagger->addTags([$iri]); + } + + + } +} diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index c7694778b6..314ffc1fd9 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -15,17 +15,16 @@ use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyAccess\PropertyAccess; +use FOS\HttpCacheBundle\CacheManager; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Event\PreUpdateEventArgs; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Common\Util\ClassUtils; -use App\Entity\Category; use App\Entity\BaseEntity; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\HttpCache\PurgerInterface; use ApiPlatform\Exception\RuntimeException; use ApiPlatform\Exception\OperationNotFoundException; use ApiPlatform\Exception\InvalidArgumentException; @@ -46,7 +45,7 @@ final class PurgeHttpCacheListener public const IRI_RELATION_DELIMITER = '#'; - public function __construct(private readonly PurgerInterface $purger, private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor, private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory) + public function __construct(private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor, private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly CacheManager $cacheManager) { $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); } @@ -103,13 +102,7 @@ public function onFlush(OnFlushEventArgs $eventArgs): void */ public function postFlush(): void { - if (empty($this->tags)) { - return; - } - - $this->purger->purge(array_values($this->tags)); - - $this->tags = []; + $this->cacheManager->flush(); } private function gatherResourceTags(object $entity): void @@ -125,7 +118,7 @@ private function gatherResourceTags(object $entity): void foreach ($metadata->getOperations() ?? [] as $operation) { if ($operation instanceof GetCollection) { $iri = $this->iriConverter->getIriFromResource($entity, UrlGeneratorInterface::ABS_PATH, $operation); - $this->tags[$iri] = $iri; + $this->cacheManager->invalidateTags([$iri]); } } $resourceIterator->next(); @@ -194,7 +187,7 @@ private function addTagForItem(mixed $value, string $property = null): void if ($property) { $iri .= self::IRI_RELATION_DELIMITER.$property; } - $this->tags[$iri] = $iri; + $this->cacheManager->invalidateTags([$iri]); } catch (RuntimeException|InvalidArgumentException) { } } diff --git a/api/src/HttpCache/TagCollector.php b/api/src/HttpCache/TagCollector.php index 6d5e79cb13..3cb9610cb6 100644 --- a/api/src/HttpCache/TagCollector.php +++ b/api/src/HttpCache/TagCollector.php @@ -5,6 +5,7 @@ namespace App\HttpCache; use Symfony\Component\PropertyInfo\Type; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; use App\HttpCache\PurgeHttpCacheListener; use App\Entity\BaseEntity; use ApiPlatform\Serializer\TagCollectorInterface; @@ -19,6 +20,10 @@ class TagCollector implements TagCollectorInterface { public const IRI_RELATION_DELIMITER = '#'; + public function __construct(private SymfonyResponseTagger $responseTagger){ + + } + public function collect(mixed $object = null, string $format = null, array $context = [], string $iri = null, mixed $data = null, string $attribute = null, ApiProperty $propertyMetadata = null, Type $type = null): void { if($object instanceof BaseEntity){ @@ -35,22 +40,22 @@ public function collect(mixed $object = null, string $format = null, array $cont private function addCacheTagForResource(array $context, ?string $iri): void { - if (isset($context['resources']) && isset($iri)) { - $context['resources'][$iri] = $iri; + if (isset($iri)) { + $this->responseTagger->addTags([$iri]); } } private function addCacheTagsForRelation(array $context, ?string $iri, ApiProperty $propertyMetadata): void { - if (isset($context['resources']) && isset($iri)) { + if (isset($iri)) { if (isset($propertyMetadata->getExtraProperties()['cacheDependencies'])) { foreach ($propertyMetadata->getExtraProperties()['cacheDependencies'] as $dependency) { $cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$dependency; - $context['resources'][$cacheTag] = $cacheTag; + $this->responseTagger->addTags([$cacheTag]); } } else { $cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$context['api_attribute']; - $context['resources'][$cacheTag] = $cacheTag; + $this->responseTagger->addTags([$cacheTag]); } } } diff --git a/api/src/Security/Voter/CampIsPrototypeVoter.php b/api/src/Security/Voter/CampIsPrototypeVoter.php index 2f7d020e32..bc61a36cb3 100644 --- a/api/src/Security/Voter/CampIsPrototypeVoter.php +++ b/api/src/Security/Voter/CampIsPrototypeVoter.php @@ -2,14 +2,15 @@ namespace App\Security\Voter; -use ApiPlatform\Api\IriConverterInterface; -use App\Entity\BelongsToCampInterface; -use App\Entity\BelongsToContentNodeTreeInterface; -use App\Util\GetCampFromContentNodeTrait; -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; +use Doctrine\ORM\EntityManagerInterface; +use App\Util\GetCampFromContentNodeTrait; +use App\Entity\BelongsToContentNodeTreeInterface; +use App\Entity\BelongsToCampInterface; +use ApiPlatform\Api\IriConverterInterface; /** * @extends Voter @@ -19,7 +20,7 @@ class CampIsPrototypeVoter extends Voter { public function __construct( private EntityManagerInterface $em, - private RequestStack $requestStack, + private SymfonyResponseTagger $responseTagger, private IriConverterInterface $iriConverter, ) { } @@ -40,13 +41,7 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter } if ($camp->isPrototype) { - // Add Camp to cache tags - $request = $this->requestStack->getCurrentRequest(); - $resources = [ - $camp->getId() - ]; - $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array) $resources); - + $this->responseTagger->addTags([$camp->getId()]); return true; } diff --git a/api/src/Security/Voter/CampRoleVoter.php b/api/src/Security/Voter/CampRoleVoter.php index ccad4a786c..45b9af99a6 100644 --- a/api/src/Security/Voter/CampRoleVoter.php +++ b/api/src/Security/Voter/CampRoleVoter.php @@ -2,16 +2,17 @@ namespace App\Security\Voter; -use ApiPlatform\Api\IriConverterInterface; -use App\Entity\BelongsToCampInterface; -use App\Entity\BelongsToContentNodeTreeInterface; -use App\Entity\CampCollaboration; -use App\Entity\User; -use App\Util\GetCampFromContentNodeTrait; -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; +use Doctrine\ORM\EntityManagerInterface; +use App\Util\GetCampFromContentNodeTrait; +use App\Entity\User; +use App\Entity\CampCollaboration; +use App\Entity\BelongsToContentNodeTreeInterface; +use App\Entity\BelongsToCampInterface; +use ApiPlatform\Api\IriConverterInterface; /** * @extends Voter @@ -28,8 +29,8 @@ class CampRoleVoter extends Voter { public function __construct( private EntityManagerInterface $em, - private RequestStack $requestStack, - private IriConverterInterface $iriConverter, + private SymfonyResponseTagger $responseTagger, + private IriConverterInterface $iriConverter ) { } @@ -61,13 +62,7 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter ; if ($campCollaboration) { - // Add CampCollaboration to cache tags - $request = $this->requestStack->getCurrentRequest(); - $resources = [ - $campCollaboration->getId() - ]; - $request->attributes->set('_resources', $request->attributes->get('_resources', []) + (array) $resources); - + $this->responseTagger->addTags([$campCollaboration->getId()]); return true; } diff --git a/api/symfony.lock b/api/symfony.lock index 5e43107745..b329aa5d26 100644 --- a/api/symfony.lock +++ b/api/symfony.lock @@ -168,6 +168,9 @@ "friendsofphp/proxy-manager-lts": { "version": "v1.0.3" }, + "friendsofsymfony/http-cache-bundle": { + "version": "2.16.2" + }, "gedmo/doctrine-extensions": { "version": "v3.0.5" }, diff --git a/docker-compose.yml b/docker-compose.yml index 61fd361cec..2caf712495 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,6 +100,7 @@ services: - caddy volumes: - ./api/docker/varnish/default.vcl:/etc/varnish/default.vcl:ro + - ./api/vendor/friendsofsymfony/http-cache/resources/config/varnish:/etc/varnish/fos tmpfs: - /var/lib/varnish/varnishd:exec ports: @@ -154,7 +155,7 @@ services: docker-host: image: qoomon/docker-host container_name: 'ecamp3-docker-host-forwarder' - cap_add: [ 'NET_ADMIN', 'NET_RAW' ] + cap_add: ['NET_ADMIN', 'NET_RAW'] restart: on-failure browserless: @@ -174,7 +175,7 @@ services: e2e: image: cypress/included:12.15.0 - profiles: [ "e2e" ] + profiles: ['e2e'] container_name: 'ecamp3-e2e' environment: - DISPLAY @@ -186,7 +187,7 @@ services: translation: image: node:18.16.1 - profiles: [ "translation" ] + profiles: ['translation'] container_name: 'ecamp3-translation' volumes: - ./translation:/translation:delegated From 07962c4b47fc6cf86834c6f397713c22db710d2e Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 7 Oct 2023 12:00:09 +0200 Subject: [PATCH 13/81] add e2e tests for http cache --- api/config/packages/http_cache.yaml | 1 + api/docker/varnish/default.vcl | 11 +++ e2e/cypress.config.js | 1 + e2e/specs/httpCache.cy.js | 119 ++++++++++++++++++++++++++++ e2e/support/commands.js | 43 ++++++++++ 5 files changed, 175 insertions(+) create mode 100644 e2e/specs/httpCache.cy.js diff --git a/api/config/packages/http_cache.yaml b/api/config/packages/http_cache.yaml index 8fac0856f6..b9b6d5555a 100644 --- a/api/config/packages/http_cache.yaml +++ b/api/config/packages/http_cache.yaml @@ -3,6 +3,7 @@ fos_http_cache: enabled: true annotations: enabled: false + max_header_value_length: 4096 proxy_client: varnish: tag_mode: purgekeys diff --git a/api/docker/varnish/default.vcl b/api/docker/varnish/default.vcl index 6fe7733deb..c8b43f22e5 100644 --- a/api/docker/varnish/default.vcl +++ b/api/docker/varnish/default.vcl @@ -69,4 +69,15 @@ sub vcl_beresp_cookie { sub vcl_deliver { call fos_tags_xkey_deliver; call fos_debug_deliver; + + # Add X-Cache header if debugging is enabled + if (resp.http.X-Cache-Debug) { + if (obj.hits > 0) { + set resp.http.X-Cache = "HIT"; + } else if (obj.uncacheable) { + set resp.http.X-Cache = "PASS"; + } else { + set resp.http.X-Cache = "MISS"; + } + } } \ No newline at end of file diff --git a/e2e/cypress.config.js b/e2e/cypress.config.js index 0f5278dbe8..308dd0cc56 100644 --- a/e2e/cypress.config.js +++ b/e2e/cypress.config.js @@ -24,5 +24,6 @@ module.exports = defineConfig({ env: { PRINT_URL: 'http://localhost:3000/print', API_ROOT_URL: 'http://localhost:3000/api', + API_ROOT_URL_CACHED: 'http://localhost:3004', }, }) diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js new file mode 100644 index 0000000000..acbf77acee --- /dev/null +++ b/e2e/specs/httpCache.cy.js @@ -0,0 +1,119 @@ +// https://docs.cypress.io/api/introduction/api.html + +const path = require('path') + +describe('HTTP cache tests', () => { + it('caches /content_types separately for each login', () => { + const uri = '/content_types' + + Cypress.session.clearAllSavedSessions() + cy.login('test@example.com') + + // first request is a cache miss + cy.request(Cypress.env('API_ROOT_URL_CACHED') + '/api/content_types.jsonhal').then( + (response) => { + const headers = response.headers + expect(headers.xkey).to.eq( + 'f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' + ) + expect(headers['x-cache']).to.eq('MISS') + } + ) + + // second request is a cache hit + cy.expectCacheHit(uri) + + // request with a new user is a cache miss + cy.login('castor@example.com') + cy.expectCacheMiss(uri) + }) + + it('caches /camp/{campId}/categories separately for each login', () => { + const uri = '/api/camps/3c79b99ab424/categories' + + Cypress.session.clearAllSavedSessions() + cy.login('test@example.com') + + // first request is a cache miss + cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { + const headers = response.headers + expect(headers.xkey).to.eq( + 'ebfd46a1c181 ebfd46a1c181#camp ebfd46a1c181#preferredContentTypes 9d7b3a220fb4 9d7b3a220fb4#root 9d7b3a220fb4#parent 9d7b3a220fb4#children 9d7b3a220fb4#contentType ebfd46a1c181#rootContentNode ebfd46a1c181#contentNodes ' + + '1a869b162875 1a869b162875#camp 1a869b162875#preferredContentTypes be9b6b7f23f6 be9b6b7f23f6#root be9b6b7f23f6#parent be9b6b7f23f6#children be9b6b7f23f6#contentType 1a869b162875#rootContentNode 1a869b162875#contentNodes ' + + 'dfa531302823 dfa531302823#camp dfa531302823#preferredContentTypes 63cbc734fa04 63cbc734fa04#root 63cbc734fa04#parent 63cbc734fa04#children 63cbc734fa04#contentType dfa531302823#rootContentNode dfa531302823#contentNodes ' + + 'a023e85227ac a023e85227ac#camp a023e85227ac#preferredContentTypes 2cce9e17a368 2cce9e17a368#root 2cce9e17a368#parent 2cce9e17a368#children 2cce9e17a368#contentType a023e85227ac#rootContentNode a023e85227ac#contentNodes ' + + '/api/camps/3c79b99ab424/categories' + ) + expect(headers['x-cache']).to.eq('MISS') + }) + + // second request is a cache hit + cy.expectCacheHit(uri) + + // request with a new user is a cache miss + cy.login('castor@example.com') + cy.expectCacheMiss(uri) + }) + + it('invalidates /camp/{campId}/categories for all users on category patch', () => { + const uri = '/api/camps/3c79b99ab424/categories' + + // bring data into defined state + Cypress.session.clearAllSavedSessions() + cy.login('castor@example.com') + cy.apiPatch('/api/categories/ebfd46a1c181', { + name: 'old_name', + }) + + // warm up cache + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + cy.login('test@example.com') + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + // touch category + cy.apiPatch('/api/categories/ebfd46a1c181', { + name: 'new_name', + }) + + // ensure cache was invalidated + cy.expectCacheMiss(uri) + cy.login('castor@example.com') + cy.expectCacheMiss(uri) + }) + + it('invalidates /camp/{campId}/categories for new contentNode child', () => { + const uri = '/api/camps/3c79b99ab424/categories' + + Cypress.session.clearAllSavedSessions() + cy.login('test@example.com') + + // warm up cache + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + // add new child to root content node (9d7b3a220fb4) of category (ebfd46a1c181) + cy.apiPost('/api/content_node/column_layouts', { + parent: '/api/content_node/column_layouts/9d7b3a220fb4', + slot: '1', + contentType: '/api/content_types/f17470519474', + }).then((response) => { + const newContentNodeUri = response.body._links.self.href + + console.log(response) + console.log(newContentNodeUri) + + // ensure cache was invalidated + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + // delete newly created contentNode + cy.apiDelete(newContentNodeUri) + + // ensure cache was invalidated + cy.expectCacheMiss(uri) + }) + }) +}) diff --git a/e2e/support/commands.js b/e2e/support/commands.js index cd8e15779f..02a14de3d1 100644 --- a/e2e/support/commands.js +++ b/e2e/support/commands.js @@ -37,3 +37,46 @@ Cypress.Commands.add('login', (identifier) => { Cypress.Commands.add('moveDownloads', () => { cy.task('moveDownloads', `${Cypress.spec.name}/${Cypress.currentTest.title}`) }) + +Cypress.Commands.add('expectCacheHit', (uri) => { + cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { + const headers = response.headers + expect(headers['x-cache']).to.eq('HIT') + }) +}) + +Cypress.Commands.add('expectCacheMiss', (uri) => { + cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { + const headers = response.headers + expect(headers['x-cache']).to.eq('MISS') + }) +}) + +Cypress.Commands.add('apiPatch', (uri, body) => { + cy.request({ + method: 'PATCH', + url: Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal', + body, + headers: { + 'Content-Type': 'application/merge-patch+json', + }, + }) +}) + +Cypress.Commands.add('apiPost', (uri, body) => { + cy.request({ + method: 'POST', + url: Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal', + body, + headers: { + 'Content-Type': 'application/hal+json', + }, + }) +}) + +Cypress.Commands.add('apiDelete', (uri) => { + cy.request({ + method: 'DELETE', + url: Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal', + }) +}) From 79e312c0ed08599deb643cd808d8f8c43f35d655 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 8 Oct 2023 20:04:02 +0200 Subject: [PATCH 14/81] test: invalidate cache on campCollaboration update --- e2e/specs/httpCache.cy.js | 78 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index acbf77acee..55f427b195 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -1,5 +1,6 @@ // https://docs.cypress.io/api/introduction/api.html +const { verify } = require('crypto') const path = require('path') describe('HTTP cache tests', () => { @@ -116,4 +117,81 @@ describe('HTTP cache tests', () => { cy.expectCacheMiss(uri) }) }) + + const getIframeDocument = () => { + return ( + cy + .get('iframe.panel-html') + // Cypress yields jQuery element, which has the real + // DOM element under property "0". + // From the real DOM iframe element we can get + // the "document" element, it is stored in "contentDocument" property + // Cypress "its" command can access deep properties using dot notation + // https://on.cypress.io/its + .its('0.contentDocument') + .should('exist') + ) + } + + const getIframeBody = () => { + // get the document + return ( + getIframeDocument() + // automatically retries until body is loaded + .its('body') + .should('not.be.undefined') + // wraps "body" DOM element to allow + // chaining more Cypress commands, like ".find(...)" + .then(cy.wrap) + ) + } + + it.only('invalidates cached data when user leaves a camp', () => { + Cypress.session.clearAllSavedSessions() + const uri = '/api/camps/3c79b99ab424/categories' + + // warm up cache + cy.login('castor@example.com') + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + // deactivate Castor + cy.login('test@example.com') + cy.visit('/camps/3c79b99ab424/GRGR/admin/collaborators') + cy.get('.v-list-item__title:contains("Castor")').click() + cy.get('button:contains("Deaktivieren")').click() + cy.get('div[role=alert]').find('button').contains('Deaktivieren').click() + + // ensure cache was invalidated + cy.login('castor@example.com') + cy.expectCacheMiss(uri) + + // delete old emails + cy.visit('localhost:3000/mail') + cy.get('a[title="Delete all emails"]').click() + cy.wait(50) + cy.get('a[title="Delete all emails"]').click() + + // invite Castor + cy.login('test@example.com') + cy.visit('/camps/3c79b99ab424/GRGR/admin/collaborators') + cy.get('.v-list-item__title:contains("Castor")').click() + cy.get('button:contains("Erneut einladen")').click() + cy.wait(100) + + // accept invitation as Castor + cy.login('castor@example.com') + cy.visit('localhost:3000/mail') + cy.get('a').contains('[eCamp3] Du wurdest ins Lager "Pfila 2023" eingeladen').click() + cy.wait(400) + getIframeBody() + .find('a') + .then(($a) => { + const href = $a.prop('href') + cy.visit(href) + cy.get('button').contains('Einladung mit aktuellem Account akzeptieren').click() + cy.visit('/camps') + cy.contains('GRGR') + }) + }) }) From 9d2f865ed9ea46cfcb90898ddf049b70909ba6e7 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 8 Oct 2023 21:56:13 +0200 Subject: [PATCH 15/81] implement: invalidate cache on campCollaboration update --- api/src/Entity/Category.php | 2 +- .../Security/Voter/CampIsPrototypeVoter.php | 13 ++-- api/src/Security/Voter/CampRoleVoter.php | 13 ++-- .../Voter/CampIsPrototypeVoterTest.php | 71 +++++++++++++------ .../Security/Voter/CampRoleVoterTest.php | 64 +++++++++++++---- e2e/specs/httpCache.cy.js | 48 ++++++++----- 6 files changed, 150 insertions(+), 61 deletions(-) diff --git a/api/src/Entity/Category.php b/api/src/Entity/Category.php index 3da450094b..5878fb8b71 100644 --- a/api/src/Entity/Category.php +++ b/api/src/Entity/Category.php @@ -56,7 +56,7 @@ securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' ), new GetCollection( - security: 'is_authenticated()', + security: 'is_granted("CAMP_COLLABORATOR", request) or is_granted("CAMP_IS_PROTOTYPE", request)', name: 'BelongsToCamp_App\Entity\Category_get_collection', uriTemplate: '/camps/{campId}/categories.{_format}', uriVariables: [ diff --git a/api/src/Security/Voter/CampIsPrototypeVoter.php b/api/src/Security/Voter/CampIsPrototypeVoter.php index 684ba439e2..6575404c67 100644 --- a/api/src/Security/Voter/CampIsPrototypeVoter.php +++ b/api/src/Security/Voter/CampIsPrototypeVoter.php @@ -5,9 +5,11 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Request; use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; use Doctrine\ORM\EntityManagerInterface; use App\Util\GetCampFromContentNodeTrait; +use App\Entity\Camp; use App\Entity\BelongsToContentNodeTreeInterface; use App\Entity\BelongsToCampInterface; use ApiPlatform\Api\IriConverterInterface; @@ -20,17 +22,20 @@ class CampIsPrototypeVoter extends Voter { public function __construct( private EntityManagerInterface $em, - private SymfonyResponseTagger $responseTagger, - private IriConverterInterface $iriConverter, + private SymfonyResponseTagger $responseTagger ) {} protected function supports($attribute, $subject): bool { return 'CAMP_IS_PROTOTYPE' === $attribute - && ($subject instanceof BelongsToCampInterface || $subject instanceof BelongsToContentNodeTreeInterface); + && ($subject instanceof BelongsToCampInterface || $subject instanceof BelongsToContentNodeTreeInterface || $subject instanceof Request); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { - $camp = $this->getCampFromInterface($subject, $this->em); + if($subject instanceof Request){ + $camp = $this->em->getRepository(Camp::class)->find($subject->attributes->get('campId')); + } else { + $camp = $this->getCampFromInterface($subject, $this->em); + } if (null === $camp) { // Allow access when camp is null. diff --git a/api/src/Security/Voter/CampRoleVoter.php b/api/src/Security/Voter/CampRoleVoter.php index 6ad90345c6..480fe82862 100644 --- a/api/src/Security/Voter/CampRoleVoter.php +++ b/api/src/Security/Voter/CampRoleVoter.php @@ -5,11 +5,13 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Request; use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; use Doctrine\ORM\EntityManagerInterface; use App\Util\GetCampFromContentNodeTrait; use App\Entity\User; use App\Entity\CampCollaboration; +use App\Entity\Camp; use App\Entity\BelongsToContentNodeTreeInterface; use App\Entity\BelongsToCampInterface; use ApiPlatform\Api\IriConverterInterface; @@ -29,13 +31,12 @@ class CampRoleVoter extends Voter { public function __construct( private EntityManagerInterface $em, - private SymfonyResponseTagger $responseTagger, - private IriConverterInterface $iriConverter + private SymfonyResponseTagger $responseTagger ) {} protected function supports($attribute, $subject): bool { return in_array($attribute, array_keys(self::RULE_MAPPING)) - && ($subject instanceof BelongsToCampInterface || $subject instanceof BelongsToContentNodeTreeInterface); + && ($subject instanceof BelongsToCampInterface || $subject instanceof BelongsToContentNodeTreeInterface || $subject instanceof Request); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { @@ -44,7 +45,11 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter return false; } - $camp = $this->getCampFromInterface($subject, $this->em); + if($subject instanceof Request){ + $camp = $this->em->getRepository(Camp::class)->find($subject->attributes->get('campId')); + } else { + $camp = $this->getCampFromInterface($subject, $this->em); + } if (null === $camp) { // Allow access when camp is null. diff --git a/api/tests/Security/Voter/CampIsPrototypeVoterTest.php b/api/tests/Security/Voter/CampIsPrototypeVoterTest.php index 6b9a792031..10d39a5b00 100644 --- a/api/tests/Security/Voter/CampIsPrototypeVoterTest.php +++ b/api/tests/Security/Voter/CampIsPrototypeVoterTest.php @@ -2,19 +2,22 @@ namespace App\Tests\Security\Voter; -use App\Entity\Activity; -use App\Entity\BaseEntity; -use App\Entity\Camp; -use App\Entity\ContentNode\ColumnLayout; -use App\Entity\Period; -use App\Entity\User; -use App\Security\Voter\CampIsPrototypeVoter; -use App\Security\Voter\CampRoleVoter; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityRepository; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; +use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\ParameterBag; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\EntityManagerInterface; +use App\Security\Voter\CampIsPrototypeVoter; +use App\Entity\User; +use App\Entity\Period; +use App\Entity\ContentNode\ColumnLayout; +use App\Entity\Camp; +use App\Entity\BaseEntity; +use App\Entity\Activity; /** * @internal @@ -23,12 +26,14 @@ class CampIsPrototypeVoterTest extends TestCase { private CampIsPrototypeVoter $voter; private TokenInterface|MockObject $token; private MockObject|EntityManagerInterface $em; + private MockObject|SymfonyResponseTagger $responseTagger; public function setUp(): void { parent::setUp(); $this->token = $this->createMock(TokenInterface::class); $this->em = $this->createMock(EntityManagerInterface::class); - $this->voter = new CampIsPrototypeVoter($this->em); + $this->responseTagger = $this->createMock(SymfonyResponseTagger::class); + $this->voter = new CampIsPrototypeVoter($this->em, $this->responseTagger); } public function testDoesntVoteWhenAttributeWrong() { @@ -38,7 +43,7 @@ public function testDoesntVoteWhenAttributeWrong() { $result = $this->voter->vote($this->token, new Period(), ['CAMP_IS_SOMETHING_ELSE']); // then - $this->assertEquals(CampRoleVoter::ACCESS_ABSTAIN, $result); + $this->assertEquals(VoterInterface::ACCESS_ABSTAIN, $result); } public function testDoesntVoteWhenSubjectDoesNotBelongToCamp() { @@ -48,7 +53,7 @@ public function testDoesntVoteWhenSubjectDoesNotBelongToCamp() { $result = $this->voter->vote($this->token, new CampIsPrototypeVoterTestDummy(), ['CAMP_IS_PROTOTYPE']); // then - $this->assertEquals(CampRoleVoter::ACCESS_ABSTAIN, $result); + $this->assertEquals(VoterInterface::ACCESS_ABSTAIN, $result); } public function testDoesntVoteWhenSubjectIsNull() { @@ -58,7 +63,7 @@ public function testDoesntVoteWhenSubjectIsNull() { $result = $this->voter->vote($this->token, null, ['CAMP_IS_PROTOTYPE']); // then - $this->assertEquals(CampRoleVoter::ACCESS_ABSTAIN, $result); + $this->assertEquals(VoterInterface::ACCESS_ABSTAIN, $result); } /** @@ -78,7 +83,7 @@ public function testGrantsAccessWhenGetCampYieldsNull() { $result = $this->voter->vote($this->token, $subject, ['CAMP_IS_PROTOTYPE']); // then - $this->assertEquals(CampRoleVoter::ACCESS_GRANTED, $result); + $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); } public function testDeniesAccessWhenCampIsntPrototype() { @@ -95,7 +100,7 @@ public function testDeniesAccessWhenCampIsntPrototype() { $result = $this->voter->vote($this->token, $subject, ['CAMP_IS_PROTOTYPE']); // then - $this->assertEquals(CampRoleVoter::ACCESS_DENIED, $result); + $this->assertEquals(VoterInterface::ACCESS_DENIED, $result); } public function testGrantsAccessViaBelongsToCampInterface() { @@ -108,11 +113,13 @@ public function testGrantsAccessViaBelongsToCampInterface() { $subject = $this->createMock(Period::class); $subject->method('getCamp')->willReturn($camp); + $this->responseTagger->expects($this->once())->method('addTags')->with([$camp->getId()]); + // when $result = $this->voter->vote($this->token, $subject, ['CAMP_IS_PROTOTYPE']); // then - $this->assertEquals(CampRoleVoter::ACCESS_GRANTED, $result); + $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); } public function testGrantsAccessViaBelongsToContentNodeTreeInterface() { @@ -134,7 +141,31 @@ public function testGrantsAccessViaBelongsToContentNodeTreeInterface() { $result = $this->voter->vote($this->token, $subject, ['CAMP_IS_PROTOTYPE']); // then - $this->assertEquals(CampRoleVoter::ACCESS_GRANTED, $result); + $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); + } + + public function testGrantsAccessViaRequestParameter() { + // given + $user = $this->createMock(User::class); + $user->method('getId')->willReturn('idFromTest'); + $this->token->method('getUser')->willReturn($user); + $camp = new Camp(); + $camp->isPrototype = true; + + $repository = $this->createMock(EntityRepository::class); + $this->em->method('getRepository')->willReturn($repository); + $repository->method('find')->willReturn($camp); + + $request = $this->createMock(Request::class); + $request->attributes = $this->createMock(ParameterBag::class); + $request->attributes->method('get')->with('campId')->willReturn('campId-123'); + + + // when + $result = $this->voter->vote($this->token, $request, ['CAMP_IS_PROTOTYPE']); + + // then + $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); } } diff --git a/api/tests/Security/Voter/CampRoleVoterTest.php b/api/tests/Security/Voter/CampRoleVoterTest.php index 3820f94880..b63a36a423 100644 --- a/api/tests/Security/Voter/CampRoleVoterTest.php +++ b/api/tests/Security/Voter/CampRoleVoterTest.php @@ -2,20 +2,23 @@ namespace App\Tests\Security\Voter; -use App\Entity\Activity; -use App\Entity\BaseEntity; -use App\Entity\Camp; -use App\Entity\CampCollaboration; -use App\Entity\ContentNode\ColumnLayout; -use App\Entity\Period; -use App\Entity\User; -use App\Security\Voter\CampRoleVoter; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\EntityRepository; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\ParameterBag; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\EntityManagerInterface; +use App\Security\Voter\CampRoleVoter; +use App\Entity\User; +use App\Entity\Period; +use App\Entity\ContentNode\ColumnLayout; +use App\Entity\CampCollaboration; +use App\Entity\Camp; +use App\Entity\BaseEntity; +use App\Entity\Activity; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; /** * @internal @@ -24,12 +27,14 @@ class CampRoleVoterTest extends TestCase { private CampRoleVoter $voter; private TokenInterface|MockObject $token; private MockObject|EntityManagerInterface $em; + private MockObject|SymfonyResponseTagger $responseTagger; public function setUp(): void { parent::setUp(); $this->token = $this->createMock(TokenInterface::class); $this->em = $this->createMock(EntityManagerInterface::class); - $this->voter = new CampRoleVoter($this->em); + $this->responseTagger = $this->createMock(SymfonyResponseTagger::class); + $this->voter = new CampRoleVoter($this->em, $this->responseTagger); } public function testDoesntVoteWhenAttributeWrong() { @@ -230,6 +235,8 @@ public function testGrantsAccessViaBelongsToCampInterface() { $subject = $this->createMock(Period::class); $subject->method('getCamp')->willReturn($camp); + $this->responseTagger->expects($this->once())->method('addTags')->with([$collaboration->getId()]); + // when $result = $this->voter->vote($this->token, $subject, ['CAMP_COLLABORATOR']); @@ -264,6 +271,35 @@ public function testGrantsAccessViaBelongsToContentNodeTreeInterface() { // then $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); } + + public function testGrantsAccessViaRequestParameter() { + // given + $user = $this->createMock(User::class); + $user->method('getId')->willReturn('idFromTest'); + $user2 = $this->createMock(User::class); + $user2->method('getId')->willReturn('idFromTest'); + $this->token->method('getUser')->willReturn($user); + $collaboration = new CampCollaboration(); + $collaboration->user = $user2; + $collaboration->status = CampCollaboration::STATUS_ESTABLISHED; + $collaboration->role = CampCollaboration::ROLE_MANAGER; + $camp = new Camp(); + $camp->collaborations->add($collaboration); + $repository = $this->createMock(EntityRepository::class); + $this->em->method('getRepository')->willReturn($repository); + $repository->method('find')->willReturn($camp); + + $request = $this->createMock(Request::class); + $request->attributes = $this->createMock(ParameterBag::class); + $request->attributes->method('get')->with('campId')->willReturn('campId-123'); + + + // when + $result = $this->voter->vote($this->token, $request, ['CAMP_COLLABORATOR']); + + // then + $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); + } } class CampRoleVoterTestDummy extends BaseEntity {} diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index 55f427b195..5ae8a6565d 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -5,21 +5,19 @@ const path = require('path') describe('HTTP cache tests', () => { it('caches /content_types separately for each login', () => { - const uri = '/content_types' + const uri = '/api/content_types' Cypress.session.clearAllSavedSessions() cy.login('test@example.com') // first request is a cache miss - cy.request(Cypress.env('API_ROOT_URL_CACHED') + '/api/content_types.jsonhal').then( - (response) => { - const headers = response.headers - expect(headers.xkey).to.eq( - 'f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' - ) - expect(headers['x-cache']).to.eq('MISS') - } - ) + cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { + const headers = response.headers + expect(headers.xkey).to.eq( + 'f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' + ) + expect(headers['x-cache']).to.eq('MISS') + }) // second request is a cache hit cy.expectCacheHit(uri) @@ -39,10 +37,17 @@ describe('HTTP cache tests', () => { cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { const headers = response.headers expect(headers.xkey).to.eq( - 'ebfd46a1c181 ebfd46a1c181#camp ebfd46a1c181#preferredContentTypes 9d7b3a220fb4 9d7b3a220fb4#root 9d7b3a220fb4#parent 9d7b3a220fb4#children 9d7b3a220fb4#contentType ebfd46a1c181#rootContentNode ebfd46a1c181#contentNodes ' + + /* campCollaboration for test@example.com */ + 'b0bdb7202a9d ' + + /* Category ES */ + 'ebfd46a1c181 ebfd46a1c181#camp ebfd46a1c181#preferredContentTypes 9d7b3a220fb4 9d7b3a220fb4#root 9d7b3a220fb4#parent 9d7b3a220fb4#children 9d7b3a220fb4#contentType ebfd46a1c181#rootContentNode ebfd46a1c181#contentNodes ' + + /* Category LA */ '1a869b162875 1a869b162875#camp 1a869b162875#preferredContentTypes be9b6b7f23f6 be9b6b7f23f6#root be9b6b7f23f6#parent be9b6b7f23f6#children be9b6b7f23f6#contentType 1a869b162875#rootContentNode 1a869b162875#contentNodes ' + + /* Category LP */ 'dfa531302823 dfa531302823#camp dfa531302823#preferredContentTypes 63cbc734fa04 63cbc734fa04#root 63cbc734fa04#parent 63cbc734fa04#children 63cbc734fa04#contentType dfa531302823#rootContentNode dfa531302823#contentNodes ' + + /* Category LS */ 'a023e85227ac a023e85227ac#camp a023e85227ac#preferredContentTypes 2cce9e17a368 2cce9e17a368#root 2cce9e17a368#parent 2cce9e17a368#children 2cce9e17a368#contentType a023e85227ac#rootContentNode a023e85227ac#contentNodes ' + + /* collection URI (for detecting addition of new categories) */ '/api/camps/3c79b99ab424/categories' ) expect(headers['x-cache']).to.eq('MISS') @@ -103,9 +108,6 @@ describe('HTTP cache tests', () => { }).then((response) => { const newContentNodeUri = response.body._links.self.href - console.log(response) - console.log(newContentNodeUri) - // ensure cache was invalidated cy.expectCacheMiss(uri) cy.expectCacheHit(uri) @@ -146,10 +148,13 @@ describe('HTTP cache tests', () => { ) } - it.only('invalidates cached data when user leaves a camp', () => { + it('invalidates cached data when user leaves a camp', () => { Cypress.session.clearAllSavedSessions() const uri = '/api/camps/3c79b99ab424/categories' + cy.intercept('PATCH', '/api/camp_collaborations/**').as('camp_collaboration') + cy.intercept('PATCH', '/api/invitations/**').as('invitations') + // warm up cache cy.login('castor@example.com') cy.expectCacheMiss(uri) @@ -161,10 +166,16 @@ describe('HTTP cache tests', () => { cy.get('.v-list-item__title:contains("Castor")').click() cy.get('button:contains("Deaktivieren")').click() cy.get('div[role=alert]').find('button').contains('Deaktivieren').click() + cy.wait('@camp_collaboration') // ensure cache was invalidated cy.login('castor@example.com') - cy.expectCacheMiss(uri) + cy.request({ + url: Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal', + failOnStatusCode: false, + }).then((response) => { + expect(response.status).to.eq(403) + }) // delete old emails cy.visit('localhost:3000/mail') @@ -177,19 +188,20 @@ describe('HTTP cache tests', () => { cy.visit('/camps/3c79b99ab424/GRGR/admin/collaborators') cy.get('.v-list-item__title:contains("Castor")').click() cy.get('button:contains("Erneut einladen")').click() - cy.wait(100) + cy.wait('@camp_collaboration') // accept invitation as Castor cy.login('castor@example.com') cy.visit('localhost:3000/mail') cy.get('a').contains('[eCamp3] Du wurdest ins Lager "Pfila 2023" eingeladen').click() - cy.wait(400) + cy.wait(200) getIframeBody() .find('a') .then(($a) => { const href = $a.prop('href') cy.visit(href) cy.get('button').contains('Einladung mit aktuellem Account akzeptieren').click() + cy.wait('@invitations') cy.visit('/camps') cy.contains('GRGR') }) From f230aee88806de1dd7c5459b124444570f1774b7 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Fri, 29 Dec 2023 15:15:33 +0100 Subject: [PATCH 16/81] fix dependencies from devel merge --- api/composer.json | 4 +- api/composer.lock | 141 +++++++++--------- .../HttpCache/AddCollectionTagsListener.php | 29 ++-- 3 files changed, 86 insertions(+), 88 deletions(-) diff --git a/api/composer.json b/api/composer.json index c623a09b6c..b1df403064 100644 --- a/api/composer.json +++ b/api/composer.json @@ -12,6 +12,7 @@ "doctrine/doctrine-migrations-bundle": "3.3.0", "doctrine/orm": "2.17.2", "exercise/htmlpurifier-bundle": "5.0", + "friendsofsymfony/http-cache-bundle": "2.16.2", "google/recaptcha": "1.3.0", "guzzlehttp/guzzle": "7.8.1", "knpuniversity/oauth2-client-bundle": "2.18.0", @@ -27,6 +28,7 @@ "swaggest/json-schema": "0.12.42", "symfony/asset": "6.4.0", "symfony/console": "6.4.1", + "symfony/dependency-injection": "6.4.1", "symfony/dotenv": "6.4.0", "symfony/expression-language": "6.4.0", "symfony/flex": "2.4.2", @@ -152,4 +154,4 @@ "url": "https://github.com/usu/api-platform-core" } ] -} \ No newline at end of file +} diff --git a/api/composer.lock b/api/composer.lock index e5c41f640d..a755af00cf 100644 --- a/api/composer.lock +++ b/api/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": "07126068f265f70cae1a87abe4b8f25b", + "content-hash": "e8b2bb60ba47bb621bd79c61b4cf7a94", "packages": [ { "name": "api-platform/core", @@ -12,7 +12,7 @@ "source": { "type": "git", "url": "https://github.com/usu/api-platform-core", - "reference": "c935d8d165f34983f7801f06d5dfe29ac02a0250" + "reference": "1be617d14bfc447da1b92743f4aa12a21433f5ec" }, "require": { "doctrine/inflector": "^1.0 || ^2.0", @@ -41,7 +41,7 @@ "symfony/var-exporter": "<6.1.1" }, "require-dev": { - "behat/behat": "^3.1", + "behat/behat": "^3.11", "behat/mink": "^1.9", "doctrine/cache": "^1.11 || ^2.1", "doctrine/common": "^3.2.2", @@ -164,7 +164,7 @@ "REST", "Swagger" ], - "time": "2023-09-29T19:13:10+00:00" + "time": "2023-12-26T09:45:19+00:00" }, { "name": "behat/transliterator", @@ -2047,16 +2047,16 @@ }, { "name": "friendsofsymfony/http-cache", - "version": "2.15.1", + "version": "2.15.3", "source": { "type": "git", "url": "https://github.com/FriendsOfSymfony/FOSHttpCache.git", - "reference": "4f54d89973faabecd4f9dbb463a4d70660de6712" + "reference": "39f6b356fb253f95067b3c29139949ddbcae3934" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCache/zipball/4f54d89973faabecd4f9dbb463a4d70660de6712", - "reference": "4f54d89973faabecd4f9dbb463a4d70660de6712", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCache/zipball/39f6b356fb253f95067b3c29139949ddbcae3934", + "reference": "39f6b356fb253f95067b3c29139949ddbcae3934", "shasum": "" }, "require": { @@ -2122,9 +2122,9 @@ ], "support": { "issues": "https://github.com/FriendsOfSymfony/FOSHttpCache/issues", - "source": "https://github.com/FriendsOfSymfony/FOSHttpCache/tree/2.15.1" + "source": "https://github.com/FriendsOfSymfony/FOSHttpCache/tree/2.15.3" }, - "time": "2023-05-17T09:08:17+00:00" + "time": "2023-12-11T10:07:11+00:00" }, { "name": "friendsofsymfony/http-cache-bundle", @@ -5989,39 +5989,40 @@ }, { "name": "symfony/dependency-injection", - "version": "v7.0.1", + "version": "v6.4.1", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "f6667642954bce638733f254c39e5b5700b47ba4" + "reference": "f88ff6428afbeb17cc648c8003bd608534750baf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f6667642954bce638733f254c39e5b5700b47ba4", - "reference": "f6667642954bce638733f254c39e5b5700b47ba4", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/f88ff6428afbeb17cc648c8003bd608534750baf", + "reference": "f88ff6428afbeb17cc648c8003bd608534750baf", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/service-contracts": "^3.3", - "symfony/var-exporter": "^6.4|^7.0" + "symfony/service-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.2.10|^7.0" }, "conflict": { "ext-psr": "<1.1|>=2", - "symfony/config": "<6.4", - "symfony/finder": "<6.4", - "symfony/yaml": "<6.4" + "symfony/config": "<6.1", + "symfony/finder": "<5.4", + "symfony/proxy-manager-bridge": "<6.3", + "symfony/yaml": "<5.4" }, "provide": { "psr/container-implementation": "1.1|2.0", "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/yaml": "^6.4|^7.0" + "symfony/config": "^6.1|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -6049,7 +6050,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.0.1" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.1" }, "funding": [ { @@ -6065,7 +6066,7 @@ "type": "tidelift" } ], - "time": "2023-12-01T15:10:06+00:00" + "time": "2023-12-01T14:56:37+00:00" }, { "name": "symfony/deprecation-contracts", @@ -6391,24 +6392,24 @@ }, { "name": "symfony/event-dispatcher", - "version": "v7.0.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "c459b40ffe67c49af6fd392aac374c9edf8a027e" + "reference": "d76d2632cfc2206eecb5ad2b26cd5934082941b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c459b40ffe67c49af6fd392aac374c9edf8a027e", - "reference": "c459b40ffe67c49af6fd392aac374c9edf8a027e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d76d2632cfc2206eecb5ad2b26cd5934082941b6", + "reference": "d76d2632cfc2206eecb5ad2b26cd5934082941b6", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<6.4", + "symfony/dependency-injection": "<5.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -6417,13 +6418,13 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^5.4|^6.0|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" + "symfony/stopwatch": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -6451,7 +6452,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.0" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.0" }, "funding": [ { @@ -6467,7 +6468,7 @@ "type": "tidelift" } ], - "time": "2023-07-27T16:29:09+00:00" + "time": "2023-07-27T06:52:43+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -7122,36 +7123,36 @@ }, { "name": "symfony/http-foundation", - "version": "v7.0.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "47d72323200934694def5d57083899d774a2b110" + "reference": "44a6d39a9cc11e154547d882d5aac1e014440771" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/47d72323200934694def5d57083899d774a2b110", - "reference": "47d72323200934694def5d57083899d774a2b110", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/44a6d39a9cc11e154547d882d5aac1e014440771", + "reference": "44a6d39a9cc11e154547d882d5aac1e014440771", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.1", "symfony/polyfill-php83": "^1.27" }, "conflict": { - "doctrine/dbal": "<3.6", - "symfony/cache": "<6.4" + "symfony/cache": "<6.3" }, "require-dev": { - "doctrine/dbal": "^3.6|^4", + "doctrine/dbal": "^2.13.1|^3|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-kernel": "^6.4|^7.0", - "symfony/mime": "^6.4|^7.0", - "symfony/rate-limiter": "^6.4|^7.0" + "symfony/cache": "^6.3|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -7179,7 +7180,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v7.0.0" + "source": "https://github.com/symfony/http-foundation/tree/v6.4.0" }, "funding": [ { @@ -7195,7 +7196,7 @@ "type": "tidelift" } ], - "time": "2023-11-07T15:10:37+00:00" + "time": "2023-11-20T16:41:16+00:00" }, { "name": "symfony/http-kernel", @@ -7716,20 +7717,20 @@ }, { "name": "symfony/options-resolver", - "version": "v7.0.0", + "version": "v6.4.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "700ff4096e346f54cb628ea650767c8130f1001f" + "reference": "22301f0e7fdeaacc14318928612dee79be99860e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", - "reference": "700ff4096e346f54cb628ea650767c8130f1001f", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", + "reference": "22301f0e7fdeaacc14318928612dee79be99860e", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -7763,7 +7764,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.0.0" + "source": "https://github.com/symfony/options-resolver/tree/v6.4.0" }, "funding": [ { @@ -7779,7 +7780,7 @@ "type": "tidelift" } ], - "time": "2023-08-08T10:20:21+00:00" + "time": "2023-08-08T10:16:24+00:00" }, { "name": "symfony/password-hasher", @@ -8941,21 +8942,21 @@ }, { "name": "symfony/service-contracts", - "version": "v3.4.0", + "version": "v3.4.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838" + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/b3313c2dbffaf71c8de2934e2ea56ed2291a3838", - "reference": "b3313c2dbffaf71c8de2934e2ea56ed2291a3838", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/fe07cbc8d837f60caf7018068e350cc5163681a0", + "reference": "fe07cbc8d837f60caf7018068e350cc5163681a0", "shasum": "" }, "require": { "php": ">=8.1", - "psr/container": "^2.0" + "psr/container": "^1.1|^2.0" }, "conflict": { "ext-psr": "<1.1|>=2" @@ -9003,7 +9004,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.4.0" + "source": "https://github.com/symfony/service-contracts/tree/v3.4.1" }, "funding": [ { @@ -9019,7 +9020,7 @@ "type": "tidelift" } ], - "time": "2023-07-30T20:28:31+00:00" + "time": "2023-12-26T14:02:43+00:00" }, { "name": "symfony/stopwatch", @@ -14321,4 +14322,4 @@ }, "platform-dev": [], "plugin-api-version": "2.6.0" -} \ No newline at end of file +} diff --git a/api/src/HttpCache/AddCollectionTagsListener.php b/api/src/HttpCache/AddCollectionTagsListener.php index 0ca8479b76..987b9c13ed 100644 --- a/api/src/HttpCache/AddCollectionTagsListener.php +++ b/api/src/HttpCache/AddCollectionTagsListener.php @@ -13,32 +13,29 @@ namespace App\HttpCache; -use Symfony\Component\HttpKernel\Event\ResponseEvent; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; -use ApiPlatform\Util\RequestAttributesExtractor; -use ApiPlatform\Util\OperationRequestInitiatorTrait; -use ApiPlatform\State\UriVariablesResolverTrait; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\CollectionOperationInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\State\UriVariablesResolverTrait; +use ApiPlatform\State\Util\OperationRequestInitiatorTrait; +use ApiPlatform\Util\RequestAttributesExtractor; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; +use Symfony\Component\HttpKernel\Event\ResponseEvent; -final class AddCollectionTagsListener -{ +final class AddCollectionTagsListener { use OperationRequestInitiatorTrait; use UriVariablesResolverTrait; - public function __construct(private readonly IriConverterInterface $iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private SymfonyResponseTagger $responseTagger) - { + public function __construct(private readonly IriConverterInterface $iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private SymfonyResponseTagger $responseTagger) { $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; } - public function onKernelResponse(ResponseEvent $event): void - { + public function onKernelResponse(ResponseEvent $event): void { $request = $event->getRequest(); $operation = $this->initializeOperation($request); - if ( !$attributes = RequestAttributesExtractor::extractAttributes($request)) { + if (!$attributes = RequestAttributesExtractor::extractAttributes($request)) { return; } @@ -49,7 +46,5 @@ public function onKernelResponse(ResponseEvent $event): void $this->responseTagger->addTags([$iri]); } - - } } From 285115d32d8944b4bc25523da5daf5067df6aacd Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Fri, 29 Dec 2023 15:27:02 +0100 Subject: [PATCH 17/81] upgrade to api-platform/core main branch (pre-3.3) --- api/composer.json | 10 ++----- api/composer.lock | 42 ++++++++++++++++------------- api/src/HttpCache/TagCollector.php | 43 +++++++++++++++--------------- 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/api/composer.json b/api/composer.json index b1df403064..91b6caf0a4 100644 --- a/api/composer.json +++ b/api/composer.json @@ -5,7 +5,7 @@ "php": ">=8.1.0", "ext-ctype": "*", "ext-iconv": "*", - "api-platform/core": "dev-chore/service-for-cache-tags", + "api-platform/core": "dev-main", "composer/package-versions-deprecated": "1.11.99", "cweagans/composer-patches": "1.7.3", "doctrine/doctrine-bundle": "2.11.1", @@ -147,11 +147,5 @@ "Allow NULL-Links": "patch/api-plattform-allow-null-links.patch" } } - }, - "repositories": [ - { - "type": "git", - "url": "https://github.com/usu/api-platform-core" - } - ] + } } diff --git a/api/composer.lock b/api/composer.lock index a755af00cf..a158f02cfa 100644 --- a/api/composer.lock +++ b/api/composer.lock @@ -4,15 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "e8b2bb60ba47bb621bd79c61b4cf7a94", + "content-hash": "357ced1089ab112ac80e098a21e40cce", "packages": [ { "name": "api-platform/core", - "version": "dev-chore/service-for-cache-tags", + "version": "dev-main", "source": { "type": "git", - "url": "https://github.com/usu/api-platform-core", - "reference": "1be617d14bfc447da1b92743f4aa12a21433f5ec" + "url": "https://github.com/api-platform/core.git", + "reference": "a749fe8494a5b3ca41139a418ed1e29c3e7b33a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/core/zipball/a749fe8494a5b3ca41139a418ed1e29c3e7b33a6", + "reference": "a749fe8494a5b3ca41139a418ed1e29c3e7b33a6", + "shasum": "" }, "require": { "doctrine/inflector": "^1.0 || ^2.0", @@ -121,6 +127,7 @@ "symfony/web-profiler-bundle": "To use the data collector.", "webonyx/graphql-php": "To support GraphQL." }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -135,12 +142,7 @@ "ApiPlatform\\": "src/" } }, - "autoload-dev": { - "psr-4": { - "ApiPlatform\\Tests\\": "tests/", - "App\\": "tests/Fixtures/app/var/tmp/src/" - } - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -154,17 +156,21 @@ "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", "homepage": "https://api-platform.com", "keywords": [ - "API", - "GraphQL", - "HAL", "Hydra", "JSON-LD", - "JSONAPI", - "OpenAPI", - "REST", - "Swagger" + "api", + "graphql", + "hal", + "jsonapi", + "openapi", + "rest", + "swagger" ], - "time": "2023-12-26T09:45:19+00:00" + "support": { + "issues": "https://github.com/api-platform/core/issues", + "source": "https://github.com/api-platform/core/tree/main" + }, + "time": "2023-12-29T10:05:46+00:00" }, { "name": "behat/transliterator", diff --git a/api/src/HttpCache/TagCollector.php b/api/src/HttpCache/TagCollector.php index 3cb9610cb6..61c06f8f72 100644 --- a/api/src/HttpCache/TagCollector.php +++ b/api/src/HttpCache/TagCollector.php @@ -4,49 +4,48 @@ namespace App\HttpCache; -use Symfony\Component\PropertyInfo\Type; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; -use App\HttpCache\PurgeHttpCacheListener; -use App\Entity\BaseEntity; -use ApiPlatform\Serializer\TagCollectorInterface; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Serializer\TagCollectorInterface; +use App\Entity\BaseEntity; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; /** * Collects cache tags during normalization. * * @author Urban Suppiger */ -class TagCollector implements TagCollectorInterface -{ +class TagCollector implements TagCollectorInterface { public const IRI_RELATION_DELIMITER = '#'; - public function __construct(private SymfonyResponseTagger $responseTagger){ + public function __construct(private SymfonyResponseTagger $responseTagger) {} - } + /** + * Collect cache tags for cache invalidation. + * + * @param array&array{iri?: string, data?: mixed, object?: mixed, property_metadata?: \ApiPlatform\Metadata\ApiProperty, api_attribute?: string, resources?: array} $context + */ + public function collect(array $context = []): void { + $iri = $context['iri']; + $object = $context['object']; - public function collect(mixed $object = null, string $format = null, array $context = [], string $iri = null, mixed $data = null, string $attribute = null, ApiProperty $propertyMetadata = null, Type $type = null): void - { - if($object instanceof BaseEntity){ + if ($object instanceof BaseEntity) { $iri = $object->getId(); } - - if($attribute){ - $this->addCacheTagsForRelation($context, $iri, $propertyMetadata); - } - elseif(is_array($data)){ + + if (isset($context['property_metadata'])) { + $this->addCacheTagsForRelation($context, $iri, $context['property_metadata']); + } elseif (\is_array($context['data'])) { $this->addCacheTagForResource($context, $iri); } } - private function addCacheTagForResource(array $context, ?string $iri): void - { + private function addCacheTagForResource(array $context, ?string $iri): void { if (isset($iri)) { $this->responseTagger->addTags([$iri]); } } - private function addCacheTagsForRelation(array $context, ?string $iri, ApiProperty $propertyMetadata): void - { + private function addCacheTagsForRelation(array $context, ?string $iri, ApiProperty $propertyMetadata): void { if (isset($iri)) { if (isset($propertyMetadata->getExtraProperties()['cacheDependencies'])) { foreach ($propertyMetadata->getExtraProperties()['cacheDependencies'] as $dependency) { @@ -59,4 +58,4 @@ private function addCacheTagsForRelation(array $context, ?string $iri, ApiProper } } } -} \ No newline at end of file +} From a0bc28505aca3319062303e49736e24ba6cd3412 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Fri, 29 Dec 2023 15:42:42 +0100 Subject: [PATCH 18/81] fix array access --- api/src/HttpCache/TagCollector.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/api/src/HttpCache/TagCollector.php b/api/src/HttpCache/TagCollector.php index 61c06f8f72..32e24098d8 100644 --- a/api/src/HttpCache/TagCollector.php +++ b/api/src/HttpCache/TagCollector.php @@ -25,14 +25,13 @@ public function __construct(private SymfonyResponseTagger $responseTagger) {} * @param array&array{iri?: string, data?: mixed, object?: mixed, property_metadata?: \ApiPlatform\Metadata\ApiProperty, api_attribute?: string, resources?: array} $context */ public function collect(array $context = []): void { - $iri = $context['iri']; - $object = $context['object']; + $iri = array_key_exists('iri', $context) ? $context['iri'] : null; - if ($object instanceof BaseEntity) { - $iri = $object->getId(); + if (array_key_exists('object', $context) && $context['object'] instanceof BaseEntity) { + $iri = $context['object']->getId(); } - if (isset($context['property_metadata'])) { + if (array_key_exists('property_metadata', $context)) { $this->addCacheTagsForRelation($context, $iri, $context['property_metadata']); } elseif (\is_array($context['data'])) { $this->addCacheTagForResource($context, $iri); From 05262ca3335d061bfb1d8f1eb67f1dc2088ceff1 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 1 Jan 2024 19:09:45 +0100 Subject: [PATCH 19/81] fix end2end-tests --- api/composer.json | 10 ++- api/composer.lock | 42 +++++------ .../HttpCache/AddCollectionTagsListener.php | 2 +- api/src/HttpCache/TagCollector.php | 69 ++++++++++++++----- docker-compose.yml | 4 +- e2e/specs/httpCache.cy.js | 2 +- 6 files changed, 81 insertions(+), 48 deletions(-) diff --git a/api/composer.json b/api/composer.json index 91b6caf0a4..949eda62b3 100644 --- a/api/composer.json +++ b/api/composer.json @@ -5,7 +5,7 @@ "php": ">=8.1.0", "ext-ctype": "*", "ext-iconv": "*", - "api-platform/core": "dev-main", + "api-platform/core": "dev-fix/tag-collector", "composer/package-versions-deprecated": "1.11.99", "cweagans/composer-patches": "1.7.3", "doctrine/doctrine-bundle": "2.11.1", @@ -147,5 +147,11 @@ "Allow NULL-Links": "patch/api-plattform-allow-null-links.patch" } } - } + }, + "repositories": [ + { + "type": "git", + "url": "https://github.com/usu/api-platform-core" + } + ] } diff --git a/api/composer.lock b/api/composer.lock index a158f02cfa..3186332f18 100644 --- a/api/composer.lock +++ b/api/composer.lock @@ -4,21 +4,15 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "357ced1089ab112ac80e098a21e40cce", + "content-hash": "ced320f5f752f04ebe5e9aceee841434", "packages": [ { "name": "api-platform/core", - "version": "dev-main", + "version": "dev-fix/tag-collector", "source": { "type": "git", - "url": "https://github.com/api-platform/core.git", - "reference": "a749fe8494a5b3ca41139a418ed1e29c3e7b33a6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/a749fe8494a5b3ca41139a418ed1e29c3e7b33a6", - "reference": "a749fe8494a5b3ca41139a418ed1e29c3e7b33a6", - "shasum": "" + "url": "https://github.com/usu/api-platform-core", + "reference": "a816884161b3d0fa7ccb2f2672e8c45e3d9c3927" }, "require": { "doctrine/inflector": "^1.0 || ^2.0", @@ -127,7 +121,6 @@ "symfony/web-profiler-bundle": "To use the data collector.", "webonyx/graphql-php": "To support GraphQL." }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -142,7 +135,12 @@ "ApiPlatform\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "ApiPlatform\\Tests\\": "tests/", + "App\\": "tests/Fixtures/app/var/tmp/src/" + } + }, "license": [ "MIT" ], @@ -156,21 +154,17 @@ "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", "homepage": "https://api-platform.com", "keywords": [ + "API", + "GraphQL", + "HAL", "Hydra", "JSON-LD", - "api", - "graphql", - "hal", - "jsonapi", - "openapi", - "rest", - "swagger" + "JSONAPI", + "OpenAPI", + "REST", + "Swagger" ], - "support": { - "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/main" - }, - "time": "2023-12-29T10:05:46+00:00" + "time": "2024-01-01T13:08:34+00:00" }, { "name": "behat/transliterator", diff --git a/api/src/HttpCache/AddCollectionTagsListener.php b/api/src/HttpCache/AddCollectionTagsListener.php index 987b9c13ed..650cf325f6 100644 --- a/api/src/HttpCache/AddCollectionTagsListener.php +++ b/api/src/HttpCache/AddCollectionTagsListener.php @@ -19,7 +19,7 @@ use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; use ApiPlatform\State\UriVariablesResolverTrait; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; -use ApiPlatform\Util\RequestAttributesExtractor; +use ApiPlatform\Symfony\Util\RequestAttributesExtractor; use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; use Symfony\Component\HttpKernel\Event\ResponseEvent; diff --git a/api/src/HttpCache/TagCollector.php b/api/src/HttpCache/TagCollector.php index 32e24098d8..8c576da393 100644 --- a/api/src/HttpCache/TagCollector.php +++ b/api/src/HttpCache/TagCollector.php @@ -25,36 +25,67 @@ public function __construct(private SymfonyResponseTagger $responseTagger) {} * @param array&array{iri?: string, data?: mixed, object?: mixed, property_metadata?: \ApiPlatform\Metadata\ApiProperty, api_attribute?: string, resources?: array} $context */ public function collect(array $context = []): void { - $iri = array_key_exists('iri', $context) ? $context['iri'] : null; + $iri = $context['iri'] ?? null; + $object = $context['object'] ?? null; - if (array_key_exists('object', $context) && $context['object'] instanceof BaseEntity) { - $iri = $context['object']->getId(); + if ($object && $object instanceof BaseEntity) { + $iri = $object->getId(); } - if (array_key_exists('property_metadata', $context)) { + if (!$iri) { + return; + } + + if (isset($context['property_metadata'])) { $this->addCacheTagsForRelation($context, $iri, $context['property_metadata']); - } elseif (\is_array($context['data'])) { - $this->addCacheTagForResource($context, $iri); + + return; } - } - private function addCacheTagForResource(array $context, ?string $iri): void { - if (isset($iri)) { - $this->responseTagger->addTags([$iri]); + // Don't include "link-only" resources (=non fully embedded resources) + if ($this->isLinkOnly($context)) { + return; } + + $this->addCacheTagForResource($iri); } - private function addCacheTagsForRelation(array $context, ?string $iri, ApiProperty $propertyMetadata): void { - if (isset($iri)) { - if (isset($propertyMetadata->getExtraProperties()['cacheDependencies'])) { - foreach ($propertyMetadata->getExtraProperties()['cacheDependencies'] as $dependency) { - $cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$dependency; - $this->responseTagger->addTags([$cacheTag]); - } - } else { - $cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$context['api_attribute']; + private function addCacheTagForResource(string $iri): void { + $this->responseTagger->addTags([$iri]); + } + + private function addCacheTagsForRelation(array $context, string $iri, ApiProperty $propertyMetadata): void { + if (isset($propertyMetadata->getExtraProperties()['cacheDependencies'])) { + foreach ($propertyMetadata->getExtraProperties()['cacheDependencies'] as $dependency) { + $cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$dependency; $this->responseTagger->addTags([$cacheTag]); } + + return; + } + + $cacheTag = $iri.PurgeHttpCacheListener::IRI_RELATION_DELIMITER.$context['api_attribute']; + $this->responseTagger->addTags([$cacheTag]); + } + + /** + * Returns true, if a resource was normalized into a link only + * Returns false, if a resource was normalized into a fully embedded resource. + */ + private function isLinkOnly(array $context): bool { + $format = $context['format'] ?? null; + $data = $context['data'] ?? null; + + // resource was normalized into JSONAPI link format + if ('jsonapi' === $format && isset($data['data']) && \is_array($data['data']) && array_keys($data['data']) === ['type', 'id']) { + return true; + } + + // resource was normalized into a string IRI only + if (\in_array($format, ['jsonld', 'jsonhal'], true) && \is_string($data)) { + return true; } + + return false; } } diff --git a/docker-compose.yml b/docker-compose.yml index 315fcbbddb..bc4ccc88b2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -94,7 +94,7 @@ services: - ./api/public:/srv/api/public:ro http-cache: - image: varnish:7.3.0-alpine + image: varnish:7.4.2-alpine container_name: 'ecamp3-http-cache' depends_on: - caddy @@ -107,6 +107,8 @@ services: - target: 80 published: 3004 protocol: tcp + command: -p http_max_hdr=96 + pdf: image: node:20.9.0 container_name: 'ecamp3-pdf' diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index 5ae8a6565d..4f78e70b9a 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -14,7 +14,7 @@ describe('HTTP cache tests', () => { cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { const headers = response.headers expect(headers.xkey).to.eq( - 'f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' + 'a4211c112939 f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' ) expect(headers['x-cache']).to.eq('MISS') }) From d79ffa0909b8bd6715c9008ad3096df200b9e313 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 1 Jan 2024 19:18:58 +0100 Subject: [PATCH 20/81] remove composer-dependency from varnish VCL --- api/docker/varnish/default.vcl | 11 ------ api/docker/varnish/fos/fos_debug.vcl | 21 ++++++++++++ api/docker/varnish/fos/fos_tags_xkey.vcl | 43 ++++++++++++++++++++++++ docker-compose.yml | 2 +- 4 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 api/docker/varnish/fos/fos_debug.vcl create mode 100644 api/docker/varnish/fos/fos_tags_xkey.vcl diff --git a/api/docker/varnish/default.vcl b/api/docker/varnish/default.vcl index c8b43f22e5..6fe7733deb 100644 --- a/api/docker/varnish/default.vcl +++ b/api/docker/varnish/default.vcl @@ -69,15 +69,4 @@ sub vcl_beresp_cookie { sub vcl_deliver { call fos_tags_xkey_deliver; call fos_debug_deliver; - - # Add X-Cache header if debugging is enabled - if (resp.http.X-Cache-Debug) { - if (obj.hits > 0) { - set resp.http.X-Cache = "HIT"; - } else if (obj.uncacheable) { - set resp.http.X-Cache = "PASS"; - } else { - set resp.http.X-Cache = "MISS"; - } - } } \ No newline at end of file diff --git a/api/docker/varnish/fos/fos_debug.vcl b/api/docker/varnish/fos/fos_debug.vcl new file mode 100644 index 0000000000..1b3adebe30 --- /dev/null +++ b/api/docker/varnish/fos/fos_debug.vcl @@ -0,0 +1,21 @@ +/* + * This file is part of the FOSHttpCache package. + * + * (c) FriendsOfSymfony + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +sub fos_debug_deliver { + # Add X-Cache header if debugging is enabled + if (resp.http.X-Cache-Debug) { + if (obj.hits > 0) { + set resp.http.X-Cache = "HIT"; + } else if (obj.uncacheable) { + set resp.http.X-Cache = "PASS"; + } else { + set resp.http.X-Cache = "MISS"; + } + } +} diff --git a/api/docker/varnish/fos/fos_tags_xkey.vcl b/api/docker/varnish/fos/fos_tags_xkey.vcl new file mode 100644 index 0000000000..1ec527bc77 --- /dev/null +++ b/api/docker/varnish/fos/fos_tags_xkey.vcl @@ -0,0 +1,43 @@ +/* + * This file is part of the FOSHttpCache package. + * + * (c) FriendsOfSymfony + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import xkey; + +sub fos_tags_xkey_recv { + if (req.method == "PURGEKEYS") { + if (!client.ip ~ invalidators) { + return (synth(405, "Not allowed")); + } + + # If neither of the headers are provided we return 400 to simplify detecting wrong configuration + if (!req.http.xkey-purge && !req.http.xkey-softpurge) { + return (synth(400, "Neither header XKey-Purge or XKey-SoftPurge set")); + } + + # Based on provided header invalidate (purge) and/or expire (softpurge) the tagged content + set req.http.n-gone = 0; + set req.http.n-softgone = 0; + if (req.http.xkey-purge) { + set req.http.n-gone = xkey.purge(req.http.xkey-purge); + } + + if (req.http.xkey-softpurge) { + set req.http.n-softgone = xkey.softpurge(req.http.xkey-softpurge); + } + + return (synth(200, "Purged "+req.http.n-gone+" objects, expired "+req.http.n-softgone+" objects")); + } +} + +sub fos_tags_xkey_deliver { + if (!resp.http.X-Cache-Debug) { + // Remove tag headers when delivering to non debug client + unset resp.http.xkey; + } +} diff --git a/docker-compose.yml b/docker-compose.yml index bc4ccc88b2..11e7187876 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,7 +100,7 @@ services: - caddy volumes: - ./api/docker/varnish/default.vcl:/etc/varnish/default.vcl:ro - - ./api/vendor/friendsofsymfony/http-cache/resources/config/varnish:/etc/varnish/fos + - ./api/docker/varnish/fos:/etc/varnish/fos:ro tmpfs: - /var/lib/varnish/varnishd:exec ports: From 3e68564f7ba922ff33b32dad966ac91453449aa1 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 1 Jan 2024 19:41:48 +0100 Subject: [PATCH 21/81] disable cache purge during unit tests --- api/config/packages/test/http_cache.yaml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 api/config/packages/test/http_cache.yaml diff --git a/api/config/packages/test/http_cache.yaml b/api/config/packages/test/http_cache.yaml new file mode 100644 index 0000000000..2b27a07cc3 --- /dev/null +++ b/api/config/packages/test/http_cache.yaml @@ -0,0 +1,4 @@ +fos_http_cache: + proxy_client: + default: noop + noop: ~ From da7e0857d2dfede573ef79bf583accbc21d0a538 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 1 Jan 2024 22:26:56 +0100 Subject: [PATCH 22/81] enable mail and http-cache in CI e2e tests --- .github/workflows/reusable-e2e-tests-run.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/reusable-e2e-tests-run.yml b/.github/workflows/reusable-e2e-tests-run.yml index 1065092467..364adcf269 100644 --- a/.github/workflows/reusable-e2e-tests-run.yml +++ b/.github/workflows/reusable-e2e-tests-run.yml @@ -49,7 +49,7 @@ jobs: docker-compose- # start necessary containers - - run: docker compose up -d php caddy frontend pdf print browserless database docker-host + - run: docker compose up -d php caddy frontend pdf print browserless database docker-host http-cache mail - uses: cypress-io/github-action@v5 with: From 973775007a9a2d1e5f3fea1c4bfa2b41414adc5c Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 1 Jan 2024 22:52:09 +0100 Subject: [PATCH 23/81] remove ScheduleEntry from PR scope + fix API tests --- api/composer.json | 2 +- api/config/services.yaml | 8 +- api/src/Doctrine/FilterByCampExtension.php | 45 ---------- api/src/Entity/ScheduleEntry.php | 12 --- .../HttpCache/AddCollectionTagsListener.php | 8 +- api/src/HttpCache/PurgeHttpCacheListener.php | 75 +++++++--------- api/src/Repository/CategoryRepository.php | 11 +-- .../Repository/ScheduleEntryRepository.php | 14 +-- .../Security/Voter/CampIsPrototypeVoter.php | 21 +++-- api/src/Security/Voter/CampRoleVoter.php | 25 +++--- api/tests/Api/Camps/ReadCampTest.php | 6 +- ...est__testOpenApiSpecMatchesSnapshot__1.yml | 88 +++++++++++++++++++ ...t__testRootEndpointMatchesSnapshot__1.json | 2 +- 13 files changed, 156 insertions(+), 161 deletions(-) delete mode 100644 api/src/Doctrine/FilterByCampExtension.php diff --git a/api/composer.json b/api/composer.json index 98ad5abece..11241a1b79 100644 --- a/api/composer.json +++ b/api/composer.json @@ -154,4 +154,4 @@ "url": "https://github.com/usu/api-platform-core" } ] -} +} \ No newline at end of file diff --git a/api/config/services.yaml b/api/config/services.yaml index 0d30010c8b..23dbc7a2f5 100644 --- a/api/config/services.yaml +++ b/api/config/services.yaml @@ -170,19 +170,13 @@ services: - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: 12 } - # Entity Filters + # Entity Filter App\Doctrine\FilterByCurrentUserExtension: tags: # FilterEagerLoadingExtension has Priority -17 and breaks the DQL generated in ContentNodeRepository => Priority -20 ensures this runs after FilterEagerLoadingExtension - { name: api_platform.doctrine.orm.query_extension.collection, priority: -20 } - { name: api_platform.doctrine.orm.query_extension.item } - App\Doctrine\FilterByCampExtension: - tags: - - { name: api_platform.doctrine.orm.query_extension.collection, priority: -19 } - - { name: api_platform.doctrine.orm.query_extension.item } - - App\Metadata\Resource\Factory\UriTemplateFactory: arguments: - '@api_platform.filter_locator' diff --git a/api/src/Doctrine/FilterByCampExtension.php b/api/src/Doctrine/FilterByCampExtension.php deleted file mode 100644 index 0573bd9116..0000000000 --- a/api/src/Doctrine/FilterByCampExtension.php +++ /dev/null @@ -1,45 +0,0 @@ -entityManager = $entityManager; - } - - public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass = null, Operation $operation = null, array $context = []): void { - if (!isset($context['uri_variables']['campId'])) { - return; - } - - $this->addWhere($queryBuilder, $queryNameGenerator, $resourceClass, $context['uri_variables']['campId']); - } - - public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void { - if (!isset($context['uri_variables']['campId'])) { - return; - } - - $this->addWhere($queryBuilder, $queryNameGenerator, $resourceClass, $context['uri_variables']['campId']); - } - - private function addWhere(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, string $campId): void { - $repository = $this->entityManager->getRepository($resourceClass); - - if (!($repository instanceof CanFilterByCampInterface) || null === $campId) { - return; - } - - $repository->filterByCamp($queryBuilder, $queryNameGenerator, $campId); - } -} diff --git a/api/src/Entity/ScheduleEntry.php b/api/src/Entity/ScheduleEntry.php index f66e132fe6..28e34ef9fb 100644 --- a/api/src/Entity/ScheduleEntry.php +++ b/api/src/Entity/ScheduleEntry.php @@ -9,7 +9,6 @@ use ApiPlatform\Metadata\Delete; use ApiPlatform\Metadata\Get; use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Link; use ApiPlatform\Metadata\Patch; use ApiPlatform\Metadata\Post; use App\Doctrine\Filter\ExpressionDateTimeFilter; @@ -49,17 +48,6 @@ securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)', validationContext: ['groups' => ScheduleEntryPostGroupSequence::class] ), - new GetCollection( - security: 'is_authenticated()', - name: 'BelongsToCamp_App\Entity\ScheduleEntry_get_collection', - uriTemplate: '/camps/{campId}/schedule_entries.{_format}', - uriVariables: [ - 'campId' => new Link( - fromClass: Camp::class, - expandedValue: '{campId}' - ), - ], - ), ], denormalizationContext: ['groups' => ['write']], normalizationContext: ['groups' => ['read']] diff --git a/api/src/HttpCache/AddCollectionTagsListener.php b/api/src/HttpCache/AddCollectionTagsListener.php index 650cf325f6..6036d8b85b 100644 --- a/api/src/HttpCache/AddCollectionTagsListener.php +++ b/api/src/HttpCache/AddCollectionTagsListener.php @@ -13,10 +13,10 @@ namespace App\HttpCache; -use ApiPlatform\Api\IriConverterInterface; -use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\IriConverterInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; use ApiPlatform\State\UriVariablesResolverTrait; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; use ApiPlatform\Symfony\Util\RequestAttributesExtractor; @@ -44,6 +44,10 @@ public function onKernelResponse(ResponseEvent $event): void { $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $attributes['resource_class']); $iri = $this->iriConverter->getIriFromResource($attributes['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => $uriVariables]); + if (!$iri) { + return; + } + $this->responseTagger->addTags([$iri]); } } diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index 314ffc1fd9..7b84ee67aa 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -13,48 +13,43 @@ namespace App\HttpCache; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyAccess\PropertyAccess; -use FOS\HttpCacheBundle\CacheManager; -use Doctrine\ORM\PersistentCollection; -use Doctrine\ORM\Event\PreUpdateEventArgs; -use Doctrine\ORM\Event\OnFlushEventArgs; -use Doctrine\ORM\EntityManagerInterface; -use Doctrine\Common\Util\ClassUtils; -use App\Entity\BaseEntity; -use ApiPlatform\Metadata\Util\ClassInfoTrait; -use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Exception\RuntimeException; -use ApiPlatform\Exception\OperationNotFoundException; +use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; +use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Api\UrlGeneratorInterface; -use ApiPlatform\Api\ResourceClassResolverInterface; -use ApiPlatform\Api\IriConverterInterface; +use ApiPlatform\Exception\OperationNotFoundException; +use ApiPlatform\Exception\RuntimeException; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use ApiPlatform\Metadata\Util\ClassInfoTrait; +use App\Entity\BaseEntity; +use Doctrine\Common\Util\ClassUtils; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\OnFlushEventArgs; +use Doctrine\ORM\Event\PreUpdateEventArgs; +use Doctrine\ORM\PersistentCollection; +use FOS\HttpCacheBundle\CacheManager; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** * Purges responses containing modified entities from the proxy cache. * * @author Kévin Dunglas */ -final class PurgeHttpCacheListener -{ +final class PurgeHttpCacheListener { use ClassInfoTrait; - private readonly PropertyAccessorInterface $propertyAccessor; - private array $tags = []; public const IRI_RELATION_DELIMITER = '#'; - public function __construct(private readonly IriConverterInterface $iriConverter, private readonly ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor, private ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly CacheManager $cacheManager) - { - $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); - } + public function __construct(private readonly IriConverterInterface|LegacyIriConverterInterface $iriConverter, private readonly LegacyResourceClassResolverInterface|ResourceClassResolverInterface $resourceClassResolver, private readonly PropertyAccessorInterface $propertyAccessor, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private readonly CacheManager $cacheManager) {} /** * Collects tags from the previous and the current version of the updated entities to purge related documents. */ - public function preUpdate(PreUpdateEventArgs $eventArgs): void - { + public function preUpdate(PreUpdateEventArgs $eventArgs): void { $object = $eventArgs->getObject(); $this->addTagForItem($object); @@ -80,8 +75,7 @@ public function preUpdate(PreUpdateEventArgs $eventArgs): void /** * Collects tags from inserted and deleted entities, including relations. */ - public function onFlush(OnFlushEventArgs $eventArgs): void - { + public function onFlush(OnFlushEventArgs $eventArgs): void { $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); $uow = $em->getUnitOfWork(); @@ -100,13 +94,11 @@ public function onFlush(OnFlushEventArgs $eventArgs): void /** * Purges tags collected during this request, and clears the tag list. */ - public function postFlush(): void - { + public function postFlush(): void { $this->cacheManager->flush(); } - private function gatherResourceTags(object $entity): void - { + private function gatherResourceTags(object $entity): void { try { $resourceClass = $this->resourceClassResolver->getResourceClass($entity); $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); @@ -123,12 +115,11 @@ private function gatherResourceTags(object $entity): void } $resourceIterator->next(); } - } catch (OperationNotFoundException|InvalidArgumentException) { + } catch (InvalidArgumentException|OperationNotFoundException) { } } - private function gatherRelationTags(EntityManagerInterface $em, object $entity): void - { + private function gatherRelationTags(EntityManagerInterface $em, object $entity): void { $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings(); foreach ($associationMappings as $property => $mappings) { $relatedProperty = $mappings['isOwningSide'] ? $mappings['inversedBy'] : $mappings['mappedBy']; @@ -151,8 +142,7 @@ private function gatherRelationTags(EntityManagerInterface $em, object $entity): } } - private function addTagsFor(mixed $value, string $property = null): void - { + private function addTagsFor(mixed $value, string $property = null): void { if (!$value || \is_scalar($value)) { return; } @@ -172,14 +162,13 @@ private function addTagsFor(mixed $value, string $property = null): void } } - private function addTagForItem(mixed $value, string $property = null): void - { + private function addTagForItem(mixed $value, string $property = null): void { if (!$this->resourceClassResolver->isResourceClass($this->getObjectClass($value))) { return; } try { - if($value instanceof BaseEntity){ + if ($value instanceof BaseEntity) { $iri = $value->getId(); } else { $iri = $this->iriConverter->getIriFromResource($value); @@ -188,7 +177,7 @@ private function addTagForItem(mixed $value, string $property = null): void $iri .= self::IRI_RELATION_DELIMITER.$property; } $this->cacheManager->invalidateTags([$iri]); - } catch (RuntimeException|InvalidArgumentException) { + } catch (InvalidArgumentException|RuntimeException) { } } -} \ No newline at end of file +} diff --git a/api/src/Repository/CategoryRepository.php b/api/src/Repository/CategoryRepository.php index 34727ba612..2afca31ebe 100644 --- a/api/src/Repository/CategoryRepository.php +++ b/api/src/Repository/CategoryRepository.php @@ -2,7 +2,6 @@ namespace App\Repository; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use App\Entity\Category; use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -15,7 +14,7 @@ * @method Category[] findAll() * @method Category[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class CategoryRepository extends ServiceEntityRepository implements CanFilterByUserInterface, CanFilterByCampInterface { +class CategoryRepository extends ServiceEntityRepository implements CanFilterByUserInterface { use FiltersByCampCollaboration; public function __construct(ManagerRegistry $registry) { @@ -27,12 +26,4 @@ public function filterByUser(QueryBuilder $queryBuilder, User $user): void { $queryBuilder->innerJoin("{$rootAlias}.camp", 'camp'); $this->filterByCampCollaboration($queryBuilder, $user); } - - public function filterByCamp(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $campId): void { - $rootAlias = $queryBuilder->getRootAliases()[0]; - $queryBuilder->andWhere( - $queryBuilder->expr()->eq("{$rootAlias}.camp", ':camp') - ); - $queryBuilder->setParameter('camp', $campId); - } } diff --git a/api/src/Repository/ScheduleEntryRepository.php b/api/src/Repository/ScheduleEntryRepository.php index 367e24d376..6b8e6e44af 100644 --- a/api/src/Repository/ScheduleEntryRepository.php +++ b/api/src/Repository/ScheduleEntryRepository.php @@ -2,7 +2,6 @@ namespace App\Repository; -use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface; use App\Entity\ScheduleEntry; use App\Entity\User; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; @@ -15,7 +14,7 @@ * @method ScheduleEntry[] findAll() * @method ScheduleEntry[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class ScheduleEntryRepository extends ServiceEntityRepository implements CanFilterByUserInterface, CanFilterByCampInterface { +class ScheduleEntryRepository extends ServiceEntityRepository implements CanFilterByUserInterface { use FiltersByCampCollaboration; public function __construct(ManagerRegistry $registry) { @@ -40,15 +39,4 @@ public function filterByUser(QueryBuilder $queryBuilder, User $user): void { $queryBuilder->innerJoin('activity.camp', 'camp'); $this->filterByCampCollaboration($queryBuilder, $user); } - - public function filterByCamp(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $campId): void { - $activityJoinAlias = $queryNameGenerator->generateJoinAlias('activity'); - - $rootAlias = $queryBuilder->getRootAliases()[0]; - $queryBuilder->innerJoin("{$rootAlias}.activity", $activityJoinAlias); - $queryBuilder->andWhere( - $queryBuilder->expr()->eq("{$activityJoinAlias}.camp", ':camp') - ); - $queryBuilder->setParameter('camp', $campId); - } } diff --git a/api/src/Security/Voter/CampIsPrototypeVoter.php b/api/src/Security/Voter/CampIsPrototypeVoter.php index 6575404c67..98620efd3a 100644 --- a/api/src/Security/Voter/CampIsPrototypeVoter.php +++ b/api/src/Security/Voter/CampIsPrototypeVoter.php @@ -2,17 +2,15 @@ namespace App\Security\Voter; -use Symfony\Component\Security\Core\Authorization\Voter\Voter; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Request; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; -use Doctrine\ORM\EntityManagerInterface; -use App\Util\GetCampFromContentNodeTrait; -use App\Entity\Camp; -use App\Entity\BelongsToContentNodeTreeInterface; use App\Entity\BelongsToCampInterface; -use ApiPlatform\Api\IriConverterInterface; +use App\Entity\BelongsToContentNodeTreeInterface; +use App\Entity\Camp; +use App\Util\GetCampFromContentNodeTrait; +use Doctrine\ORM\EntityManagerInterface; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** * @extends Voter @@ -31,7 +29,7 @@ protected function supports($attribute, $subject): bool { } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { - if($subject instanceof Request){ + if ($subject instanceof Request) { $camp = $this->em->getRepository(Camp::class)->find($subject->attributes->get('campId')); } else { $camp = $this->getCampFromInterface($subject, $this->em); @@ -46,6 +44,7 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter if ($camp->isPrototype) { $this->responseTagger->addTags([$camp->getId()]); + return true; } diff --git a/api/src/Security/Voter/CampRoleVoter.php b/api/src/Security/Voter/CampRoleVoter.php index 480fe82862..36b02e5307 100644 --- a/api/src/Security/Voter/CampRoleVoter.php +++ b/api/src/Security/Voter/CampRoleVoter.php @@ -2,19 +2,17 @@ namespace App\Security\Voter; -use Symfony\Component\Security\Core\Authorization\Voter\Voter; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; -use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Request; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; -use Doctrine\ORM\EntityManagerInterface; -use App\Util\GetCampFromContentNodeTrait; -use App\Entity\User; -use App\Entity\CampCollaboration; -use App\Entity\Camp; -use App\Entity\BelongsToContentNodeTreeInterface; use App\Entity\BelongsToCampInterface; -use ApiPlatform\Api\IriConverterInterface; +use App\Entity\BelongsToContentNodeTreeInterface; +use App\Entity\Camp; +use App\Entity\CampCollaboration; +use App\Entity\User; +use App\Util\GetCampFromContentNodeTrait; +use Doctrine\ORM\EntityManagerInterface; +use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; /** * @extends Voter @@ -45,7 +43,7 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter return false; } - if($subject instanceof Request){ + if ($subject instanceof Request) { $camp = $this->em->getRepository(Camp::class)->find($subject->attributes->get('campId')); } else { $camp = $this->getCampFromInterface($subject, $this->em); @@ -67,6 +65,7 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter if ($campCollaboration) { $this->responseTagger->addTags([$campCollaboration->getId()]); + return true; } diff --git a/api/tests/Api/Camps/ReadCampTest.php b/api/tests/Api/Camps/ReadCampTest.php index bf21263ff0..7491feece8 100644 --- a/api/tests/Api/Camps/ReadCampTest.php +++ b/api/tests/Api/Camps/ReadCampTest.php @@ -64,7 +64,7 @@ public function testGetSingleCampIsAllowedForGuest() { 'materialLists' => ['href' => '/material_lists?camp=%2Fcamps%2F'.$camp->getId()], 'campCollaborations' => ['href' => '/camp_collaborations?camp=%2Fcamps%2F'.$camp->getId()], 'periods' => ['href' => '/periods?camp=%2Fcamps%2F'.$camp->getId()], - 'categories' => ['href' => '/categories?camp=%2Fcamps%2F'.$camp->getId()], + 'categories' => ['href' => "/camps/{$camp->getId()}/categories"], ], ]); } @@ -92,7 +92,7 @@ public function testGetSingleCampIsAllowedForMember() { 'materialLists' => ['href' => '/material_lists?camp=%2Fcamps%2F'.$camp->getId()], 'campCollaborations' => ['href' => '/camp_collaborations?camp=%2Fcamps%2F'.$camp->getId()], 'periods' => ['href' => '/periods?camp=%2Fcamps%2F'.$camp->getId()], - 'categories' => ['href' => '/categories?camp=%2Fcamps%2F'.$camp->getId()], + 'categories' => ['href' => "/camps/{$camp->getId()}/categories"], ], ]); } @@ -119,7 +119,7 @@ public function testGetSingleCampIsAllowedForManager() { 'materialLists' => ['href' => '/material_lists?camp=%2Fcamps%2F'.$camp->getId()], 'campCollaborations' => ['href' => '/camp_collaborations?camp=%2Fcamps%2F'.$camp->getId()], 'periods' => ['href' => '/periods?camp=%2Fcamps%2F'.$camp->getId()], - 'categories' => ['href' => '/categories?camp=%2Fcamps%2F'.$camp->getId()], + 'categories' => ['href' => "/camps/{$camp->getId()}/categories"], ], ]); } diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 4e96ef437b..dc8f703265 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -20401,6 +20401,94 @@ paths: summary: 'Creates a Camp resource.' tags: - Camp + '/camps/{campId}/categories': + get: + deprecated: false + description: 'Retrieves the collection of Category resources.' + operationId: BelongsToCamp_App\Entity\Category_get_collection + parameters: + - + allowEmptyValue: false + allowReserved: false + deprecated: false + description: 'Camp identifier' + explode: false + in: path + name: campId + required: true + schema: + type: string + style: simple + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: false + in: query + name: camp + required: false + schema: + type: string + style: form + - + allowEmptyValue: true + allowReserved: false + deprecated: false + description: '' + explode: true + in: query + name: 'camp[]' + required: false + schema: + items: + type: string + type: array + style: form + responses: + 200: + content: + application/hal+json: + schema: + properties: + _embedded: { items: { $ref: '#/components/schemas/Category.jsonhal-read' }, type: array } + _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } + itemsPerPage: { minimum: 0, type: integer } + totalItems: { minimum: 0, type: integer } + required: + - _embedded + - _links + type: object + application/json: + schema: + items: + $ref: '#/components/schemas/Category-read' + type: array + application/ld+json: + schema: + properties: + 'hydra:member': { items: { $ref: '#/components/schemas/Category.jsonld-read' }, type: array } + 'hydra:search': { properties: { '@type': { type: string }, 'hydra:mapping': { items: { properties: { '@type': { type: string }, property: { type: ['null', string] }, required: { type: boolean }, variable: { type: string } }, type: object }, type: array }, 'hydra:template': { type: string }, 'hydra:variableRepresentation': { type: string } }, type: object } + 'hydra:totalItems': { minimum: 0, type: integer } + 'hydra:view': { example: { '@id': string, 'hydra:first': string, 'hydra:last': string, 'hydra:next': string, 'hydra:previous': string, type: string }, properties: { '@id': { format: iri-reference, type: string }, '@type': { type: string }, 'hydra:first': { format: iri-reference, type: string }, 'hydra:last': { format: iri-reference, type: string }, 'hydra:next': { format: iri-reference, type: string }, 'hydra:previous': { format: iri-reference, type: string } }, type: object } + required: + - 'hydra:member' + type: object + application/vnd.api+json: + schema: + items: + $ref: '#/components/schemas/Category-read' + type: array + text/html: + schema: + items: + $ref: '#/components/schemas/Category-read' + type: array + description: 'Category collection' + summary: 'Retrieves the collection of Category resources.' + tags: + - Category + parameters: [] '/camps/{id}': delete: deprecated: false diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json index c4aa27d322..bd6a423f65 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json @@ -21,7 +21,7 @@ "templated": true }, "categories": { - "href": "\/categories{\/id}{?camp,camp[]}", + "href": "\/categories{\/id}{\/action}{?camp,camp[]}", "templated": true }, "columnLayouts": { From cee4832a37c9e89872e2318aa1fbd03545faab89 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 1 Jan 2024 22:59:26 +0100 Subject: [PATCH 24/81] fix e2e lint --- e2e/specs/httpCache.cy.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index 4f78e70b9a..c34a999489 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -1,8 +1,3 @@ -// https://docs.cypress.io/api/introduction/api.html - -const { verify } = require('crypto') -const path = require('path') - describe('HTTP cache tests', () => { it('caches /content_types separately for each login', () => { const uri = '/api/content_types' @@ -180,6 +175,7 @@ describe('HTTP cache tests', () => { // delete old emails cy.visit('localhost:3000/mail') cy.get('a[title="Delete all emails"]').click() + /* eslint-disable cypress/no-unnecessary-waiting */ cy.wait(50) cy.get('a[title="Delete all emails"]').click() @@ -194,6 +190,7 @@ describe('HTTP cache tests', () => { cy.login('castor@example.com') cy.visit('localhost:3000/mail') cy.get('a').contains('[eCamp3] Du wurdest ins Lager "Pfila 2023" eingeladen').click() + /* eslint-disable cypress/no-unnecessary-waiting */ cy.wait(200) getIframeBody() .find('a') From 3bc83d24b7fa5e35724af9f6217b9081f2bb91d3 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 2 Jan 2024 10:10:11 +0100 Subject: [PATCH 25/81] only generate tags for routes with caching enabled --- .../HttpCache/AddCollectionTagsListener.php | 11 ++++++++- api/src/HttpCache/TagCollector.php | 24 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/api/src/HttpCache/AddCollectionTagsListener.php b/api/src/HttpCache/AddCollectionTagsListener.php index 6036d8b85b..363f305e58 100644 --- a/api/src/HttpCache/AddCollectionTagsListener.php +++ b/api/src/HttpCache/AddCollectionTagsListener.php @@ -35,7 +35,16 @@ public function onKernelResponse(ResponseEvent $event): void { $request = $event->getRequest(); $operation = $this->initializeOperation($request); - if (!$attributes = RequestAttributesExtractor::extractAttributes($request)) { + if ( + (!$attributes = RequestAttributesExtractor::extractAttributes($request)) + || $request->attributes->get('_api_platform_disable_listeners') + ) { + return; + } + + if ( + !TagCollector::isCacheable($operation, $request->getRequestUri()) + ) { return; } diff --git a/api/src/HttpCache/TagCollector.php b/api/src/HttpCache/TagCollector.php index 8c576da393..62b4823d87 100644 --- a/api/src/HttpCache/TagCollector.php +++ b/api/src/HttpCache/TagCollector.php @@ -5,6 +5,9 @@ namespace App\HttpCache; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operation; use ApiPlatform\Serializer\TagCollectorInterface; use App\Entity\BaseEntity; use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; @@ -17,14 +20,21 @@ class TagCollector implements TagCollectorInterface { public const IRI_RELATION_DELIMITER = '#'; + // only add tags for request URI matching below regex + private const PATH_REGEX = '^/(content_types|camps/[0-9a-f]*/categories)'; + public function __construct(private SymfonyResponseTagger $responseTagger) {} /** * Collect cache tags for cache invalidation. * - * @param array&array{iri?: string, data?: mixed, object?: mixed, property_metadata?: \ApiPlatform\Metadata\ApiProperty, api_attribute?: string, resources?: array} $context + * @param array&array{iri?: string, data?: mixed, object?: mixed, property_metadata?: \ApiPlatform\Metadata\ApiProperty, api_attribute?: string, resources?: array, request_uri?: string, root_operation?: Operation} $context */ public function collect(array $context = []): void { + if (!self::isCacheable($context['root_operation'] ?? null, $context['request_uri'] ?? null)) { + return; + } + $iri = $context['iri'] ?? null; $object = $context['object'] ?? null; @@ -50,6 +60,18 @@ public function collect(array $context = []): void { $this->addCacheTagForResource($iri); } + public static function isCacheable(?Operation $operation, ?string $requestUri): bool { + if (!($operation instanceof GetCollection || $operation instanceof Get)) { + return false; + } + + if (preg_match('{'.self::PATH_REGEX.'}', $requestUri)) { + return true; + } + + return false; + } + private function addCacheTagForResource(string $iri): void { $this->responseTagger->addTags([$iri]); } From 4a36fc2ace68532c63361ce06930c8d176168379 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 2 Jan 2024 15:57:41 +0100 Subject: [PATCH 26/81] enable cached-API for print --- api/docker/varnish/default.vcl | 23 +++++++++++++++++++---- print/print.env | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/api/docker/varnish/default.vcl b/api/docker/varnish/default.vcl index 6fe7733deb..db315f062b 100644 --- a/api/docker/varnish/default.vcl +++ b/api/docker/varnish/default.vcl @@ -52,6 +52,11 @@ sub vcl_hash { cookie.keep("localhost_jwt_hp,localhost_jwt_s"); hash_data(cookie.get_string()); } + + # using URL (=path), but not using Host/ServerIP; this allows to share cache between print & normal API calls + hash_data(req.url); + + return(lookup); } sub vcl_req_cookie { @@ -60,13 +65,23 @@ sub vcl_req_cookie { return (hash); } -sub vcl_beresp_cookie { +sub vcl_backend_response { + if (bereq.uncacheable) { + return (deliver); + } + call vcl_beresp_stale; + # Varnish by default disables caching whenever the reponse header "Set-Cookie" is set in the request (default safe behavior) - # this bypasses the default behaviour - return (deliver); + # commenting the following line bypasses the default behaviour + # call vcl_beresp_cookie; + + call vcl_beresp_control; + call vcl_beresp_vary; + return (deliver); } sub vcl_deliver { call fos_tags_xkey_deliver; call fos_debug_deliver; -} \ No newline at end of file +} + diff --git a/print/print.env b/print/print.env index 8f6db3f4fa..f0785dda77 100644 --- a/print/print.env +++ b/print/print.env @@ -1,4 +1,4 @@ -NUXT_INTERNAL_API_ROOT_URL=http://caddy:3000/api +NUXT_INTERNAL_API_ROOT_URL=http://http-cache:80/api NUXT_SENTRY_PRINT_DSN= NUXT_SENTRY_ENVIRONMENT=local NUXT_BROWSER_WS_ENDPOINT=ws://browserless:3000 From 8b48975e86ae2481ded4b277500a7a21ba664660 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Wed, 3 Jan 2024 09:23:43 +0100 Subject: [PATCH 27/81] only generate tags for routes with caching enabled (refactor) --- .../HttpCache/AddCollectionTagsListener.php | 9 +--- api/src/HttpCache/ResponseTagger.php | 48 +++++++++++++++++++ api/src/HttpCache/TagCollector.php | 24 +--------- .../Security/Voter/CampIsPrototypeVoter.php | 4 +- api/src/Security/Voter/CampRoleVoter.php | 4 +- .../Voter/CampIsPrototypeVoterTest.php | 6 +-- .../Security/Voter/CampRoleVoterTest.php | 6 +-- 7 files changed, 60 insertions(+), 41 deletions(-) create mode 100644 api/src/HttpCache/ResponseTagger.php diff --git a/api/src/HttpCache/AddCollectionTagsListener.php b/api/src/HttpCache/AddCollectionTagsListener.php index 363f305e58..9142ba70d2 100644 --- a/api/src/HttpCache/AddCollectionTagsListener.php +++ b/api/src/HttpCache/AddCollectionTagsListener.php @@ -20,14 +20,13 @@ use ApiPlatform\State\UriVariablesResolverTrait; use ApiPlatform\State\Util\OperationRequestInitiatorTrait; use ApiPlatform\Symfony\Util\RequestAttributesExtractor; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; use Symfony\Component\HttpKernel\Event\ResponseEvent; final class AddCollectionTagsListener { use OperationRequestInitiatorTrait; use UriVariablesResolverTrait; - public function __construct(private readonly IriConverterInterface $iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private SymfonyResponseTagger $responseTagger) { + public function __construct(private readonly IriConverterInterface $iriConverter, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory, private ResponseTagger $responseTagger) { $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory; } @@ -42,12 +41,6 @@ public function onKernelResponse(ResponseEvent $event): void { return; } - if ( - !TagCollector::isCacheable($operation, $request->getRequestUri()) - ) { - return; - } - if ($operation instanceof CollectionOperationInterface) { // Allows to purge collections $uriVariables = $this->getOperationUriVariables($operation, $request->attributes->all(), $attributes['resource_class']); diff --git a/api/src/HttpCache/ResponseTagger.php b/api/src/HttpCache/ResponseTagger.php new file mode 100644 index 0000000000..6ae53c28df --- /dev/null +++ b/api/src/HttpCache/ResponseTagger.php @@ -0,0 +1,48 @@ + + */ +class ResponseTagger { + // only add tags for request URI matching below regex + private const PATH_REGEX = '^/(content_types|camps/[0-9a-f]*/categories)'; + + public function __construct( + private SymfonyResponseTagger $responseTagger, + private RequestStack $requestStack + ) {} + + /** + * Add tags to be set on the response. + * + * Only adds tags for requests that are cacheable + * + * @param string[] $tags List of tags to add + */ + public function addTags(array $tags) { + if ($this->isCacheable()) { + $this->responseTagger->addTags($tags); + } + } + + private function isCacheable(): bool { + $request = $this->requestStack->getCurrentRequest(); + + if (!$request->isMethodCacheable()) { + return false; + } + + $requestUri = $request->getRequestUri(); + + return (bool) preg_match('{'.self::PATH_REGEX.'}', $requestUri); + } +} diff --git a/api/src/HttpCache/TagCollector.php b/api/src/HttpCache/TagCollector.php index 62b4823d87..74ed7a340f 100644 --- a/api/src/HttpCache/TagCollector.php +++ b/api/src/HttpCache/TagCollector.php @@ -5,12 +5,9 @@ namespace App\HttpCache; use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Operation; use ApiPlatform\Serializer\TagCollectorInterface; use App\Entity\BaseEntity; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; /** * Collects cache tags during normalization. @@ -20,10 +17,7 @@ class TagCollector implements TagCollectorInterface { public const IRI_RELATION_DELIMITER = '#'; - // only add tags for request URI matching below regex - private const PATH_REGEX = '^/(content_types|camps/[0-9a-f]*/categories)'; - - public function __construct(private SymfonyResponseTagger $responseTagger) {} + public function __construct(private ResponseTagger $responseTagger) {} /** * Collect cache tags for cache invalidation. @@ -31,10 +25,6 @@ public function __construct(private SymfonyResponseTagger $responseTagger) {} * @param array&array{iri?: string, data?: mixed, object?: mixed, property_metadata?: \ApiPlatform\Metadata\ApiProperty, api_attribute?: string, resources?: array, request_uri?: string, root_operation?: Operation} $context */ public function collect(array $context = []): void { - if (!self::isCacheable($context['root_operation'] ?? null, $context['request_uri'] ?? null)) { - return; - } - $iri = $context['iri'] ?? null; $object = $context['object'] ?? null; @@ -60,18 +50,6 @@ public function collect(array $context = []): void { $this->addCacheTagForResource($iri); } - public static function isCacheable(?Operation $operation, ?string $requestUri): bool { - if (!($operation instanceof GetCollection || $operation instanceof Get)) { - return false; - } - - if (preg_match('{'.self::PATH_REGEX.'}', $requestUri)) { - return true; - } - - return false; - } - private function addCacheTagForResource(string $iri): void { $this->responseTagger->addTags([$iri]); } diff --git a/api/src/Security/Voter/CampIsPrototypeVoter.php b/api/src/Security/Voter/CampIsPrototypeVoter.php index 98620efd3a..ccec8eda92 100644 --- a/api/src/Security/Voter/CampIsPrototypeVoter.php +++ b/api/src/Security/Voter/CampIsPrototypeVoter.php @@ -5,9 +5,9 @@ use App\Entity\BelongsToCampInterface; use App\Entity\BelongsToContentNodeTreeInterface; use App\Entity\Camp; +use App\HttpCache\ResponseTagger; use App\Util\GetCampFromContentNodeTrait; use Doctrine\ORM\EntityManagerInterface; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -20,7 +20,7 @@ class CampIsPrototypeVoter extends Voter { public function __construct( private EntityManagerInterface $em, - private SymfonyResponseTagger $responseTagger + private ResponseTagger $responseTagger ) {} protected function supports($attribute, $subject): bool { diff --git a/api/src/Security/Voter/CampRoleVoter.php b/api/src/Security/Voter/CampRoleVoter.php index 36b02e5307..79062b0233 100644 --- a/api/src/Security/Voter/CampRoleVoter.php +++ b/api/src/Security/Voter/CampRoleVoter.php @@ -7,9 +7,9 @@ use App\Entity\Camp; use App\Entity\CampCollaboration; use App\Entity\User; +use App\HttpCache\ResponseTagger; use App\Util\GetCampFromContentNodeTrait; use Doctrine\ORM\EntityManagerInterface; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -29,7 +29,7 @@ class CampRoleVoter extends Voter { public function __construct( private EntityManagerInterface $em, - private SymfonyResponseTagger $responseTagger + private ResponseTagger $responseTagger ) {} protected function supports($attribute, $subject): bool { diff --git a/api/tests/Security/Voter/CampIsPrototypeVoterTest.php b/api/tests/Security/Voter/CampIsPrototypeVoterTest.php index f2e08adb79..aa186a42f2 100644 --- a/api/tests/Security/Voter/CampIsPrototypeVoterTest.php +++ b/api/tests/Security/Voter/CampIsPrototypeVoterTest.php @@ -8,10 +8,10 @@ use App\Entity\ContentNode\ColumnLayout; use App\Entity\Period; use App\Entity\User; +use App\HttpCache\ResponseTagger; use App\Security\Voter\CampIsPrototypeVoter; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\ParameterBag; @@ -26,13 +26,13 @@ class CampIsPrototypeVoterTest extends TestCase { private CampIsPrototypeVoter $voter; private MockObject|TokenInterface $token; private EntityManagerInterface|MockObject $em; - private MockObject|SymfonyResponseTagger $responseTagger; + private MockObject|ResponseTagger $responseTagger; public function setUp(): void { parent::setUp(); $this->token = $this->createMock(TokenInterface::class); $this->em = $this->createMock(EntityManagerInterface::class); - $this->responseTagger = $this->createMock(SymfonyResponseTagger::class); + $this->responseTagger = $this->createMock(ResponseTagger::class); $this->voter = new CampIsPrototypeVoter($this->em, $this->responseTagger); } diff --git a/api/tests/Security/Voter/CampRoleVoterTest.php b/api/tests/Security/Voter/CampRoleVoterTest.php index d2adb7bb78..82141045b5 100644 --- a/api/tests/Security/Voter/CampRoleVoterTest.php +++ b/api/tests/Security/Voter/CampRoleVoterTest.php @@ -9,10 +9,10 @@ use App\Entity\ContentNode\ColumnLayout; use App\Entity\Period; use App\Entity\User; +use App\HttpCache\ResponseTagger; use App\Security\Voter\CampRoleVoter; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use FOS\HttpCacheBundle\Http\SymfonyResponseTagger; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\ParameterBag; @@ -27,13 +27,13 @@ class CampRoleVoterTest extends TestCase { private CampRoleVoter $voter; private MockObject|TokenInterface $token; private EntityManagerInterface|MockObject $em; - private MockObject|SymfonyResponseTagger $responseTagger; + private MockObject|ResponseTagger $responseTagger; public function setUp(): void { parent::setUp(); $this->token = $this->createMock(TokenInterface::class); $this->em = $this->createMock(EntityManagerInterface::class); - $this->responseTagger = $this->createMock(SymfonyResponseTagger::class); + $this->responseTagger = $this->createMock(ResponseTagger::class); $this->voter = new CampRoleVoter($this->em, $this->responseTagger); } From 7bd9bcb40a363c9f90b45717a00734704cb7e6eb Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Wed, 3 Jan 2024 10:52:02 +0100 Subject: [PATCH 28/81] add additional test coverage --- api/composer.lock | 19 +++++-------- e2e/specs/httpCache.cy.js | 56 +++++++++++++++++++++++++++++++++++++++ e2e/support/commands.js | 7 +++++ 3 files changed, 70 insertions(+), 12 deletions(-) diff --git a/api/composer.lock b/api/composer.lock index bcd724b7e7..998c83b239 100644 --- a/api/composer.lock +++ b/api/composer.lock @@ -12,7 +12,7 @@ "source": { "type": "git", "url": "https://github.com/usu/api-platform-core", - "reference": "a816884161b3d0fa7ccb2f2672e8c45e3d9c3927" + "reference": "ac8a7aa2213dfad70834ca173172a8e936cd7379" }, "require": { "doctrine/inflector": "^1.0 || ^2.0", @@ -10907,16 +10907,16 @@ }, { "name": "fakerphp/faker", - "version": "v1.23.0", + "version": "v1.23.1", "source": { "type": "git", "url": "https://github.com/FakerPHP/Faker.git", - "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01" + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", - "reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b", + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b", "shasum": "" }, "require": { @@ -10942,11 +10942,6 @@ "ext-mbstring": "Required for multibyte Unicode string functionality." }, "type": "library", - "extra": { - "branch-alias": { - "dev-main": "v1.21-dev" - } - }, "autoload": { "psr-4": { "Faker\\": "src/Faker/" @@ -10969,9 +10964,9 @@ ], "support": { "issues": "https://github.com/FakerPHP/Faker/issues", - "source": "https://github.com/FakerPHP/Faker/tree/v1.23.0" + "source": "https://github.com/FakerPHP/Faker/tree/v1.23.1" }, - "time": "2023-06-12T08:44:38+00:00" + "time": "2024-01-02T13:46:09+00:00" }, { "name": "felixfbecker/advanced-json-rpc", diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index c34a999489..0ab6487024 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -22,6 +22,23 @@ describe('HTTP cache tests', () => { cy.expectCacheMiss(uri) }) + it('caches /content_types/318e064ea0c9', () => { + const uri = '/api/content_types/318e064ea0c9' + + Cypress.session.clearAllSavedSessions() + cy.login('test@example.com') + + // first request is a cache miss + cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { + const headers = response.headers + expect(headers.xkey).to.eq('318e064ea0c9') + expect(headers['x-cache']).to.eq('MISS') + }) + + // second request is a cache hit + cy.expectCacheHit(uri) + }) + it('caches /camp/{campId}/categories separately for each login', () => { const uri = '/api/camps/3c79b99ab424/categories' @@ -115,6 +132,38 @@ describe('HTTP cache tests', () => { }) }) + it('invalidates /camp/{campId}/categories for new category', () => { + const uri = '/api/camps/3c79b99ab424/categories' + + Cypress.session.clearAllSavedSessions() + cy.login('test@example.com') + + // warm up cache + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + // add new category to camp + cy.apiPost('/api/categories', { + camp: '/api/camps/3c79b99ab424', + short: 'new', + name: 'new Category', + color: '#000000', + numberingStyle: '1', + }).then((response) => { + const newContentNodeUri = response.body._links.self.href + + // ensure cache was invalidated + cy.expectCacheMiss(uri) + cy.expectCacheHit(uri) + + // delete newly created contentNode + cy.apiDelete(newContentNodeUri) + + // ensure cache was invalidated + cy.expectCacheMiss(uri) + }) + }) + const getIframeDocument = () => { return ( cy @@ -203,4 +252,11 @@ describe('HTTP cache tests', () => { cy.contains('GRGR') }) }) + + it("doesn't cache /camps", () => { + const uri = '/api/camps' + Cypress.session.clearAllSavedSessions() + cy.login('test@example.com') + cy.expectCachePass(uri) + }) }) diff --git a/e2e/support/commands.js b/e2e/support/commands.js index 02a14de3d1..1e5cc9faa9 100644 --- a/e2e/support/commands.js +++ b/e2e/support/commands.js @@ -52,6 +52,13 @@ Cypress.Commands.add('expectCacheMiss', (uri) => { }) }) +Cypress.Commands.add('expectCachePass', (uri) => { + cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { + const headers = response.headers + expect(headers['x-cache']).to.eq('PASS') + }) +}) + Cypress.Commands.add('apiPatch', (uri, body) => { cy.request({ method: 'PATCH', From afe1a6dad6d390b9b0fb90ec44fd33e30702b7ec Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 6 Jan 2024 07:42:37 +0100 Subject: [PATCH 29/81] configure deployment for varnish --- .helm/ecamp3/files/vcl | 1 + .helm/ecamp3/templates/_helpers.tpl | 21 ++++++++ .../templates/api_cache_deployment.yaml | 51 +++++++++++++++++++ .helm/ecamp3/templates/api_cache_service.yaml | 18 +++++++ .../templates/api_cache_vcl_configmap.yaml | 18 +++++++ .helm/ecamp3/templates/api_configmap.yaml | 3 ++ .helm/ecamp3/templates/api_ingress.yaml | 6 +++ .helm/ecamp3/templates/print_ingress.yaml | 1 + .helm/ecamp3/values.yaml | 18 ++++++- api/config/packages/http_cache.yaml | 2 + api/docker/varnish/vcl/_config.vcl | 9 ++++ api/docker/varnish/{ => vcl}/default.vcl | 35 ++++++------- api/docker/varnish/{fos => vcl}/fos_debug.vcl | 0 .../varnish/{fos => vcl}/fos_tags_xkey.vcl | 10 ++-- docker-compose.yml | 3 +- 15 files changed, 171 insertions(+), 25 deletions(-) create mode 120000 .helm/ecamp3/files/vcl create mode 100644 .helm/ecamp3/templates/api_cache_deployment.yaml create mode 100644 .helm/ecamp3/templates/api_cache_service.yaml create mode 100644 .helm/ecamp3/templates/api_cache_vcl_configmap.yaml create mode 100644 api/docker/varnish/vcl/_config.vcl rename api/docker/varnish/{ => vcl}/default.vcl (73%) rename api/docker/varnish/{fos => vcl}/fos_debug.vcl (100%) rename api/docker/varnish/{fos => vcl}/fos_tags_xkey.vcl (78%) diff --git a/.helm/ecamp3/files/vcl b/.helm/ecamp3/files/vcl new file mode 120000 index 0000000000..e036f3976d --- /dev/null +++ b/.helm/ecamp3/files/vcl @@ -0,0 +1 @@ +../../../api/docker/varnish/vcl \ No newline at end of file diff --git a/.helm/ecamp3/templates/_helpers.tpl b/.helm/ecamp3/templates/_helpers.tpl index c61fbfd962..ecb73edadc 100644 --- a/.helm/ecamp3/templates/_helpers.tpl +++ b/.helm/ecamp3/templates/_helpers.tpl @@ -82,6 +82,19 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this {{- end }} {{- end }} +{{/* +Name for all HTTP cache-related resources. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +*/}} +{{- define "apiCache.name" -}} +{{- $name := default .Chart.Name .Values.chartNameOverride }} +{{- if contains $name (include "app.name" .) }} +{{- printf "%s-api-cache" (include "app.name" .) | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s-api-cache" (include "app.name" .) $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} + {{/* Name for all db_backup_job releated resources. We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). @@ -227,6 +240,14 @@ app.kubernetes.io/name: {{ include "chart.name" . }}-browserless {{ include "app.commonSelectorLabels" . }} {{- end }} +{{/* +Selector labels for HTTP Cache +*/}} +{{- define "apiCache.selectorLabels" -}} +app.kubernetes.io/name: {{ include "chart.name" . }}-api-cache +{{ include "app.commonSelectorLabels" . }} +{{- end }} + {{/* Selector labels for db-backup-job */}} diff --git a/.helm/ecamp3/templates/api_cache_deployment.yaml b/.helm/ecamp3/templates/api_cache_deployment.yaml new file mode 100644 index 0000000000..afa5f18416 --- /dev/null +++ b/.helm/ecamp3/templates/api_cache_deployment.yaml @@ -0,0 +1,51 @@ +{{- if .Values.apiCache.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "apiCache.name" . }} + labels: + {{- include "apiCache.selectorLabels" . | nindent 4 }} + {{- include "app.commonLabels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + {{- include "apiCache.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "apiCache.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "app.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + enableServiceLinks: false + containers: + - name: {{ .Chart.Name }}-api-cache + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.apiCache.image.repository }}:{{ .Values.apiCache.image.tag | default .Values.imageTag }}" + imagePullPolicy: {{ .Values.apiCache.image.pullPolicy }} + ports: + - name: api-cache-http + containerPort: {{ .Values.apiCache.varnishHttpPort }} + protocol: TCP + env: + - name: VARNISH_SIZE + value: "{{ .Values.apiCache.varnishSize }}" + - name: VARNISH_HTTP_PORT + value: "{{ .Values.apiCache.varnishHttpPort }}" + resources: + {{- toYaml .Values.apiCache.resources | nindent 12 }} + volumeMounts: + - name: vcl-configmap + mountPath: /etc/varnish + volumes: + - name: vcl-configmap + configMap: + name: {{ include "apiCache.name" . }}-vcl-configmap +{{- end }} \ No newline at end of file diff --git a/.helm/ecamp3/templates/api_cache_service.yaml b/.helm/ecamp3/templates/api_cache_service.yaml new file mode 100644 index 0000000000..021cac6696 --- /dev/null +++ b/.helm/ecamp3/templates/api_cache_service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.apiCache.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "apiCache.name" . }} + labels: + {{- include "apiCache.selectorLabels" . | nindent 4 }} + {{- include "app.commonLabels" . | nindent 4 }} +spec: + type: {{ .Values.apiCache.service.type }} + ports: + - port: {{ .Values.apiCache.service.port }} + targetPort: api-cache-http + protocol: TCP + name: api-cache-http + selector: + {{- include "apiCache.selectorLabels" . | nindent 4 }} +{{- end }} \ No newline at end of file diff --git a/.helm/ecamp3/templates/api_cache_vcl_configmap.yaml b/.helm/ecamp3/templates/api_cache_vcl_configmap.yaml new file mode 100644 index 0000000000..17c6c64078 --- /dev/null +++ b/.helm/ecamp3/templates/api_cache_vcl_configmap.yaml @@ -0,0 +1,18 @@ +{{- if .Values.apiCache.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "apiCache.name" . }}-vcl-configmap + labels: + {{- include "apiCache.selectorLabels" . | nindent 4 }} + {{- include "app.commonLabels" . | nindent 4 }} +data: +# includes all files except the ones starting with _ +{{ (.Files.Glob "files/vcl/[!_]*").AsConfig | indent 2 }} + # override backend config + _config.vcl: |- + backend default { + .host = "{{ include "api.name" .}}"; + .port = "{{ .Values.api.service.port }}"; + } +{{- end }} \ No newline at end of file diff --git a/.helm/ecamp3/templates/api_configmap.yaml b/.helm/ecamp3/templates/api_configmap.yaml index a719502da9..0c394a7473 100644 --- a/.helm/ecamp3/templates/api_configmap.yaml +++ b/.helm/ecamp3/templates/api_configmap.yaml @@ -22,3 +22,6 @@ data: SENTRY_API_DSN: {{ "" | quote }} {{- end }} FRONTEND_BASE_URL: {{ include "frontend.url" . | quote }} + {{- if .Values.apiCache.enabled }} + VARNISH_API_URL: {{ printf "%s:%d" (include "apiCache.name" .) (.Values.apiCache.service.port | int) | quote }} + {{- end}} diff --git a/.helm/ecamp3/templates/api_ingress.yaml b/.helm/ecamp3/templates/api_ingress.yaml index b104b3f3d1..245b568d7b 100644 --- a/.helm/ecamp3/templates/api_ingress.yaml +++ b/.helm/ecamp3/templates/api_ingress.yaml @@ -29,7 +29,13 @@ spec: pathType: Prefix backend: service: + {{- if .Values.apiCache.enabled }} + name: {{ include "apiCache.name" . }} + port: + number: {{ .Values.apiCache.service.port }} + {{- else }} name: {{ include "api.name" . }} port: number: {{ .Values.api.service.port }} + {{- end }} {{- end }} diff --git a/.helm/ecamp3/templates/print_ingress.yaml b/.helm/ecamp3/templates/print_ingress.yaml index bbe663161d..334c283fd7 100644 --- a/.helm/ecamp3/templates/print_ingress.yaml +++ b/.helm/ecamp3/templates/print_ingress.yaml @@ -12,6 +12,7 @@ metadata: {{- end }} {{- include "ingress.basicAuth.annotations" . | nindent 4 }} {{- if not (.Values.print.ingress.readTimeoutSeconds | empty) }} + nginx.ingress.kubernetes.io/use-regex: "true" nginx.ingress.kubernetes.io/proxy-read-timeout: {{ .Values.print.ingress.readTimeoutSeconds | quote }} {{- end }} spec: diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index d81b83c296..24a5ca7238 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -219,7 +219,23 @@ ingress: className: nginx tls: - +apiCache: + enabled: true + image: + repository: "docker.io/library/varnish" + pullPolicy: IfNotPresent + # Overrides the image tag whose shared default is .Values.imageTag + tag: "7.4.2-alpine" + service: + type: ClusterIP + port: 3000 + varnishSize: 50M + varnishHttpPort: 8080 + resources: + requests: + cpu: 10m + memory: 100Mi + autoscaling: enabled: false minReplicas: 1 diff --git a/api/config/packages/http_cache.yaml b/api/config/packages/http_cache.yaml index b9b6d5555a..6b0b67021b 100644 --- a/api/config/packages/http_cache.yaml +++ b/api/config/packages/http_cache.yaml @@ -1,4 +1,6 @@ fos_http_cache: + debug: + enabled: true # this sets the X-Cache-Debug response header; can be removed later-on tags: enabled: true annotations: diff --git a/api/docker/varnish/vcl/_config.vcl b/api/docker/varnish/vcl/_config.vcl new file mode 100644 index 0000000000..4a0a0510e7 --- /dev/null +++ b/api/docker/varnish/vcl/_config.vcl @@ -0,0 +1,9 @@ +backend default { + .host = "caddy"; + .port = "3000"; +} + +# Hosts allowed to send BAN requests +# acl invalidators { +# "php"; +# } \ No newline at end of file diff --git a/api/docker/varnish/default.vcl b/api/docker/varnish/vcl/default.vcl similarity index 73% rename from api/docker/varnish/default.vcl rename to api/docker/varnish/vcl/default.vcl index db315f062b..2040907a98 100644 --- a/api/docker/varnish/default.vcl +++ b/api/docker/varnish/vcl/default.vcl @@ -3,39 +3,36 @@ vcl 4.0; import std; import xkey; import cookie; +import var; +include "./_config.vcl"; +include "./fos_tags_xkey.vcl"; +include "./fos_debug.vcl"; -include "./fos/fos_tags_xkey.vcl"; -include "./fos/fos_debug.vcl"; - +sub vcl_recv { -backend default { - .host = "caddy"; - .port = "3000"; -} + var.set("originalUrl", req.http.X-Forwarded-Prefix + req.url); -# Hosts allowed to send BAN requests -acl invalidators { - "php"; -} + if(var.get("originalUrl") ~ "^/api/varnish/healthcheck") { + return(synth(200,"OK")); + } -sub vcl_recv { # Support xkey purge requests # see https://raw.githubusercontent.com/varnish/varnish-modules/master/src/vmod_xkey.vcc call fos_tags_xkey_recv; # exclude other services (frontend, print, etc.) - if (req.url !~ "^/api") { + if (var.get("originalUrl") !~ "^/api") { return(pass); } # exclude API documentation, profiler and graphql endpoint - if (req.url ~ "^/api/docs" - || req.url ~ "^/api/graphql" - || req.url ~ "^/api/bundles" - || req.url ~ "^/api/contexts" - || req.url ~ "^/api/_profiler" - || req.url ~ "^/api/_wdt") { + if (var.get("originalUrl") ~ "^/api/docs" + || var.get("originalUrl") ~ "^/api/graphql" + || var.get("originalUrl") ~ "^/api/bundles" + || var.get("originalUrl") ~ "^/api/contexts" + || var.get("originalUrl") ~ "^/api/_profiler" + || var.get("originalUrl") ~ "^/api/_wdt") { return(pass); } diff --git a/api/docker/varnish/fos/fos_debug.vcl b/api/docker/varnish/vcl/fos_debug.vcl similarity index 100% rename from api/docker/varnish/fos/fos_debug.vcl rename to api/docker/varnish/vcl/fos_debug.vcl diff --git a/api/docker/varnish/fos/fos_tags_xkey.vcl b/api/docker/varnish/vcl/fos_tags_xkey.vcl similarity index 78% rename from api/docker/varnish/fos/fos_tags_xkey.vcl rename to api/docker/varnish/vcl/fos_tags_xkey.vcl index 1ec527bc77..f3bc740b8b 100644 --- a/api/docker/varnish/fos/fos_tags_xkey.vcl +++ b/api/docker/varnish/vcl/fos_tags_xkey.vcl @@ -11,9 +11,13 @@ import xkey; sub fos_tags_xkey_recv { if (req.method == "PURGEKEYS") { - if (!client.ip ~ invalidators) { - return (synth(405, "Not allowed")); - } + + # comparing client.ip with the invalidators array doesn't work in Kubernetes, + # because client.ip is the POD IP of the API, which is not known at the time of setup + # TODO: find alternative solution for auth check for PURGING + # if (!client.ip ~ invalidators) { + # return (synth(405, "Not allowed")); + # } # If neither of the headers are provided we return 400 to simplify detecting wrong configuration if (!req.http.xkey-purge && !req.http.xkey-softpurge) { diff --git a/docker-compose.yml b/docker-compose.yml index 11e7187876..d14f0a0c63 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -99,8 +99,7 @@ services: depends_on: - caddy volumes: - - ./api/docker/varnish/default.vcl:/etc/varnish/default.vcl:ro - - ./api/docker/varnish/fos:/etc/varnish/fos:ro + - ./api/docker/varnish/vcl/:/etc/varnish/:ro tmpfs: - /var/lib/varnish/varnishd:exec ports: From 1195ebd6133004ccff89787071aa65693a9e5840 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 7 Jan 2024 13:10:07 +0100 Subject: [PATCH 30/81] allow purge requests on port 8081 only --- .helm/ecamp3/templates/api_cache_deployment.yaml | 8 ++++++++ .helm/ecamp3/templates/api_cache_service.yaml | 6 +++++- .helm/ecamp3/templates/api_configmap.yaml | 2 +- .helm/ecamp3/templates/api_ingress.yaml | 2 +- .helm/ecamp3/values.yaml | 5 ++++- api/.env | 2 +- api/docker/varnish/vcl/_config.vcl | 7 +------ api/docker/varnish/vcl/fos_tags_xkey.vcl | 10 ++++------ docker-compose.yml | 4 ++-- 9 files changed, 27 insertions(+), 19 deletions(-) diff --git a/.helm/ecamp3/templates/api_cache_deployment.yaml b/.helm/ecamp3/templates/api_cache_deployment.yaml index afa5f18416..d32541feba 100644 --- a/.helm/ecamp3/templates/api_cache_deployment.yaml +++ b/.helm/ecamp3/templates/api_cache_deployment.yaml @@ -34,11 +34,19 @@ spec: - name: api-cache-http containerPort: {{ .Values.apiCache.varnishHttpPort }} protocol: TCP + - name: api-cache-purge + containerPort: {{ .Values.apiCache.varnishPurgePort }} + protocol: TCP env: - name: VARNISH_SIZE value: "{{ .Values.apiCache.varnishSize }}" - name: VARNISH_HTTP_PORT value: "{{ .Values.apiCache.varnishHttpPort }}" + args: + - -a + - {{ printf ":%d,HTTP" (.Values.apiCache.varnishPurgePort | int) }} + - -p + - http_max_hdr=96 resources: {{- toYaml .Values.apiCache.resources | nindent 12 }} volumeMounts: diff --git a/.helm/ecamp3/templates/api_cache_service.yaml b/.helm/ecamp3/templates/api_cache_service.yaml index 021cac6696..497b55f4b0 100644 --- a/.helm/ecamp3/templates/api_cache_service.yaml +++ b/.helm/ecamp3/templates/api_cache_service.yaml @@ -9,10 +9,14 @@ metadata: spec: type: {{ .Values.apiCache.service.type }} ports: - - port: {{ .Values.apiCache.service.port }} + - port: {{ .Values.apiCache.service.ports.http }} targetPort: api-cache-http protocol: TCP name: api-cache-http + - port: {{ .Values.apiCache.service.ports.purge }} + targetPort: api-cache-purge + protocol: TCP + name: api-cache-purge selector: {{- include "apiCache.selectorLabels" . | nindent 4 }} {{- end }} \ No newline at end of file diff --git a/.helm/ecamp3/templates/api_configmap.yaml b/.helm/ecamp3/templates/api_configmap.yaml index 0c394a7473..937cd95fad 100644 --- a/.helm/ecamp3/templates/api_configmap.yaml +++ b/.helm/ecamp3/templates/api_configmap.yaml @@ -23,5 +23,5 @@ data: {{- end }} FRONTEND_BASE_URL: {{ include "frontend.url" . | quote }} {{- if .Values.apiCache.enabled }} - VARNISH_API_URL: {{ printf "%s:%d" (include "apiCache.name" .) (.Values.apiCache.service.port | int) | quote }} + VARNISH_API_URL: {{ printf "%s:%d" (include "apiCache.name" .) (.Values.apiCache.service.ports.purge | int) | quote }} {{- end}} diff --git a/.helm/ecamp3/templates/api_ingress.yaml b/.helm/ecamp3/templates/api_ingress.yaml index 245b568d7b..f3b98141a9 100644 --- a/.helm/ecamp3/templates/api_ingress.yaml +++ b/.helm/ecamp3/templates/api_ingress.yaml @@ -32,7 +32,7 @@ spec: {{- if .Values.apiCache.enabled }} name: {{ include "apiCache.name" . }} port: - number: {{ .Values.apiCache.service.port }} + number: {{ .Values.apiCache.service.ports.http }} {{- else }} name: {{ include "api.name" . }} port: diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index 24a5ca7238..543adf54b6 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -228,9 +228,12 @@ apiCache: tag: "7.4.2-alpine" service: type: ClusterIP - port: 3000 + ports: + http: 3000 + purge: 3001 varnishSize: 50M varnishHttpPort: 8080 + varnishPurgePort: 8081 resources: requests: cpu: 10m diff --git a/api/.env b/api/.env index d2ff1f5613..a159328d49 100644 --- a/api/.env +++ b/api/.env @@ -17,7 +17,7 @@ TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 ADDITIONAL_TRUSTED_HOSTS=localhost COOKIE_PREFIX=localhost_ -VARNISH_API_URL=http://http-cache:80 +VARNISH_API_URL=http://http-cache:8081 ###> symfony/framework-bundle ### APP_ENV=dev diff --git a/api/docker/varnish/vcl/_config.vcl b/api/docker/varnish/vcl/_config.vcl index 4a0a0510e7..1f81b8bdab 100644 --- a/api/docker/varnish/vcl/_config.vcl +++ b/api/docker/varnish/vcl/_config.vcl @@ -1,9 +1,4 @@ backend default { .host = "caddy"; .port = "3000"; -} - -# Hosts allowed to send BAN requests -# acl invalidators { -# "php"; -# } \ No newline at end of file +} \ No newline at end of file diff --git a/api/docker/varnish/vcl/fos_tags_xkey.vcl b/api/docker/varnish/vcl/fos_tags_xkey.vcl index f3bc740b8b..ea2e468d1e 100644 --- a/api/docker/varnish/vcl/fos_tags_xkey.vcl +++ b/api/docker/varnish/vcl/fos_tags_xkey.vcl @@ -12,12 +12,10 @@ import xkey; sub fos_tags_xkey_recv { if (req.method == "PURGEKEYS") { - # comparing client.ip with the invalidators array doesn't work in Kubernetes, - # because client.ip is the POD IP of the API, which is not known at the time of setup - # TODO: find alternative solution for auth check for PURGING - # if (!client.ip ~ invalidators) { - # return (synth(405, "Not allowed")); - # } + # only allow purge requests on port 8081 + if (std.port(server.ip) != 8081) { + return (synth(405, "Not allowed")); + } # If neither of the headers are provided we return 400 to simplify detecting wrong configuration if (!req.http.xkey-purge && !req.http.xkey-softpurge) { diff --git a/docker-compose.yml b/docker-compose.yml index d14f0a0c63..622cc58957 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -103,10 +103,10 @@ services: tmpfs: - /var/lib/varnish/varnishd:exec ports: - - target: 80 + - target: 8080 published: 3004 protocol: tcp - command: -p http_max_hdr=96 + command: -a :8080 -a :8081 -p http_max_hdr=96 pdf: image: node:20.9.0 From 171052560e996914cd49ca8caadc347f3905dd12 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 7 Jan 2024 14:03:20 +0100 Subject: [PATCH 31/81] reset cache-control header after varnish --- api/docker/varnish/vcl/default.vcl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/docker/varnish/vcl/default.vcl b/api/docker/varnish/vcl/default.vcl index 2040907a98..bd70475637 100644 --- a/api/docker/varnish/vcl/default.vcl +++ b/api/docker/varnish/vcl/default.vcl @@ -80,5 +80,10 @@ sub vcl_backend_response { sub vcl_deliver { call fos_tags_xkey_deliver; call fos_debug_deliver; + + # reset cache control header to avoid caching by any other upstream proxies + if (resp.http.Content-Type ~ "application/hal\+json"){ + set resp.http.Cache-Control = "no-cache, private"; + } } From b3b6dc35a93e8f225cfa629b079b65567d909f40 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 9 Jan 2024 17:47:32 +0100 Subject: [PATCH 32/81] change api-platform/core back to main branch --- api/composer.json | 12 +++--------- api/composer.lock | 42 ++++++++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/api/composer.json b/api/composer.json index 11241a1b79..51ac28e98d 100644 --- a/api/composer.json +++ b/api/composer.json @@ -5,7 +5,7 @@ "php": ">=8.1.0", "ext-ctype": "*", "ext-iconv": "*", - "api-platform/core": "dev-fix/tag-collector", + "api-platform/core": "dev-main", "composer/package-versions-deprecated": "1.11.99", "cweagans/composer-patches": "1.7.3", "doctrine/doctrine-bundle": "2.11.1", @@ -147,11 +147,5 @@ "Allow NULL-Links": "patch/api-plattform-allow-null-links.patch" } } - }, - "repositories": [ - { - "type": "git", - "url": "https://github.com/usu/api-platform-core" - } - ] -} \ No newline at end of file + } +} diff --git a/api/composer.lock b/api/composer.lock index 998c83b239..46b4321e83 100644 --- a/api/composer.lock +++ b/api/composer.lock @@ -4,15 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "68723456a9b9262234f982002cd3b0f8", + "content-hash": "87c092085b5831e1255c6244205d5f7f", "packages": [ { "name": "api-platform/core", - "version": "dev-fix/tag-collector", + "version": "dev-main", "source": { "type": "git", - "url": "https://github.com/usu/api-platform-core", - "reference": "ac8a7aa2213dfad70834ca173172a8e936cd7379" + "url": "https://github.com/api-platform/core.git", + "reference": "28ff0ac4c960e3f5442b8b173507c092c1ce7019" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/api-platform/core/zipball/28ff0ac4c960e3f5442b8b173507c092c1ce7019", + "reference": "28ff0ac4c960e3f5442b8b173507c092c1ce7019", + "shasum": "" }, "require": { "doctrine/inflector": "^1.0 || ^2.0", @@ -121,6 +127,7 @@ "symfony/web-profiler-bundle": "To use the data collector.", "webonyx/graphql-php": "To support GraphQL." }, + "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -135,12 +142,7 @@ "ApiPlatform\\": "src/" } }, - "autoload-dev": { - "psr-4": { - "ApiPlatform\\Tests\\": "tests/", - "App\\": "tests/Fixtures/app/var/tmp/src/" - } - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -154,17 +156,21 @@ "description": "Build a fully-featured hypermedia or GraphQL API in minutes!", "homepage": "https://api-platform.com", "keywords": [ - "API", - "GraphQL", - "HAL", "Hydra", "JSON-LD", - "JSONAPI", - "OpenAPI", - "REST", - "Swagger" + "api", + "graphql", + "hal", + "jsonapi", + "openapi", + "rest", + "swagger" ], - "time": "2024-01-01T13:08:34+00:00" + "support": { + "issues": "https://github.com/api-platform/core/issues", + "source": "https://github.com/api-platform/core/tree/main" + }, + "time": "2024-01-05T08:45:32+00:00" }, { "name": "behat/transliterator", From 60ce02fcd9a335e8c0e9ce4dc8bfe54bf7932bca Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 14 Jan 2024 09:08:57 +0100 Subject: [PATCH 33/81] use COOKIE_PREFIX in JWT cookie extraction --- .../templates/api_cache_deployment.yaml | 2 ++ api/docker/varnish/vcl/default.vcl | 19 ++++++++++++++----- docker-compose.yml | 2 ++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.helm/ecamp3/templates/api_cache_deployment.yaml b/.helm/ecamp3/templates/api_cache_deployment.yaml index d32541feba..1aabe54a7f 100644 --- a/.helm/ecamp3/templates/api_cache_deployment.yaml +++ b/.helm/ecamp3/templates/api_cache_deployment.yaml @@ -42,6 +42,8 @@ spec: value: "{{ .Values.apiCache.varnishSize }}" - name: VARNISH_HTTP_PORT value: "{{ .Values.apiCache.varnishHttpPort }}" + - name: COOKIE_PREFIX + value: {{ include "api.cookiePrefix" . | quote }} args: - -a - {{ printf ":%d,HTTP" (.Values.apiCache.varnishPurgePort | int) }} diff --git a/api/docker/varnish/vcl/default.vcl b/api/docker/varnish/vcl/default.vcl index bd70475637..53431544d7 100644 --- a/api/docker/varnish/vcl/default.vcl +++ b/api/docker/varnish/vcl/default.vcl @@ -40,15 +40,24 @@ sub vcl_recv { if (req.url !~ "\.jsonhal$" && req.http.Accept !~ "application/hal\+json"){ return(pass); } -} -sub vcl_hash { + # Extract JWT cookie for later use in vcl_hash + # Failsafe: Pass cache if JWT cookie is not set (also for example, if COOKIE_PREFIX is not properly configured) if (req.http.Cookie) { - # Include JWT cookies in cache hash cookie.parse(req.http.Cookie); - cookie.keep("localhost_jwt_hp,localhost_jwt_s"); - hash_data(cookie.get_string()); + cookie.keep(std.getenv("COOKIE_PREFIX") + "jwt_hp," + std.getenv("COOKIE_PREFIX") + "jwt_s"); + + if(cookie.get_string() == ""){ + return(pass); + } + + var.set("JWT", cookie.get_string()); } +} + +sub vcl_hash { + # Include JWT cookies in cache hash + hash_data(var.get("JWT")); # using URL (=path), but not using Host/ServerIP; this allows to share cache between print & normal API calls hash_data(req.url); diff --git a/docker-compose.yml b/docker-compose.yml index 622cc58957..4a569d227e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -107,6 +107,8 @@ services: published: 3004 protocol: tcp command: -a :8080 -a :8081 -p http_max_hdr=96 + environment: + - COOKIE_PREFIX=localhost_ pdf: image: node:20.9.0 From 60cc980e6db98e950ef168cbe64be7f60fea64d9 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 14 Jan 2024 09:14:36 +0100 Subject: [PATCH 34/81] same port config on localhost as on deployment --- docker-compose.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 4a569d227e..57f98e5165 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -106,9 +106,10 @@ services: - target: 8080 published: 3004 protocol: tcp - command: -a :8080 -a :8081 -p http_max_hdr=96 + command: -a :8081,HTTP -p http_max_hdr=96 environment: - COOKIE_PREFIX=localhost_ + - VARNISH_HTTP_PORT=8080 pdf: image: node:20.9.0 From eb3210b015331f42317b33c9444c25d0b555661f Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 14 Jan 2024 12:55:57 +0100 Subject: [PATCH 35/81] allow enabling/disabling caching via environment variables --- api/.env | 1 + api/config/packages/http_cache.yaml | 10 +-- api/config/packages/test/http_cache.yaml | 3 +- api/config/services.yaml | 13 ++++ api/src/HttpCache/CacheControlListener.php | 37 ++++++++++ api/src/HttpCache/VarnishProxyClient.php | 82 ++++++++++++++++++++++ 6 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 api/src/HttpCache/CacheControlListener.php create mode 100644 api/src/HttpCache/VarnishProxyClient.php diff --git a/api/.env b/api/.env index a159328d49..986360172b 100644 --- a/api/.env +++ b/api/.env @@ -18,6 +18,7 @@ TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 ADDITIONAL_TRUSTED_HOSTS=localhost COOKIE_PREFIX=localhost_ VARNISH_API_URL=http://http-cache:8081 +API_CACHE_ENABLED=true ###> symfony/framework-bundle ### APP_ENV=dev diff --git a/api/config/packages/http_cache.yaml b/api/config/packages/http_cache.yaml index 6b0b67021b..2d3b348ee4 100644 --- a/api/config/packages/http_cache.yaml +++ b/api/config/packages/http_cache.yaml @@ -3,16 +3,12 @@ fos_http_cache: enabled: true # this sets the X-Cache-Debug response header; can be removed later-on tags: enabled: true + response_header: xkey annotations: enabled: false max_header_value_length: 4096 - proxy_client: - varnish: - tag_mode: purgekeys - tags_header: xkey-purge - http: - servers: - - '%env(VARNISH_API_URL)%' + cache_manager: + custom_proxy_client: App\HttpCache\VarnishProxyClient cache_control: defaults: overwrite: true diff --git a/api/config/packages/test/http_cache.yaml b/api/config/packages/test/http_cache.yaml index 2b27a07cc3..419340180e 100644 --- a/api/config/packages/test/http_cache.yaml +++ b/api/config/packages/test/http_cache.yaml @@ -1,4 +1,5 @@ fos_http_cache: proxy_client: - default: noop noop: ~ + cache_manager: + custom_proxy_client: fos_http_cache.proxy_client.noop diff --git a/api/config/services.yaml b/api/config/services.yaml index 23dbc7a2f5..1789f52c39 100644 --- a/api/config/services.yaml +++ b/api/config/services.yaml @@ -4,6 +4,7 @@ # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: + app.httpCache.maxHeaderSize: 31500 # lower than Vanish default http_resp_size (32k) services: # default configuration for services in *this* file @@ -169,6 +170,18 @@ services: tags: - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: 12 } + App\HttpCache\CacheControlListener: + arguments: + $apiCacheEnabled: '%env(API_CACHE_ENABLED)%' + $maxHeaderSize: '%app.httpCache.maxHeaderSize%' + $headerKey: '%fos_http_cache.tag_handler.response_header%' + tags: + - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse, priority: -1 } # priority -1, executed after FOSHttpCacheBundle Listeners + + App\HttpCache\VarnishProxyClient: + arguments: + $apiCacheEnabled: '%env(API_CACHE_ENABLED)%' + $varnishApiUrl: '%env(VARNISH_API_URL)%' # Entity Filter App\Doctrine\FilterByCurrentUserExtension: diff --git a/api/src/HttpCache/CacheControlListener.php b/api/src/HttpCache/CacheControlListener.php new file mode 100644 index 0000000000..494c533494 --- /dev/null +++ b/api/src/HttpCache/CacheControlListener.php @@ -0,0 +1,37 @@ +apiCacheEnabled = filter_var($apiCacheEnabled, FILTER_VALIDATE_BOOLEAN); + $this->maxHeaderSize = intval($maxHeaderSize); + } + + public function onKernelResponse(ResponseEvent $event): void { + $response = $event->getResponse(); + $headerSize = strlen($response->headers->__toString()); + + if (!$this->apiCacheEnabled || $headerSize > $this->maxHeaderSize) { + $response->headers->remove('cache-control'); + $response->setCache(['no_cache' => true, 'private' => true]); + + $response->headers->remove($this->headerKey); + + return; + } + } +} diff --git a/api/src/HttpCache/VarnishProxyClient.php b/api/src/HttpCache/VarnishProxyClient.php new file mode 100644 index 0000000000..7eaf9b25c3 --- /dev/null +++ b/api/src/HttpCache/VarnishProxyClient.php @@ -0,0 +1,82 @@ +apiCacheEnabled = filter_var($apiCacheEnabled, FILTER_VALIDATE_BOOLEAN); + + if ($this->isCacheEnabled()) { + parent::__construct( + new HttpDispatcher([$varnishApiUrl]), + [ + 'tag_mode' => 'purgekeys', + 'tags_header' => 'xkey-purge', + ] + ); + } + } + + public function ban(array $headers) { + if ($this->isCacheEnabled()) { + return parent::ban($headers); + } + + return $this; + } + + public function banPath($path, $contentType = null, $hosts = null) { + if ($this->isCacheEnabled()) { + return parent::banPath($path, $contentType, $hosts); + } + + return $this; + } + + public function invalidateTags(array $tags) { + if ($this->isCacheEnabled()) { + return parent::invalidateTags($tags); + } + + return $this; + } + + public function purge($url, array $headers = []) { + if ($this->isCacheEnabled()) { + return parent::purge($url, $headers); + } + + return $this; + } + + public function refresh($url, array $headers = []) { + if ($this->isCacheEnabled()) { + return parent::refresh(${$url}, $headers); + } + + return $this; + } + + public function flush() { + if ($this->isCacheEnabled()) { + return parent::flush(); + } + + return 0; + } + + private function isCacheEnabled() { + return $this->apiCacheEnabled && '' !== $this->varnishApiUrl; + } +} From 2a63d18982440500499c5c2d6601a69719ca6ea5 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 14 Jan 2024 14:12:00 +0100 Subject: [PATCH 36/81] disable caching for requests with query params --- api/docker/varnish/vcl/default.vcl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/api/docker/varnish/vcl/default.vcl b/api/docker/varnish/vcl/default.vcl index 53431544d7..15df5b7472 100644 --- a/api/docker/varnish/vcl/default.vcl +++ b/api/docker/varnish/vcl/default.vcl @@ -41,6 +41,11 @@ sub vcl_recv { return(pass); } + # exclude any request with query parameters, until cache handling of query params is properly implemented + if (req.url ~ "\?"){ + return(pass); + } + # Extract JWT cookie for later use in vcl_hash # Failsafe: Pass cache if JWT cookie is not set (also for example, if COOKIE_PREFIX is not properly configured) if (req.http.Cookie) { From a806c737ab519e85971070def384eeab0bb6b42c Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 14 Jan 2024 14:21:12 +0100 Subject: [PATCH 37/81] allow enabling/disabling caching in deploymet (default:disabled) --- .github/workflows/reusable-dev-deployment.yml | 3 ++- .github/workflows/reusable-stage-prod-deployment.yml | 1 + .helm/.env-example | 2 ++ .helm/deploy-to-cluster.sh | 1 + .helm/ecamp3/templates/api_configmap.yaml | 3 +++ .helm/ecamp3/values.yaml | 2 +- 6 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/reusable-dev-deployment.yml b/.github/workflows/reusable-dev-deployment.yml index aee99e9045..e3d57cbe43 100644 --- a/.github/workflows/reusable-dev-deployment.yml +++ b/.github/workflows/reusable-dev-deployment.yml @@ -60,7 +60,7 @@ jobs: | Latest commit | [${{ inputs.sha }}](https://github.com/${{ github.repository }}/commit/${{ inputs.sha }}) | | Latest deploy log | [${{ steps.job-url.outputs.html_url }}](${{ steps.job-url.outputs.html_url }}) | comment_tag: feature-branch-deployment-status - + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 with: ref: ${{ inputs.sha }} @@ -91,6 +91,7 @@ jobs: --set ingress.basicAuth.enabled=${{ vars.BASIC_AUTH_ENABLED || false }} \ --set ingress.basicAuth.username=${{ secrets.BASIC_AUTH_USERNAME }} \ --set ingress.basicAuth.password='${{ secrets.BASIC_AUTH_PASSWORD }}' \ + --set apiCache.enabled=${{ vars.API_CACHE_ENABLED || false }} \ --set mail.dummyEnabled=true \ --set postgresql.url='${{ secrets.POSTGRES_URL }}/ecamp3${{ inputs.name }}?sslmode=require' \ --set postgresql.adminUrl='${{ secrets.POSTGRES_ADMIN_URL }}/ecamp3${{ inputs.name }}?sslmode=require' \ diff --git a/.github/workflows/reusable-stage-prod-deployment.yml b/.github/workflows/reusable-stage-prod-deployment.yml index c1de8a81b1..1ccc6fbd3f 100644 --- a/.github/workflows/reusable-stage-prod-deployment.yml +++ b/.github/workflows/reusable-stage-prod-deployment.yml @@ -50,6 +50,7 @@ jobs: --set ingress.basicAuth.enabled=${{ vars.BASIC_AUTH_ENABLED || false }} \ --set ingress.basicAuth.username=${{ secrets.BASIC_AUTH_USERNAME }} \ --set ingress.basicAuth.password='${{ secrets.BASIC_AUTH_PASSWORD }}' \ + --set apiCache.enabled=${{ vars.API_CACHE_ENABLED || false }} \ --set mail.dsn=${{ secrets.MAILER_DSN }} \ --set postgresql.url='${{ secrets.POSTGRES_URL }}/${{ secrets.DB_NAME }}?sslmode=require' \ --set postgresql.dropDBOnUninstall=false \ diff --git a/.helm/.env-example b/.helm/.env-example index 0d8077eee5..31e831ba5b 100644 --- a/.helm/.env-example +++ b/.helm/.env-example @@ -5,6 +5,8 @@ domain=ecamp3.ch POSTGRES_URL= POSTGRES_ADMIN_URL= +API_CACHE_ENABLED=false + BASIC_AUTH_ENABLED=false BASIC_AUTH_USERNAME=test BASIC_AUTH_PASSWORD=test diff --git a/.helm/deploy-to-cluster.sh b/.helm/deploy-to-cluster.sh index 13f571f63d..b0fc81598c 100755 --- a/.helm/deploy-to-cluster.sh +++ b/.helm/deploy-to-cluster.sh @@ -40,6 +40,7 @@ for i in 1; do values="$values --set ingress.basicAuth.enabled=$BASIC_AUTH_ENABLED" values="$values --set ingress.basicAuth.username=$BASIC_AUTH_USERNAME" values="$values --set ingress.basicAuth.password=$BASIC_AUTH_PASSWORD" + values="$values --set apiCache.enabled=$API_CACHE_ENABLED" values="$values --set postgresql.enabled=false" values="$values --set postgresql.url=$POSTGRES_URL/ecamp3$instance_name-"$i"?sslmode=require" values="$values --set postgresql.adminUrl=$POSTGRES_ADMIN_URL/ecamp3$instance_name-"$i"?sslmode=require" diff --git a/.helm/ecamp3/templates/api_configmap.yaml b/.helm/ecamp3/templates/api_configmap.yaml index 937cd95fad..ef96a5d5c3 100644 --- a/.helm/ecamp3/templates/api_configmap.yaml +++ b/.helm/ecamp3/templates/api_configmap.yaml @@ -22,6 +22,9 @@ data: SENTRY_API_DSN: {{ "" | quote }} {{- end }} FRONTEND_BASE_URL: {{ include "frontend.url" . | quote }} + API_CACHE_ENABLED: {{ .Values.apiCache.enabled | quote }} {{- if .Values.apiCache.enabled }} VARNISH_API_URL: {{ printf "%s:%d" (include "apiCache.name" .) (.Values.apiCache.service.ports.purge | int) | quote }} + {{- else }} + VARNISH_API_URL: {{ "" | quote }} {{- end}} diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index 543adf54b6..28e4e44284 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -220,7 +220,7 @@ ingress: tls: apiCache: - enabled: true + enabled: false image: repository: "docker.io/library/varnish" pullPolicy: IfNotPresent From 4fa3b28a1d21dddfe9e4d4819cffaebf73e3ece5 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 14 Jan 2024 14:29:15 +0100 Subject: [PATCH 38/81] enable cache again (for the purpose of testing this PR) --- .helm/ecamp3/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index 28e4e44284..543adf54b6 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -220,7 +220,7 @@ ingress: tls: apiCache: - enabled: false + enabled: true image: repository: "docker.io/library/varnish" pullPolicy: IfNotPresent From 64c651c9044a19b2a65a9e78bd0e97db0c935eb5 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 23 Jan 2024 14:46:02 +0100 Subject: [PATCH 39/81] upgrade to http-cache-bundle/3.x-dev --- api/composer.json | 11 +- api/composer.lock | 332 +++++++++++++++++++++++----------------------- 2 files changed, 175 insertions(+), 168 deletions(-) diff --git a/api/composer.json b/api/composer.json index 51ac28e98d..eeed29b35c 100644 --- a/api/composer.json +++ b/api/composer.json @@ -12,7 +12,8 @@ "doctrine/doctrine-migrations-bundle": "3.3.0", "doctrine/orm": "2.17.2", "exercise/htmlpurifier-bundle": "5.0", - "friendsofsymfony/http-cache-bundle": "2.16.2", + "friendsofsymfony/http-cache": "^3.x-dev", + "friendsofsymfony/http-cache-bundle": "dev-remove-sf-bundle", "google/recaptcha": "1.3.0", "guzzlehttp/guzzle": "7.8.1", "knpuniversity/oauth2-client-bundle": "2.18.0", @@ -147,5 +148,11 @@ "Allow NULL-Links": "patch/api-plattform-allow-null-links.patch" } } - } + }, + "repositories": [ + { + "type": "git", + "url": "https://github.com/toxicity1985/FOSHttpCacheBundle" + } + ] } diff --git a/api/composer.lock b/api/composer.lock index 46b4321e83..4952c0470c 100644 --- a/api/composer.lock +++ b/api/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": "87c092085b5831e1255c6244205d5f7f", + "content-hash": "28f4f70e6eac8c6957332c7ee4e58c7e", "packages": [ { "name": "api-platform/core", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/api-platform/core.git", - "reference": "28ff0ac4c960e3f5442b8b173507c092c1ce7019" + "reference": "2e9b9ff7bbe6cb3ccb063e5c79de1f3121803c22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/28ff0ac4c960e3f5442b8b173507c092c1ce7019", - "reference": "28ff0ac4c960e3f5442b8b173507c092c1ce7019", + "url": "https://api.github.com/repos/api-platform/core/zipball/2e9b9ff7bbe6cb3ccb063e5c79de1f3121803c22", + "reference": "2e9b9ff7bbe6cb3ccb063e5c79de1f3121803c22", "shasum": "" }, "require": { @@ -26,13 +26,13 @@ "psr/cache": "^1.0 || ^2.0 || ^3.0", "psr/container": "^1.0 || ^2.0", "symfony/deprecation-contracts": "^3.1", - "symfony/http-foundation": "^6.1 || ^7.0", - "symfony/http-kernel": "^6.1 || ^7.0", - "symfony/property-access": "^6.1 || ^7.0", - "symfony/property-info": "^6.1 || ^7.0", - "symfony/serializer": "^6.1 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0", + "symfony/property-access": "^6.4 || ^7.0", + "symfony/property-info": "^6.4 || ^7.0", + "symfony/serializer": "^6.4 || ^7.0", "symfony/translation-contracts": "^3.3", - "symfony/web-link": "^6.1 || ^7.0", + "symfony/web-link": "^6.4 || ^7.0", "willdurand/negotiation": "^3.0" }, "conflict": { @@ -54,6 +54,7 @@ "doctrine/dbal": "^3.4.0", "doctrine/doctrine-bundle": "^1.12 || ^2.0", "doctrine/mongodb-odm": "^2.2", + "doctrine/mongodb-odm-bundle": "^4.0 || ^5.0", "doctrine/orm": "^2.14", "elasticsearch/elasticsearch": "^7.11 || ^8.4", "friends-of-behat/mink-browserkit-driver": "^1.3.1", @@ -65,47 +66,47 @@ "phpspec/prophecy-phpunit": "^2.0", "phpstan/extension-installer": "^1.1", "phpstan/phpdoc-parser": "^1.13", - "phpstan/phpstan": "^1.1", + "phpstan/phpstan": "^1.10", "phpstan/phpstan-doctrine": "^1.0", "phpstan/phpstan-phpunit": "^1.0", "phpstan/phpstan-symfony": "^1.0", - "phpunit/phpunit": "^9.5", + "phpunit/phpunit": "^9.6", "psr/log": "^1.0 || ^2.0 || ^3.0", "ramsey/uuid": "^3.9.7 || ^4.0", "ramsey/uuid-doctrine": "^1.4 || ^2.0", "sebastian/comparator": "<5.0", "soyuka/contexts": "v3.3.9", "soyuka/stubs-mongodb": "^1.0", - "symfony/asset": "^6.1 || ^7.0", - "symfony/browser-kit": "^6.1 || ^7.0", - "symfony/cache": "^6.1 || ^7.0", - "symfony/config": "^6.1 || ^7.0", - "symfony/console": "^6.1 || ^7.0", - "symfony/css-selector": "^6.1 || ^7.0", - "symfony/dependency-injection": "^6.1 || ^7.0.12", - "symfony/doctrine-bridge": "^6.1 || ^7.0", - "symfony/dom-crawler": "^6.1 || ^7.0", - "symfony/error-handler": "^6.1 || ^7.0", - "symfony/event-dispatcher": "^6.1 || ^7.0", - "symfony/expression-language": "^6.1 || ^7.0", - "symfony/finder": "^6.1 || ^7.0", - "symfony/form": "^6.1 || ^7.0", - "symfony/framework-bundle": "^6.1 || ^7.0", - "symfony/http-client": "^6.1 || ^7.0", - "symfony/intl": "^6.1 || ^7.0", + "symfony/asset": "^6.4 || ^7.0", + "symfony/browser-kit": "^6.4 || ^7.0", + "symfony/cache": "^6.4 || ^7.0", + "symfony/config": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/css-selector": "^6.4 || ^7.0", + "symfony/dependency-injection": "^6.4 || ^7.0.12", + "symfony/doctrine-bridge": "^6.4 || ^7.0", + "symfony/dom-crawler": "^6.4 || ^7.0", + "symfony/error-handler": "^6.4 || ^7.0", + "symfony/event-dispatcher": "^6.4 || ^7.0", + "symfony/expression-language": "^6.4 || ^7.0", + "symfony/finder": "^6.4 || ^7.0", + "symfony/form": "^6.4 || ^7.0", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/http-client": "^6.4 || ^7.0", + "symfony/intl": "^6.4 || ^7.0", "symfony/maker-bundle": "^1.24", "symfony/mercure-bundle": "*", - "symfony/messenger": "^6.1 || ^7.0", - "symfony/phpunit-bridge": "^6.1 || ^7.0", - "symfony/routing": "^6.1 || ^7.0", - "symfony/security-bundle": "^6.1 || ^7.0", - "symfony/security-core": "^6.1 || ^7.0", - "symfony/stopwatch": "^6.1 || ^7.0", - "symfony/twig-bundle": "^6.1 || ^7.0", - "symfony/uid": "^6.1 || ^7.0", - "symfony/validator": "^6.1 || ^7.0", - "symfony/web-profiler-bundle": "^6.1 || ^7.0", - "symfony/yaml": "^6.1 || ^7.0", + "symfony/messenger": "^6.4 || ^7.0", + "symfony/phpunit-bridge": "^6.4 || ^7.0", + "symfony/routing": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/security-core": "^6.4 || ^7.0", + "symfony/stopwatch": "^6.4 || ^7.0", + "symfony/twig-bundle": "^6.4 || ^7.0", + "symfony/uid": "^6.4 || ^7.0", + "symfony/validator": "^6.4 || ^7.0", + "symfony/web-profiler-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", "twig/twig": "^1.42.3 || ^2.12 || ^3.0", "webonyx/graphql-php": "^14.0 || ^15.0" }, @@ -134,7 +135,7 @@ "dev-main": "3.3.x-dev" }, "symfony": { - "require": "^6.1 || ^7.0" + "require": "^6.4 || ^7.0" } }, "autoload": { @@ -168,9 +169,9 @@ ], "support": { "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/main" + "source": "https://github.com/api-platform/core/tree/v3.3.0-alpha.1" }, - "time": "2024-01-05T08:45:32+00:00" + "time": "2024-01-19T19:26:10+00:00" }, { "name": "behat/transliterator", @@ -811,16 +812,16 @@ }, { "name": "doctrine/dbal", - "version": "3.7.2", + "version": "3.7.3", "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "0ac3c270590e54910715e9a1a044cc368df282b2" + "reference": "ce594cbc39a4866c544f1a970d285ff0548221ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/0ac3c270590e54910715e9a1a044cc368df282b2", - "reference": "0ac3c270590e54910715e9a1a044cc368df282b2", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/ce594cbc39a4866c544f1a970d285ff0548221ad", + "reference": "ce594cbc39a4866c544f1a970d285ff0548221ad", "shasum": "" }, "require": { @@ -836,14 +837,14 @@ "doctrine/coding-standard": "12.0.0", "fig/log-test": "^1", "jetbrains/phpstorm-stubs": "2023.1", - "phpstan/phpstan": "1.10.42", + "phpstan/phpstan": "1.10.56", "phpstan/phpstan-strict-rules": "^1.5", - "phpunit/phpunit": "9.6.13", + "phpunit/phpunit": "9.6.15", "psalm/plugin-phpunit": "0.18.4", "slevomat/coding-standard": "8.13.1", - "squizlabs/php_codesniffer": "3.7.2", - "symfony/cache": "^5.4|^6.0", - "symfony/console": "^4.4|^5.4|^6.0", + "squizlabs/php_codesniffer": "3.8.1", + "symfony/cache": "^5.4|^6.0|^7.0", + "symfony/console": "^4.4|^5.4|^6.0|^7.0", "vimeo/psalm": "4.30.0" }, "suggest": { @@ -904,7 +905,7 @@ ], "support": { "issues": "https://github.com/doctrine/dbal/issues", - "source": "https://github.com/doctrine/dbal/tree/3.7.2" + "source": "https://github.com/doctrine/dbal/tree/3.7.3" }, "funding": [ { @@ -920,7 +921,7 @@ "type": "tidelift" } ], - "time": "2023-11-19T08:06:58+00:00" + "time": "2024-01-21T07:53:09+00:00" }, { "name": "doctrine/deprecations", @@ -1272,16 +1273,16 @@ }, { "name": "doctrine/inflector", - "version": "2.0.8", + "version": "2.0.9", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff" + "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/f9301a5b2fb1216b2b08f02ba04dc45423db6bff", - "reference": "f9301a5b2fb1216b2b08f02ba04dc45423db6bff", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/2930cd5ef353871c821d5c43ed030d39ac8cfe65", + "reference": "2930cd5ef353871c821d5c43ed030d39ac8cfe65", "shasum": "" }, "require": { @@ -1343,7 +1344,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.8" + "source": "https://github.com/doctrine/inflector/tree/2.0.9" }, "funding": [ { @@ -1359,7 +1360,7 @@ "type": "tidelift" } ], - "time": "2023-06-16T13:40:37+00:00" + "time": "2024-01-15T18:05:13+00:00" }, { "name": "doctrine/instantiator", @@ -2053,44 +2054,45 @@ }, { "name": "friendsofsymfony/http-cache", - "version": "2.15.3", + "version": "3.x-dev", "source": { "type": "git", "url": "https://github.com/FriendsOfSymfony/FOSHttpCache.git", - "reference": "39f6b356fb253f95067b3c29139949ddbcae3934" + "reference": "e446b18c2fe80484684c67ebd1848ec726c8a4ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCache/zipball/39f6b356fb253f95067b3c29139949ddbcae3934", - "reference": "39f6b356fb253f95067b3c29139949ddbcae3934", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCache/zipball/e446b18c2fe80484684c67ebd1848ec726c8a4ad", + "reference": "e446b18c2fe80484684c67ebd1848ec726c8a4ad", "shasum": "" }, "require": { - "php": "^7.2 || ^8.0", + "php": "^8.1", "php-http/client-common": "^1.1.0 || ^2.0", "php-http/client-implementation": "^1.0 || ^2.0", "php-http/discovery": "^1.12", "php-http/message": "^1.0 || ^2.0", "php-http/message-factory": "^1.0", - "symfony/event-dispatcher": "^4.3 || ^5.0 || ^6.0", - "symfony/options-resolver": "^4.3 || ^5.0 || ^6.0" + "symfony/event-dispatcher": "^6.4 || ^7.0", + "symfony/options-resolver": "^6.4 || ^7.0" }, "conflict": { "toflar/psr6-symfony-http-cache-store": "<2.2.1" }, "require-dev": { - "mockery/mockery": "^1.3.1", + "mockery/mockery": "^1.6.0", "monolog/monolog": "^1.0", "php-http/guzzle7-adapter": "^0.1.1", - "php-http/mock-client": "^1.2", - "symfony/http-kernel": "^4.3 || ^5.0 || ^6.0", - "symfony/phpunit-bridge": "^5.0 || ^6.0", - "symfony/process": "^4.3 || ^5.0 || ^6.0" + "php-http/mock-client": "^1.6.0", + "phpunit/phpunit": "^9", + "symfony/http-kernel": "^6.4|| ^7.0", + "symfony/process": "^6.4|| ^7.0" }, "suggest": { "friendsofsymfony/http-cache-bundle": "For integration with the Symfony framework", "monolog/monolog": "For logging issues while invalidating" }, + "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -2128,30 +2130,24 @@ ], "support": { "issues": "https://github.com/FriendsOfSymfony/FOSHttpCache/issues", - "source": "https://github.com/FriendsOfSymfony/FOSHttpCache/tree/2.15.3" + "source": "https://github.com/FriendsOfSymfony/FOSHttpCache/tree/3.x" }, - "time": "2023-12-11T10:07:11+00:00" + "time": "2024-01-22T10:58:38+00:00" }, { "name": "friendsofsymfony/http-cache-bundle", - "version": "2.16.2", + "version": "dev-remove-sf-bundle", "source": { "type": "git", - "url": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle.git", - "reference": "4308b5776d3e1c16cb8257429e55395e272ee0bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCacheBundle/zipball/4308b5776d3e1c16cb8257429e55395e272ee0bb", - "reference": "4308b5776d3e1c16cb8257429e55395e272ee0bb", - "shasum": "" + "url": "https://github.com/toxicity1985/FOSHttpCacheBundle", + "reference": "4108dc1cc20ff25d79c84e07137809b27f2e5f3b" }, "require": { - "friendsofsymfony/http-cache": "^2.15", - "php": "^7.3 || ^8.0", - "symfony/framework-bundle": "^4.4.0 || ^5.0 || ^6.0", - "symfony/http-foundation": "^4.4.0 || ^5.0 || ^6.0", - "symfony/http-kernel": "^4.4.0 || ^5.0 || ^6.0" + "friendsofsymfony/http-cache": "^3.x-dev", + "php": "^8.1", + "symfony/framework-bundle": "^6.4 || ^7.0", + "symfony/http-foundation": "^6.4 || ^7.0", + "symfony/http-kernel": "^6.4 || ^7.0" }, "conflict": { "symfony/monolog-bridge": "<3.4.4", @@ -2160,27 +2156,26 @@ "require-dev": { "doctrine/annotations": "^1.11", "guzzlehttp/guzzle": "^7.2", - "matthiasnoback/symfony-dependency-injection-test": "^4.0", + "matthiasnoback/symfony-config-test": "^4.3.0 || ^5.1", + "matthiasnoback/symfony-dependency-injection-test": "^4.3.1 || ^5.0", "mockery/mockery": "^1.3.2", "monolog/monolog": "*", "php-http/discovery": "^1.13", "php-http/guzzle7-adapter": "^0.1.1", "php-http/httplug": "^2.2.0", "php-http/message": "^1.0 || ^2.0", - "sebastian/exporter": "^2.0", - "sensio/framework-extra-bundle": "^4.0 || ^5.5.1 || ^6.0", - "symfony/browser-kit": "^4.4 || ^5.0 || ^6.0", - "symfony/console": "^4.4 || ^5.0 || ^6.0", - "symfony/css-selector": "^4.4 || ^5.0 || ^6.0", - "symfony/expression-language": "^4.4 || ^5.0 || ^6.0", - "symfony/finder": "^4.4 || ^5.0 || ^6.0", + "phpunit/phpunit": "^9.6.15", + "symfony/browser-kit": "^6.4 || ^7.0", + "symfony/console": "^6.4 || ^7.0", + "symfony/css-selector": "^6.4 || ^7.0", + "symfony/expression-language": "^6.4 || ^7.0", + "symfony/finder": "^6.4 || ^7.0", "symfony/monolog-bundle": "^3.0", - "symfony/phpunit-bridge": "v5.3.7", - "symfony/routing": "^4.4 || ^5.0 || ^6.0", - "symfony/security-bundle": "^4.4 || ^5.0 || ^6.0", - "symfony/twig-bundle": "^4.4 || ^5.0 || ^6.0", - "symfony/yaml": "^4.4 || ^5.0 || ^6.0", - "twig/twig": "^2.13" + "symfony/routing": "^6.4 || ^7.0", + "symfony/security-bundle": "^6.4 || ^7.0", + "symfony/twig-bundle": "^6.4 || ^7.0", + "symfony/yaml": "^6.4 || ^7.0", + "twig/twig": "^v3.8" }, "suggest": { "jean-beru/fos-http-cache-cloudfront": "To use CloudFront proxy", @@ -2194,7 +2189,14 @@ "FOS\\HttpCacheBundle\\": "src/" } }, - "notification-url": "https://packagist.org/downloads/", + "autoload-dev": { + "psr-4": { + "FOS\\HttpCacheBundle\\Tests\\": "tests/" + }, + "classmap": [ + "tests/Functional/Fixtures/app/AppKernel.php" + ] + }, "license": [ "MIT" ], @@ -2205,8 +2207,8 @@ }, { "name": "Driebit", - "email": "tech@driebit.nl", - "homepage": "http://www.driebit.nl" + "homepage": "http://www.driebit.nl", + "email": "tech@driebit.nl" }, { "name": "Community contributions", @@ -2223,11 +2225,7 @@ "purge", "varnish" ], - "support": { - "issues": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle/issues", - "source": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle/tree/2.16.2" - }, - "time": "2023-08-03T08:52:18+00:00" + "time": "2023-12-12T15:22:09+00:00" }, { "name": "gedmo/doctrine-extensions", @@ -3851,16 +3849,16 @@ }, { "name": "php-http/promise", - "version": "1.2.1", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/php-http/promise.git", - "reference": "44a67cb59f708f826f3bec35f22030b3edb90119" + "reference": "2916a606d3b390f4e9e8e2b8dd68581508be0f07" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/promise/zipball/44a67cb59f708f826f3bec35f22030b3edb90119", - "reference": "44a67cb59f708f826f3bec35f22030b3edb90119", + "url": "https://api.github.com/repos/php-http/promise/zipball/2916a606d3b390f4e9e8e2b8dd68581508be0f07", + "reference": "2916a606d3b390f4e9e8e2b8dd68581508be0f07", "shasum": "" }, "require": { @@ -3897,9 +3895,9 @@ ], "support": { "issues": "https://github.com/php-http/promise/issues", - "source": "https://github.com/php-http/promise/tree/1.2.1" + "source": "https://github.com/php-http/promise/tree/1.3.0" }, - "time": "2023-11-08T12:57:08+00:00" + "time": "2024-01-04T18:49:48+00:00" }, { "name": "phpdocumentor/reflection-common", @@ -4013,16 +4011,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.7.3", + "version": "1.8.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419" + "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", - "reference": "3219c6ee25c9ea71e3d9bbaf39c67c9ebd499419", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fad452781b3d774e3337b0c0b245dd8e5a4455fc", + "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc", "shasum": "" }, "require": { @@ -4065,9 +4063,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.7.3" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.0" }, - "time": "2023-08-12T11:01:26+00:00" + "time": "2024-01-11T11:49:22+00:00" }, { "name": "phplang/scope-exit", @@ -4206,16 +4204,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "1.24.5", + "version": "1.25.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc" + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fedf211ff14ec8381c9bf5714e33a7a552dd1acc", - "reference": "fedf211ff14ec8381c9bf5714e33a7a552dd1acc", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/bd84b629c8de41aa2ae82c067c955e06f1b00240", + "reference": "bd84b629c8de41aa2ae82c067c955e06f1b00240", "shasum": "" }, "require": { @@ -4247,9 +4245,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/1.24.5" + "source": "https://github.com/phpstan/phpdoc-parser/tree/1.25.0" }, - "time": "2023-12-16T09:33:33+00:00" + "time": "2024-01-04T17:06:16+00:00" }, { "name": "psr/cache", @@ -6398,24 +6396,24 @@ }, { "name": "symfony/event-dispatcher", - "version": "v6.4.2", + "version": "v7.0.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "e95216850555cd55e71b857eb9d6c2674124603a" + "reference": "098b62ae81fdd6cbf941f355059f617db28f4f9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/e95216850555cd55e71b857eb9d6c2674124603a", - "reference": "e95216850555cd55e71b857eb9d6c2674124603a", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/098b62ae81fdd6cbf941f355059f617db28f4f9a", + "reference": "098b62ae81fdd6cbf941f355059f617db28f4f9a", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/event-dispatcher-contracts": "^2.5|^3" }, "conflict": { - "symfony/dependency-injection": "<5.4", + "symfony/dependency-injection": "<6.4", "symfony/service-contracts": "<2.5" }, "provide": { @@ -6424,13 +6422,13 @@ }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-foundation": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^5.4|^6.0|^7.0" + "symfony/stopwatch": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -6458,7 +6456,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.2" + "source": "https://github.com/symfony/event-dispatcher/tree/v7.0.2" }, "funding": [ { @@ -6474,7 +6472,7 @@ "type": "tidelift" } ], - "time": "2023-12-27T22:16:42+00:00" + "time": "2023-12-27T22:24:19+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -7129,36 +7127,36 @@ }, { "name": "symfony/http-foundation", - "version": "v6.4.2", + "version": "v7.0.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "172d807f9ef3fc3fbed8377cc57c20d389269271" + "reference": "47d72323200934694def5d57083899d774a2b110" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/172d807f9ef3fc3fbed8377cc57c20d389269271", - "reference": "172d807f9ef3fc3fbed8377cc57c20d389269271", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/47d72323200934694def5d57083899d774a2b110", + "reference": "47d72323200934694def5d57083899d774a2b110", "shasum": "" }, "require": { - "php": ">=8.1", - "symfony/deprecation-contracts": "^2.5|^3", + "php": ">=8.2", "symfony/polyfill-mbstring": "~1.1", "symfony/polyfill-php83": "^1.27" }, "conflict": { - "symfony/cache": "<6.3" + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4" }, "require-dev": { - "doctrine/dbal": "^2.13.1|^3|^4", + "doctrine/dbal": "^3.6|^4", "predis/predis": "^1.1|^2.0", - "symfony/cache": "^6.3|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/expression-language": "^5.4|^6.0|^7.0", - "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", - "symfony/mime": "^5.4|^6.0|^7.0", - "symfony/rate-limiter": "^5.4|^6.0|^7.0" + "symfony/cache": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -7186,7 +7184,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v6.4.2" + "source": "https://github.com/symfony/http-foundation/tree/v7.0.0" }, "funding": [ { @@ -7202,7 +7200,7 @@ "type": "tidelift" } ], - "time": "2023-12-27T22:16:42+00:00" + "time": "2023-11-07T15:10:37+00:00" }, { "name": "symfony/http-kernel", @@ -7723,20 +7721,20 @@ }, { "name": "symfony/options-resolver", - "version": "v6.4.0", + "version": "v7.0.0", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e" + "reference": "700ff4096e346f54cb628ea650767c8130f1001f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/22301f0e7fdeaacc14318928612dee79be99860e", - "reference": "22301f0e7fdeaacc14318928612dee79be99860e", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/700ff4096e346f54cb628ea650767c8130f1001f", + "reference": "700ff4096e346f54cb628ea650767c8130f1001f", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3" }, "type": "library", @@ -7770,7 +7768,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v6.4.0" + "source": "https://github.com/symfony/options-resolver/tree/v7.0.0" }, "funding": [ { @@ -7786,7 +7784,7 @@ "type": "tidelift" } ], - "time": "2023-08-08T10:16:24+00:00" + "time": "2023-08-08T10:20:21+00:00" }, { "name": "symfony/password-hasher", @@ -14272,7 +14270,9 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "api-platform/core": 20 + "api-platform/core": 20, + "friendsofsymfony/http-cache": 20, + "friendsofsymfony/http-cache-bundle": 20 }, "prefer-stable": false, "prefer-lowest": false, From 562041d2a9769178e8fdca13c624bcda3aba3a54 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 23 Jan 2024 16:51:20 +0100 Subject: [PATCH 40/81] use use_symfony_listeners for backwards compatibility --- api/config/packages/api_platform.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/api/config/packages/api_platform.yaml b/api/config/packages/api_platform.yaml index 86cc9603be..31dc1951fd 100644 --- a/api/config/packages/api_platform.yaml +++ b/api/config/packages/api_platform.yaml @@ -2,6 +2,7 @@ api_platform: title: eCamp v3 version: 1.0.0 show_webby: false + use_symfony_listeners: true mapping: paths: - '%kernel.project_dir%/src/Entity' From 8c4aacaa460c96b88ee1b3c46b0ca98568e75771 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 23 Jan 2024 18:39:53 +0100 Subject: [PATCH 41/81] fix API tests (after upgrade to api-platform 3.3) --- api/src/State/ResetPasswordCreateProcessor.php | 8 ++++---- api/tests/Api/Invitations/AcceptInvitationTest.php | 10 ++++++++-- api/tests/Api/Invitations/RejectInvitationTest.php | 10 ++++++++-- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/api/src/State/ResetPasswordCreateProcessor.php b/api/src/State/ResetPasswordCreateProcessor.php index e7f43e7356..ffabb57e39 100644 --- a/api/src/State/ResetPasswordCreateProcessor.php +++ b/api/src/State/ResetPasswordCreateProcessor.php @@ -15,7 +15,7 @@ use Symfony\Component\PasswordHasher\PasswordHasherInterface; /** - * @implements ProcessorInterface + * @implements ProcessorInterface */ class ResetPasswordCreateProcessor implements ProcessorInterface { public function __construct( @@ -29,7 +29,7 @@ public function __construct( /** * @param ResetPassword $data */ - public function process($data, Operation $operation, array $uriVariables = [], array $context = []): ResetPassword { + public function process($data, Operation $operation, array $uriVariables = [], array $context = []): null { $resp = $this->reCaptcha->verify($data->recaptchaToken); if (!$resp->isSuccess()) { throw new HttpException(422, 'ReCaptcha failed'); @@ -38,7 +38,7 @@ public function process($data, Operation $operation, array $uriVariables = [], a $user = $this->userRepository->loadUserByIdentifier($data->email); if (null == $user) { - return $data; + return null; } $resetKey = IdGenerator::generateRandomHexString(64); @@ -49,7 +49,7 @@ public function process($data, Operation $operation, array $uriVariables = [], a $this->mailService->sendPasswordResetLink($user, $data); - return $data; + return null; } private function getResetKeyHasher(): PasswordHasherInterface { diff --git a/api/tests/Api/Invitations/AcceptInvitationTest.php b/api/tests/Api/Invitations/AcceptInvitationTest.php index f97858c56e..08f76f7962 100644 --- a/api/tests/Api/Invitations/AcceptInvitationTest.php +++ b/api/tests/Api/Invitations/AcceptInvitationTest.php @@ -182,7 +182,10 @@ public static function invalidMethods(): array { * @throws TransportExceptionInterface */ public function testNotFoundWhenInviteKeyDoesNotMatch() { - static::createClientWithCredentials()->request('PATCH', '/invitations/notExisting/'.Invitation::ACCEPT); + static::createClientWithCredentials()->request('PATCH', '/invitations/notExisting/'.Invitation::ACCEPT, [ + 'json' => [], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ]); $this->assertResponseStatusCodeSame(404); } @@ -193,7 +196,10 @@ public function testNotFoundWhenInviteKeyDoesNotMatch() { * @throws ClientExceptionInterface */ public function testNotFoundWhenNoInviteKey() { - static::createClientWithCredentials()->request('PATCH', '/invitations/'.Invitation::ACCEPT); + static::createClientWithCredentials()->request('PATCH', '/invitations/'.Invitation::ACCEPT, [ + 'json' => [], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ]); $this->assertResponseStatusCodeSame(404); } } diff --git a/api/tests/Api/Invitations/RejectInvitationTest.php b/api/tests/Api/Invitations/RejectInvitationTest.php index 53c465ef6a..f3c7d0c3d3 100644 --- a/api/tests/Api/Invitations/RejectInvitationTest.php +++ b/api/tests/Api/Invitations/RejectInvitationTest.php @@ -187,7 +187,10 @@ public static function invalidMethods(): array { * @throws TransportExceptionInterface */ public function testNotFoundWhenInviteKeyDoesNotMatch() { - static::createClientWithCredentials()->request('PATCH', '/invitations/notExisting/'.Invitation::REJECT); + static::createClientWithCredentials()->request('PATCH', '/invitations/notExisting/'.Invitation::REJECT, [ + 'json' => [], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ]); $this->assertResponseStatusCodeSame(404); } @@ -198,7 +201,10 @@ public function testNotFoundWhenInviteKeyDoesNotMatch() { * @throws ClientExceptionInterface */ public function testNotFoundWhenNoInviteKey() { - static::createClientWithCredentials()->request('PATCH', '/invitations/'.Invitation::REJECT); + static::createClientWithCredentials()->request('PATCH', '/invitations/'.Invitation::REJECT, [ + 'json' => [], + 'headers' => ['Content-Type' => 'application/merge-patch+json'], + ]); $this->assertResponseStatusCodeSame(404); } } From 9921354bfe813f0ab5b20178ba99dfcd411779b7 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 23 Jan 2024 19:13:41 +0100 Subject: [PATCH 42/81] fix e2e tests --- api/config/packages/http_cache.yaml | 1 + e2e/specs/httpCache.cy.js | 10 +++--- e2e/specs/nuxtPrint.cy.js | 55 +++++++++++++++-------------- print/print.env | 2 +- 4 files changed, 35 insertions(+), 33 deletions(-) diff --git a/api/config/packages/http_cache.yaml b/api/config/packages/http_cache.yaml index 2d3b348ee4..d9bcee0126 100644 --- a/api/config/packages/http_cache.yaml +++ b/api/config/packages/http_cache.yaml @@ -7,6 +7,7 @@ fos_http_cache: annotations: enabled: false max_header_value_length: 4096 + separator: ' ' cache_manager: custom_proxy_client: App\HttpCache\VarnishProxyClient cache_control: diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index 0ab6487024..c08174b281 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -74,12 +74,12 @@ describe('HTTP cache tests', () => { }) it('invalidates /camp/{campId}/categories for all users on category patch', () => { - const uri = '/api/camps/3c79b99ab424/categories' + const uri = '/api/camps/9c2447aefe38/categories' // bring data into defined state Cypress.session.clearAllSavedSessions() - cy.login('castor@example.com') - cy.apiPatch('/api/categories/ebfd46a1c181', { + cy.login('bruce@wayne.com') + cy.apiPatch('/api/categories/c5e1bc565094', { name: 'old_name', }) @@ -92,13 +92,13 @@ describe('HTTP cache tests', () => { cy.expectCacheHit(uri) // touch category - cy.apiPatch('/api/categories/ebfd46a1c181', { + cy.apiPatch('/api/categories/c5e1bc565094', { name: 'new_name', }) // ensure cache was invalidated cy.expectCacheMiss(uri) - cy.login('castor@example.com') + cy.login('bruce@wayne.com') cy.expectCacheMiss(uri) }) diff --git a/e2e/specs/nuxtPrint.cy.js b/e2e/specs/nuxtPrint.cy.js index 99f450b7bf..fac0bd2daf 100644 --- a/e2e/specs/nuxtPrint.cy.js +++ b/e2e/specs/nuxtPrint.cy.js @@ -6,34 +6,35 @@ describe('Nuxt print test', () => { it('shows print preview', () => { cy.login('test@example.com') - cy.request(Cypress.env('API_ROOT_URL') + '/camps.jsonhal').then((response) => { - const body = response.body - const campUri = body._links.items[1].href - const camp = body._embedded.items[1] - - let printConfig = { - language: 'en', - documentName: 'camp', - camp: campUri, - contents: [ - { - type: 'Cover', - options: {}, - }, - ], + cy.request(Cypress.env('API_ROOT_URL') + '/camps/3c79b99ab424.jsonhal').then( + (response) => { + const camp = response.body + const campUri = camp._links.self.href + + let printConfig = { + language: 'en', + documentName: 'camp', + camp: campUri, + contents: [ + { + type: 'Cover', + options: {}, + }, + ], + } + + cy.visit( + Cypress.env('PRINT_URL') + + '/?config=' + + encodeURIComponent(JSON.stringify(printConfig)) + ) + cy.contains(camp.name) + cy.contains(camp.title) + cy.contains(camp.motto) + + cy.get('#content_0_cover').should('have.css', 'font-size', '50px') // this ensures Tailwind is properly built and integrated } - - cy.visit( - Cypress.env('PRINT_URL') + - '/?config=' + - encodeURIComponent(JSON.stringify(printConfig)) - ) - cy.contains(camp.name) - cy.contains(camp.title) - cy.contains(camp.motto) - - cy.get('#content_0_cover').should('have.css', 'font-size', '50px') // this ensures Tailwind is properly built and integrated - }) + ) }) it('downloads PDF', () => { diff --git a/print/print.env b/print/print.env index f0785dda77..04e3244978 100644 --- a/print/print.env +++ b/print/print.env @@ -1,4 +1,4 @@ -NUXT_INTERNAL_API_ROOT_URL=http://http-cache:80/api +NUXT_INTERNAL_API_ROOT_URL=http://http-cache:8080/api NUXT_SENTRY_PRINT_DSN= NUXT_SENTRY_ENVIRONMENT=local NUXT_BROWSER_WS_ENDPOINT=ws://browserless:3000 From 7703e00507cb6b440cd063e2c9d84946948f21ef Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 23 Jan 2024 19:53:16 +0100 Subject: [PATCH 43/81] use new enable_link_security to protect subresources --- api/config/packages/api_platform.yaml | 1 + api/src/Entity/Category.php | 4 +-- .../Security/Voter/CampIsPrototypeVoter.php | 9 ++---- api/src/Security/Voter/CampRoleVoter.php | 9 ++---- .../Api/Categories/ListCategoriesTest.php | 29 ++++++++++++++++++ .../Voter/CampIsPrototypeVoterTest.php | 25 ---------------- .../Security/Voter/CampRoleVoterTest.php | 30 ------------------- 7 files changed, 36 insertions(+), 71 deletions(-) diff --git a/api/config/packages/api_platform.yaml b/api/config/packages/api_platform.yaml index 31dc1951fd..c63e72f3e8 100644 --- a/api/config/packages/api_platform.yaml +++ b/api/config/packages/api_platform.yaml @@ -3,6 +3,7 @@ api_platform: version: 1.0.0 show_webby: false use_symfony_listeners: true + enable_link_security: true mapping: paths: - '%kernel.project_dir%/src/Entity' diff --git a/api/src/Entity/Category.php b/api/src/Entity/Category.php index 1f192f72fe..99871f6722 100644 --- a/api/src/Entity/Category.php +++ b/api/src/Entity/Category.php @@ -56,13 +56,13 @@ securityPostDenormalize: 'is_granted("CAMP_MEMBER", object) or is_granted("CAMP_MANAGER", object)' ), new GetCollection( - security: 'is_granted("CAMP_COLLABORATOR", request) or is_granted("CAMP_IS_PROTOTYPE", request)', name: 'BelongsToCamp_App\Entity\Category_get_collection', uriTemplate: '/camps/{campId}/categories.{_format}', uriVariables: [ 'campId' => new Link( fromClass: Camp::class, - fromProperty: 'categories' + toProperty: 'camp', + security: 'is_granted("CAMP_COLLABORATOR", camp) or is_granted("CAMP_IS_PROTOTYPE", camp)' ), ], ), diff --git a/api/src/Security/Voter/CampIsPrototypeVoter.php b/api/src/Security/Voter/CampIsPrototypeVoter.php index ccec8eda92..29a15a0abc 100644 --- a/api/src/Security/Voter/CampIsPrototypeVoter.php +++ b/api/src/Security/Voter/CampIsPrototypeVoter.php @@ -8,7 +8,6 @@ use App\HttpCache\ResponseTagger; use App\Util\GetCampFromContentNodeTrait; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -25,15 +24,11 @@ public function __construct( protected function supports($attribute, $subject): bool { return 'CAMP_IS_PROTOTYPE' === $attribute - && ($subject instanceof BelongsToCampInterface || $subject instanceof BelongsToContentNodeTreeInterface || $subject instanceof Request); + && ($subject instanceof BelongsToCampInterface || $subject instanceof BelongsToContentNodeTreeInterface); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { - if ($subject instanceof Request) { - $camp = $this->em->getRepository(Camp::class)->find($subject->attributes->get('campId')); - } else { - $camp = $this->getCampFromInterface($subject, $this->em); - } + $camp = $this->getCampFromInterface($subject, $this->em); if (null === $camp) { // Allow access when camp is null. diff --git a/api/src/Security/Voter/CampRoleVoter.php b/api/src/Security/Voter/CampRoleVoter.php index 79062b0233..833015c2cd 100644 --- a/api/src/Security/Voter/CampRoleVoter.php +++ b/api/src/Security/Voter/CampRoleVoter.php @@ -10,7 +10,6 @@ use App\HttpCache\ResponseTagger; use App\Util\GetCampFromContentNodeTrait; use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\Voter; @@ -34,7 +33,7 @@ public function __construct( protected function supports($attribute, $subject): bool { return in_array($attribute, array_keys(self::RULE_MAPPING)) - && ($subject instanceof BelongsToCampInterface || $subject instanceof BelongsToContentNodeTreeInterface || $subject instanceof Request); + && ($subject instanceof BelongsToCampInterface || $subject instanceof BelongsToContentNodeTreeInterface); } protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool { @@ -43,11 +42,7 @@ protected function voteOnAttribute(string $attribute, mixed $subject, TokenInter return false; } - if ($subject instanceof Request) { - $camp = $this->em->getRepository(Camp::class)->find($subject->attributes->get('campId')); - } else { - $camp = $this->getCampFromInterface($subject, $this->em); - } + $camp = $this->getCampFromInterface($subject, $this->em); if (null === $camp) { // Allow access when camp is null. diff --git a/api/tests/Api/Categories/ListCategoriesTest.php b/api/tests/Api/Categories/ListCategoriesTest.php index a4cd979b39..5458b62cac 100644 --- a/api/tests/Api/Categories/ListCategoriesTest.php +++ b/api/tests/Api/Categories/ListCategoriesTest.php @@ -96,4 +96,33 @@ public function testListCategoriesFilteredByCampPrototypeIsAllowedForUnrelatedUs ['href' => $this->getIriFor('category1campPrototype')], ], $response->toArray()['_links']['items']); } + + public function testListCategoriesAsCampSubresourceIsAllowedForCollaborator() { + $camp = static::getFixture('camp1'); + $response = static::createClientWithCredentials()->request('GET', '/camps/'.$camp->getId().'/categories'); + $this->assertResponseStatusCodeSame(200); + $this->assertJsonContains([ + 'totalItems' => 3, + '_links' => [ + 'items' => [], + ], + '_embedded' => [ + 'items' => [], + ], + ]); + $this->assertEqualsCanonicalizing([ + ['href' => $this->getIriFor('category1')], + ['href' => $this->getIriFor('category2')], + ['href' => $this->getIriFor('categoryWithNoActivities')], + ], $response->toArray()['_links']['items']); + } + + public function testListCategoriesAsCampSubresourceIsDeniedForUnrelatedUser() { + $camp = static::getFixture('camp1'); + static::createClientWithCredentials(['email' => static::$fixtures['user4unrelated']->getEmail()]) + ->request('GET', '/camps/'.$camp->getId().'/categories') + ; + + $this->assertResponseStatusCodeSame(404); + } } diff --git a/api/tests/Security/Voter/CampIsPrototypeVoterTest.php b/api/tests/Security/Voter/CampIsPrototypeVoterTest.php index aa186a42f2..ac3e2c847f 100644 --- a/api/tests/Security/Voter/CampIsPrototypeVoterTest.php +++ b/api/tests/Security/Voter/CampIsPrototypeVoterTest.php @@ -14,8 +14,6 @@ use Doctrine\ORM\EntityRepository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\ParameterBag; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; @@ -143,29 +141,6 @@ public function testGrantsAccessViaBelongsToContentNodeTreeInterface() { // then $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); } - - public function testGrantsAccessViaRequestParameter() { - // given - $user = $this->createMock(User::class); - $user->method('getId')->willReturn('idFromTest'); - $this->token->method('getUser')->willReturn($user); - $camp = new Camp(); - $camp->isPrototype = true; - - $repository = $this->createMock(EntityRepository::class); - $this->em->method('getRepository')->willReturn($repository); - $repository->method('find')->willReturn($camp); - - $request = $this->createMock(Request::class); - $request->attributes = $this->createMock(ParameterBag::class); - $request->attributes->method('get')->with('campId')->willReturn('campId-123'); - - // when - $result = $this->voter->vote($this->token, $request, ['CAMP_IS_PROTOTYPE']); - - // then - $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); - } } class CampIsPrototypeVoterTestDummy extends BaseEntity {} diff --git a/api/tests/Security/Voter/CampRoleVoterTest.php b/api/tests/Security/Voter/CampRoleVoterTest.php index 82141045b5..9c779a5713 100644 --- a/api/tests/Security/Voter/CampRoleVoterTest.php +++ b/api/tests/Security/Voter/CampRoleVoterTest.php @@ -15,8 +15,6 @@ use Doctrine\ORM\EntityRepository; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Symfony\Component\HttpFoundation\ParameterBag; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; @@ -271,34 +269,6 @@ public function testGrantsAccessViaBelongsToContentNodeTreeInterface() { // then $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); } - - public function testGrantsAccessViaRequestParameter() { - // given - $user = $this->createMock(User::class); - $user->method('getId')->willReturn('idFromTest'); - $user2 = $this->createMock(User::class); - $user2->method('getId')->willReturn('idFromTest'); - $this->token->method('getUser')->willReturn($user); - $collaboration = new CampCollaboration(); - $collaboration->user = $user2; - $collaboration->status = CampCollaboration::STATUS_ESTABLISHED; - $collaboration->role = CampCollaboration::ROLE_MANAGER; - $camp = new Camp(); - $camp->collaborations->add($collaboration); - $repository = $this->createMock(EntityRepository::class); - $this->em->method('getRepository')->willReturn($repository); - $repository->method('find')->willReturn($camp); - - $request = $this->createMock(Request::class); - $request->attributes = $this->createMock(ParameterBag::class); - $request->attributes->method('get')->with('campId')->willReturn('campId-123'); - - // when - $result = $this->voter->vote($this->token, $request, ['CAMP_COLLABORATOR']); - - // then - $this->assertEquals(VoterInterface::ACCESS_GRANTED, $result); - } } class CampRoleVoterTestDummy extends BaseEntity {} From b68c6ba7b66ec8badf6293f1c5c028811b498ad5 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Wed, 24 Jan 2024 19:53:30 +0100 Subject: [PATCH 44/81] fix tests --- api/tests/State/ResetPasswordCreateProcessorTest.php | 4 +++- e2e/specs/httpCache.cy.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/api/tests/State/ResetPasswordCreateProcessorTest.php b/api/tests/State/ResetPasswordCreateProcessorTest.php index d63d26cc9c..bfc2e65d83 100644 --- a/api/tests/State/ResetPasswordCreateProcessorTest.php +++ b/api/tests/State/ResetPasswordCreateProcessorTest.php @@ -96,6 +96,8 @@ public function testCreateWithUnknownEmailDoesNotCreateResetKey() { $this->resetPassword->email = self::EMAIL; $data = $this->processor->process($this->resetPassword, new Post()); + + self::assertThat($data, self::isNull()); } public function testCreateWithKnowneMailCreatesResetKey() { @@ -124,6 +126,6 @@ public function testCreateWithKnowneMailCreatesResetKey() { $this->resetPassword->email = self::EMAIL; $data = $this->processor->process($this->resetPassword, new Post()); - self::assertThat($data->id, self::logicalNot(self::isNull())); + self::assertThat($data, self::isNull()); } } diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index c08174b281..0cb588c280 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -218,7 +218,7 @@ describe('HTTP cache tests', () => { url: Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal', failOnStatusCode: false, }).then((response) => { - expect(response.status).to.eq(403) + expect(response.status).to.eq(404) }) // delete old emails From c13290a57d4ab4a87cdb7b9951ef8ee0c087beae Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 30 Mar 2024 10:44:09 +0100 Subject: [PATCH 45/81] fix cs & psalm --- api/src/HttpCache/PurgeHttpCacheListener.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index 7b84ee67aa..39b0da0a04 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -110,7 +110,9 @@ private function gatherResourceTags(object $entity): void { foreach ($metadata->getOperations() ?? [] as $operation) { if ($operation instanceof GetCollection) { $iri = $this->iriConverter->getIriFromResource($entity, UrlGeneratorInterface::ABS_PATH, $operation); - $this->cacheManager->invalidateTags([$iri]); + if ($iri) { + $this->cacheManager->invalidateTags([$iri]); + } } } $resourceIterator->next(); @@ -142,7 +144,7 @@ private function gatherRelationTags(EntityManagerInterface $em, object $entity): } } - private function addTagsFor(mixed $value, string $property = null): void { + private function addTagsFor(mixed $value, ?string $property = null): void { if (!$value || \is_scalar($value)) { return; } @@ -162,7 +164,7 @@ private function addTagsFor(mixed $value, string $property = null): void { } } - private function addTagForItem(mixed $value, string $property = null): void { + private function addTagForItem(mixed $value, ?string $property = null): void { if (!$this->resourceClassResolver->isResourceClass($this->getObjectClass($value))) { return; } @@ -173,10 +175,12 @@ private function addTagForItem(mixed $value, string $property = null): void { } else { $iri = $this->iriConverter->getIriFromResource($value); } - if ($property) { + if ($iri && $property) { $iri .= self::IRI_RELATION_DELIMITER.$property; } - $this->cacheManager->invalidateTags([$iri]); + if ($iri) { + $this->cacheManager->invalidateTags([$iri]); + } } catch (InvalidArgumentException|RuntimeException) { } } From ed26dc7ae0c255fbd10d5118aba38390b99193c1 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 31 Mar 2024 09:16:33 +0200 Subject: [PATCH 46/81] fix e2e tests --- e2e/specs/httpCache.cy.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index 0cb588c280..f502331cb8 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -238,7 +238,9 @@ describe('HTTP cache tests', () => { // accept invitation as Castor cy.login('castor@example.com') cy.visit('localhost:3000/mail') - cy.get('a').contains('[eCamp3] Du wurdest ins Lager "Pfila 2023" eingeladen').click() + cy.get('a') + .contains('[eCamp v3] Du wurdest ins Lager "Pfila 2023" eingeladen') + .click() /* eslint-disable cypress/no-unnecessary-waiting */ cy.wait(200) getIframeBody() From 51922783b557ed85d452b1a1957d980477ca8f53 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 31 Mar 2024 11:05:11 +0200 Subject: [PATCH 47/81] deployment sidecar for logging & prometheus metrics --- .../templates/api_cache_deployment.yaml | 55 ++++++++++++++++++- .helm/ecamp3/templates/api_cache_service.yaml | 4 ++ .helm/ecamp3/values.yaml | 26 +++++++++ .ops/observability/prometheus-values-dev.yml | 10 ++++ 4 files changed, 94 insertions(+), 1 deletion(-) diff --git a/.helm/ecamp3/templates/api_cache_deployment.yaml b/.helm/ecamp3/templates/api_cache_deployment.yaml index 1aabe54a7f..936022dea7 100644 --- a/.helm/ecamp3/templates/api_cache_deployment.yaml +++ b/.helm/ecamp3/templates/api_cache_deployment.yaml @@ -15,6 +15,9 @@ spec: metadata: labels: {{- include "apiCache.selectorLabels" . | nindent 8 }} + annotations: + checksum/vclConfigmap: {{ include (print $.Template.BasePath "/api_cache_vcl_configmap.yaml") . | sha256sum }} + rollme: {{ .Values.imageTag | quote }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: @@ -25,7 +28,7 @@ spec: {{- toYaml .Values.podSecurityContext | nindent 8 }} enableServiceLinks: false containers: - - name: {{ .Chart.Name }}-api-cache + - name: {{ .Chart.Name }}-api-cache-varnishd securityContext: {{- toYaml .Values.securityContext | nindent 12 }} image: "{{ .Values.apiCache.image.repository }}:{{ .Values.apiCache.image.tag | default .Values.imageTag }}" @@ -54,8 +57,58 @@ spec: volumeMounts: - name: vcl-configmap mountPath: /etc/varnish + - name: vsm + mountPath: /var/lib/varnish + {{- if .Values.apiCache.logging.enabled }} + - name: {{ .Chart.Name }}-api-cache-varnishncsa + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.apiCache.image.repository }}:{{ .Values.apiCache.image.tag | default .Values.imageTag }}" + imagePullPolicy: {{ .Values.apiCache.image.pullPolicy }} + command: + - varnishncsa + {{- if .Values.apiCache.logging.customOutputJsonFormat }} + - -j + {{- end }} + {{- if .Values.apiCache.logging.customOutput }} + - -F + - {{ .Values.apiCache.logging.customOutput | quote }} + {{- end }} + - -t + - {{ .Values.apiCache.logging.timeout | quote }} + resources: + {{- toYaml .Values.apiCache.logging.resources | nindent 12 }} + volumeMounts: + - name: vsm + mountPath: /var/lib/varnish + {{- end }} + {{- if .Values.apiCache.prometheus.enabled }} + - name: {{ .Chart.Name }}-api-cache-prometheus-exporter + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.apiCache.prometheus.image.repository }}:{{ .Values.apiCache.prometheus.image.tag }}" + imagePullPolicy: {{ .Values.apiCache.prometheus.image.pullPolicy }} + ports: + - name: api-cache-metrics + containerPort: {{ .Values.apiCache.prometheus.port }} + protocol: TCP + resources: + {{- toYaml .Values.apiCache.prometheus.resources | nindent 12 }} + command: + - prometheus-varnish-exporter + - -web.telemetry-path + - "{{ .Values.apiCache.prometheus.path }}" + - -web.listen-address + - ":{{ .Values.apiCache.prometheus.port }}" + volumeMounts: + - name: vsm + mountPath: /var/lib/varnish + {{- end }} volumes: - name: vcl-configmap configMap: name: {{ include "apiCache.name" . }}-vcl-configmap + - name: vsm + emptyDir: + medium: Memory {{- end }} \ No newline at end of file diff --git a/.helm/ecamp3/templates/api_cache_service.yaml b/.helm/ecamp3/templates/api_cache_service.yaml index 497b55f4b0..924ed140d0 100644 --- a/.helm/ecamp3/templates/api_cache_service.yaml +++ b/.helm/ecamp3/templates/api_cache_service.yaml @@ -17,6 +17,10 @@ spec: targetPort: api-cache-purge protocol: TCP name: api-cache-purge + - port: {{ .Values.apiCache.prometheus.port }} + targetPort: api-cache-metrics + protocol: TCP + name: api-cache-metrics selector: {{- include "apiCache.selectorLabels" . | nindent 4 }} {{- end }} \ No newline at end of file diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index f25195f096..ac04b9fccb 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -240,6 +240,32 @@ apiCache: requests: cpu: 10m memory: 100Mi + logging: + enabled: true + customOutput: null + customOutputJsonFormat: false + # Timeout before returning error on initial VSM connection. + # If set the VSM connection is retried every 0.5 seconds for this many seconds. + # If zero the connection is attempted only once and will fail immediately if unsuccessful. + # If set to "off", the connection will not fail, allowing the utility to start and wait indefinetely for the Varnish instance to appear. + # Defaults to "off" in this case. + timeout: "off" + resources: + requests: + cpu: 10m + memory: 20Mi + prometheus: + enabled: true + image: + repository: softonic/varnish + tag: 7.2.1 + pullPolicy: IfNotPresent + path: "/metrics" + port: 9131 + resources: + requests: + cpu: 10m + memory: 20Mi autoscaling: enabled: false diff --git a/.ops/observability/prometheus-values-dev.yml b/.ops/observability/prometheus-values-dev.yml index e20b3f6a45..892f22954a 100644 --- a/.ops/observability/prometheus-values-dev.yml +++ b/.ops/observability/prometheus-values-dev.yml @@ -31,6 +31,16 @@ prometheus: - default endpoints: - port: "api-metrics" + - name: "varnish" + selector: + matchLabels: + app.kubernetes.io/instance: ecamp3-dev + app.kubernetes.io/name: ecamp3-api-cache + namespaceSelector: + matchNames: + - default + endpoints: + - port: "api-cache-metrics" prometheusSpec: storageSpec: volumeClaimTemplate: From ac6748884bf743d219a96b6b98528649422c4ccc Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 31 Mar 2024 11:18:25 +0200 Subject: [PATCH 48/81] fix deployment: reduce port name length --- .helm/ecamp3/templates/api_cache_deployment.yaml | 6 +++--- .helm/ecamp3/templates/api_cache_service.yaml | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.helm/ecamp3/templates/api_cache_deployment.yaml b/.helm/ecamp3/templates/api_cache_deployment.yaml index 936022dea7..8a39421866 100644 --- a/.helm/ecamp3/templates/api_cache_deployment.yaml +++ b/.helm/ecamp3/templates/api_cache_deployment.yaml @@ -34,10 +34,10 @@ spec: image: "{{ .Values.apiCache.image.repository }}:{{ .Values.apiCache.image.tag | default .Values.imageTag }}" imagePullPolicy: {{ .Values.apiCache.image.pullPolicy }} ports: - - name: api-cache-http + - name: http containerPort: {{ .Values.apiCache.varnishHttpPort }} protocol: TCP - - name: api-cache-purge + - name: purge containerPort: {{ .Values.apiCache.varnishPurgePort }} protocol: TCP env: @@ -89,7 +89,7 @@ spec: image: "{{ .Values.apiCache.prometheus.image.repository }}:{{ .Values.apiCache.prometheus.image.tag }}" imagePullPolicy: {{ .Values.apiCache.prometheus.image.pullPolicy }} ports: - - name: api-cache-metrics + - name: metrics containerPort: {{ .Values.apiCache.prometheus.port }} protocol: TCP resources: diff --git a/.helm/ecamp3/templates/api_cache_service.yaml b/.helm/ecamp3/templates/api_cache_service.yaml index 924ed140d0..fa9d92b0e5 100644 --- a/.helm/ecamp3/templates/api_cache_service.yaml +++ b/.helm/ecamp3/templates/api_cache_service.yaml @@ -10,17 +10,17 @@ spec: type: {{ .Values.apiCache.service.type }} ports: - port: {{ .Values.apiCache.service.ports.http }} - targetPort: api-cache-http + targetPort: http protocol: TCP - name: api-cache-http + name: http - port: {{ .Values.apiCache.service.ports.purge }} - targetPort: api-cache-purge + targetPort: purge protocol: TCP - name: api-cache-purge + name: purge - port: {{ .Values.apiCache.prometheus.port }} - targetPort: api-cache-metrics + targetPort: metrics protocol: TCP - name: api-cache-metrics + name: metrics selector: {{- include "apiCache.selectorLabels" . | nindent 4 }} {{- end }} \ No newline at end of file From aa6401a1e22bd4e36b9bd721a47dcdafcabc1682 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 1 Apr 2024 08:25:16 +0200 Subject: [PATCH 49/81] improve logging format and try fixing user permission --- .helm/ecamp3/templates/api_cache_deployment.yaml | 4 ++-- .helm/ecamp3/values.yaml | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.helm/ecamp3/templates/api_cache_deployment.yaml b/.helm/ecamp3/templates/api_cache_deployment.yaml index 8a39421866..daf9289f96 100644 --- a/.helm/ecamp3/templates/api_cache_deployment.yaml +++ b/.helm/ecamp3/templates/api_cache_deployment.yaml @@ -72,7 +72,7 @@ spec: {{- end }} {{- if .Values.apiCache.logging.customOutput }} - -F - - {{ .Values.apiCache.logging.customOutput | quote }} + - {{ .Values.apiCache.logging.customOutput | squote }} {{- end }} - -t - {{ .Values.apiCache.logging.timeout | quote }} @@ -85,7 +85,7 @@ spec: {{- if .Values.apiCache.prometheus.enabled }} - name: {{ .Chart.Name }}-api-cache-prometheus-exporter securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} + {{- toYaml .Values.apiCache.prometheus.securityContext | nindent 12 }} image: "{{ .Values.apiCache.prometheus.image.repository }}:{{ .Values.apiCache.prometheus.image.tag }}" imagePullPolicy: {{ .Values.apiCache.prometheus.image.pullPolicy }} ports: diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index ac04b9fccb..e1841b8a75 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -242,8 +242,8 @@ apiCache: memory: 100Mi logging: enabled: true - customOutput: null - customOutputJsonFormat: false + customOutput: '{ "received_at": "%t", "response_bytes": %b, "request_bytes": %I, "time_taken": %D, "first_line": "%r", "status": %s, "handling": "%{Varnish:handling}x" }' + customOutputJsonFormat: true # Timeout before returning error on initial VSM connection. # If set the VSM connection is retried every 0.5 seconds for this many seconds. # If zero the connection is attempted only once and will fail immediately if unsuccessful. @@ -266,6 +266,8 @@ apiCache: requests: cpu: 10m memory: 20Mi + securityContext: + runAsUser: 101 autoscaling: enabled: false From de2c216e2edb0ad6ea4f14bd6b3914e269465e4a Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 28 Apr 2024 16:29:23 +0200 Subject: [PATCH 50/81] use own Dockerfile for varnish --- .docker-hub/varnish/Dockerfile | 13 +++++++++++++ .github/workflows/reusable-build-and-push.yml | 12 ++++++++++++ .helm/build-images.sh | 4 ++++ .helm/deploy-to-cluster.sh | 2 ++ .helm/ecamp3/templates/api_cache_deployment.yaml | 6 +++--- .helm/ecamp3/values.yaml | 10 ++-------- docker-compose.yml | 2 +- 7 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 .docker-hub/varnish/Dockerfile diff --git a/.docker-hub/varnish/Dockerfile b/.docker-hub/varnish/Dockerfile new file mode 100644 index 0000000000..e69fd49102 --- /dev/null +++ b/.docker-hub/varnish/Dockerfile @@ -0,0 +1,13 @@ +ARG VERSION=7.5.0 + +FROM varnish:${VERSION} + +USER root + +RUN set -e; \ + apt-get update; \ + apt-get -y install prometheus-varnish-exporter; + +RUN rm -rf /var/lib/apt/lists/*; + +USER varnish \ No newline at end of file diff --git a/.github/workflows/reusable-build-and-push.yml b/.github/workflows/reusable-build-and-push.yml index 94fad2b106..a7a3a7fd31 100644 --- a/.github/workflows/reusable-build-and-push.yml +++ b/.github/workflows/reusable-build-and-push.yml @@ -95,6 +95,18 @@ jobs: cache-from: type=gha,scope=print cache-to: type=gha,scope=print,mode=max + - name: Build and push varnish docker image + uses: docker/build-push-action@v5 + with: + push: true + file: .docker-hub/varnish/Dockerfile + tags: | + ${{ ((inputs.tag != '') && format('{0}/ecamp3-varnish:{1}', vars.DOCKER_HUB_USERNAME, inputs.tag) || '') }} + ${{ vars.DOCKER_HUB_USERNAME }}/ecamp3-varnish:${{ inputs.sha }} + context: . + cache-from: type=gha,scope=print + cache-to: type=gha,scope=print,mode=max + - name: Build and push db-backup-restore docker image uses: docker/build-push-action@v5 with: diff --git a/.helm/build-images.sh b/.helm/build-images.sh index 6ff76fb40b..9ba604edd0 100755 --- a/.helm/build-images.sh +++ b/.helm/build-images.sh @@ -39,6 +39,10 @@ print_image_tag="${docker_hub_account}/ecamp3-print:${version}" docker build "$REPO_DIR" -f "$REPO_DIR"/.docker-hub/print/Dockerfile $print_sentry_build_args -t "$print_image_tag" docker push "$print_image_tag" +varnish_image_tag="${docker_hub_account}/ecamp3-varnish:${version}" +docker build "$REPO_DIR" -f "$REPO_DIR"/.docker-hub/varnish/Dockerfile -t "$varnish_image_tag" +docker push "$varnish_image_tag" + export REPO_OWNER=${docker_hub_account} export VERSION=${version} db_backup_restore_docker_compose_path="$REPO_DIR"/.helm/ecamp3/files/db-backup-restore-image/docker-compose.yml diff --git a/.helm/deploy-to-cluster.sh b/.helm/deploy-to-cluster.sh index 3ea1c7b60c..241d202e58 100755 --- a/.helm/deploy-to-cluster.sh +++ b/.helm/deploy-to-cluster.sh @@ -99,6 +99,8 @@ for i in 1; do values="$values --set $imagespec.image.repository=docker.io/${docker_hub_account}/ecamp3-api-$imagespec" done + values="$values --set apiCache.image.repository=docker.io/${docker_hub_account}/ecamp3-varnish" + values="$values --set postgresql.dbBackupRestoreImage.pullPolicy=$pull_policy" values="$values --set postgresql.dbBackupRestoreImage.repository=docker.io/${docker_hub_account}/ecamp3-db-backup-restore" diff --git a/.helm/ecamp3/templates/api_cache_deployment.yaml b/.helm/ecamp3/templates/api_cache_deployment.yaml index daf9289f96..79abc053d8 100644 --- a/.helm/ecamp3/templates/api_cache_deployment.yaml +++ b/.helm/ecamp3/templates/api_cache_deployment.yaml @@ -85,9 +85,9 @@ spec: {{- if .Values.apiCache.prometheus.enabled }} - name: {{ .Chart.Name }}-api-cache-prometheus-exporter securityContext: - {{- toYaml .Values.apiCache.prometheus.securityContext | nindent 12 }} - image: "{{ .Values.apiCache.prometheus.image.repository }}:{{ .Values.apiCache.prometheus.image.tag }}" - imagePullPolicy: {{ .Values.apiCache.prometheus.image.pullPolicy }} + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.apiCache.image.repository }}:{{ .Values.apiCache.image.tag | default .Values.imageTag }}" + imagePullPolicy: {{ .Values.apiCache.image.pullPolicy }} ports: - name: metrics containerPort: {{ .Values.apiCache.prometheus.port }} diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index 62b3768b22..73d19707e0 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -225,10 +225,10 @@ ingress: apiCache: enabled: true image: - repository: "docker.io/library/varnish" + repository: "docker.io/ecamp/ecamp3-varnish" pullPolicy: IfNotPresent # Overrides the image tag whose shared default is .Values.imageTag - tag: "7.4.2-alpine" + tag: service: type: ClusterIP ports: @@ -257,18 +257,12 @@ apiCache: memory: 20Mi prometheus: enabled: true - image: - repository: softonic/varnish - tag: 7.2.1 - pullPolicy: IfNotPresent path: "/metrics" port: 9131 resources: requests: cpu: 10m memory: 20Mi - securityContext: - runAsUser: 101 autoscaling: enabled: false diff --git a/docker-compose.yml b/docker-compose.yml index 712ad001c8..7b3e78fa12 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -94,7 +94,7 @@ services: - ./api/public:/srv/api/public:ro http-cache: - image: varnish:7.4.2-alpine + image: varnish:7.5.0 container_name: 'ecamp3-http-cache' depends_on: - caddy From 547bb0221e60b4b81ecb41acbe05a22c8c292d1b Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 28 Apr 2024 18:49:10 +0200 Subject: [PATCH 51/81] copy PurgeHttpCacheListenerTest from ApiPlatform --- .helm/deploy-to-cluster.sh | 2 +- .helm/ecamp3/values.yaml | 2 +- api/composer.json | 1 + api/composer.lock | 123 +++++++- .../HttpCache/Entity/ContainNonResource.php | 45 +++ api/tests/HttpCache/Entity/Dummy.php | 273 ++++++++++++++++++ api/tests/HttpCache/Entity/DummyFriend.php | 82 ++++++ .../HttpCache/Entity/DummyNoGetOperation.php | 46 +++ .../HttpCache/Entity/EmbeddableDummy.php | 115 ++++++++ api/tests/HttpCache/Entity/FourthLevel.php | 62 ++++ api/tests/HttpCache/Entity/NotAResource.php | 38 +++ api/tests/HttpCache/Entity/ParentDummy.php | 40 +++ api/tests/HttpCache/Entity/RelatedDummy.php | 189 ++++++++++++ .../HttpCache/Entity/RelatedOwnedDummy.php | 71 +++++ .../HttpCache/Entity/RelatedOwningDummy.php | 73 +++++ .../HttpCache/Entity/RelatedToDummyFriend.php | 112 +++++++ api/tests/HttpCache/Entity/ThirdLevel.php | 96 ++++++ .../HttpCache/PurgeHttpCacheListenerTest.php | 215 ++++++++++++++ 18 files changed, 1582 insertions(+), 3 deletions(-) create mode 100644 api/tests/HttpCache/Entity/ContainNonResource.php create mode 100644 api/tests/HttpCache/Entity/Dummy.php create mode 100644 api/tests/HttpCache/Entity/DummyFriend.php create mode 100644 api/tests/HttpCache/Entity/DummyNoGetOperation.php create mode 100644 api/tests/HttpCache/Entity/EmbeddableDummy.php create mode 100644 api/tests/HttpCache/Entity/FourthLevel.php create mode 100644 api/tests/HttpCache/Entity/NotAResource.php create mode 100644 api/tests/HttpCache/Entity/ParentDummy.php create mode 100644 api/tests/HttpCache/Entity/RelatedDummy.php create mode 100644 api/tests/HttpCache/Entity/RelatedOwnedDummy.php create mode 100644 api/tests/HttpCache/Entity/RelatedOwningDummy.php create mode 100644 api/tests/HttpCache/Entity/RelatedToDummyFriend.php create mode 100644 api/tests/HttpCache/Entity/ThirdLevel.php create mode 100644 api/tests/HttpCache/PurgeHttpCacheListenerTest.php diff --git a/.helm/deploy-to-cluster.sh b/.helm/deploy-to-cluster.sh index 241d202e58..e9b67f40c9 100755 --- a/.helm/deploy-to-cluster.sh +++ b/.helm/deploy-to-cluster.sh @@ -105,7 +105,7 @@ for i in 1; do values="$values --set postgresql.dbBackupRestoreImage.repository=docker.io/${docker_hub_account}/ecamp3-db-backup-restore" helm uninstall ecamp3-"$instance_name"-"$i" || true - helm upgrade --install ecamp3-"$instance_name"-"$i" $SCRIPT_DIR/ecamp3 $values + helm upgrade --install --debug --dry-run ecamp3-"$instance_name"-"$i" $SCRIPT_DIR/ecamp3 $values done rm -f private.pem diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index 73d19707e0..1de067627d 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -243,7 +243,7 @@ apiCache: memory: 100Mi logging: enabled: true - customOutput: '{ "received_at": "%t", "response_bytes": %b, "request_bytes": %I, "time_taken": %D, "first_line": "%r", "status": %s, "handling": "%{Varnish:handling}x" }' + customOutput: '{ "received_at": "%t", "method": "%m", "url": "%U", "query": "%q", "response_bytes": %b, "time_taken": %D, "status": %s, "handling": "%{Varnish:handling}x" }' customOutputJsonFormat: true # Timeout before returning error on initial VSM connection. # If set the VSM connection is retried every 0.5 seconds for this many seconds. diff --git a/api/composer.json b/api/composer.json index f217759ff5..49695bc4f0 100644 --- a/api/composer.json +++ b/api/composer.json @@ -56,6 +56,7 @@ "hautelook/alice-bundle": "2.13.0", "justinrainbow/json-schema": "5.2.13", "php-coveralls/php-coveralls": "2.7.0", + "phpspec/prophecy-phpunit": "2.2", "phpstan/phpstan": "1.10.67", "phpunit/phpunit": "10.5.20", "rector/rector": "1.0.4", diff --git a/api/composer.lock b/api/composer.lock index 12897e62cf..459ecbdbed 100644 --- a/api/composer.lock +++ b/api/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": "125a380b7546393d91f2577c8af958bd", + "content-hash": "38a728c33b1a9055cc136fdf91b1609c", "packages": [ { "name": "api-platform/core", @@ -11854,6 +11854,127 @@ }, "time": "2023-11-22T10:21:01+00:00" }, + { + "name": "phpspec/prophecy", + "version": "v1.19.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/67a759e7d8746d501c41536ba40cd9c0a07d6a87", + "reference": "67a759e7d8746d501c41536ba40cd9c0a07d6a87", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.2 || ^2.0", + "php": "^7.2 || 8.0.* || 8.1.* || 8.2.* || 8.3.*", + "phpdocumentor/reflection-docblock": "^5.2", + "sebastian/comparator": "^3.0 || ^4.0 || ^5.0 || ^6.0", + "sebastian/recursion-context": "^3.0 || ^4.0 || ^5.0 || ^6.0" + }, + "require-dev": { + "phpspec/phpspec": "^6.0 || ^7.0", + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^8.0 || ^9.0 || ^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\": "src/Prophecy" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "dev", + "fake", + "mock", + "spy", + "stub" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy/issues", + "source": "https://github.com/phpspec/prophecy/tree/v1.19.0" + }, + "time": "2024-02-29T11:52:51+00:00" + }, + { + "name": "phpspec/prophecy-phpunit", + "version": "v2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy-phpunit.git", + "reference": "16e1247e139434bce0bac09848bc5c8d882940fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy-phpunit/zipball/16e1247e139434bce0bac09848bc5c8d882940fc", + "reference": "16e1247e139434bce0bac09848bc5c8d882940fc", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8", + "phpspec/prophecy": "^1.18", + "phpunit/phpunit": "^9.1 || ^10.1 || ^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Prophecy\\PhpUnit\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christophe Coevoet", + "email": "stof@notk.org" + } + ], + "description": "Integrating the Prophecy mocking library in PHPUnit test cases", + "homepage": "http://phpspec.net", + "keywords": [ + "phpunit", + "prophecy" + ], + "support": { + "issues": "https://github.com/phpspec/prophecy-phpunit/issues", + "source": "https://github.com/phpspec/prophecy-phpunit/tree/v2.2.0" + }, + "time": "2024-03-01T08:33:58+00:00" + }, { "name": "phpstan/phpstan", "version": "1.10.67", diff --git a/api/tests/HttpCache/Entity/ContainNonResource.php b/api/tests/HttpCache/Entity/ContainNonResource.php new file mode 100644 index 0000000000..f5ebe0073c --- /dev/null +++ b/api/tests/HttpCache/Entity/ContainNonResource.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Resource linked to a standard object. + * + * @author Kévin Dunglas + */ +#[ApiResource(filters: ['my_dummy.property'], normalizationContext: ['groups' => ['contain_non_resource']])] +#[ORM\Entity] +class ContainNonResource { + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Groups('contain_non_resource')] + public $id; + + /** + * @var ContainNonResource + */ + #[Groups('contain_non_resource')] + public $nested; + + /** + * @var NotAResource + */ + #[Groups('contain_non_resource')] + public $notAResource; +} diff --git a/api/tests/HttpCache/Entity/Dummy.php b/api/tests/HttpCache/Entity/Dummy.php new file mode 100644 index 0000000000..781da7df8e --- /dev/null +++ b/api/tests/HttpCache/Entity/Dummy.php @@ -0,0 +1,273 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Link; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Dummy. + * + * @author Kévin Dunglas + */ +#[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false, 'rfc_7807_compliant_errors' => false])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] +#[ORM\Entity] +class Dummy { + /** + * @var null|string A short description of the item + */ + #[ApiProperty(iris: ['https://schema.org/description'])] + #[ORM\Column(nullable: true)] + public $description; + + /** + * @var null|string A dummy + */ + #[ORM\Column(nullable: true)] + public $dummy; + + /** + * @var null|bool A dummy boolean + */ + #[ORM\Column(type: 'boolean', nullable: true)] + public ?bool $dummyBoolean = null; + + /** + * @var null|\DateTime A dummy date + */ + #[ApiProperty(iris: ['https://schema.org/DateTime'])] + #[ORM\Column(type: 'datetime', nullable: true)] + public $dummyDate; + + /** + * @var null|float A dummy float + */ + #[ORM\Column(type: 'float', nullable: true)] + public $dummyFloat; + + /** + * @var null|string A dummy price + */ + #[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)] + public $dummyPrice; + + #[ApiProperty(push: true)] + #[ORM\ManyToOne(targetEntity: RelatedDummy::class)] + public ?RelatedDummy $relatedDummy = null; + + #[ORM\ManyToMany(targetEntity: RelatedDummy::class)] + public Collection|iterable $relatedDummies; + + /** + * @var null|array serialize data + */ + #[ORM\Column(type: 'json', nullable: true)] + public $jsonData = []; + + /** + * @var null|array + */ + #[ORM\Column(type: 'simple_array', nullable: true)] + public $arrayData = []; + + /** + * @var null|string + */ + #[ORM\Column(nullable: true)] + public $nameConverted; + + /** + * @var null|RelatedOwnedDummy + */ + #[ORM\OneToOne(targetEntity: RelatedOwnedDummy::class, cascade: ['persist'], mappedBy: 'owningDummy')] + public $relatedOwnedDummy; + + /** + * @var null|RelatedOwningDummy + */ + #[ORM\OneToOne(targetEntity: RelatedOwningDummy::class, cascade: ['persist'], inversedBy: 'ownedDummy')] + public $relatedOwningDummy; + + /** + * @var null|int The id + */ + #[ORM\Column(type: 'integer', nullable: true)] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private $id; + + /** + * @var string The dummy name + */ + #[ApiProperty(iris: ['https://schema.org/name'])] + #[ORM\Column] + #[Assert\NotBlank] + private string $name; + + /** + * @var null|string The dummy name alias + */ + #[ApiProperty(iris: ['https://schema.org/alternateName'])] + #[ORM\Column(nullable: true)] + private $alias; + + /** + * @var array foo + */ + private ?array $foo = null; + + public function __construct() { + $this->relatedDummies = new ArrayCollection(); + } + + public static function staticMethod(): void {} + + public function getId() { + return $this->id; + } + + public function setId($id): void { + $this->id = $id; + } + + public function setName(string $name): void { + $this->name = $name; + } + + public function getName(): string { + return $this->name; + } + + public function setAlias($alias): void { + $this->alias = $alias; + } + + public function getAlias() { + return $this->alias; + } + + public function setDescription($description): void { + $this->description = $description; + } + + public function getDescription() { + return $this->description; + } + + public function fooBar($baz): void {} + + public function getFoo(): ?array { + return $this->foo; + } + + public function setFoo(?array $foo = null): void { + $this->foo = $foo; + } + + public function setDummyDate(?\DateTime $dummyDate = null): void { + $this->dummyDate = $dummyDate; + } + + public function getDummyDate() { + return $this->dummyDate; + } + + public function setDummyPrice($dummyPrice) { + $this->dummyPrice = $dummyPrice; + + return $this; + } + + public function getDummyPrice() { + return $this->dummyPrice; + } + + public function setJsonData($jsonData): void { + $this->jsonData = $jsonData; + } + + public function getJsonData() { + return $this->jsonData; + } + + public function setArrayData($arrayData): void { + $this->arrayData = $arrayData; + } + + public function getArrayData() { + return $this->arrayData; + } + + public function getRelatedDummy(): ?RelatedDummy { + return $this->relatedDummy; + } + + public function setRelatedDummy(RelatedDummy $relatedDummy): void { + $this->relatedDummy = $relatedDummy; + } + + public function addRelatedDummy(RelatedDummy $relatedDummy): void { + $this->relatedDummies->add($relatedDummy); + } + + public function getRelatedOwnedDummy() { + return $this->relatedOwnedDummy; + } + + public function setRelatedOwnedDummy(RelatedOwnedDummy $relatedOwnedDummy): void { + $this->relatedOwnedDummy = $relatedOwnedDummy; + if ($this !== $this->relatedOwnedDummy->getOwningDummy()) { + $this->relatedOwnedDummy->setOwningDummy($this); + } + } + + public function getRelatedOwningDummy() { + return $this->relatedOwningDummy; + } + + public function setRelatedOwningDummy(RelatedOwningDummy $relatedOwningDummy): void { + $this->relatedOwningDummy = $relatedOwningDummy; + } + + public function isDummyBoolean(): ?bool { + return $this->dummyBoolean; + } + + /** + * @param bool $dummyBoolean + */ + public function setDummyBoolean($dummyBoolean): void { + $this->dummyBoolean = $dummyBoolean; + } + + public function setDummy($dummy = null): void { + $this->dummy = $dummy; + } + + public function getDummy() { + return $this->dummy; + } + + public function getRelatedDummies(): Collection|iterable { + return $this->relatedDummies; + } +} diff --git a/api/tests/HttpCache/Entity/DummyFriend.php b/api/tests/HttpCache/Entity/DummyFriend.php new file mode 100644 index 0000000000..6974ad2edb --- /dev/null +++ b/api/tests/HttpCache/Entity/DummyFriend.php @@ -0,0 +1,82 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Dummy Friend. + * + * @author Kévin Dunglas + */ +#[ApiResource] +#[ORM\Entity] +class DummyFriend implements \Stringable { + /** + * @var null|int The id + */ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + + /** + * @var string The dummy name + */ + #[ApiProperty(types: ['https://schema.org/name'])] + #[ORM\Column] + #[Assert\NotBlank] + #[Groups(['fakemanytomany', 'friends'])] + private string $name; + + public function __toString(): string { + return (string) $this->getId(); + } + + /** + * Get id. + */ + public function getId(): ?int { + return $this->id; + } + + /** + * Set id. + * + * @param int $id the value to set + */ + public function setId(int $id): void { + $this->id = $id; + } + + /** + * Get name. + */ + public function getName(): ?string { + return $this->name; + } + + /** + * Set name. + * + * @param string $name the value to set + */ + public function setName(string $name): void { + $this->name = $name; + } +} diff --git a/api/tests/HttpCache/Entity/DummyNoGetOperation.php b/api/tests/HttpCache/Entity/DummyNoGetOperation.php new file mode 100644 index 0000000000..154c345767 --- /dev/null +++ b/api/tests/HttpCache/Entity/DummyNoGetOperation.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use Doctrine\ORM\Mapping as ORM; + +/** + * DummyNoGetOperation. + * + * @author Grégoire Hébert gregoire@les-tilleuls.coop + */ +#[ApiResource(operations: [new Put(), new Post()])] +#[ORM\Entity] +class DummyNoGetOperation { + /** + * @var string + */ + #[ORM\Column] + public $lorem; + + /** + * @var int The id + */ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private $id; + + public function setId($id): void { + $this->id = $id; + } +} diff --git a/api/tests/HttpCache/Entity/EmbeddableDummy.php b/api/tests/HttpCache/Entity/EmbeddableDummy.php new file mode 100644 index 0000000000..4dc247f844 --- /dev/null +++ b/api/tests/HttpCache/Entity/EmbeddableDummy.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiProperty; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Embeddable Dummy. + * + * @author Jordan Samouh + */ +#[ORM\Embeddable] +class EmbeddableDummy { + /** + * @var null|bool A dummy boolean + */ + #[ORM\Column(type: 'boolean', nullable: true)] + public ?bool $dummyBoolean = null; + + /** + * @var null|\DateTime A dummy date + */ + #[ORM\Column(type: 'datetime', nullable: true)] + #[Assert\DateTime] + public ?\DateTime $dummyDate = null; + + /** + * @var null|float A dummy float + */ + #[ORM\Column(type: 'float', nullable: true)] + public ?float $dummyFloat = null; + + /** + * @var null|string A dummy price + */ + #[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)] + public ?string $dummyPrice = null; + #[ORM\Column(type: 'string', nullable: true)] + #[Groups(['barcelona', 'chicago'])] + protected $symfony; + + /** + * @var string The dummy name + */ + #[ApiProperty(identifier: true)] + #[ORM\Column(nullable: true)] + #[Groups(['embed'])] + private ?string $dummyName = null; + + public function __construct() {} + + public static function staticMethod(): void {} + + public function getDummyName(): ?string { + return $this->dummyName; + } + + public function setDummyName(string $dummyName): void { + $this->dummyName = $dummyName; + } + + public function isDummyBoolean(): ?bool { + return $this->dummyBoolean; + } + + public function setDummyBoolean(bool $dummyBoolean): void { + $this->dummyBoolean = $dummyBoolean; + } + + public function getDummyDate(): ?\DateTime { + return $this->dummyDate; + } + + public function setDummyDate(\DateTime $dummyDate): void { + $this->dummyDate = $dummyDate; + } + + public function getDummyFloat(): ?float { + return $this->dummyFloat; + } + + public function setDummyFloat(float $dummyFloat): void { + $this->dummyFloat = $dummyFloat; + } + + public function getDummyPrice(): ?string { + return $this->dummyPrice; + } + + public function setDummyPrice(string $dummyPrice): void { + $this->dummyPrice = $dummyPrice; + } + + public function getSymfony() { + return $this->symfony; + } + + public function setSymfony($symfony): void { + $this->symfony = $symfony; + } +} diff --git a/api/tests/HttpCache/Entity/FourthLevel.php b/api/tests/HttpCache/Entity/FourthLevel.php new file mode 100644 index 0000000000..cffc836cf0 --- /dev/null +++ b/api/tests/HttpCache/Entity/FourthLevel.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Link; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Fourth Level. + * + * @author Alan Poulain + */ +#[ApiResource] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] +#[ORM\Entity] +class FourthLevel { + #[ORM\OneToMany(targetEntity: ThirdLevel::class, cascade: ['persist'], mappedBy: 'badFourthLevel')] + public null|Collection|iterable $badThirdLevel = null; + + /** + * @var null|int The id + */ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue] + private ?int $id = null; + #[ORM\Column(type: 'integer')] + #[Groups(['barcelona', 'chicago'])] + private int $level = 4; + + public function getId(): ?int { + return $this->id; + } + + public function getLevel(): ?int { + return $this->level; + } + + public function setLevel(int $level): void { + $this->level = $level; + } +} diff --git a/api/tests/HttpCache/Entity/NotAResource.php b/api/tests/HttpCache/Entity/NotAResource.php new file mode 100644 index 0000000000..6ff72ea663 --- /dev/null +++ b/api/tests/HttpCache/Entity/NotAResource.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * This class is not mapped as an API resource. + * + * @author Kévin Dunglas + */ +class NotAResource { + public function __construct( + #[Groups('contain_non_resource')] + private $foo, + #[Groups('contain_non_resource')] + private $bar + ) {} + + public function getFoo() { + return $this->foo; + } + + public function getBar() { + return $this->bar; + } +} diff --git a/api/tests/HttpCache/Entity/ParentDummy.php b/api/tests/HttpCache/Entity/ParentDummy.php new file mode 100644 index 0000000000..4698b78fd4 --- /dev/null +++ b/api/tests/HttpCache/Entity/ParentDummy.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Parent Dummy. + * + * @author Kévin Dunglas + */ +#[ORM\MappedSuperclass] +class ParentDummy { + /** + * @var null|int The age + */ + #[ORM\Column(type: 'integer', nullable: true)] + #[Groups(['friends'])] + private $age; + + public function getAge() { + return $this->age; + } + + public function setAge($age) { + return $this->age = $age; + } +} diff --git a/api/tests/HttpCache/Entity/RelatedDummy.php b/api/tests/HttpCache/Entity/RelatedDummy.php new file mode 100644 index 0000000000..0b2a525aa1 --- /dev/null +++ b/api/tests/HttpCache/Entity/RelatedDummy.php @@ -0,0 +1,189 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Doctrine\Orm\Filter\DateFilter; +use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter; +use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; +use ApiPlatform\Metadata\ApiFilter; +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\GraphQl\Mutation; +use ApiPlatform\Metadata\GraphQl\Query; +use ApiPlatform\Metadata\Link; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Related Dummy. + * + * @author Kévin Dunglas + */ +#[ApiResource( + graphQlOperations: [ + new Query(name: 'item_query'), + new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']]), + ], + types: ['https://schema.org/Product'], + normalizationContext: ['groups' => ['friends']], + filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'] +)] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] +#[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] +#[ORM\Entity] +class RelatedDummy extends ParentDummy implements \Stringable { + /** + * @var null|string A name + */ + #[ApiProperty(iris: ['RelatedDummy.name'])] + #[ORM\Column(nullable: true)] + #[Groups(['friends'])] + public $name; + + /** + * @var null|\DateTime A dummy date + */ + #[ORM\Column(type: 'datetime', nullable: true)] + #[Assert\DateTime] + #[Groups(['friends'])] + #[ApiFilter(filterClass: DateFilter::class)] + public $dummyDate; + + #[ORM\ManyToOne(targetEntity: ThirdLevel::class, cascade: ['persist'], inversedBy: 'relatedDummies')] + #[Groups(['barcelona', 'chicago', 'friends'])] + public ?ThirdLevel $thirdLevel = null; + + #[ORM\OneToMany(targetEntity: RelatedToDummyFriend::class, cascade: ['persist'], mappedBy: 'relatedDummy')] + #[Groups(['fakemanytomany', 'friends'])] + public Collection|iterable $relatedToDummyFriend; + + /** + * @var null|bool A dummy bool + */ + #[ORM\Column(type: 'boolean', nullable: true)] + #[Groups(['friends'])] + public ?bool $dummyBoolean = null; + + #[ORM\Embedded(class: 'EmbeddableDummy')] + #[Groups(['friends'])] + public ?EmbeddableDummy $embeddedDummy = null; + + #[ApiProperty(deprecationReason: 'This property is deprecated for upgrade test')] + #[ORM\Column] + #[Groups(['barcelona', 'chicago', 'friends'])] + #[ApiFilter(filterClass: SearchFilter::class)] + #[ApiFilter(filterClass: ExistsFilter::class)] + protected $symfony = 'symfony'; + #[ApiProperty(writable: false)] + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + #[Groups(['chicago', 'friends'])] + private $id; + + public function __construct() { + $this->relatedToDummyFriend = new ArrayCollection(); + $this->embeddedDummy = new EmbeddableDummy(); + } + + public function __toString(): string { + return (string) $this->getId(); + } + + public function getId() { + return $this->id; + } + + public function setId($id): void { + $this->id = $id; + } + + public function setName($name): void { + $this->name = $name; + } + + public function getName() { + return $this->name; + } + + public function getSymfony() { + return $this->symfony; + } + + public function setSymfony($symfony): void { + $this->symfony = $symfony; + } + + public function setDummyDate(\DateTime $dummyDate): void { + $this->dummyDate = $dummyDate; + } + + public function getDummyDate() { + return $this->dummyDate; + } + + public function isDummyBoolean(): ?bool { + return $this->dummyBoolean; + } + + /** + * @param bool $dummyBoolean + */ + public function setDummyBoolean($dummyBoolean): void { + $this->dummyBoolean = $dummyBoolean; + } + + public function getThirdLevel(): ?ThirdLevel { + return $this->thirdLevel; + } + + public function setThirdLevel(?ThirdLevel $thirdLevel = null): void { + $this->thirdLevel = $thirdLevel; + } + + /** + * Get relatedToDummyFriend. + */ + public function getRelatedToDummyFriend(): Collection|iterable { + return $this->relatedToDummyFriend; + } + + /** + * Set relatedToDummyFriend. + * + * @param RelatedToDummyFriend $relatedToDummyFriend the value to set + */ + public function addRelatedToDummyFriend(RelatedToDummyFriend $relatedToDummyFriend): void { + $this->relatedToDummyFriend->add($relatedToDummyFriend); + } + + public function getEmbeddedDummy(): EmbeddableDummy { + return $this->embeddedDummy; + } + + public function setEmbeddedDummy(EmbeddableDummy $embeddedDummy): void { + $this->embeddedDummy = $embeddedDummy; + } +} diff --git a/api/tests/HttpCache/Entity/RelatedOwnedDummy.php b/api/tests/HttpCache/Entity/RelatedOwnedDummy.php new file mode 100644 index 0000000000..ae6b94a1e8 --- /dev/null +++ b/api/tests/HttpCache/Entity/RelatedOwnedDummy.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * Related Owned Dummy. + * + * @author Sergey V. Ryabov + */ +#[ApiResource(types: ['https://schema.org/Product'])] +#[ORM\Entity] +class RelatedOwnedDummy { + /** + * @var null|string A name + */ + #[ORM\Column(nullable: true)] + public ?string $name = null; + #[ORM\OneToOne(targetEntity: Dummy::class, cascade: ['persist'], inversedBy: 'relatedOwnedDummy')] + #[ORM\JoinColumn(nullable: false)] + public ?Dummy $owningDummy = null; + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + + public function getId(): ?int { + return $this->id; + } + + public function setId(int $id): void { + $this->id = $id; + } + + public function setName(?string $name): void { + $this->name = $name; + } + + public function getName(): ?string { + return $this->name; + } + + /** + * Get owning dummy. + */ + public function getOwningDummy(): ?Dummy { + return $this->owningDummy; + } + + /** + * Set owning dummy. + * + * @param Dummy $owningDummy the value to set + */ + public function setOwningDummy(Dummy $owningDummy): void { + $this->owningDummy = $owningDummy; + } +} diff --git a/api/tests/HttpCache/Entity/RelatedOwningDummy.php b/api/tests/HttpCache/Entity/RelatedOwningDummy.php new file mode 100644 index 0000000000..a1911810ce --- /dev/null +++ b/api/tests/HttpCache/Entity/RelatedOwningDummy.php @@ -0,0 +1,73 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiResource; +use Doctrine\ORM\Mapping as ORM; + +/** + * Related Owning Dummy. + * + * @author Sergey V. Ryabov + */ +#[ApiResource(types: ['https://schema.org/Product'])] +#[ORM\Entity] +class RelatedOwningDummy { + /** + * @var null|string A name + */ + #[ORM\Column(nullable: true)] + public $name; + #[ORM\OneToOne(targetEntity: Dummy::class, cascade: ['persist'], mappedBy: 'relatedOwningDummy')] + public ?Dummy $ownedDummy = null; + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private $id; + + public function getId() { + return $this->id; + } + + public function setId($id): void { + $this->id = $id; + } + + public function setName($name): void { + $this->name = $name; + } + + public function getName() { + return $this->name; + } + + /** + * Get owned dummy. + */ + public function getOwnedDummy(): Dummy { + return $this->ownedDummy; + } + + /** + * Set owned dummy. + * + * @param Dummy $ownedDummy the value to set + */ + public function setOwnedDummy(Dummy $ownedDummy): void { + $this->ownedDummy = $ownedDummy; + if ($this !== $this->ownedDummy->getRelatedOwningDummy()) { + $this->ownedDummy->setRelatedOwningDummy($this); + } + } +} diff --git a/api/tests/HttpCache/Entity/RelatedToDummyFriend.php b/api/tests/HttpCache/Entity/RelatedToDummyFriend.php new file mode 100644 index 0000000000..8e5bd9a149 --- /dev/null +++ b/api/tests/HttpCache/Entity/RelatedToDummyFriend.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Link; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * Related To Dummy Friend represent an association table for a manytomany relation. + */ +#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'], extraProperties: ['rfc_7807_compliant_errors' => false])] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] +#[ORM\Entity] +class RelatedToDummyFriend { + /** + * @var string The dummy name + */ + #[ApiProperty(types: ['https://schema.org/name'])] + #[ORM\Column] + #[Assert\NotBlank] + #[Groups(['fakemanytomany', 'friends'])] + private $name; + + /** + * @var null|string The dummy description + */ + #[ORM\Column(nullable: true)] + #[Groups(['fakemanytomany', 'friends'])] + private ?string $description = null; + #[ORM\Id] + #[ORM\ManyToOne(targetEntity: DummyFriend::class)] + #[ORM\JoinColumn(name: 'dummyfriend_id', referencedColumnName: 'id', nullable: false)] + #[Groups(['fakemanytomany', 'friends'])] + #[Assert\NotNull] + private DummyFriend $dummyFriend; + #[ORM\Id] + #[ORM\ManyToOne(targetEntity: RelatedDummy::class, inversedBy: 'relatedToDummyFriend')] + #[ORM\JoinColumn(name: 'relateddummy_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] + #[Assert\NotNull] + private RelatedDummy $relatedDummy; + + public function setName($name): void { + $this->name = $name; + } + + public function getName() { + return $this->name; + } + + public function getDescription(): ?string { + return $this->description; + } + + /** + * @param null|string $description + */ + public function setDescription($description): void { + $this->description = $description; + } + + /** + * Gets dummyFriend. + */ + public function getDummyFriend(): DummyFriend { + return $this->dummyFriend; + } + + /** + * Sets dummyFriend. + * + * @param DummyFriend $dummyFriend the value to set + */ + public function setDummyFriend(DummyFriend $dummyFriend): void { + $this->dummyFriend = $dummyFriend; + } + + /** + * Gets relatedDummy. + */ + public function getRelatedDummy(): RelatedDummy { + return $this->relatedDummy; + } + + /** + * Sets relatedDummy. + * + * @param RelatedDummy $relatedDummy the value to set + */ + public function setRelatedDummy(RelatedDummy $relatedDummy): void { + $this->relatedDummy = $relatedDummy; + } +} diff --git a/api/tests/HttpCache/Entity/ThirdLevel.php b/api/tests/HttpCache/Entity/ThirdLevel.php new file mode 100644 index 0000000000..c13df9b8e2 --- /dev/null +++ b/api/tests/HttpCache/Entity/ThirdLevel.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache\Entity; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Link; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation\Groups; + +/** + * Third Level. + * + * @author Kévin Dunglas + */ +#[ApiResource] +#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] +#[ORM\Entity] +class ThirdLevel { + #[ORM\ManyToOne(targetEntity: FourthLevel::class, cascade: ['persist'])] + #[Groups(['barcelona', 'chicago', 'friends'])] + public ?FourthLevel $fourthLevel = null; + #[ORM\ManyToOne(targetEntity: FourthLevel::class, cascade: ['persist'])] + public $badFourthLevel; + + #[ORM\OneToMany(mappedBy: 'thirdLevel', targetEntity: RelatedDummy::class)] + public Collection|iterable $relatedDummies; + + /** + * @var null|int The id + */ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + #[ORM\Column(type: 'integer')] + #[Groups(['barcelona', 'chicago'])] + private int $level = 3; + #[ORM\Column(type: 'boolean')] + private bool $test = true; + + public function __construct() { + $this->relatedDummies = new ArrayCollection(); + } + + public function getId(): ?int { + return $this->id; + } + + public function getLevel(): ?int { + return $this->level; + } + + /** + * @param int $level + */ + public function setLevel($level): void { + $this->level = $level; + } + + public function isTest(): bool { + return $this->test; + } + + /** + * @param bool $test + */ + public function setTest($test): void { + $this->test = $test; + } + + public function getFourthLevel(): ?FourthLevel { + return $this->fourthLevel; + } + + public function setFourthLevel(?FourthLevel $fourthLevel = null): void { + $this->fourthLevel = $fourthLevel; + } +} diff --git a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php new file mode 100644 index 0000000000..f12e691060 --- /dev/null +++ b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php @@ -0,0 +1,215 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace App\Tests\HttpCache; + +use ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener; +use ApiPlatform\Exception\InvalidArgumentException; +use ApiPlatform\Exception\ItemNotFoundException; +use ApiPlatform\HttpCache\PurgerInterface; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\ResourceClassResolverInterface; +use ApiPlatform\Metadata\UrlGeneratorInterface; +use App\Tests\HttpCache\Entity\ContainNonResource; +use App\Tests\HttpCache\Entity\Dummy; +use App\Tests\HttpCache\Entity\DummyNoGetOperation; +use App\Tests\HttpCache\Entity\NotAResource; +use App\Tests\HttpCache\Entity\RelatedDummy; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Event\OnFlushEventArgs; +use Doctrine\ORM\Event\PreUpdateEventArgs; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\UnitOfWork; +use PHPUnit\Framework\TestCase; +use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * @author Kévin Dunglas + * + * @internal + */ +class PurgeHttpCacheListenerTest extends TestCase { + use ProphecyTrait; + + public function testOnFlush(): void { + $toInsert1 = new Dummy(); + $toInsert2 = new Dummy(); + + $toUpdate1 = new Dummy(); + $toUpdate1->setId(1); + $toUpdate2 = new Dummy(); + $toUpdate2->setId(2); + + $toDelete1 = new Dummy(); + $toDelete1->setId(3); + $toDelete2 = new Dummy(); + $toDelete2->setId(4); + + $toDeleteNoPurge = new DummyNoGetOperation(); + $toDeleteNoPurge->setId(5); + + $purgerProphecy = $this->prophesize(PurgerInterface::class); + $purgerProphecy->purge(['/dummies', '/dummies/1', '/dummies/2', '/dummies/3', '/dummies/4'])->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, new GetCollection())->willReturn('/dummies')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource(DummyNoGetOperation::class, UrlGeneratorInterface::ABS_PATH, new GetCollection())->willThrow(new InvalidArgumentException())->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource($toUpdate1)->willReturn('/dummies/1')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource($toUpdate2)->willReturn('/dummies/2')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource($toDelete1)->willReturn('/dummies/3')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource($toDelete2)->willReturn('/dummies/4')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource($toDeleteNoPurge)->shouldNotBeCalled(); + $iriConverterProphecy->getIriFromResource(Argument::any())->willThrow(new ItemNotFoundException()); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled(); + $resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class)->shouldBeCalled(); + $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyNoGetOperation::class))->willReturn(DummyNoGetOperation::class)->shouldBeCalled(); + + $uowProphecy = $this->prophesize(UnitOfWork::class); + $uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1, $toInsert2])->shouldBeCalled(); + $uowProphecy->getScheduledEntityUpdates()->willReturn([$toUpdate1, $toUpdate2])->shouldBeCalled(); + $uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete1, $toDelete2, $toDeleteNoPurge])->shouldBeCalled(); + + $emProphecy = $this->prophesize(EntityManagerInterface::class); + $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled(); + $dummyClassMetadata = new ClassMetadata(Dummy::class); + // @phpstan-ignore-next-line + $dummyClassMetadata->associationMappings = [ + 'relatedDummy' => [], + 'relatedOwningDummy' => [], + ]; + $emProphecy->getClassMetadata(Dummy::class)->willReturn($dummyClassMetadata)->shouldBeCalled(); + $emProphecy->getClassMetadata(DummyNoGetOperation::class)->willReturn(new ClassMetadata(DummyNoGetOperation::class))->shouldBeCalled(); + $eventArgs = new OnFlushEventArgs($emProphecy->reveal()); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedDummy')->willReturn(true); + $propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(false); + $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedDummy')->willReturn(null)->shouldBeCalled(); + $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(null)->shouldNotBeCalled(); + + $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal()); + $listener->onFlush($eventArgs); + $listener->postFlush(); + } + + public function testPreUpdate(): void { + $oldRelatedDummy = new RelatedDummy(); + $oldRelatedDummy->setId(1); + + $newRelatedDummy = new RelatedDummy(); + $newRelatedDummy->setId(2); + + $dummy = new Dummy(); + $dummy->setId(1); + + $purgerProphecy = $this->prophesize(PurgerInterface::class); + $purgerProphecy->purge(['/dummies', '/dummies/1', '/related_dummies/old', '/related_dummies/new'])->shouldBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, new GetCollection())->willReturn('/dummies')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource($dummy)->willReturn('/dummies/1')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource($oldRelatedDummy)->willReturn('/related_dummies/old')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource($newRelatedDummy)->willReturn('/related_dummies/new')->shouldBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled(); + $resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class)->shouldBeCalled(); + + $emProphecy = $this->prophesize(EntityManagerInterface::class); + + $classMetadata = new ClassMetadata(Dummy::class); + $classMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class]); + $emProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadata)->shouldBeCalled(); + + $changeSet = ['relatedDummy' => [$oldRelatedDummy, $newRelatedDummy]]; + $eventArgs = new PreUpdateEventArgs($dummy, $emProphecy->reveal(), $changeSet); + + $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal()); + $listener->preUpdate($eventArgs); + $listener->postFlush(); + } + + public function testNothingToPurge(): void { + $dummyNoGetOperation = new DummyNoGetOperation(); + $dummyNoGetOperation->setId(1); + + $purgerProphecy = $this->prophesize(PurgerInterface::class); + $purgerProphecy->purge([])->shouldNotBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(DummyNoGetOperation::class, UrlGeneratorInterface::ABS_PATH, new GetCollection())->willThrow(new InvalidArgumentException())->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource($dummyNoGetOperation)->shouldNotBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyNoGetOperation::class))->willReturn(DummyNoGetOperation::class)->shouldBeCalled(); + + $emProphecy = $this->prophesize(EntityManagerInterface::class); + + $classMetadata = new ClassMetadata(DummyNoGetOperation::class); + $emProphecy->getClassMetadata(DummyNoGetOperation::class)->willReturn($classMetadata)->shouldBeCalled(); + + $changeSet = ['lorem' => 'ipsum']; + $eventArgs = new PreUpdateEventArgs($dummyNoGetOperation, $emProphecy->reveal(), $changeSet); + + $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal()); + $listener->preUpdate($eventArgs); + $listener->postFlush(); + } + + public function testNotAResourceClass(): void { + $containNonResource = new ContainNonResource(); + $nonResource = new NotAResource('foo', 'bar'); + + $purgerProphecy = $this->prophesize(PurgerInterface::class); + $purgerProphecy->purge([])->shouldNotBeCalled(); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(ContainNonResource::class, UrlGeneratorInterface::ABS_PATH, Argument::any())->willReturn('/dummies/1'); + $iriConverterProphecy->getIriFromResource($nonResource)->shouldNotBeCalled(); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->getResourceClass(Argument::type(ContainNonResource::class))->willReturn(ContainNonResource::class)->shouldBeCalled(); + $resourceClassResolverProphecy->isResourceClass(NotAResource::class)->willReturn(false)->shouldBeCalled(); + + $uowProphecy = $this->prophesize(UnitOfWork::class); + $uowProphecy->getScheduledEntityInsertions()->willReturn([$containNonResource])->shouldBeCalled(); + $uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled(); + $uowProphecy->getScheduledEntityDeletions()->willReturn([])->shouldBeCalled(); + + $emProphecy = $this->prophesize(EntityManagerInterface::class); + $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled(); + + $dummyClassMetadata = new ClassMetadata(ContainNonResource::class); + // @phpstan-ignore-next-line + $dummyClassMetadata->associationMappings = [ + 'notAResource' => [], + 'collectionOfNotAResource' => ['targetEntity' => NotAResource::class], + ]; + $emProphecy->getClassMetadata(ContainNonResource::class)->willReturn($dummyClassMetadata); + $eventArgs = new OnFlushEventArgs($emProphecy->reveal()); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'notAResource')->willReturn(true); + $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldNotBeCalled(); + $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'notAResource')->shouldBeCalled()->willReturn($nonResource); + $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldNotBeCalled(); + + $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal()); + $listener->onFlush($eventArgs); + } +} From 27f945062dd6c512d24408a673dcda0f1775e626 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 28 Apr 2024 19:18:50 +0200 Subject: [PATCH 52/81] remove unnecessary test entities --- api/tests/HttpCache/Entity/BaseEntity.php | 25 +++ .../HttpCache/Entity/ContainNonResource.php | 10 +- api/tests/HttpCache/Entity/Dummy.php | 209 +----------------- api/tests/HttpCache/Entity/DummyFriend.php | 82 ------- .../HttpCache/Entity/DummyNoGetOperation.php | 14 +- .../HttpCache/Entity/EmbeddableDummy.php | 115 ---------- api/tests/HttpCache/Entity/FourthLevel.php | 62 ------ api/tests/HttpCache/Entity/NotAResource.php | 4 - api/tests/HttpCache/Entity/ParentDummy.php | 40 ---- api/tests/HttpCache/Entity/RelatedDummy.php | 167 +------------- .../HttpCache/Entity/RelatedOwnedDummy.php | 71 ------ .../HttpCache/Entity/RelatedOwningDummy.php | 29 +-- .../HttpCache/Entity/RelatedToDummyFriend.php | 112 ---------- api/tests/HttpCache/Entity/ThirdLevel.php | 96 -------- 14 files changed, 30 insertions(+), 1006 deletions(-) create mode 100644 api/tests/HttpCache/Entity/BaseEntity.php delete mode 100644 api/tests/HttpCache/Entity/DummyFriend.php delete mode 100644 api/tests/HttpCache/Entity/EmbeddableDummy.php delete mode 100644 api/tests/HttpCache/Entity/FourthLevel.php delete mode 100644 api/tests/HttpCache/Entity/ParentDummy.php delete mode 100644 api/tests/HttpCache/Entity/RelatedOwnedDummy.php delete mode 100644 api/tests/HttpCache/Entity/RelatedToDummyFriend.php delete mode 100644 api/tests/HttpCache/Entity/ThirdLevel.php diff --git a/api/tests/HttpCache/Entity/BaseEntity.php b/api/tests/HttpCache/Entity/BaseEntity.php new file mode 100644 index 0000000000..453f3a6018 --- /dev/null +++ b/api/tests/HttpCache/Entity/BaseEntity.php @@ -0,0 +1,25 @@ +id; + } + + public function setId($id): void { + $this->id = $id; + } +} diff --git a/api/tests/HttpCache/Entity/ContainNonResource.php b/api/tests/HttpCache/Entity/ContainNonResource.php index f5ebe0073c..c1f7e2426e 100644 --- a/api/tests/HttpCache/Entity/ContainNonResource.php +++ b/api/tests/HttpCache/Entity/ContainNonResource.php @@ -13,7 +13,6 @@ namespace App\Tests\HttpCache\Entity; -use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; @@ -22,15 +21,8 @@ * * @author Kévin Dunglas */ -#[ApiResource(filters: ['my_dummy.property'], normalizationContext: ['groups' => ['contain_non_resource']])] #[ORM\Entity] -class ContainNonResource { - #[ORM\Column(type: 'integer')] - #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'AUTO')] - #[Groups('contain_non_resource')] - public $id; - +class ContainNonResource extends BaseEntity { /** * @var ContainNonResource */ diff --git a/api/tests/HttpCache/Entity/Dummy.php b/api/tests/HttpCache/Entity/Dummy.php index 781da7df8e..7ccd50806d 100644 --- a/api/tests/HttpCache/Entity/Dummy.php +++ b/api/tests/HttpCache/Entity/Dummy.php @@ -13,210 +13,33 @@ namespace App\Tests\HttpCache\Entity; -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Link; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Validator\Constraints as Assert; /** * Dummy. * * @author Kévin Dunglas */ -#[ApiResource(filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], extraProperties: ['standard_put' => false, 'rfc_7807_compliant_errors' => false])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy')], status: 200, filters: ['my_dummy.boolean', 'my_dummy.date', 'my_dummy.exists', 'my_dummy.numeric', 'my_dummy.order', 'my_dummy.range', 'my_dummy.search', 'my_dummy.property'], operations: [new Get()])] #[ORM\Entity] -class Dummy { - /** - * @var null|string A short description of the item - */ - #[ApiProperty(iris: ['https://schema.org/description'])] - #[ORM\Column(nullable: true)] - public $description; - - /** - * @var null|string A dummy - */ - #[ORM\Column(nullable: true)] - public $dummy; - - /** - * @var null|bool A dummy boolean - */ - #[ORM\Column(type: 'boolean', nullable: true)] - public ?bool $dummyBoolean = null; - - /** - * @var null|\DateTime A dummy date - */ - #[ApiProperty(iris: ['https://schema.org/DateTime'])] - #[ORM\Column(type: 'datetime', nullable: true)] - public $dummyDate; - - /** - * @var null|float A dummy float - */ - #[ORM\Column(type: 'float', nullable: true)] - public $dummyFloat; - - /** - * @var null|string A dummy price - */ - #[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)] - public $dummyPrice; - - #[ApiProperty(push: true)] +class Dummy extends BaseEntity { #[ORM\ManyToOne(targetEntity: RelatedDummy::class)] public ?RelatedDummy $relatedDummy = null; #[ORM\ManyToMany(targetEntity: RelatedDummy::class)] public Collection|iterable $relatedDummies; - /** - * @var null|array serialize data - */ - #[ORM\Column(type: 'json', nullable: true)] - public $jsonData = []; - - /** - * @var null|array - */ - #[ORM\Column(type: 'simple_array', nullable: true)] - public $arrayData = []; - - /** - * @var null|string - */ - #[ORM\Column(nullable: true)] - public $nameConverted; - - /** - * @var null|RelatedOwnedDummy - */ - #[ORM\OneToOne(targetEntity: RelatedOwnedDummy::class, cascade: ['persist'], mappedBy: 'owningDummy')] - public $relatedOwnedDummy; - /** * @var null|RelatedOwningDummy */ #[ORM\OneToOne(targetEntity: RelatedOwningDummy::class, cascade: ['persist'], inversedBy: 'ownedDummy')] public $relatedOwningDummy; - /** - * @var null|int The id - */ - #[ORM\Column(type: 'integer', nullable: true)] - #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'AUTO')] - private $id; - - /** - * @var string The dummy name - */ - #[ApiProperty(iris: ['https://schema.org/name'])] - #[ORM\Column] - #[Assert\NotBlank] - private string $name; - - /** - * @var null|string The dummy name alias - */ - #[ApiProperty(iris: ['https://schema.org/alternateName'])] - #[ORM\Column(nullable: true)] - private $alias; - - /** - * @var array foo - */ - private ?array $foo = null; - public function __construct() { $this->relatedDummies = new ArrayCollection(); } - public static function staticMethod(): void {} - - public function getId() { - return $this->id; - } - - public function setId($id): void { - $this->id = $id; - } - - public function setName(string $name): void { - $this->name = $name; - } - - public function getName(): string { - return $this->name; - } - - public function setAlias($alias): void { - $this->alias = $alias; - } - - public function getAlias() { - return $this->alias; - } - - public function setDescription($description): void { - $this->description = $description; - } - - public function getDescription() { - return $this->description; - } - - public function fooBar($baz): void {} - - public function getFoo(): ?array { - return $this->foo; - } - - public function setFoo(?array $foo = null): void { - $this->foo = $foo; - } - - public function setDummyDate(?\DateTime $dummyDate = null): void { - $this->dummyDate = $dummyDate; - } - - public function getDummyDate() { - return $this->dummyDate; - } - - public function setDummyPrice($dummyPrice) { - $this->dummyPrice = $dummyPrice; - - return $this; - } - - public function getDummyPrice() { - return $this->dummyPrice; - } - - public function setJsonData($jsonData): void { - $this->jsonData = $jsonData; - } - - public function getJsonData() { - return $this->jsonData; - } - - public function setArrayData($arrayData): void { - $this->arrayData = $arrayData; - } - - public function getArrayData() { - return $this->arrayData; - } - public function getRelatedDummy(): ?RelatedDummy { return $this->relatedDummy; } @@ -229,17 +52,6 @@ public function addRelatedDummy(RelatedDummy $relatedDummy): void { $this->relatedDummies->add($relatedDummy); } - public function getRelatedOwnedDummy() { - return $this->relatedOwnedDummy; - } - - public function setRelatedOwnedDummy(RelatedOwnedDummy $relatedOwnedDummy): void { - $this->relatedOwnedDummy = $relatedOwnedDummy; - if ($this !== $this->relatedOwnedDummy->getOwningDummy()) { - $this->relatedOwnedDummy->setOwningDummy($this); - } - } - public function getRelatedOwningDummy() { return $this->relatedOwningDummy; } @@ -248,25 +60,6 @@ public function setRelatedOwningDummy(RelatedOwningDummy $relatedOwningDummy): v $this->relatedOwningDummy = $relatedOwningDummy; } - public function isDummyBoolean(): ?bool { - return $this->dummyBoolean; - } - - /** - * @param bool $dummyBoolean - */ - public function setDummyBoolean($dummyBoolean): void { - $this->dummyBoolean = $dummyBoolean; - } - - public function setDummy($dummy = null): void { - $this->dummy = $dummy; - } - - public function getDummy() { - return $this->dummy; - } - public function getRelatedDummies(): Collection|iterable { return $this->relatedDummies; } diff --git a/api/tests/HttpCache/Entity/DummyFriend.php b/api/tests/HttpCache/Entity/DummyFriend.php deleted file mode 100644 index 6974ad2edb..0000000000 --- a/api/tests/HttpCache/Entity/DummyFriend.php +++ /dev/null @@ -1,82 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Tests\HttpCache\Entity; - -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Metadata\ApiResource; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; -use Symfony\Component\Validator\Constraints as Assert; - -/** - * Dummy Friend. - * - * @author Kévin Dunglas - */ -#[ApiResource] -#[ORM\Entity] -class DummyFriend implements \Stringable { - /** - * @var null|int The id - */ - #[ORM\Column(type: 'integer')] - #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'AUTO')] - private ?int $id = null; - - /** - * @var string The dummy name - */ - #[ApiProperty(types: ['https://schema.org/name'])] - #[ORM\Column] - #[Assert\NotBlank] - #[Groups(['fakemanytomany', 'friends'])] - private string $name; - - public function __toString(): string { - return (string) $this->getId(); - } - - /** - * Get id. - */ - public function getId(): ?int { - return $this->id; - } - - /** - * Set id. - * - * @param int $id the value to set - */ - public function setId(int $id): void { - $this->id = $id; - } - - /** - * Get name. - */ - public function getName(): ?string { - return $this->name; - } - - /** - * Set name. - * - * @param string $name the value to set - */ - public function setName(string $name): void { - $this->name = $name; - } -} diff --git a/api/tests/HttpCache/Entity/DummyNoGetOperation.php b/api/tests/HttpCache/Entity/DummyNoGetOperation.php index 154c345767..1521623d86 100644 --- a/api/tests/HttpCache/Entity/DummyNoGetOperation.php +++ b/api/tests/HttpCache/Entity/DummyNoGetOperation.php @@ -25,22 +25,10 @@ */ #[ApiResource(operations: [new Put(), new Post()])] #[ORM\Entity] -class DummyNoGetOperation { +class DummyNoGetOperation extends BaseEntity { /** * @var string */ #[ORM\Column] public $lorem; - - /** - * @var int The id - */ - #[ORM\Column(type: 'integer')] - #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'AUTO')] - private $id; - - public function setId($id): void { - $this->id = $id; - } } diff --git a/api/tests/HttpCache/Entity/EmbeddableDummy.php b/api/tests/HttpCache/Entity/EmbeddableDummy.php deleted file mode 100644 index 4dc247f844..0000000000 --- a/api/tests/HttpCache/Entity/EmbeddableDummy.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Tests\HttpCache\Entity; - -use ApiPlatform\Metadata\ApiProperty; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; -use Symfony\Component\Validator\Constraints as Assert; - -/** - * Embeddable Dummy. - * - * @author Jordan Samouh - */ -#[ORM\Embeddable] -class EmbeddableDummy { - /** - * @var null|bool A dummy boolean - */ - #[ORM\Column(type: 'boolean', nullable: true)] - public ?bool $dummyBoolean = null; - - /** - * @var null|\DateTime A dummy date - */ - #[ORM\Column(type: 'datetime', nullable: true)] - #[Assert\DateTime] - public ?\DateTime $dummyDate = null; - - /** - * @var null|float A dummy float - */ - #[ORM\Column(type: 'float', nullable: true)] - public ?float $dummyFloat = null; - - /** - * @var null|string A dummy price - */ - #[ORM\Column(type: 'decimal', precision: 10, scale: 2, nullable: true)] - public ?string $dummyPrice = null; - #[ORM\Column(type: 'string', nullable: true)] - #[Groups(['barcelona', 'chicago'])] - protected $symfony; - - /** - * @var string The dummy name - */ - #[ApiProperty(identifier: true)] - #[ORM\Column(nullable: true)] - #[Groups(['embed'])] - private ?string $dummyName = null; - - public function __construct() {} - - public static function staticMethod(): void {} - - public function getDummyName(): ?string { - return $this->dummyName; - } - - public function setDummyName(string $dummyName): void { - $this->dummyName = $dummyName; - } - - public function isDummyBoolean(): ?bool { - return $this->dummyBoolean; - } - - public function setDummyBoolean(bool $dummyBoolean): void { - $this->dummyBoolean = $dummyBoolean; - } - - public function getDummyDate(): ?\DateTime { - return $this->dummyDate; - } - - public function setDummyDate(\DateTime $dummyDate): void { - $this->dummyDate = $dummyDate; - } - - public function getDummyFloat(): ?float { - return $this->dummyFloat; - } - - public function setDummyFloat(float $dummyFloat): void { - $this->dummyFloat = $dummyFloat; - } - - public function getDummyPrice(): ?string { - return $this->dummyPrice; - } - - public function setDummyPrice(string $dummyPrice): void { - $this->dummyPrice = $dummyPrice; - } - - public function getSymfony() { - return $this->symfony; - } - - public function setSymfony($symfony): void { - $this->symfony = $symfony; - } -} diff --git a/api/tests/HttpCache/Entity/FourthLevel.php b/api/tests/HttpCache/Entity/FourthLevel.php deleted file mode 100644 index cffc836cf0..0000000000 --- a/api/tests/HttpCache/Entity/FourthLevel.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Tests\HttpCache\Entity; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Link; -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; - -/** - * Fourth Level. - * - * @author Alan Poulain - */ -#[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel'), 'thirdLevel' => new Link(fromClass: ThirdLevel::class, identifiers: [], expandedValue: 'third_level', fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/third_levels/{id}/fourth_level{._format}', uriVariables: ['id' => new Link(fromClass: ThirdLevel::class, identifiers: ['id'], fromProperty: 'fourthLevel')], status: 200, operations: [new Get()])] -#[ORM\Entity] -class FourthLevel { - #[ORM\OneToMany(targetEntity: ThirdLevel::class, cascade: ['persist'], mappedBy: 'badFourthLevel')] - public null|Collection|iterable $badThirdLevel = null; - - /** - * @var null|int The id - */ - #[ORM\Column(type: 'integer')] - #[ORM\Id] - #[ORM\GeneratedValue] - private ?int $id = null; - #[ORM\Column(type: 'integer')] - #[Groups(['barcelona', 'chicago'])] - private int $level = 4; - - public function getId(): ?int { - return $this->id; - } - - public function getLevel(): ?int { - return $this->level; - } - - public function setLevel(int $level): void { - $this->level = $level; - } -} diff --git a/api/tests/HttpCache/Entity/NotAResource.php b/api/tests/HttpCache/Entity/NotAResource.php index 6ff72ea663..dc84939516 100644 --- a/api/tests/HttpCache/Entity/NotAResource.php +++ b/api/tests/HttpCache/Entity/NotAResource.php @@ -13,8 +13,6 @@ namespace App\Tests\HttpCache\Entity; -use Symfony\Component\Serializer\Annotation\Groups; - /** * This class is not mapped as an API resource. * @@ -22,9 +20,7 @@ */ class NotAResource { public function __construct( - #[Groups('contain_non_resource')] private $foo, - #[Groups('contain_non_resource')] private $bar ) {} diff --git a/api/tests/HttpCache/Entity/ParentDummy.php b/api/tests/HttpCache/Entity/ParentDummy.php deleted file mode 100644 index 4698b78fd4..0000000000 --- a/api/tests/HttpCache/Entity/ParentDummy.php +++ /dev/null @@ -1,40 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Tests\HttpCache\Entity; - -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; - -/** - * Parent Dummy. - * - * @author Kévin Dunglas - */ -#[ORM\MappedSuperclass] -class ParentDummy { - /** - * @var null|int The age - */ - #[ORM\Column(type: 'integer', nullable: true)] - #[Groups(['friends'])] - private $age; - - public function getAge() { - return $this->age; - } - - public function setAge($age) { - return $this->age = $age; - } -} diff --git a/api/tests/HttpCache/Entity/RelatedDummy.php b/api/tests/HttpCache/Entity/RelatedDummy.php index 0b2a525aa1..c5e82b4d65 100644 --- a/api/tests/HttpCache/Entity/RelatedDummy.php +++ b/api/tests/HttpCache/Entity/RelatedDummy.php @@ -13,177 +13,12 @@ namespace App\Tests\HttpCache\Entity; -use ApiPlatform\Doctrine\Orm\Filter\DateFilter; -use ApiPlatform\Doctrine\Orm\Filter\ExistsFilter; -use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; -use ApiPlatform\Metadata\ApiFilter; -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\GraphQl\Mutation; -use ApiPlatform\Metadata\GraphQl\Query; -use ApiPlatform\Metadata\Link; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; -use Symfony\Component\Validator\Constraints as Assert; /** * Related Dummy. * * @author Kévin Dunglas */ -#[ApiResource( - graphQlOperations: [ - new Query(name: 'item_query'), - new Mutation(name: 'update', normalizationContext: ['groups' => ['chicago', 'fakemanytomany']], denormalizationContext: ['groups' => ['friends']]), - ], - types: ['https://schema.org/Product'], - normalizationContext: ['groups' => ['friends']], - filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'] -)] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id{._format}', uriVariables: ['id' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies')], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: self::class, identifiers: ['id'])], status: 200, types: ['https://schema.org/Product'], filters: ['related_dummy.friends', 'related_dummy.complex_sub_query'], normalizationContext: ['groups' => ['friends']], operations: [new Get()])] -#[ApiFilter(filterClass: SearchFilter::class, properties: ['id'])] #[ORM\Entity] -class RelatedDummy extends ParentDummy implements \Stringable { - /** - * @var null|string A name - */ - #[ApiProperty(iris: ['RelatedDummy.name'])] - #[ORM\Column(nullable: true)] - #[Groups(['friends'])] - public $name; - - /** - * @var null|\DateTime A dummy date - */ - #[ORM\Column(type: 'datetime', nullable: true)] - #[Assert\DateTime] - #[Groups(['friends'])] - #[ApiFilter(filterClass: DateFilter::class)] - public $dummyDate; - - #[ORM\ManyToOne(targetEntity: ThirdLevel::class, cascade: ['persist'], inversedBy: 'relatedDummies')] - #[Groups(['barcelona', 'chicago', 'friends'])] - public ?ThirdLevel $thirdLevel = null; - - #[ORM\OneToMany(targetEntity: RelatedToDummyFriend::class, cascade: ['persist'], mappedBy: 'relatedDummy')] - #[Groups(['fakemanytomany', 'friends'])] - public Collection|iterable $relatedToDummyFriend; - - /** - * @var null|bool A dummy bool - */ - #[ORM\Column(type: 'boolean', nullable: true)] - #[Groups(['friends'])] - public ?bool $dummyBoolean = null; - - #[ORM\Embedded(class: 'EmbeddableDummy')] - #[Groups(['friends'])] - public ?EmbeddableDummy $embeddedDummy = null; - - #[ApiProperty(deprecationReason: 'This property is deprecated for upgrade test')] - #[ORM\Column] - #[Groups(['barcelona', 'chicago', 'friends'])] - #[ApiFilter(filterClass: SearchFilter::class)] - #[ApiFilter(filterClass: ExistsFilter::class)] - protected $symfony = 'symfony'; - #[ApiProperty(writable: false)] - #[ORM\Column(type: 'integer')] - #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'AUTO')] - #[Groups(['chicago', 'friends'])] - private $id; - - public function __construct() { - $this->relatedToDummyFriend = new ArrayCollection(); - $this->embeddedDummy = new EmbeddableDummy(); - } - - public function __toString(): string { - return (string) $this->getId(); - } - - public function getId() { - return $this->id; - } - - public function setId($id): void { - $this->id = $id; - } - - public function setName($name): void { - $this->name = $name; - } - - public function getName() { - return $this->name; - } - - public function getSymfony() { - return $this->symfony; - } - - public function setSymfony($symfony): void { - $this->symfony = $symfony; - } - - public function setDummyDate(\DateTime $dummyDate): void { - $this->dummyDate = $dummyDate; - } - - public function getDummyDate() { - return $this->dummyDate; - } - - public function isDummyBoolean(): ?bool { - return $this->dummyBoolean; - } - - /** - * @param bool $dummyBoolean - */ - public function setDummyBoolean($dummyBoolean): void { - $this->dummyBoolean = $dummyBoolean; - } - - public function getThirdLevel(): ?ThirdLevel { - return $this->thirdLevel; - } - - public function setThirdLevel(?ThirdLevel $thirdLevel = null): void { - $this->thirdLevel = $thirdLevel; - } - - /** - * Get relatedToDummyFriend. - */ - public function getRelatedToDummyFriend(): Collection|iterable { - return $this->relatedToDummyFriend; - } - - /** - * Set relatedToDummyFriend. - * - * @param RelatedToDummyFriend $relatedToDummyFriend the value to set - */ - public function addRelatedToDummyFriend(RelatedToDummyFriend $relatedToDummyFriend): void { - $this->relatedToDummyFriend->add($relatedToDummyFriend); - } - - public function getEmbeddedDummy(): EmbeddableDummy { - return $this->embeddedDummy; - } - - public function setEmbeddedDummy(EmbeddableDummy $embeddedDummy): void { - $this->embeddedDummy = $embeddedDummy; - } -} +class RelatedDummy extends BaseEntity {} diff --git a/api/tests/HttpCache/Entity/RelatedOwnedDummy.php b/api/tests/HttpCache/Entity/RelatedOwnedDummy.php deleted file mode 100644 index ae6b94a1e8..0000000000 --- a/api/tests/HttpCache/Entity/RelatedOwnedDummy.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Tests\HttpCache\Entity; - -use ApiPlatform\Metadata\ApiResource; -use Doctrine\ORM\Mapping as ORM; - -/** - * Related Owned Dummy. - * - * @author Sergey V. Ryabov - */ -#[ApiResource(types: ['https://schema.org/Product'])] -#[ORM\Entity] -class RelatedOwnedDummy { - /** - * @var null|string A name - */ - #[ORM\Column(nullable: true)] - public ?string $name = null; - #[ORM\OneToOne(targetEntity: Dummy::class, cascade: ['persist'], inversedBy: 'relatedOwnedDummy')] - #[ORM\JoinColumn(nullable: false)] - public ?Dummy $owningDummy = null; - #[ORM\Column(type: 'integer')] - #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'AUTO')] - private ?int $id = null; - - public function getId(): ?int { - return $this->id; - } - - public function setId(int $id): void { - $this->id = $id; - } - - public function setName(?string $name): void { - $this->name = $name; - } - - public function getName(): ?string { - return $this->name; - } - - /** - * Get owning dummy. - */ - public function getOwningDummy(): ?Dummy { - return $this->owningDummy; - } - - /** - * Set owning dummy. - * - * @param Dummy $owningDummy the value to set - */ - public function setOwningDummy(Dummy $owningDummy): void { - $this->owningDummy = $owningDummy; - } -} diff --git a/api/tests/HttpCache/Entity/RelatedOwningDummy.php b/api/tests/HttpCache/Entity/RelatedOwningDummy.php index a1911810ce..afbac8b50a 100644 --- a/api/tests/HttpCache/Entity/RelatedOwningDummy.php +++ b/api/tests/HttpCache/Entity/RelatedOwningDummy.php @@ -13,7 +13,6 @@ namespace App\Tests\HttpCache\Entity; -use ApiPlatform\Metadata\ApiResource; use Doctrine\ORM\Mapping as ORM; /** @@ -21,36 +20,10 @@ * * @author Sergey V. Ryabov */ -#[ApiResource(types: ['https://schema.org/Product'])] #[ORM\Entity] -class RelatedOwningDummy { - /** - * @var null|string A name - */ - #[ORM\Column(nullable: true)] - public $name; +class RelatedOwningDummy extends BaseEntity { #[ORM\OneToOne(targetEntity: Dummy::class, cascade: ['persist'], mappedBy: 'relatedOwningDummy')] public ?Dummy $ownedDummy = null; - #[ORM\Column(type: 'integer')] - #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'AUTO')] - private $id; - - public function getId() { - return $this->id; - } - - public function setId($id): void { - $this->id = $id; - } - - public function setName($name): void { - $this->name = $name; - } - - public function getName() { - return $this->name; - } /** * Get owned dummy. diff --git a/api/tests/HttpCache/Entity/RelatedToDummyFriend.php b/api/tests/HttpCache/Entity/RelatedToDummyFriend.php deleted file mode 100644 index 8e5bd9a149..0000000000 --- a/api/tests/HttpCache/Entity/RelatedToDummyFriend.php +++ /dev/null @@ -1,112 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Tests\HttpCache\Entity; - -use ApiPlatform\Metadata\ApiProperty; -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\GetCollection; -use ApiPlatform\Metadata\Link; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; -use Symfony\Component\Validator\Constraints as Assert; - -/** - * Related To Dummy Friend represent an association table for a manytomany relation. - */ -#[ApiResource(normalizationContext: ['groups' => ['fakemanytomany']], filters: ['related_to_dummy_friend.name'], extraProperties: ['rfc_7807_compliant_errors' => false])] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/related_to_dummy_friends{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], toProperty: 'relatedDummy')], status: 200, filters: ['related_to_dummy_friend.name'], normalizationContext: ['groups' => ['fakemanytomany']], operations: [new GetCollection()])] -#[ORM\Entity] -class RelatedToDummyFriend { - /** - * @var string The dummy name - */ - #[ApiProperty(types: ['https://schema.org/name'])] - #[ORM\Column] - #[Assert\NotBlank] - #[Groups(['fakemanytomany', 'friends'])] - private $name; - - /** - * @var null|string The dummy description - */ - #[ORM\Column(nullable: true)] - #[Groups(['fakemanytomany', 'friends'])] - private ?string $description = null; - #[ORM\Id] - #[ORM\ManyToOne(targetEntity: DummyFriend::class)] - #[ORM\JoinColumn(name: 'dummyfriend_id', referencedColumnName: 'id', nullable: false)] - #[Groups(['fakemanytomany', 'friends'])] - #[Assert\NotNull] - private DummyFriend $dummyFriend; - #[ORM\Id] - #[ORM\ManyToOne(targetEntity: RelatedDummy::class, inversedBy: 'relatedToDummyFriend')] - #[ORM\JoinColumn(name: 'relateddummy_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')] - #[Assert\NotNull] - private RelatedDummy $relatedDummy; - - public function setName($name): void { - $this->name = $name; - } - - public function getName() { - return $this->name; - } - - public function getDescription(): ?string { - return $this->description; - } - - /** - * @param null|string $description - */ - public function setDescription($description): void { - $this->description = $description; - } - - /** - * Gets dummyFriend. - */ - public function getDummyFriend(): DummyFriend { - return $this->dummyFriend; - } - - /** - * Sets dummyFriend. - * - * @param DummyFriend $dummyFriend the value to set - */ - public function setDummyFriend(DummyFriend $dummyFriend): void { - $this->dummyFriend = $dummyFriend; - } - - /** - * Gets relatedDummy. - */ - public function getRelatedDummy(): RelatedDummy { - return $this->relatedDummy; - } - - /** - * Sets relatedDummy. - * - * @param RelatedDummy $relatedDummy the value to set - */ - public function setRelatedDummy(RelatedDummy $relatedDummy): void { - $this->relatedDummy = $relatedDummy; - } -} diff --git a/api/tests/HttpCache/Entity/ThirdLevel.php b/api/tests/HttpCache/Entity/ThirdLevel.php deleted file mode 100644 index c13df9b8e2..0000000000 --- a/api/tests/HttpCache/Entity/ThirdLevel.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace App\Tests\HttpCache\Entity; - -use ApiPlatform\Metadata\ApiResource; -use ApiPlatform\Metadata\Get; -use ApiPlatform\Metadata\Link; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; - -/** - * Third Level. - * - * @author Kévin Dunglas - */ -#[ApiResource] -#[ApiResource(uriTemplate: '/dummies/{id}/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: Dummy::class, identifiers: ['id'], fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/id/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_dummies/{id}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owned_dummies/{id}/owning_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwnedDummy::class, identifiers: ['id'], fromProperty: 'owningDummy'), 'owningDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owning_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ApiResource(uriTemplate: '/related_owning_dummies/{id}/owned_dummy/related_dummies/{relatedDummies}/third_level{._format}', uriVariables: ['id' => new Link(fromClass: RelatedOwningDummy::class, identifiers: ['id'], fromProperty: 'ownedDummy'), 'ownedDummy' => new Link(fromClass: Dummy::class, identifiers: [], expandedValue: 'owned_dummy', fromProperty: 'relatedDummies'), 'relatedDummies' => new Link(fromClass: RelatedDummy::class, identifiers: ['id'], fromProperty: 'thirdLevel')], status: 200, operations: [new Get()])] -#[ORM\Entity] -class ThirdLevel { - #[ORM\ManyToOne(targetEntity: FourthLevel::class, cascade: ['persist'])] - #[Groups(['barcelona', 'chicago', 'friends'])] - public ?FourthLevel $fourthLevel = null; - #[ORM\ManyToOne(targetEntity: FourthLevel::class, cascade: ['persist'])] - public $badFourthLevel; - - #[ORM\OneToMany(mappedBy: 'thirdLevel', targetEntity: RelatedDummy::class)] - public Collection|iterable $relatedDummies; - - /** - * @var null|int The id - */ - #[ORM\Column(type: 'integer')] - #[ORM\Id] - #[ORM\GeneratedValue(strategy: 'AUTO')] - private ?int $id = null; - #[ORM\Column(type: 'integer')] - #[Groups(['barcelona', 'chicago'])] - private int $level = 3; - #[ORM\Column(type: 'boolean')] - private bool $test = true; - - public function __construct() { - $this->relatedDummies = new ArrayCollection(); - } - - public function getId(): ?int { - return $this->id; - } - - public function getLevel(): ?int { - return $this->level; - } - - /** - * @param int $level - */ - public function setLevel($level): void { - $this->level = $level; - } - - public function isTest(): bool { - return $this->test; - } - - /** - * @param bool $test - */ - public function setTest($test): void { - $this->test = $test; - } - - public function getFourthLevel(): ?FourthLevel { - return $this->fourthLevel; - } - - public function setFourthLevel(?FourthLevel $fourthLevel = null): void { - $this->fourthLevel = $fourthLevel; - } -} From ec122df54199026a3a1f96f12d1a1819cb2c47de Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 28 Apr 2024 23:18:38 +0200 Subject: [PATCH 53/81] fix & adapt legacy tests --- .../HttpCache/Entity/ContainNonResource.php | 11 +- api/tests/HttpCache/Entity/Dummy.php | 17 --- api/tests/HttpCache/Entity/RelatedDummy.php | 11 +- .../HttpCache/PurgeHttpCacheListenerTest.php | 116 +++++++++++------- 4 files changed, 84 insertions(+), 71 deletions(-) diff --git a/api/tests/HttpCache/Entity/ContainNonResource.php b/api/tests/HttpCache/Entity/ContainNonResource.php index c1f7e2426e..84eaf510b9 100644 --- a/api/tests/HttpCache/Entity/ContainNonResource.php +++ b/api/tests/HttpCache/Entity/ContainNonResource.php @@ -14,7 +14,6 @@ namespace App\Tests\HttpCache\Entity; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Groups; /** * Resource linked to a standard object. @@ -24,14 +23,12 @@ #[ORM\Entity] class ContainNonResource extends BaseEntity { /** - * @var ContainNonResource + * @var NotAResource */ - #[Groups('contain_non_resource')] - public $nested; + public $notAResource; /** - * @var NotAResource + * @var NotAResource[] */ - #[Groups('contain_non_resource')] - public $notAResource; + public $collectionOfNotAResource; } diff --git a/api/tests/HttpCache/Entity/Dummy.php b/api/tests/HttpCache/Entity/Dummy.php index 7ccd50806d..5faf455df9 100644 --- a/api/tests/HttpCache/Entity/Dummy.php +++ b/api/tests/HttpCache/Entity/Dummy.php @@ -13,8 +13,6 @@ namespace App\Tests\HttpCache\Entity; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -27,19 +25,12 @@ class Dummy extends BaseEntity { #[ORM\ManyToOne(targetEntity: RelatedDummy::class)] public ?RelatedDummy $relatedDummy = null; - #[ORM\ManyToMany(targetEntity: RelatedDummy::class)] - public Collection|iterable $relatedDummies; - /** * @var null|RelatedOwningDummy */ #[ORM\OneToOne(targetEntity: RelatedOwningDummy::class, cascade: ['persist'], inversedBy: 'ownedDummy')] public $relatedOwningDummy; - public function __construct() { - $this->relatedDummies = new ArrayCollection(); - } - public function getRelatedDummy(): ?RelatedDummy { return $this->relatedDummy; } @@ -48,10 +39,6 @@ public function setRelatedDummy(RelatedDummy $relatedDummy): void { $this->relatedDummy = $relatedDummy; } - public function addRelatedDummy(RelatedDummy $relatedDummy): void { - $this->relatedDummies->add($relatedDummy); - } - public function getRelatedOwningDummy() { return $this->relatedOwningDummy; } @@ -59,8 +46,4 @@ public function getRelatedOwningDummy() { public function setRelatedOwningDummy(RelatedOwningDummy $relatedOwningDummy): void { $this->relatedOwningDummy = $relatedOwningDummy; } - - public function getRelatedDummies(): Collection|iterable { - return $this->relatedDummies; - } } diff --git a/api/tests/HttpCache/Entity/RelatedDummy.php b/api/tests/HttpCache/Entity/RelatedDummy.php index c5e82b4d65..a812f9c9f7 100644 --- a/api/tests/HttpCache/Entity/RelatedDummy.php +++ b/api/tests/HttpCache/Entity/RelatedDummy.php @@ -13,6 +13,8 @@ namespace App\Tests\HttpCache\Entity; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; /** @@ -21,4 +23,11 @@ * @author Kévin Dunglas */ #[ORM\Entity] -class RelatedDummy extends BaseEntity {} +class RelatedDummy extends BaseEntity { + #[ORM\OneToMany(targetEntity: Dummy::class)] + public Collection|iterable $dummies; + + public function __construct() { + $this->dummies = new ArrayCollection(); + } +} diff --git a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php index f12e691060..4b3932eda1 100644 --- a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php +++ b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php @@ -13,24 +13,28 @@ namespace App\Tests\HttpCache; -use ApiPlatform\Doctrine\EventListener\PurgeHttpCacheListener; -use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\HttpCache\PurgerInterface; +use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\IriConverterInterface; +use ApiPlatform\Metadata\Operations; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; +use App\HttpCache\PurgeHttpCacheListener; use App\Tests\HttpCache\Entity\ContainNonResource; use App\Tests\HttpCache\Entity\Dummy; use App\Tests\HttpCache\Entity\DummyNoGetOperation; use App\Tests\HttpCache\Entity\NotAResource; use App\Tests\HttpCache\Entity\RelatedDummy; +use App\Tests\HttpCache\Entity\RelatedOwningDummy; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Event\PreUpdateEventArgs; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\UnitOfWork; +use FOS\HttpCacheBundle\CacheManager; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -48,11 +52,6 @@ public function testOnFlush(): void { $toInsert1 = new Dummy(); $toInsert2 = new Dummy(); - $toUpdate1 = new Dummy(); - $toUpdate1->setId(1); - $toUpdate2 = new Dummy(); - $toUpdate2->setId(2); - $toDelete1 = new Dummy(); $toDelete1->setId(3); $toDelete2 = new Dummy(); @@ -61,18 +60,31 @@ public function testOnFlush(): void { $toDeleteNoPurge = new DummyNoGetOperation(); $toDeleteNoPurge->setId(5); - $purgerProphecy = $this->prophesize(PurgerInterface::class); - $purgerProphecy->purge(['/dummies', '/dummies/1', '/dummies/2', '/dummies/3', '/dummies/4'])->shouldBeCalled(); + $cacheManagerProphecy = $this->prophesize(CacheManager::class); + $cacheManagerProphecy->invalidateTags(['/dummies'])->willReturn($cacheManagerProphecy)->shouldBeCalled(); + $cacheManagerProphecy->invalidateTags(['/dummies/3'])->willReturn($cacheManagerProphecy)->shouldBeCalled(); + $cacheManagerProphecy->invalidateTags(['/dummies/4'])->willReturn($cacheManagerProphecy)->shouldBeCalled(); + $cacheManagerProphecy->flush(Argument::any())->willReturn(0); + + $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $operation = (new GetCollection())->withShortName('Dummy')->withClass(Dummy::class); + $metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [ + (new ApiResource('Dummy')) + ->withShortName('Dummy') + ->withOperations(new Operations([ + 'get' => $operation, + ])), + ]))->shouldBeCalled(); + $metadataFactoryProphecy->create(DummyNoGetOperation::class)->willReturn(new ResourceMetadataCollection('DummyNoGetOperation', [ + (new ApiResource('DummyNoGetOperation')) + ->withShortName('DummyNoGetOperation'), + ]))->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, new GetCollection())->willReturn('/dummies')->shouldBeCalled(); - $iriConverterProphecy->getIriFromResource(DummyNoGetOperation::class, UrlGeneratorInterface::ABS_PATH, new GetCollection())->willThrow(new InvalidArgumentException())->shouldBeCalled(); - $iriConverterProphecy->getIriFromResource($toUpdate1)->willReturn('/dummies/1')->shouldBeCalled(); - $iriConverterProphecy->getIriFromResource($toUpdate2)->willReturn('/dummies/2')->shouldBeCalled(); + $iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, Argument::type(GetCollection::class))->willReturn('/dummies')->shouldBeCalled(); $iriConverterProphecy->getIriFromResource($toDelete1)->willReturn('/dummies/3')->shouldBeCalled(); $iriConverterProphecy->getIriFromResource($toDelete2)->willReturn('/dummies/4')->shouldBeCalled(); - $iriConverterProphecy->getIriFromResource($toDeleteNoPurge)->shouldNotBeCalled(); - $iriConverterProphecy->getIriFromResource(Argument::any())->willThrow(new ItemNotFoundException()); + $iriConverterProphecy->getIriFromResource($toDeleteNoPurge)->willReturn(null)->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled(); @@ -81,17 +93,14 @@ public function testOnFlush(): void { $uowProphecy = $this->prophesize(UnitOfWork::class); $uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1, $toInsert2])->shouldBeCalled(); - $uowProphecy->getScheduledEntityUpdates()->willReturn([$toUpdate1, $toUpdate2])->shouldBeCalled(); $uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete1, $toDelete2, $toDeleteNoPurge])->shouldBeCalled(); $emProphecy = $this->prophesize(EntityManagerInterface::class); $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled(); $dummyClassMetadata = new ClassMetadata(Dummy::class); - // @phpstan-ignore-next-line - $dummyClassMetadata->associationMappings = [ - 'relatedDummy' => [], - 'relatedOwningDummy' => [], - ]; + $dummyClassMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']); + $dummyClassMetadata->mapOneToOne(['fieldName' => 'relatedOwningDummy', 'targetEntity' => RelatedOwningDummy::class, 'inversedBy' => 'ownedDummy']); + $emProphecy->getClassMetadata(Dummy::class)->willReturn($dummyClassMetadata)->shouldBeCalled(); $emProphecy->getClassMetadata(DummyNoGetOperation::class)->willReturn(new ClassMetadata(DummyNoGetOperation::class))->shouldBeCalled(); $eventArgs = new OnFlushEventArgs($emProphecy->reveal()); @@ -99,10 +108,10 @@ public function testOnFlush(): void { $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); $propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedDummy')->willReturn(true); $propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(false); - $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedDummy')->willReturn(null)->shouldBeCalled(); - $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(null)->shouldNotBeCalled(); + $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedDummy')->willReturn(null); + $propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(null); - $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal()); + $listener = new PurgeHttpCacheListener($iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), $metadataFactoryProphecy->reveal(), $cacheManagerProphecy->reveal()); $listener->onFlush($eventArgs); $listener->postFlush(); } @@ -117,29 +126,34 @@ public function testPreUpdate(): void { $dummy = new Dummy(); $dummy->setId(1); - $purgerProphecy = $this->prophesize(PurgerInterface::class); - $purgerProphecy->purge(['/dummies', '/dummies/1', '/related_dummies/old', '/related_dummies/new'])->shouldBeCalled(); + $cacheManagerProphecy = $this->prophesize(CacheManager::class); + $cacheManagerProphecy->invalidateTags(['/dummies/1'])->shouldBeCalled()->willReturn($cacheManagerProphecy); + $cacheManagerProphecy->invalidateTags(['/related_dummies/old#dummies'])->shouldBeCalled()->willReturn($cacheManagerProphecy); + $cacheManagerProphecy->invalidateTags(['/related_dummies/new#dummies'])->shouldBeCalled()->willReturn($cacheManagerProphecy); + $cacheManagerProphecy->flush(Argument::any())->willReturn(0); + + $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(Dummy::class, UrlGeneratorInterface::ABS_PATH, new GetCollection())->willReturn('/dummies')->shouldBeCalled(); $iriConverterProphecy->getIriFromResource($dummy)->willReturn('/dummies/1')->shouldBeCalled(); $iriConverterProphecy->getIriFromResource($oldRelatedDummy)->willReturn('/related_dummies/old')->shouldBeCalled(); $iriConverterProphecy->getIriFromResource($newRelatedDummy)->willReturn('/related_dummies/new')->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled(); - $resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class)->shouldBeCalled(); $emProphecy = $this->prophesize(EntityManagerInterface::class); $classMetadata = new ClassMetadata(Dummy::class); - $classMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class]); + $classMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']); $emProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadata)->shouldBeCalled(); $changeSet = ['relatedDummy' => [$oldRelatedDummy, $newRelatedDummy]]; $eventArgs = new PreUpdateEventArgs($dummy, $emProphecy->reveal(), $changeSet); - $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal()); + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $listener = new PurgeHttpCacheListener($iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), $metadataFactoryProphecy->reveal(), $cacheManagerProphecy->reveal()); $listener->preUpdate($eventArgs); $listener->postFlush(); } @@ -151,12 +165,17 @@ public function testNothingToPurge(): void { $purgerProphecy = $this->prophesize(PurgerInterface::class); $purgerProphecy->purge([])->shouldNotBeCalled(); + $cacheManagerProphecy = $this->prophesize(CacheManager::class); + $cacheManagerProphecy->invalidateTags(Argument::any())->shouldNotBeCalled(); + $cacheManagerProphecy->flush(Argument::any())->willReturn(0); + + $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource(DummyNoGetOperation::class, UrlGeneratorInterface::ABS_PATH, new GetCollection())->willThrow(new InvalidArgumentException())->shouldBeCalled(); - $iriConverterProphecy->getIriFromResource($dummyNoGetOperation)->shouldNotBeCalled(); + $iriConverterProphecy->getIriFromResource($dummyNoGetOperation)->willReturn(null)->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyNoGetOperation::class))->willReturn(DummyNoGetOperation::class)->shouldBeCalled(); + $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled(); $emProphecy = $this->prophesize(EntityManagerInterface::class); @@ -166,7 +185,9 @@ public function testNothingToPurge(): void { $changeSet = ['lorem' => 'ipsum']; $eventArgs = new PreUpdateEventArgs($dummyNoGetOperation, $emProphecy->reveal(), $changeSet); - $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal()); + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + + $listener = new PurgeHttpCacheListener($iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), $metadataFactoryProphecy->reveal(), $cacheManagerProphecy->reveal()); $listener->preUpdate($eventArgs); $listener->postFlush(); } @@ -175,8 +196,15 @@ public function testNotAResourceClass(): void { $containNonResource = new ContainNonResource(); $nonResource = new NotAResource('foo', 'bar'); - $purgerProphecy = $this->prophesize(PurgerInterface::class); - $purgerProphecy->purge([])->shouldNotBeCalled(); + $cacheManagerProphecy = $this->prophesize(CacheManager::class); + $cacheManagerProphecy->invalidateTags(Argument::any())->shouldNotBeCalled(); + $cacheManagerProphecy->flush(Argument::any())->willReturn(0); + + $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $metadataFactoryProphecy->create(ContainNonResource::class)->willReturn(new ResourceMetadataCollection('ContainNonResource', [ + (new ApiResource('ContainNonResource')) + ->withShortName('ContainNonResource'), + ]))->shouldBeCalled(); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $iriConverterProphecy->getIriFromResource(ContainNonResource::class, UrlGeneratorInterface::ABS_PATH, Argument::any())->willReturn('/dummies/1'); @@ -188,28 +216,24 @@ public function testNotAResourceClass(): void { $uowProphecy = $this->prophesize(UnitOfWork::class); $uowProphecy->getScheduledEntityInsertions()->willReturn([$containNonResource])->shouldBeCalled(); - $uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled(); $uowProphecy->getScheduledEntityDeletions()->willReturn([])->shouldBeCalled(); $emProphecy = $this->prophesize(EntityManagerInterface::class); $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled(); $dummyClassMetadata = new ClassMetadata(ContainNonResource::class); - // @phpstan-ignore-next-line - $dummyClassMetadata->associationMappings = [ - 'notAResource' => [], - 'collectionOfNotAResource' => ['targetEntity' => NotAResource::class], - ]; + $dummyClassMetadata->mapManyToOne(['fieldName' => 'notAResource', 'targetEntity' => NotAResource::class, 'inversedBy' => 'resources']); + $dummyClassMetadata->mapOneToMany(['fieldName' => 'collectionOfNotAResource', 'targetEntity' => NotAResource::class, 'mappedBy' => 'resource']); $emProphecy->getClassMetadata(ContainNonResource::class)->willReturn($dummyClassMetadata); $eventArgs = new OnFlushEventArgs($emProphecy->reveal()); $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'notAResource')->willReturn(true); - $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldNotBeCalled(); + $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->willReturn(true); $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'notAResource')->shouldBeCalled()->willReturn($nonResource); - $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldNotBeCalled(); + $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldBeCalled()->willReturn([$nonResource]); - $listener = new PurgeHttpCacheListener($purgerProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal()); + $listener = new PurgeHttpCacheListener($iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), $metadataFactoryProphecy->reveal(), $cacheManagerProphecy->reveal()); $listener->onFlush($eventArgs); } } From d846b19f7d9f40510a4e445c07a479fe6befb4a5 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 28 Apr 2024 23:25:37 +0200 Subject: [PATCH 54/81] fix phpstan --- .../Serializer/Normalizer/RelatedCollectionLinkNormalizer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/Serializer/Normalizer/RelatedCollectionLinkNormalizer.php b/api/src/Serializer/Normalizer/RelatedCollectionLinkNormalizer.php index 54bcbe29f1..d35edb69b7 100644 --- a/api/src/Serializer/Normalizer/RelatedCollectionLinkNormalizer.php +++ b/api/src/Serializer/Normalizer/RelatedCollectionLinkNormalizer.php @@ -2,7 +2,7 @@ namespace App\Serializer\Normalizer; -use ApiPlatform\Api\FilterInterface; +use ApiPlatform\Doctrine\Common\Filter\SearchFilterInterface; use ApiPlatform\Doctrine\Common\PropertyHelperTrait; use ApiPlatform\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Exception\OperationNotFoundException; @@ -256,7 +256,7 @@ private function exactSearchFilterExists(string $resourceClass, mixed $propertyN $filterIds = OperationHelper::findOneByType($resourceMetadataCollection, GetCollection::class)?->getFilters() ?? []; return 0 < count(array_filter($filterIds, function ($filterId) use ($resourceClass, $propertyName) { - /** @var FilterInterface $filter */ + /** @var SearchFilterInterface $filter */ $filter = $this->filterLocator->get($filterId); if (!$filter instanceof SearchFilter) { return false; From 7be7027f5f4dc325aaaa92ec47188d7221a4409b Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sun, 28 Apr 2024 23:34:57 +0200 Subject: [PATCH 55/81] fix e2e test --- e2e/specs/httpCache.cy.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index f502331cb8..67c2fceb84 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -9,7 +9,7 @@ describe('HTTP cache tests', () => { cy.request(Cypress.env('API_ROOT_URL_CACHED') + uri + '.jsonhal').then((response) => { const headers = response.headers expect(headers.xkey).to.eq( - 'a4211c112939 f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' + 'c462edd869f3 5e2028c55ee4 a4211c112939 f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' ) expect(headers['x-cache']).to.eq('MISS') }) @@ -78,7 +78,7 @@ describe('HTTP cache tests', () => { // bring data into defined state Cypress.session.clearAllSavedSessions() - cy.login('bruce@wayne.com') + cy.login('felicity@smoak.com') cy.apiPatch('/api/categories/c5e1bc565094', { name: 'old_name', }) @@ -98,7 +98,7 @@ describe('HTTP cache tests', () => { // ensure cache was invalidated cy.expectCacheMiss(uri) - cy.login('bruce@wayne.com') + cy.login('felicity@smoak.com') cy.expectCacheMiss(uri) }) From ff3d262966b996935d3f8a790315b4ad0783143a Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 06:53:59 +0200 Subject: [PATCH 56/81] upgrade to api-platform 3.3.2 --- api/composer.json | 2 +- api/composer.lock | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/api/composer.json b/api/composer.json index a305e837ba..99a7b3f3f7 100644 --- a/api/composer.json +++ b/api/composer.json @@ -5,7 +5,7 @@ "php": ">=8.1.0", "ext-ctype": "*", "ext-iconv": "*", - "api-platform/core": "dev-main", + "api-platform/core": "3.3.2", "composer/package-versions-deprecated": "1.11.99", "cweagans/composer-patches": "1.7.3", "doctrine/doctrine-bundle": "2.12.0", diff --git a/api/composer.lock b/api/composer.lock index 38a4a5e30e..0119ba608b 100644 --- a/api/composer.lock +++ b/api/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "f8382add31d7073112720b2f7d3bb601", + "content-hash": "c553565df5491c586cec73e9e3702046", "packages": [ { "name": "api-platform/core", - "version": "dev-main", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/api-platform/core.git", - "reference": "e867d07f59b82d5f1bdca69e096ddf452dd7efc8" + "reference": "6fa4bb7ff347a77688bf1815ea3dcb9db38a2e52" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/api-platform/core/zipball/e867d07f59b82d5f1bdca69e096ddf452dd7efc8", - "reference": "e867d07f59b82d5f1bdca69e096ddf452dd7efc8", + "url": "https://api.github.com/repos/api-platform/core/zipball/6fa4bb7ff347a77688bf1815ea3dcb9db38a2e52", + "reference": "6fa4bb7ff347a77688bf1815ea3dcb9db38a2e52", "shasum": "" }, "require": { @@ -130,7 +130,6 @@ "symfony/web-profiler-bundle": "To use the data collector.", "webonyx/graphql-php": "To support GraphQL." }, - "default-branch": true, "type": "library", "extra": { "branch-alias": { @@ -191,9 +190,9 @@ ], "support": { "issues": "https://github.com/api-platform/core/issues", - "source": "https://github.com/api-platform/core/tree/main" + "source": "https://github.com/api-platform/core/tree/v3.3.2" }, - "time": "2024-04-29T13:25:15+00:00" + "time": "2024-05-03T09:37:25+00:00" }, { "name": "behat/transliterator", @@ -14367,7 +14366,6 @@ "aliases": [], "minimum-stability": "stable", "stability-flags": { - "api-platform/core": 20, "friendsofsymfony/http-cache": 20, "friendsofsymfony/http-cache-bundle": 20 }, From 04c764e47d63ed75e9e6a3c041e2217b98341421 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 07:33:30 +0200 Subject: [PATCH 57/81] extend e2e tests by comparing response body --- e2e/specs/httpCache.cy.js | 15 +- .../responses/categories_collection.json | 254 ++++++++++++++++++ .../responses/content_types_collection.json | 174 ++++++++++++ e2e/specs/responses/content_types_entity.json | 13 + 4 files changed, 453 insertions(+), 3 deletions(-) create mode 100644 e2e/specs/responses/categories_collection.json create mode 100644 e2e/specs/responses/content_types_collection.json create mode 100644 e2e/specs/responses/content_types_entity.json diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index 67c2fceb84..5fb034d0c8 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -12,6 +12,9 @@ describe('HTTP cache tests', () => { 'c462edd869f3 5e2028c55ee4 a4211c112939 f17470519474 1a0f84e322c8 3ef17bd1df72 4f0c657fecef 44dcc7493c65 cfccaecd4bad 318e064ea0c9 /api/content_types' ) expect(headers['x-cache']).to.eq('MISS') + cy.readFile('./specs/responses/content_types_collection.json').then((data) => + expect(response.body).to.deep.equal(data) + ) }) // second request is a cache hit @@ -33,6 +36,9 @@ describe('HTTP cache tests', () => { const headers = response.headers expect(headers.xkey).to.eq('318e064ea0c9') expect(headers['x-cache']).to.eq('MISS') + cy.readFile('./specs/responses/content_types_entity.json').then((data) => + expect(response.body).to.deep.equal(data) + ) }) // second request is a cache hit @@ -63,6 +69,9 @@ describe('HTTP cache tests', () => { '/api/camps/3c79b99ab424/categories' ) expect(headers['x-cache']).to.eq('MISS') + cy.readFile('./specs/responses/categories_collection.json').then((data) => + expect(response.body).to.deep.equal(data) + ) }) // second request is a cache hit @@ -78,7 +87,7 @@ describe('HTTP cache tests', () => { // bring data into defined state Cypress.session.clearAllSavedSessions() - cy.login('felicity@smoak.com') + cy.login('bruce@wayne.com') cy.apiPatch('/api/categories/c5e1bc565094', { name: 'old_name', }) @@ -87,7 +96,7 @@ describe('HTTP cache tests', () => { cy.expectCacheMiss(uri) cy.expectCacheHit(uri) - cy.login('test@example.com') + cy.login('felicity@smoak.com') cy.expectCacheMiss(uri) cy.expectCacheHit(uri) @@ -98,7 +107,7 @@ describe('HTTP cache tests', () => { // ensure cache was invalidated cy.expectCacheMiss(uri) - cy.login('felicity@smoak.com') + cy.login('bruce@wayne.com') cy.expectCacheMiss(uri) }) diff --git a/e2e/specs/responses/categories_collection.json b/e2e/specs/responses/categories_collection.json new file mode 100644 index 0000000000..b03855db2b --- /dev/null +++ b/e2e/specs/responses/categories_collection.json @@ -0,0 +1,254 @@ +{ + "_links": { + "self": { + "href": "/api/camps/3c79b99ab424/categories.jsonhal" + }, + "items": [ + { + "href": "/api/categories/ebfd46a1c181" + }, + { + "href": "/api/categories/1a869b162875" + }, + { + "href": "/api/categories/dfa531302823" + }, + { + "href": "/api/categories/a023e85227ac" + } + ] + }, + "totalItems": 4, + "_embedded": { + "items": [ + { + "_links": { + "self": { + "href": "/api/categories/ebfd46a1c181" + }, + "camp": { + "href": "/api/camps/3c79b99ab424" + }, + "preferredContentTypes": { + "href": "/api/content_types?categories=%2Fapi%2Fcategories%2Febfd46a1c181" + }, + "rootContentNode": { + "href": "/api/content_node/column_layouts/9d7b3a220fb4" + }, + "contentNodes": { + "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2F9d7b3a220fb4" + } + }, + "_embedded": { + "rootContentNode": { + "_links": { + "self": { + "href": "/api/content_node/column_layouts/9d7b3a220fb4" + }, + "root": { + "href": "/api/content_node/column_layouts/9d7b3a220fb4" + }, + "parent": null, + "children": [], + "contentType": { + "href": "/api/content_types/f17470519474" + } + }, + "data": { + "columns": [ + { + "slot": "1", + "width": 12 + } + ] + }, + "slot": null, + "position": 0, + "instanceName": null, + "id": "9d7b3a220fb4", + "contentTypeName": "ColumnLayout" + } + }, + "short": "ES", + "name": "Essen", + "color": "#BBBBBB", + "numberingStyle": "-", + "id": "ebfd46a1c181" + }, + { + "_links": { + "self": { + "href": "/api/categories/1a869b162875" + }, + "camp": { + "href": "/api/camps/3c79b99ab424" + }, + "preferredContentTypes": { + "href": "/api/content_types?categories=%2Fapi%2Fcategories%2F1a869b162875" + }, + "rootContentNode": { + "href": "/api/content_node/column_layouts/be9b6b7f23f6" + }, + "contentNodes": { + "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2Fbe9b6b7f23f6" + } + }, + "_embedded": { + "rootContentNode": { + "_links": { + "self": { + "href": "/api/content_node/column_layouts/be9b6b7f23f6" + }, + "root": { + "href": "/api/content_node/column_layouts/be9b6b7f23f6" + }, + "parent": null, + "children": [ + { + "href": "/api/content_node/responsive_layouts/179ba93a4bb9" + } + ], + "contentType": { + "href": "/api/content_types/f17470519474" + } + }, + "data": { + "columns": [ + { + "slot": "1", + "width": 12 + } + ] + }, + "slot": null, + "position": 0, + "instanceName": null, + "id": "be9b6b7f23f6", + "contentTypeName": "ColumnLayout" + } + }, + "short": "LA", + "name": "Lageraktivität", + "color": "#FF9800", + "numberingStyle": "A", + "id": "1a869b162875" + }, + { + "_links": { + "self": { + "href": "/api/categories/dfa531302823" + }, + "camp": { + "href": "/api/camps/3c79b99ab424" + }, + "preferredContentTypes": { + "href": "/api/content_types?categories=%2Fapi%2Fcategories%2Fdfa531302823" + }, + "rootContentNode": { + "href": "/api/content_node/column_layouts/63cbc734fa04" + }, + "contentNodes": { + "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2F63cbc734fa04" + } + }, + "_embedded": { + "rootContentNode": { + "_links": { + "self": { + "href": "/api/content_node/column_layouts/63cbc734fa04" + }, + "root": { + "href": "/api/content_node/column_layouts/63cbc734fa04" + }, + "parent": null, + "children": [ + { + "href": "/api/content_node/responsive_layouts/801027c511e6" + } + ], + "contentType": { + "href": "/api/content_types/f17470519474" + } + }, + "data": { + "columns": [ + { + "slot": "1", + "width": 12 + } + ] + }, + "slot": null, + "position": 0, + "instanceName": null, + "id": "63cbc734fa04", + "contentTypeName": "ColumnLayout" + } + }, + "short": "LP", + "name": "Lagerprogramm", + "color": "#90B7E4", + "numberingStyle": "1", + "id": "dfa531302823" + }, + { + "_links": { + "self": { + "href": "/api/categories/a023e85227ac" + }, + "camp": { + "href": "/api/camps/3c79b99ab424" + }, + "preferredContentTypes": { + "href": "/api/content_types?categories=%2Fapi%2Fcategories%2Fa023e85227ac" + }, + "rootContentNode": { + "href": "/api/content_node/column_layouts/2cce9e17a368" + }, + "contentNodes": { + "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2F2cce9e17a368" + } + }, + "_embedded": { + "rootContentNode": { + "_links": { + "self": { + "href": "/api/content_node/column_layouts/2cce9e17a368" + }, + "root": { + "href": "/api/content_node/column_layouts/2cce9e17a368" + }, + "parent": null, + "children": [ + { + "href": "/api/content_node/responsive_layouts/80d79bc8f484" + } + ], + "contentType": { + "href": "/api/content_types/f17470519474" + } + }, + "data": { + "columns": [ + { + "slot": "1", + "width": 12 + } + ] + }, + "slot": null, + "position": 0, + "instanceName": "est", + "id": "2cce9e17a368", + "contentTypeName": "ColumnLayout" + } + }, + "short": "LS", + "name": "Lagersport", + "color": "#4DBB52", + "numberingStyle": "1", + "id": "a023e85227ac" + } + ] + } +} \ No newline at end of file diff --git a/e2e/specs/responses/content_types_collection.json b/e2e/specs/responses/content_types_collection.json new file mode 100644 index 0000000000..a1361e57ab --- /dev/null +++ b/e2e/specs/responses/content_types_collection.json @@ -0,0 +1,174 @@ +{ + "_links": { + "self": { + "href": "/api/content_types.jsonhal" + }, + "items": [ + { + "href": "/api/content_types/c462edd869f3" + }, + { + "href": "/api/content_types/5e2028c55ee4" + }, + { + "href": "/api/content_types/a4211c112939" + }, + { + "href": "/api/content_types/f17470519474" + }, + { + "href": "/api/content_types/1a0f84e322c8" + }, + { + "href": "/api/content_types/3ef17bd1df72" + }, + { + "href": "/api/content_types/4f0c657fecef" + }, + { + "href": "/api/content_types/44dcc7493c65" + }, + { + "href": "/api/content_types/cfccaecd4bad" + }, + { + "href": "/api/content_types/318e064ea0c9" + } + ] + }, + "totalItems": 10, + "_embedded": { + "items": [ + { + "_links": { + "self": { + "href": "/api/content_types/c462edd869f3" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2Fc462edd869f3" + } + }, + "name": "LearningObjectives", + "active": true, + "id": "c462edd869f3" + }, + { + "_links": { + "self": { + "href": "/api/content_types/5e2028c55ee4" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F5e2028c55ee4" + } + }, + "name": "LearningTopics", + "active": true, + "id": "5e2028c55ee4" + }, + { + "_links": { + "self": { + "href": "/api/content_types/a4211c112939" + }, + "contentNodes": { + "href": "/api/content_node/responsive_layouts?contentType=%2Fapi%2Fcontent_types%2Fa4211c112939" + } + }, + "name": "ResponsiveLayout", + "active": true, + "id": "a4211c112939" + }, + { + "_links": { + "self": { + "href": "/api/content_types/f17470519474" + }, + "contentNodes": { + "href": "/api/content_node/column_layouts?contentType=%2Fapi%2Fcontent_types%2Ff17470519474" + } + }, + "name": "ColumnLayout", + "active": true, + "id": "f17470519474" + }, + { + "_links": { + "self": { + "href": "/api/content_types/1a0f84e322c8" + }, + "contentNodes": { + "href": "/api/content_node/multi_selects?contentType=%2Fapi%2Fcontent_types%2F1a0f84e322c8" + } + }, + "name": "LAThematicArea", + "active": true, + "id": "1a0f84e322c8" + }, + { + "_links": { + "self": { + "href": "/api/content_types/3ef17bd1df72" + }, + "contentNodes": { + "href": "/api/content_node/material_nodes?contentType=%2Fapi%2Fcontent_types%2F3ef17bd1df72" + } + }, + "name": "Material", + "active": true, + "id": "3ef17bd1df72" + }, + { + "_links": { + "self": { + "href": "/api/content_types/4f0c657fecef" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F4f0c657fecef" + } + }, + "name": "Notes", + "active": true, + "id": "4f0c657fecef" + }, + { + "_links": { + "self": { + "href": "/api/content_types/44dcc7493c65" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F44dcc7493c65" + } + }, + "name": "SafetyConcept", + "active": true, + "id": "44dcc7493c65" + }, + { + "_links": { + "self": { + "href": "/api/content_types/cfccaecd4bad" + }, + "contentNodes": { + "href": "/api/content_node/storyboards?contentType=%2Fapi%2Fcontent_types%2Fcfccaecd4bad" + } + }, + "name": "Storyboard", + "active": true, + "id": "cfccaecd4bad" + }, + { + "_links": { + "self": { + "href": "/api/content_types/318e064ea0c9" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F318e064ea0c9" + } + }, + "name": "Storycontext", + "active": true, + "id": "318e064ea0c9" + } + ] + } +} \ No newline at end of file diff --git a/e2e/specs/responses/content_types_entity.json b/e2e/specs/responses/content_types_entity.json new file mode 100644 index 0000000000..bf5dcd4311 --- /dev/null +++ b/e2e/specs/responses/content_types_entity.json @@ -0,0 +1,13 @@ +{ + "_links": { + "self": { + "href": "/api/content_types/318e064ea0c9" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F318e064ea0c9" + } + }, + "name": "Storycontext", + "active": true, + "id": "318e064ea0c9" +} \ No newline at end of file From d4d1a25a3dc130d527bcf8926af5b35aa2184dbd Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 07:33:42 +0200 Subject: [PATCH 58/81] update snapshots --- ...est__testOpenApiSpecMatchesSnapshot__1.yml | 20958 +++++++--------- 1 file changed, 8415 insertions(+), 12543 deletions(-) diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 268b807bd3..4d64b77728 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -402,10 +402,117 @@ components: - scheduleEntries - title type: object - Activity.jsonapi-read: + Activity.jsonapi: deprecated: false description: 'A piece of programme that will be carried out once or multiple times in a camp.' properties: + activityResponsibles: + items: + $ref: '#/components/schemas/ActivityResponsible.jsonapi' + readOnly: true + type: array + contentNodes: + description: 'All the content nodes that make up the tree of programme content.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + location: + description: "The physical location where this activity's programme will be carried out." + example: Spielwiese + maxLength: 64 + type: string + title: + description: 'The title of this activity that is shown in the picasso.' + example: Sportolympiade + maxLength: 32 + type: string + required: + - location + - title + type: object + id: + type: string + relationships: + properties: + activityResponsibles: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + camp: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + category: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + contentNodes: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + progressLabel: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + rootContentNode: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + scheduleEntries: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + required: + - activityResponsibles + - category + - scheduleEntries + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - [] + - + $ref: '#/components/schemas/ScheduleEntry.jsonapi' + - + $ref: '#/components/schemas/ScheduleEntry.jsonapi' + - + $ref: '#/components/schemas/ScheduleEntry.jsonapi' + - + $ref: '#/components/schemas/ScheduleEntry.jsonapi' + - + $ref: '#/components/schemas/ScheduleEntry.jsonapi' + - + $ref: '#/components/schemas/ScheduleEntry.jsonapi' + readOnly: true + type: array + scheduleEntries: + items: + $ref: '#/components/schemas/ScheduleEntry.jsonapi' + readOnly: true + type: array + type: object + Activity.jsonhal-read: + deprecated: false + description: 'A piece of programme that will be carried out once or multiple times in a camp.' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object activityResponsibles: description: 'The list of people that are responsible for planning or carrying out this activity.' items: @@ -455,7 +562,7 @@ components: - 'null' - string rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read' + $ref: '#/components/schemas/ColumnLayout.jsonhal-read' description: |- The programme contents, organized as a tree of content nodes. The root content node cannot be exchanged, but all the contents attached to it can. @@ -485,13 +592,22 @@ components: - scheduleEntries - title type: object - Activity.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + Activity.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false description: 'A piece of programme that will be carried out once or multiple times in a camp.' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object activityResponsibles: items: - $ref: '#/components/schemas/ActivityResponsible.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' + $ref: '#/components/schemas/ActivityResponsible.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' readOnly: true type: array camp: @@ -530,12 +646,12 @@ components: progressLabel: anyOf: - - $ref: '#/components/schemas/ActivityProgressLabel.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' + $ref: '#/components/schemas/ActivityProgressLabel.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' - type: 'null' readOnly: true rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' + $ref: '#/components/schemas/ColumnLayout.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' description: |- The programme contents, organized as a tree of content nodes. The root content node cannot be exchanged, but all the contents attached to it can. @@ -543,7 +659,7 @@ components: readOnly: true scheduleEntries: items: - $ref: '#/components/schemas/ScheduleEntry.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' + $ref: '#/components/schemas/ScheduleEntry.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' readOnly: true type: array title: @@ -558,13 +674,22 @@ components: - scheduleEntries - title type: object - Activity.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + Activity.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: 'A piece of programme that will be carried out once or multiple times in a camp.' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object activityResponsibles: items: - $ref: '#/components/schemas/ActivityResponsible.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/ActivityResponsible.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' readOnly: true type: array camp: @@ -576,7 +701,7 @@ components: category: anyOf: - - $ref: '#/components/schemas/Category.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/Category.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - type: 'null' readOnly: true @@ -603,12 +728,12 @@ components: progressLabel: anyOf: - - $ref: '#/components/schemas/ActivityProgressLabel.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/ActivityProgressLabel.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - type: 'null' readOnly: true rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/ColumnLayout.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' description: |- The programme contents, organized as a tree of content nodes. The root content node cannot be exchanged, but all the contents attached to it can. @@ -616,7 +741,7 @@ components: readOnly: true scheduleEntries: items: - $ref: '#/components/schemas/ScheduleEntry.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/ScheduleEntry.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' readOnly: true type: array title: @@ -631,10 +756,19 @@ components: - scheduleEntries - title type: object - Activity.jsonapi-read_ScheduleEntry.Activity: + Activity.jsonhal-read_ScheduleEntry.Activity: deprecated: false description: '' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object activityResponsibles: description: 'The list of people that are responsible for planning or carrying out this activity.' items: @@ -684,7 +818,7 @@ components: - 'null' - string rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read_ScheduleEntry.Activity' + $ref: '#/components/schemas/ColumnLayout.jsonhal-read_ScheduleEntry.Activity' description: |- The programme contents, organized as a tree of content nodes. The root content node cannot be exchanged, but all the contents attached to it can. @@ -714,43 +848,19 @@ components: - scheduleEntries - title type: object - Activity.jsonapi-write: - deprecated: false - description: 'A piece of programme that will be carried out once or multiple times in a camp.' - properties: - category: - description: |- - The category to which this activity belongs. The category determines color and numbering scheme - of the activity, and is used for marking similar activities. Must be in the same camp as the activity. - example: /categories/1a2b3c4d - format: iri-reference - type: string - location: - description: "The physical location where this activity's programme will be carried out." - example: Spielwiese - maxLength: 64 - type: string - progressLabel: - description: 'The current assigned ProgressLabel.' - example: /progress_labels/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - title: - description: 'The title of this activity that is shown in the picasso.' - example: Sportolympiade - maxLength: 32 - type: string - required: - - category - - location - - title - type: object - Activity.jsonapi-write_create: + Activity.jsonhal-write_create: deprecated: false description: 'A piece of programme that will be carried out once or multiple times in a camp.' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object category: description: |- The category to which this activity belongs. The category determines color and numbering scheme @@ -785,7 +895,7 @@ components: period: /periods/1a2b3c4a start: '2023-05-01T15:00:00+00:00' items: - $ref: '#/components/schemas/ScheduleEntry.jsonapi-write_create' + $ref: '#/components/schemas/ScheduleEntry.jsonhal-write_create' minItems: 1 type: array title: @@ -799,19 +909,33 @@ components: - scheduleEntries - title type: object - Activity.jsonhal-read: + Activity.jsonld-read: deprecated: false description: 'A piece of programme that will be carried out once or multiple times in a camp.' properties: - _links: - properties: - self: + '@context': + oneOf: + - + additionalProperties: true properties: - href: - format: iri-reference + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] type: string + required: + - '@vocab' + - hydra type: object - type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string activityResponsibles: description: 'The list of people that are responsible for planning or carrying out this activity.' items: @@ -861,7 +985,7 @@ components: - 'null' - string rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonhal-read' + $ref: '#/components/schemas/ColumnLayout.jsonld-read' description: |- The programme contents, organized as a tree of content nodes. The root content node cannot be exchanged, but all the contents attached to it can. @@ -891,22 +1015,19 @@ components: - scheduleEntries - title type: object - Activity.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + Activity.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false description: 'A piece of programme that will be carried out once or multiple times in a camp.' properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string activityResponsibles: items: - $ref: '#/components/schemas/ActivityResponsible.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' + $ref: '#/components/schemas/ActivityResponsible.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' readOnly: true type: array camp: @@ -945,12 +1066,12 @@ components: progressLabel: anyOf: - - $ref: '#/components/schemas/ActivityProgressLabel.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' + $ref: '#/components/schemas/ActivityProgressLabel.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' - type: 'null' readOnly: true rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' + $ref: '#/components/schemas/ColumnLayout.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' description: |- The programme contents, organized as a tree of content nodes. The root content node cannot be exchanged, but all the contents attached to it can. @@ -958,7 +1079,7 @@ components: readOnly: true scheduleEntries: items: - $ref: '#/components/schemas/ScheduleEntry.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' + $ref: '#/components/schemas/ScheduleEntry.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' readOnly: true type: array title: @@ -973,22 +1094,36 @@ components: - scheduleEntries - title type: object - Activity.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + Activity.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: 'A piece of programme that will be carried out once or multiple times in a camp.' properties: - _links: - properties: - self: + '@context': + oneOf: + - + additionalProperties: true properties: - href: - format: iri-reference + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] type: string + required: + - '@vocab' + - hydra type: object - type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string activityResponsibles: items: - $ref: '#/components/schemas/ActivityResponsible.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/ActivityResponsible.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' readOnly: true type: array camp: @@ -1000,7 +1135,7 @@ components: category: anyOf: - - $ref: '#/components/schemas/Category.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/Category.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - type: 'null' readOnly: true @@ -1027,12 +1162,12 @@ components: progressLabel: anyOf: - - $ref: '#/components/schemas/ActivityProgressLabel.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/ActivityProgressLabel.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - type: 'null' readOnly: true rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/ColumnLayout.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' description: |- The programme contents, organized as a tree of content nodes. The root content node cannot be exchanged, but all the contents attached to it can. @@ -1040,7 +1175,7 @@ components: readOnly: true scheduleEntries: items: - $ref: '#/components/schemas/ScheduleEntry.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/ScheduleEntry.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' readOnly: true type: array title: @@ -1055,24 +1190,38 @@ components: - scheduleEntries - title type: object - Activity.jsonhal-read_ScheduleEntry.Activity: + Activity.jsonld-read_ScheduleEntry.Activity: deprecated: false description: '' properties: - _links: - properties: - self: + '@context': + oneOf: + - + additionalProperties: true properties: - href: - format: iri-reference + '@vocab': type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra type: object - type: object - activityResponsibles: - description: 'The list of people that are responsible for planning or carrying out this activity.' - items: - example: 'https://example.com/' - format: iri-reference + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + activityResponsibles: + description: 'The list of people that are responsible for planning or carrying out this activity.' + items: + example: 'https://example.com/' + format: iri-reference type: string readOnly: true type: array @@ -1117,7 +1266,7 @@ components: - 'null' - string rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonhal-read_ScheduleEntry.Activity' + $ref: '#/components/schemas/ColumnLayout.jsonld-read_ScheduleEntry.Activity' description: |- The programme contents, organized as a tree of content nodes. The root content node cannot be exchanged, but all the contents attached to it can. @@ -1147,19 +1296,10 @@ components: - scheduleEntries - title type: object - Activity.jsonhal-write_create: + Activity.jsonld-write_create: deprecated: false description: 'A piece of programme that will be carried out once or multiple times in a camp.' properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object category: description: |- The category to which this activity belongs. The category determines color and numbering scheme @@ -1194,7 +1334,7 @@ components: period: /periods/1a2b3c4a start: '2023-05-01T15:00:00+00:00' items: - $ref: '#/components/schemas/ScheduleEntry.jsonhal-write_create' + $ref: '#/components/schemas/ScheduleEntry.jsonld-write_create' minItems: 1 type: array title: @@ -1208,451 +1348,203 @@ components: - scheduleEntries - title type: object - Activity.jsonld-read: + ActivityProgressLabel-read: deprecated: false - description: 'A piece of programme that will be carried out once or multiple times in a camp.' + description: |- + Progress labels in a camp. + To each activity one label can be assigned. properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - activityResponsibles: - description: 'The list of people that are responsible for planning or carrying out this activity.' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array camp: - description: 'The camp to which this activity belongs.' + description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d format: iri-reference - readOnly: true - type: string - category: - description: |- - The category to which this activity belongs. The category determines color and numbering scheme - of the activity, and is used for marking similar activities. Must be in the same camp as the activity. - example: /categories/1a2b3c4d - format: iri-reference type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - location: - description: "The physical location where this activity's programme will be carried out." - example: Spielwiese - maxLength: 64 - type: string - progressLabel: - description: 'The current assigned ProgressLabel.' - example: /progress_labels/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonld-read' - description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. - example: /content_nodes/1a2b3c4d - readOnly: true - scheduleEntries: - description: "The list of points in time when this activity's programme will be carried out." - example: - - - end: '2023-05-01T16:00:00+00:00' - period: /periods/1a2b3c4a - start: '2023-05-01T15:00:00+00:00' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array + position: + default: -1 + example: -1 + type: integer title: - description: 'The title of this activity that is shown in the picasso.' - example: Sportolympiade + example: Planned maxLength: 32 type: string required: - - activityResponsibles - - category - - location - - scheduleEntries + - camp + - position - title type: object - Activity.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ActivityProgressLabel-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false - description: 'A piece of programme that will be carried out once or multiple times in a camp.' + description: '' properties: - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - activityResponsibles: - items: - $ref: '#/components/schemas/ActivityResponsible.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' - readOnly: true - type: array camp: - description: 'The camp to which this activity belongs.' + description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d format: iri-reference - readOnly: true - type: string - category: - description: |- - The category to which this activity belongs. The category determines color and numbering scheme - of the activity, and is used for marking similar activities. Must be in the same camp as the activity. - example: /categories/1a2b3c4d - format: iri-reference type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - location: - description: "The physical location where this activity's programme will be carried out." - example: Spielwiese - maxLength: 64 - type: string - progressLabel: - anyOf: - - - $ref: '#/components/schemas/ActivityProgressLabel.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' - - - type: 'null' - readOnly: true - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' - description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. - example: /content_nodes/1a2b3c4d - readOnly: true - scheduleEntries: - items: - $ref: '#/components/schemas/ScheduleEntry.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' - readOnly: true - type: array + position: + default: -1 + example: -1 + type: integer title: - description: 'The title of this activity that is shown in the picasso.' - example: Sportolympiade + example: Planned maxLength: 32 type: string required: - - activityResponsibles - - category - - location - - scheduleEntries + - camp + - position - title type: object - Activity.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ActivityProgressLabel-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false - description: 'A piece of programme that will be carried out once or multiple times in a camp.' + description: '' properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - activityResponsibles: - items: - $ref: '#/components/schemas/ActivityResponsible.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - readOnly: true - type: array camp: - description: 'The camp to which this activity belongs.' + description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d format: iri-reference - readOnly: true type: string - category: - anyOf: - - - $ref: '#/components/schemas/Category.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - - - type: 'null' - readOnly: true - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - location: - description: "The physical location where this activity's programme will be carried out." - example: Spielwiese - maxLength: 64 - type: string - progressLabel: - anyOf: - - - $ref: '#/components/schemas/ActivityProgressLabel.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - - - type: 'null' - readOnly: true - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. - example: /content_nodes/1a2b3c4d - readOnly: true - scheduleEntries: - items: - $ref: '#/components/schemas/ScheduleEntry.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - readOnly: true - type: array + position: + default: -1 + example: -1 + type: integer title: - description: 'The title of this activity that is shown in the picasso.' - example: Sportolympiade + example: Planned maxLength: 32 type: string required: - - activityResponsibles - - category - - location - - scheduleEntries + - camp + - position - title type: object - Activity.jsonld-read_ScheduleEntry.Activity: + ActivityProgressLabel-write: deprecated: false - description: '' + description: |- + Progress labels in a camp. + To each activity one label can be assigned. properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - activityResponsibles: - description: 'The list of people that are responsible for planning or carrying out this activity.' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - camp: - description: 'The camp to which this activity belongs.' - example: /camps/1a2b3c4d - format: iri-reference - readOnly: true - type: string - category: - description: |- - The category to which this activity belongs. The category determines color and numbering scheme - of the activity, and is used for marking similar activities. Must be in the same camp as the activity. - example: /categories/1a2b3c4d - format: iri-reference - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - location: - description: "The physical location where this activity's programme will be carried out." - example: Spielwiese - maxLength: 64 - type: string - progressLabel: - description: 'The current assigned ProgressLabel.' - example: /progress_labels/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonld-read_ScheduleEntry.Activity' - description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. - example: /content_nodes/1a2b3c4d - readOnly: true - scheduleEntries: - description: "The list of points in time when this activity's programme will be carried out." - example: - - - end: '2023-05-01T16:00:00+00:00' - period: /periods/1a2b3c4a - start: '2023-05-01T15:00:00+00:00' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array + position: + default: -1 + example: -1 + type: integer title: - description: 'The title of this activity that is shown in the picasso.' - example: Sportolympiade + example: Planned maxLength: 32 type: string required: - - activityResponsibles - - category - - location - - scheduleEntries + - position - title type: object - Activity.jsonld-write_create: + ActivityProgressLabel-write_create: deprecated: false - description: 'A piece of programme that will be carried out once or multiple times in a camp.' + description: |- + Progress labels in a camp. + To each activity one label can be assigned. properties: - category: - description: |- - The category to which this activity belongs. The category determines color and numbering scheme - of the activity, and is used for marking similar activities. Must be in the same camp as the activity. - example: /categories/1a2b3c4d - format: iri-reference - type: string - copyActivitySource: - description: 'Copy contents from this source activity.' - example: /activities/1a2b3c4d + camp: + description: 'The camp to which this label belongs.' + example: /camps/1a2b3c4d format: iri-reference - type: - - 'null' - - string - location: - description: "The physical location where this activity's programme will be carried out." - example: Spielwiese - maxLength: 64 type: string - progressLabel: - description: 'The current assigned ProgressLabel.' - example: /progress_labels/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - scheduleEntries: - description: "The list of points in time when this activity's programme will be carried out." - example: - - - end: '2023-05-01T16:00:00+00:00' - period: /periods/1a2b3c4a - start: '2023-05-01T15:00:00+00:00' - items: - $ref: '#/components/schemas/ScheduleEntry.jsonld-write_create' - minItems: 1 - type: array + position: + default: -1 + example: -1 + type: integer title: - description: 'The title of this activity that is shown in the picasso.' - example: Sportolympiade + example: Planned maxLength: 32 type: string required: - - category - - location - - scheduleEntries + - camp + - position - title type: object - ActivityProgressLabel-read: + ActivityProgressLabel.jsonapi: + deprecated: false + description: |- + Progress labels in a camp. + To each activity one label can be assigned. + properties: + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + position: + default: -1 + example: -1 + type: integer + title: + example: Planned + maxLength: 32 + type: string + required: + - position + - title + type: object + id: + type: string + relationships: + properties: + camp: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + required: + - camp + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - + $ref: '#/components/schemas/Camp.jsonapi' + readOnly: true + type: array + type: object + ActivityProgressLabel.jsonhal-read: deprecated: false description: |- Progress labels in a camp. To each activity one label can be assigned. properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object camp: description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d @@ -1677,10 +1569,19 @@ components: - position - title type: object - ActivityProgressLabel-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ActivityProgressLabel.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false description: '' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object camp: description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d @@ -1705,10 +1606,19 @@ components: - position - title type: object - ActivityProgressLabel-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ActivityProgressLabel.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object camp: description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d @@ -1733,30 +1643,21 @@ components: - position - title type: object - ActivityProgressLabel-write: - deprecated: false - description: |- - Progress labels in a camp. - To each activity one label can be assigned. - properties: - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 - type: string - required: - - position - - title - type: object - ActivityProgressLabel-write_create: + ActivityProgressLabel.jsonhal-write_create: deprecated: false description: |- Progress labels in a camp. To each activity one label can be assigned. properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object camp: description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d @@ -1775,12 +1676,35 @@ components: - position - title type: object - ActivityProgressLabel.jsonapi-read: + ActivityProgressLabel.jsonld-read: deprecated: false description: |- Progress labels in a camp. To each activity one label can be assigned. properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string camp: description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d @@ -1805,10 +1729,33 @@ components: - position - title type: object - ActivityProgressLabel.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ActivityProgressLabel.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false description: '' properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string camp: description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d @@ -1833,10 +1780,33 @@ components: - position - title type: object - ActivityProgressLabel.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ActivityProgressLabel.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string camp: description: 'The camp to which this label belongs.' example: /camps/1a2b3c4d @@ -1861,25 +1831,7 @@ components: - position - title type: object - ActivityProgressLabel.jsonapi-write: - deprecated: false - description: |- - Progress labels in a camp. - To each activity one label can be assigned. - properties: - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 - type: string - required: - - position - - title - type: object - ActivityProgressLabel.jsonapi-write_create: + ActivityProgressLabel.jsonld-write_create: deprecated: false description: |- Progress labels in a camp. @@ -1903,24 +1855,18 @@ components: - position - title type: object - ActivityProgressLabel.jsonhal-read: + ActivityResponsible-read: deprecated: false - description: |- - Progress labels in a camp. - To each activity one label can be assigned. + description: 'A person that is responsible for planning or carrying out an activity.' properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - description: 'The camp to which this label belongs.' - example: /camps/1a2b3c4d + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d format: iri-reference type: string id: @@ -1929,35 +1875,22 @@ components: maxLength: 16 readOnly: true type: string - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 - type: string required: - - camp - - position - - title + - activity + - campCollaboration type: object - ActivityProgressLabel.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ActivityResponsible-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false description: '' properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - description: 'The camp to which this label belongs.' - example: /camps/1a2b3c4d + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d format: iri-reference type: string id: @@ -1966,35 +1899,22 @@ components: maxLength: 16 readOnly: true type: string - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 - type: string required: - - camp - - position - - title + - activity + - campCollaboration type: object - ActivityProgressLabel.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ActivityResponsible-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - description: 'The camp to which this label belongs.' - example: /camps/1a2b3c4d + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d format: iri-reference type: string id: @@ -2003,57 +1923,203 @@ components: maxLength: 16 readOnly: true type: string - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 + required: + - activity + - campCollaboration + type: object + ActivityResponsible-write: + deprecated: false + description: 'A person that is responsible for planning or carrying out an activity.' + properties: + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d + format: iri-reference type: string required: - - camp - - position - - title + - activity + - campCollaboration type: object - ActivityProgressLabel.jsonhal-write_create: + ActivityResponsible.jsonapi: deprecated: false - description: |- - Progress labels in a camp. - To each activity one label can be assigned. + description: 'A person that is responsible for planning or carrying out an activity.' properties: - _links: + data: properties: - self: + attributes: properties: - href: - format: iri-reference + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true type: string type: object + id: + type: string + relationships: + properties: + activity: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + campCollaboration: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + required: + - activity + - campCollaboration + type: object + type: + type: string + required: + - id + - type type: object - camp: - description: 'The camp to which this label belongs.' - example: /camps/1a2b3c4d + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - + $ref: '#/components/schemas/Activity.jsonapi' + - + $ref: '#/components/schemas/Activity.jsonapi' + readOnly: true + type: array + type: object + ActivityResponsible.jsonhal-read: + deprecated: false + description: 'A person that is responsible for planning or carrying out an activity.' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d format: iri-reference type: string - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d + format: iri-reference + type: string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true type: string required: - - camp - - position - - title + - activity + - campCollaboration type: object - ActivityProgressLabel.jsonld-read: + ActivityResponsible.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false - description: |- - Progress labels in a camp. - To each activity one label can be assigned. + description: '' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d + format: iri-reference + type: string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + required: + - activity + - campCollaboration + type: object + ActivityResponsible.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + deprecated: false + description: '' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d + format: iri-reference + type: string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + required: + - activity + - campCollaboration + type: object + ActivityResponsible.jsonhal-write: + deprecated: false + description: 'A person that is responsible for planning or carrying out an activity.' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d + format: iri-reference + type: string + required: + - activity + - campCollaboration + type: object + ActivityResponsible.jsonld-read: + deprecated: false + description: 'A person that is responsible for planning or carrying out an activity.' properties: '@context': oneOf: @@ -2078,9 +2144,14 @@ components: '@type': readOnly: true type: string - camp: - description: 'The camp to which this label belongs.' - example: /camps/1a2b3c4d + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d format: iri-reference type: string id: @@ -2089,20 +2160,11 @@ components: maxLength: 16 readOnly: true type: string - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 - type: string required: - - camp - - position - - title + - activity + - campCollaboration type: object - ActivityProgressLabel.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ActivityResponsible.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false description: '' properties: @@ -2129,9 +2191,14 @@ components: '@type': readOnly: true type: string - camp: - description: 'The camp to which this label belongs.' - example: /camps/1a2b3c4d + activity: + description: 'The activity that the person is responsible for.' + example: /activities/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' + example: /camp_collaborations/1a2b3c4d format: iri-reference type: string id: @@ -2140,20 +2207,11 @@ components: maxLength: 16 readOnly: true type: string - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 - type: string required: - - camp - - position - - title + - activity + - campCollaboration type: object - ActivityProgressLabel.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ActivityResponsible.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: @@ -2180,58 +2238,6 @@ components: '@type': readOnly: true type: string - camp: - description: 'The camp to which this label belongs.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 - type: string - required: - - camp - - position - - title - type: object - ActivityProgressLabel.jsonld-write_create: - deprecated: false - description: |- - Progress labels in a camp. - To each activity one label can be assigned. - properties: - camp: - description: 'The camp to which this label belongs.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - position: - default: -1 - example: -1 - type: integer - title: - example: Planned - maxLength: 32 - type: string - required: - - camp - - position - - title - type: object - ActivityResponsible-read: - deprecated: false - description: 'A person that is responsible for planning or carrying out an activity.' - properties: activity: description: 'The activity that the person is responsible for.' example: /activities/1a2b3c4d @@ -2252,9 +2258,9 @@ components: - activity - campCollaboration type: object - ActivityResponsible-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ActivityResponsible.jsonld-write: deprecated: false - description: '' + description: 'A person that is responsible for planning or carrying out an activity.' properties: activity: description: 'The activity that the person is responsible for.' @@ -2266,71 +2272,98 @@ components: example: /camp_collaborations/1a2b3c4d format: iri-reference type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string required: - activity - campCollaboration type: object - ActivityResponsible-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + Camp-read: deprecated: false - description: '' + description: |- + The main entity that eCamp is designed to manage. Contains programme which may be + distributed across multiple time periods. properties: - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 + activities: + description: |- + All the programme that will be carried out during the camp. An activity may be carried out + multiple times in the same camp. + example: '/activities?camp=%2Fcamps%2F1a2b3c4d' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true - type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible-write: - deprecated: false - description: 'A person that is responsible for planning or carrying out an activity.' - properties: - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonapi-read: - deprecated: false - description: 'A person that is responsible for planning or carrying out an activity.' - properties: - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d + type: array + addressCity: + description: 'The name of the town where the camp will take place.' + example: Hintertüpfingen + maxLength: 128 + type: + - 'null' + - string + addressName: + description: 'A textual description of the location of the camp.' + example: 'Wiese hinter der alten Mühle' + maxLength: 128 + type: + - 'null' + - string + addressStreet: + description: 'The street name and number (if any) of the location of the camp.' + example: 'Schönriedweg 23' + maxLength: 128 + type: + - 'null' + - string + addressZipcode: + description: 'The zipcode of the location of the camp.' + example: '1234' + maxLength: 128 + type: + - 'null' + - string + campCollaborations: + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + categories: + description: 'Types of programme, such as sports activities or meal times.' + example: '["/categories/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + coachName: + description: 'The name of the Y+S coach who is in charge of the camp.' + example: 'Albert Anderegg' + maxLength: 64 + type: + - 'null' + - string + courseKind: + description: 'The official name for the type of this course.' + example: 'PBS AG 123-23' + maxLength: 64 + type: + - 'null' + - string + courseNumber: + description: 'The official course number, identifying this course.' + example: 'PBS AG 123-23' + maxLength: 64 + type: + - 'null' + - string + creator: + description: |- + The person that created the camp. This value never changes, even when the person + leaves the camp. + example: 'https://example.com/' format: iri-reference + readOnly: true type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' @@ -2338,47 +2371,195 @@ components: maxLength: 16 readOnly: true type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: - deprecated: false - description: '' - properties: - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference + isPrototype: + description: 'Whether this camp may serve as a template for creating other camps.' + example: true + readOnly: true + type: boolean + kind: + description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' + example: Zeltlager + maxLength: 64 + type: + - 'null' + - string + materialLists: + description: |- + Lists for collecting the required materials needed for carrying out the programme. Each collaborator + has a material list, and there may be more, such as shopping lists. + example: '["/material_lists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + motto: + description: "The thematic topic (if any) of the camp's programme and storyline." + example: Piraten + maxLength: 128 + type: + - 'null' + - string + name: + description: 'A short name for the camp.' + example: 'SoLa 2022' + maxLength: 32 type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 + organizer: + description: 'The name of the organization which plans and carries out the camp.' + example: 'Pfadi Luftig' + maxLength: 64 + type: + - 'null' + - string + periods: + description: 'The time periods of the camp, there must be at least one. Periods in a camp may not overlap.' + example: + - + description: Hauptlager + end: '2022-01-08' + start: '2022-01-01' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + printYSLogoOnPicasso: + description: 'Whether the Y+S logo should be printed on the picasso of this camp.' + example: true + type: boolean + profiles: + description: 'All profiles of the users collaborating in this camp.' + example: '/profiles?user.collaborations.camp=%2Fcamps%2F1a2b3c4d' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + progressLabels: + description: 'All the progress labels within this camp.' + example: '["/progress_labels/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true + type: array + title: + description: 'The full title of the camp.' + example: 'Abteilungs-Sommerlager 2022' + maxLength: 32 type: string + trainingAdvisorName: + description: 'The name of the training advisor who is in charge of the course.' + example: 'Albert Anderegg' + maxLength: 64 + type: + - 'null' + - string required: - - activity - - campCollaboration + - activities + - campCollaborations + - categories + - materialLists + - name + - periods + - printYSLogoOnPicasso + - progressLabels + - title type: object - ActivityResponsible.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: deprecated: false - description: '' + description: |- + The main entity that eCamp is designed to manage. Contains programme which may be + distributed across multiple time periods. properties: - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d + activities: + description: |- + All the programme that will be carried out during the camp. An activity may be carried out + multiple times in the same camp. + example: '/activities?camp=%2Fcamps%2F1a2b3c4d' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + addressCity: + description: 'The name of the town where the camp will take place.' + example: Hintertüpfingen + maxLength: 128 + type: + - 'null' + - string + addressName: + description: 'A textual description of the location of the camp.' + example: 'Wiese hinter der alten Mühle' + maxLength: 128 + type: + - 'null' + - string + addressStreet: + description: 'The street name and number (if any) of the location of the camp.' + example: 'Schönriedweg 23' + maxLength: 128 + type: + - 'null' + - string + addressZipcode: + description: 'The zipcode of the location of the camp.' + example: '1234' + maxLength: 128 + type: + - 'null' + - string + campCollaborations: + description: |- + The people working on planning and carrying out the camp. Only collaborators have access + to the camp's contents. + items: + $ref: '#/components/schemas/CampCollaboration-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + readOnly: true + type: array + categories: + description: 'Types of programme, such as sports activities or meal times.' + example: '["/categories/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + coachName: + description: 'The name of the Y+S coach who is in charge of the camp.' + example: 'Albert Anderegg' + maxLength: 64 + type: + - 'null' + - string + courseKind: + description: 'The official name for the type of this course.' + example: 'PBS AG 123-23' + maxLength: 64 + type: + - 'null' + - string + courseNumber: + description: 'The official course number, identifying this course.' + example: 'PBS AG 123-23' + maxLength: 64 + type: + - 'null' + - string + creator: + description: |- + The person that created the camp. This value never changes, even when the person + leaves the camp. + example: 'https://example.com/' format: iri-reference + readOnly: true type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' @@ -2386,318 +2567,101 @@ components: maxLength: 16 readOnly: true type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonapi-write: - deprecated: false - description: 'A person that is responsible for planning or carrying out an activity.' - properties: - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonhal-read: - deprecated: false - description: 'A person that is responsible for planning or carrying out an activity.' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference + isPrototype: + description: 'Whether this camp may serve as a template for creating other camps.' + example: true + readOnly: true + type: boolean + kind: + description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' + example: Zeltlager + maxLength: 64 + type: + - 'null' + - string + materialLists: + description: |- + Lists for collecting the required materials needed for carrying out the programme. Each collaborator + has a material list, and there may be more, such as shopping lists. + example: '["/material_lists/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + motto: + description: "The thematic topic (if any) of the camp's programme and storyline." + example: Piraten + maxLength: 128 + type: + - 'null' + - string + name: + description: 'A short name for the camp.' + example: 'SoLa 2022' + maxLength: 32 type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 + organizer: + description: 'The name of the organization which plans and carries out the camp.' + example: 'Pfadi Luftig' + maxLength: 64 + type: + - 'null' + - string + periods: + items: + $ref: '#/components/schemas/Period-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + readOnly: true + type: array + printYSLogoOnPicasso: + description: 'Whether the Y+S logo should be printed on the picasso of this camp.' + example: true + type: boolean + profiles: + description: 'All profiles of the users collaborating in this camp.' + example: '/profiles?user.collaborations.camp=%2Fcamps%2F1a2b3c4d' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + progressLabels: + description: 'All the progress labels within this camp.' + example: '["/progress_labels/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true + type: array + title: + description: 'The full title of the camp.' + example: 'Abteilungs-Sommerlager 2022' + maxLength: 32 type: string + trainingAdvisorName: + description: 'The name of the training advisor who is in charge of the course.' + example: 'Albert Anderegg' + maxLength: 64 + type: + - 'null' + - string required: - - activity - - campCollaboration + - activities + - campCollaborations + - categories + - materialLists + - name + - periods + - printYSLogoOnPicasso + - progressLabels + - title type: object - ActivityResponsible.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + Camp-read_CampCollaboration.Camp_CampCollaboration.User: deprecated: false description: '' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: - deprecated: false - description: '' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonhal-write: - deprecated: false - description: 'A person that is responsible for planning or carrying out an activity.' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonld-read: - deprecated: false - description: 'A person that is responsible for planning or carrying out an activity.' - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: - deprecated: false - description: '' - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: - deprecated: false - description: '' - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - required: - - activity - - campCollaboration - type: object - ActivityResponsible.jsonld-write: - deprecated: false - description: 'A person that is responsible for planning or carrying out an activity.' - properties: - activity: - description: 'The activity that the person is responsible for.' - example: /activities/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The person that is responsible. Must be a collaborator in the same camp as the activity.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - required: - - activity - - campCollaboration - type: object - Camp-read: - deprecated: false - description: |- - The main entity that eCamp is designed to manage. Contains programme which may be - distributed across multiple time periods. properties: activities: description: |- @@ -2887,11 +2851,9 @@ components: - progressLabels - title type: object - Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + Camp-read_Period.Camp_Period.Days: deprecated: false - description: |- - The main entity that eCamp is designed to manage. Contains programme which may be - distributed across multiple time periods. + description: '' properties: activities: description: |- @@ -2933,12 +2895,10 @@ components: - 'null' - string campCollaborations: - description: |- - The people working on planning and carrying out the camp. Only collaborators have access - to the camp's contents. items: - $ref: '#/components/schemas/CampCollaboration-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - readOnly: true + example: 'https://example.com/' + format: iri-reference + type: string type: array categories: description: 'Types of programme, such as sports activities or meal times.' @@ -3027,9 +2987,16 @@ components: - 'null' - string periods: + description: 'The time periods of the camp, there must be at least one. Periods in a camp may not overlap.' + example: + - + description: Hauptlager + end: '2022-01-08' + start: '2022-01-01' items: - $ref: '#/components/schemas/Period-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - readOnly: true + example: 'https://example.com/' + format: iri-reference + type: string type: array printYSLogoOnPicasso: description: 'Whether the Y+S logo should be printed on the picasso of this camp.' @@ -3076,21 +3043,12 @@ components: - progressLabels - title type: object - Camp-read_CampCollaboration.Camp_CampCollaboration.User: + Camp-write_create: deprecated: false - description: '' + description: |- + The main entity that eCamp is designed to manage. Contains programme which may be + distributed across multiple time periods. properties: - activities: - description: |- - All the programme that will be carried out during the camp. An activity may be carried out - multiple times in the same camp. - example: '/activities?camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array addressCity: description: 'The name of the town where the camp will take place.' example: Hintertüpfingen @@ -3119,21 +3077,14 @@ components: type: - 'null' - string - campCollaborations: - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - categories: - description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array + campPrototype: + description: 'The prototype camp that will be used as a template to create this camp.' + example: /camps/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + writeOnly: true coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3155,25 +3106,6 @@ components: type: - 'null' - string - creator: - description: |- - The person that created the camp. This value never changes, even when the person - leaves the camp. - example: 'https://example.com/' - format: iri-reference - readOnly: true - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - isPrototype: - description: 'Whether this camp may serve as a template for creating other camps.' - example: true - readOnly: true - type: boolean kind: description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' example: Zeltlager @@ -3181,17 +3113,6 @@ components: type: - 'null' - string - materialLists: - description: |- - Lists for collecting the required materials needed for carrying out the programme. Each collaborator - has a material list, and there may be more, such as shopping lists. - example: '["/material_lists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array motto: description: "The thematic topic (if any) of the camp's programme and storyline." example: Piraten @@ -3219,32 +3140,13 @@ components: end: '2022-01-08' start: '2022-01-01' items: - example: 'https://example.com/' - format: iri-reference - type: string + $ref: '#/components/schemas/Period-write_create' + minItems: 1 type: array printYSLogoOnPicasso: description: 'Whether the Y+S logo should be printed on the picasso of this camp.' example: true type: boolean - profiles: - description: 'All profiles of the users collaborating in this camp.' - example: '/profiles?user.collaborations.camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - progressLabels: - description: 'All the progress labels within this camp.' - example: '["/progress_labels/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array title: description: 'The full title of the camp.' example: 'Abteilungs-Sommerlager 2022' @@ -3258,31 +3160,17 @@ components: - 'null' - string required: - - activities - - campCollaborations - - categories - - materialLists - name - periods - printYSLogoOnPicasso - - progressLabels - title type: object - Camp-read_Period.Camp_Period.Days: + Camp-write_update: deprecated: false - description: '' + description: |- + The main entity that eCamp is designed to manage. Contains programme which may be + distributed across multiple time periods. properties: - activities: - description: |- - All the programme that will be carried out during the camp. An activity may be carried out - multiple times in the same camp. - example: '/activities?camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array addressCity: description: 'The name of the town where the camp will take place.' example: Hintertüpfingen @@ -3311,21 +3199,6 @@ components: type: - 'null' - string - campCollaborations: - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - categories: - description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3347,25 +3220,6 @@ components: type: - 'null' - string - creator: - description: |- - The person that created the camp. This value never changes, even when the person - leaves the camp. - example: 'https://example.com/' - format: iri-reference - readOnly: true - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - isPrototype: - description: 'Whether this camp may serve as a template for creating other camps.' - example: true - readOnly: true - type: boolean kind: description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' example: Zeltlager @@ -3373,17 +3227,6 @@ components: type: - 'null' - string - materialLists: - description: |- - Lists for collecting the required materials needed for carrying out the programme. Each collaborator - has a material list, and there may be more, such as shopping lists. - example: '["/material_lists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array motto: description: "The thematic topic (if any) of the camp's programme and storyline." example: Piraten @@ -3403,40 +3246,10 @@ components: type: - 'null' - string - periods: - description: 'The time periods of the camp, there must be at least one. Periods in a camp may not overlap.' - example: - - - description: Hauptlager - end: '2022-01-08' - start: '2022-01-01' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array printYSLogoOnPicasso: description: 'Whether the Y+S logo should be printed on the picasso of this camp.' example: true type: boolean - profiles: - description: 'All profiles of the users collaborating in this camp.' - example: '/profiles?user.collaborations.camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - progressLabels: - description: 'All the progress labels within this camp.' - example: '["/progress_labels/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array title: description: 'The full title of the camp.' example: 'Abteilungs-Sommerlager 2022' @@ -3450,246 +3263,197 @@ components: - 'null' - string required: - - activities - - campCollaborations - - categories - - materialLists - name - - periods - printYSLogoOnPicasso - - progressLabels - title type: object - Camp-write_create: + Camp.jsonapi: deprecated: false description: |- The main entity that eCamp is designed to manage. Contains programme which may be distributed across multiple time periods. properties: - addressCity: - description: 'The name of the town where the camp will take place.' - example: Hintertüpfingen - maxLength: 128 - type: - - 'null' - - string - addressName: - description: 'A textual description of the location of the camp.' - example: 'Wiese hinter der alten Mühle' - maxLength: 128 - type: - - 'null' - - string - addressStreet: - description: 'The street name and number (if any) of the location of the camp.' - example: 'Schönriedweg 23' - maxLength: 128 - type: - - 'null' - - string - addressZipcode: - description: 'The zipcode of the location of the camp.' - example: '1234' - maxLength: 128 - type: - - 'null' - - string - campPrototype: - description: 'The prototype camp that will be used as a template to create this camp.' - example: /camps/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - writeOnly: true - coachName: - description: 'The name of the Y+S coach who is in charge of the camp.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - courseKind: - description: 'The official name for the type of this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - courseNumber: - description: 'The official course number, identifying this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - kind: - description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' - example: Zeltlager - maxLength: 64 - type: - - 'null' - - string - motto: - description: "The thematic topic (if any) of the camp's programme and storyline." - example: Piraten - maxLength: 128 - type: - - 'null' - - string - name: - description: 'A short name for the camp.' - example: 'SoLa 2022' - maxLength: 32 - type: string - organizer: - description: 'The name of the organization which plans and carries out the camp.' - example: 'Pfadi Luftig' - maxLength: 64 - type: - - 'null' - - string + campCollaborations: + description: |- + The people working on planning and carrying out the camp. Only collaborators have access + to the camp's contents. + items: + $ref: '#/components/schemas/CampCollaboration.jsonapi' + readOnly: true + type: array + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + addressCity: + description: 'The name of the town where the camp will take place.' + example: Hintertüpfingen + maxLength: 128 + type: ['null', string] + addressName: + description: 'A textual description of the location of the camp.' + example: 'Wiese hinter der alten Mühle' + maxLength: 128 + type: ['null', string] + addressStreet: + description: 'The street name and number (if any) of the location of the camp.' + example: 'Schönriedweg 23' + maxLength: 128 + type: ['null', string] + addressZipcode: + description: 'The zipcode of the location of the camp.' + example: '1234' + maxLength: 128 + type: ['null', string] + coachName: + description: 'The name of the Y+S coach who is in charge of the camp.' + example: 'Albert Anderegg' + maxLength: 64 + type: ['null', string] + courseKind: + description: 'The official name for the type of this course.' + example: 'PBS AG 123-23' + maxLength: 64 + type: ['null', string] + courseNumber: + description: 'The official course number, identifying this course.' + example: 'PBS AG 123-23' + maxLength: 64 + type: ['null', string] + isPrototype: + description: 'Whether this camp may serve as a template for creating other camps.' + example: true + readOnly: true + type: boolean + kind: + description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' + example: Zeltlager + maxLength: 64 + type: ['null', string] + motto: + description: "The thematic topic (if any) of the camp's programme and storyline." + example: Piraten + maxLength: 128 + type: ['null', string] + name: + description: 'A short name for the camp.' + example: 'SoLa 2022' + maxLength: 32 + type: string + organizer: + description: 'The name of the organization which plans and carries out the camp.' + example: 'Pfadi Luftig' + maxLength: 64 + type: ['null', string] + printYSLogoOnPicasso: + description: 'Whether the Y+S logo should be printed on the picasso of this camp.' + example: true + type: boolean + title: + description: 'The full title of the camp.' + example: 'Abteilungs-Sommerlager 2022' + maxLength: 32 + type: string + trainingAdvisorName: + description: 'The name of the training advisor who is in charge of the course.' + example: 'Albert Anderegg' + maxLength: 64 + type: ['null', string] + required: + - name + - printYSLogoOnPicasso + - title + type: object + id: + type: string + relationships: + properties: + activities: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + campCollaborations: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + categories: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + creator: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + materialLists: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + periods: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + progressLabels: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + required: + - activities + - campCollaborations + - categories + - materialLists + - periods + - progressLabels + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - + $ref: '#/components/schemas/CampCollaboration.jsonapi' + - + $ref: '#/components/schemas/CampCollaboration.jsonapi' + - + $ref: '#/components/schemas/CampCollaboration.jsonapi' + - + $ref: '#/components/schemas/CampCollaboration.jsonapi' + - + $ref: '#/components/schemas/CampCollaboration.jsonapi' + - + $ref: '#/components/schemas/CampCollaboration.jsonapi' + - + $ref: '#/components/schemas/CampCollaboration.jsonapi' + readOnly: true + type: array periods: - description: 'The time periods of the camp, there must be at least one. Periods in a camp may not overlap.' - example: - - - description: Hauptlager - end: '2022-01-08' - start: '2022-01-01' items: - $ref: '#/components/schemas/Period-write_create' - minItems: 1 + $ref: '#/components/schemas/Period.jsonapi' + readOnly: true + type: array + profiles: + description: 'All profiles of the users collaborating in this camp.' + example: '/profiles?user.collaborations.camp=%2Fcamps%2F1a2b3c4d' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true type: array - printYSLogoOnPicasso: - description: 'Whether the Y+S logo should be printed on the picasso of this camp.' - example: true - type: boolean - title: - description: 'The full title of the camp.' - example: 'Abteilungs-Sommerlager 2022' - maxLength: 32 - type: string - trainingAdvisorName: - description: 'The name of the training advisor who is in charge of the course.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - required: - - name - - periods - - printYSLogoOnPicasso - - title - type: object - Camp-write_update: - deprecated: false - description: |- - The main entity that eCamp is designed to manage. Contains programme which may be - distributed across multiple time periods. - properties: - addressCity: - description: 'The name of the town where the camp will take place.' - example: Hintertüpfingen - maxLength: 128 - type: - - 'null' - - string - addressName: - description: 'A textual description of the location of the camp.' - example: 'Wiese hinter der alten Mühle' - maxLength: 128 - type: - - 'null' - - string - addressStreet: - description: 'The street name and number (if any) of the location of the camp.' - example: 'Schönriedweg 23' - maxLength: 128 - type: - - 'null' - - string - addressZipcode: - description: 'The zipcode of the location of the camp.' - example: '1234' - maxLength: 128 - type: - - 'null' - - string - coachName: - description: 'The name of the Y+S coach who is in charge of the camp.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - courseKind: - description: 'The official name for the type of this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - courseNumber: - description: 'The official course number, identifying this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - kind: - description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' - example: Zeltlager - maxLength: 64 - type: - - 'null' - - string - motto: - description: "The thematic topic (if any) of the camp's programme and storyline." - example: Piraten - maxLength: 128 - type: - - 'null' - - string - name: - description: 'A short name for the camp.' - example: 'SoLa 2022' - maxLength: 32 - type: string - organizer: - description: 'The name of the organization which plans and carries out the camp.' - example: 'Pfadi Luftig' - maxLength: 64 - type: - - 'null' - - string - printYSLogoOnPicasso: - description: 'Whether the Y+S logo should be printed on the picasso of this camp.' - example: true - type: boolean - title: - description: 'The full title of the camp.' - example: 'Abteilungs-Sommerlager 2022' - maxLength: 32 - type: string - trainingAdvisorName: - description: 'The name of the training advisor who is in charge of the course.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - required: - - name - - printYSLogoOnPicasso - - title type: object - Camp.jsonapi-read: + Camp.jsonhal-read: deprecated: false description: |- The main entity that eCamp is designed to manage. Contains programme which may be distributed across multiple time periods. properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object activities: description: |- All the programme that will be carried out during the camp. An activity may be carried out @@ -3878,12 +3642,21 @@ components: - progressLabels - title type: object - Camp.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + Camp.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: deprecated: false description: |- The main entity that eCamp is designed to manage. Contains programme which may be distributed across multiple time periods. properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object activities: description: |- All the programme that will be carried out during the camp. An activity may be carried out @@ -3928,7 +3701,7 @@ components: The people working on planning and carrying out the camp. Only collaborators have access to the camp's contents. items: - $ref: '#/components/schemas/CampCollaboration.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/CampCollaboration.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' readOnly: true type: array categories: @@ -4019,7 +3792,7 @@ components: - string periods: items: - $ref: '#/components/schemas/Period.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Period.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' readOnly: true type: array printYSLogoOnPicasso: @@ -4067,10 +3840,19 @@ components: - progressLabels - title type: object - Camp.jsonapi-read_CampCollaboration.Camp_CampCollaboration.User: + Camp.jsonhal-read_CampCollaboration.Camp_CampCollaboration.User: deprecated: false description: '' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object activities: description: |- All the programme that will be carried out during the camp. An activity may be carried out @@ -4259,10 +4041,19 @@ components: - progressLabels - title type: object - Camp.jsonapi-read_Period.Camp_Period.Days: + Camp.jsonhal-read_Period.Camp_Period.Days: deprecated: false description: '' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object activities: description: |- All the programme that will be carried out during the camp. An activity may be carried out @@ -4451,12 +4242,21 @@ components: - progressLabels - title type: object - Camp.jsonapi-write_create: + Camp.jsonhal-write_create: deprecated: false description: |- The main entity that eCamp is designed to manage. Contains programme which may be distributed across multiple time periods. properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object addressCity: description: 'The name of the town where the camp will take place.' example: Hintertüpfingen @@ -4548,7 +4348,7 @@ components: end: '2022-01-08' start: '2022-01-01' items: - $ref: '#/components/schemas/Period.jsonapi-write_create' + $ref: '#/components/schemas/Period.jsonhal-write_create' minItems: 1 type: array printYSLogoOnPicasso: @@ -4573,123 +4373,35 @@ components: - printYSLogoOnPicasso - title type: object - Camp.jsonapi-write_update: + Camp.jsonld-read: deprecated: false description: |- The main entity that eCamp is designed to manage. Contains programme which may be distributed across multiple time periods. properties: - addressCity: - description: 'The name of the town where the camp will take place.' - example: Hintertüpfingen - maxLength: 128 - type: - - 'null' - - string - addressName: - description: 'A textual description of the location of the camp.' - example: 'Wiese hinter der alten Mühle' - maxLength: 128 - type: - - 'null' - - string - addressStreet: - description: 'The street name and number (if any) of the location of the camp.' - example: 'Schönriedweg 23' - maxLength: 128 - type: - - 'null' - - string - addressZipcode: - description: 'The zipcode of the location of the camp.' - example: '1234' - maxLength: 128 - type: - - 'null' - - string - coachName: - description: 'The name of the Y+S coach who is in charge of the camp.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - courseKind: - description: 'The official name for the type of this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - courseNumber: - description: 'The official course number, identifying this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - kind: - description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' - example: Zeltlager - maxLength: 64 - type: - - 'null' - - string - motto: - description: "The thematic topic (if any) of the camp's programme and storyline." - example: Piraten - maxLength: 128 - type: - - 'null' - - string - name: - description: 'A short name for the camp.' - example: 'SoLa 2022' - maxLength: 32 - type: string - organizer: - description: 'The name of the organization which plans and carries out the camp.' - example: 'Pfadi Luftig' - maxLength: 64 - type: - - 'null' - - string - printYSLogoOnPicasso: - description: 'Whether the Y+S logo should be printed on the picasso of this camp.' - example: true - type: boolean - title: - description: 'The full title of the camp.' - example: 'Abteilungs-Sommerlager 2022' - maxLength: 32 - type: string - trainingAdvisorName: - description: 'The name of the training advisor who is in charge of the course.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - required: - - name - - printYSLogoOnPicasso - - title - type: object - Camp.jsonhal-read: - deprecated: false - description: |- - The main entity that eCamp is designed to manage. Contains programme which may be - distributed across multiple time periods. - properties: - _links: - properties: - self: + '@context': + oneOf: + - + additionalProperties: true properties: - href: - format: iri-reference + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] type: string + required: + - '@vocab' + - hydra type: object - type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string activities: description: |- All the programme that will be carried out during the camp. An activity may be carried out @@ -4878,21 +4590,35 @@ components: - progressLabels - title type: object - Camp.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + Camp.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: deprecated: false description: |- The main entity that eCamp is designed to manage. Contains programme which may be distributed across multiple time periods. properties: - _links: - properties: - self: + '@context': + oneOf: + - + additionalProperties: true properties: - href: - format: iri-reference + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] type: string + required: + - '@vocab' + - hydra type: object - type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string activities: description: |- All the programme that will be carried out during the camp. An activity may be carried out @@ -4937,7 +4663,7 @@ components: The people working on planning and carrying out the camp. Only collaborators have access to the camp's contents. items: - $ref: '#/components/schemas/CampCollaboration.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/CampCollaboration.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' readOnly: true type: array categories: @@ -5028,7 +4754,7 @@ components: - string periods: items: - $ref: '#/components/schemas/Period.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Period.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' readOnly: true type: array printYSLogoOnPicasso: @@ -5076,19 +4802,33 @@ components: - progressLabels - title type: object - Camp.jsonhal-read_CampCollaboration.Camp_CampCollaboration.User: + Camp.jsonld-read_CampCollaboration.Camp_CampCollaboration.User: deprecated: false description: '' properties: - _links: - properties: - self: + '@context': + oneOf: + - + additionalProperties: true properties: - href: - format: iri-reference + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] type: string + required: + - '@vocab' + - hydra type: object - type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string activities: description: |- All the programme that will be carried out during the camp. An activity may be carried out @@ -5277,19 +5017,33 @@ components: - progressLabels - title type: object - Camp.jsonhal-read_Period.Camp_Period.Days: + Camp.jsonld-read_Period.Camp_Period.Days: deprecated: false description: '' properties: - _links: - properties: - self: + '@context': + oneOf: + - + additionalProperties: true properties: - href: - format: iri-reference + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] type: string + required: + - '@vocab' + - hydra type: object - type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string activities: description: |- All the programme that will be carried out during the camp. An activity may be carried out @@ -5478,21 +5232,12 @@ components: - progressLabels - title type: object - Camp.jsonhal-write_create: + Camp.jsonld-write_create: deprecated: false description: |- The main entity that eCamp is designed to manage. Contains programme which may be distributed across multiple time periods. properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object addressCity: description: 'The name of the town where the camp will take place.' example: Hintertüpfingen @@ -5584,7 +5329,7 @@ components: end: '2022-01-08' start: '2022-01-01' items: - $ref: '#/components/schemas/Period.jsonhal-write_create' + $ref: '#/components/schemas/Period.jsonld-write_create' minItems: 1 type: array printYSLogoOnPicasso: @@ -5609,117 +5354,75 @@ components: - printYSLogoOnPicasso - title type: object - Camp.jsonld-read: + CampCollaboration-read: deprecated: false - description: |- - The main entity that eCamp is designed to manage. Contains programme which may be - distributed across multiple time periods. + description: 'A user participating in some way in the planning or realization of a camp.' properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true + camp: + description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + example: /camps/1a2b3c4d + format: iri-reference type: string - activities: - description: |- - All the programme that will be carried out during the camp. An activity may be carried out - multiple times in the same camp. - example: '/activities?camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 readOnly: true - type: array - addressCity: - description: 'The name of the town where the camp will take place.' - example: Hintertüpfingen - maxLength: 128 - type: - - 'null' - - string - addressName: - description: 'A textual description of the location of the camp.' - example: 'Wiese hinter der alten Mühle' - maxLength: 128 - type: - - 'null' - - string - addressStreet: - description: 'The street name and number (if any) of the location of the camp.' - example: 'Schönriedweg 23' - maxLength: 128 - type: - - 'null' - - string - addressZipcode: - description: 'The zipcode of the location of the camp.' - example: '1234' + type: string + inviteEmail: + description: |- + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email maxLength: 128 + minLength: 1 type: - 'null' - string - campCollaborations: - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - categories: - description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - coachName: - description: 'The name of the Y+S coach who is in charge of the camp.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - courseKind: - description: 'The official name for the type of this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - courseNumber: - description: 'The official course number, identifying this course.' - example: 'PBS AG 123-23' - maxLength: 64 + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 + type: string + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: + - established + - inactive + - invited + example: inactive + maxLength: 16 + type: string + user: + description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' + example: /users/1a2b3c4d + format: iri-reference type: - 'null' - string - creator: - description: |- - The person that created the camp. This value never changes, even when the person - leaves the camp. - example: 'https://example.com/' + required: + - camp + - role + - status + type: object + CampCollaboration-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + deprecated: false + description: '' + properties: + camp: + description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + example: /camps/1a2b3c4d format: iri-reference - readOnly: true type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' @@ -5727,218 +5430,284 @@ components: maxLength: 16 readOnly: true type: string - isPrototype: - description: 'Whether this camp may serve as a template for creating other camps.' - example: true - readOnly: true - type: boolean - kind: - description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' - example: Zeltlager - maxLength: 64 - type: - - 'null' - - string - materialLists: + inviteEmail: description: |- - Lists for collecting the required materials needed for carrying out the programme. Each collaborator - has a material list, and there may be more, such as shopping lists. - example: '["/material_lists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - motto: - description: "The thematic topic (if any) of the camp's programme and storyline." - example: Piraten + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email maxLength: 128 + minLength: 1 type: - 'null' - string - name: - description: 'A short name for the camp.' - example: 'SoLa 2022' - maxLength: 32 + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 type: string - organizer: - description: 'The name of the organization which plans and carries out the camp.' - example: 'Pfadi Luftig' - maxLength: 64 - type: - - 'null' - - string - periods: - description: 'The time periods of the camp, there must be at least one. Periods in a camp may not overlap.' - example: + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: + - established + - inactive + - invited + example: inactive + maxLength: 16 + type: string + user: + anyOf: - - description: Hauptlager - end: '2022-01-08' - start: '2022-01-01' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - printYSLogoOnPicasso: - description: 'Whether the Y+S logo should be printed on the picasso of this camp.' - example: true - type: boolean - profiles: - description: 'All profiles of the users collaborating in this camp.' - example: '/profiles?user.collaborations.camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - progressLabels: - description: 'All the progress labels within this camp.' - example: '["/progress_labels/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + $ref: '#/components/schemas/User-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + - + type: 'null' readOnly: true - type: array - title: - description: 'The full title of the camp.' - example: 'Abteilungs-Sommerlager 2022' - maxLength: 32 - type: string - trainingAdvisorName: - description: 'The name of the training advisor who is in charge of the course.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string required: - - activities - - campCollaborations - - categories - - materialLists - - name - - periods - - printYSLogoOnPicasso - - progressLabels - - title + - camp + - role + - status type: object - Camp.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + CampCollaboration-read_CampCollaboration.Camp_CampCollaboration.User: deprecated: false - description: |- - The main entity that eCamp is designed to manage. Contains programme which may be - distributed across multiple time periods. + description: 'A user participating in some way in the planning or realization of a camp.' properties: - '@context': - oneOf: + camp: + anyOf: - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object + $ref: '#/components/schemas/Camp-read_CampCollaboration.Camp_CampCollaboration.User' - - type: string - readOnly: true - '@id': + type: 'null' readOnly: true - type: string - '@type': + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 readOnly: true type: string - activities: + inviteEmail: description: |- - All the programme that will be carried out during the camp. An activity may be carried out - multiple times in the same camp. - example: '/activities?camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - addressCity: - description: 'The name of the town where the camp will take place.' - example: Hintertüpfingen - maxLength: 128 - type: - - 'null' - - string - addressName: - description: 'A textual description of the location of the camp.' - example: 'Wiese hinter der alten Mühle' + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email maxLength: 128 + minLength: 1 type: - 'null' - string - addressStreet: - description: 'The street name and number (if any) of the location of the camp.' - example: 'Schönriedweg 23' + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 + type: string + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: + - established + - inactive + - invited + example: inactive + maxLength: 16 + type: string + user: + anyOf: + - + $ref: '#/components/schemas/User-read_CampCollaboration.Camp_CampCollaboration.User' + - + type: 'null' + readOnly: true + required: + - camp + - role + - status + type: object + CampCollaboration-resend_invitation: + deprecated: false + description: 'A user participating in some way in the planning or realization of a camp.' + type: object + CampCollaboration-write_create: + deprecated: false + description: 'A user participating in some way in the planning or realization of a camp.' + properties: + camp: + description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + inviteEmail: + description: |- + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email maxLength: 128 + minLength: 1 type: - 'null' - string - addressZipcode: - description: 'The zipcode of the location of the camp.' - example: '1234' - maxLength: 128 + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 + type: string + user: + description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' + example: /users/1a2b3c4d + format: iri-reference type: - 'null' - string - campCollaborations: + required: + - camp + - role + type: object + CampCollaboration-write_update: + deprecated: false + description: 'A user participating in some way in the planning or realization of a camp.' + properties: + role: description: |- - The people working on planning and carrying out the camp. Only collaborators have access - to the camp's contents. - items: - $ref: '#/components/schemas/CampCollaboration.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - readOnly: true - type: array - categories: - description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 + type: string + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: + - established + - inactive + - invited + example: inactive + maxLength: 16 + type: string + required: + - role + - status + type: object + CampCollaboration.jsonapi: + deprecated: false + description: 'A user participating in some way in the planning or realization of a camp.' + properties: + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + inviteEmail: + description: |- + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: { url: 'https://schema.org/email' } + format: email + maxLength: 128 + minLength: 1 + readOnly: true + type: ['null', string] + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: [guest, manager, member] + example: member + maxLength: 16 + type: string + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: [established, inactive, invited] + example: inactive + maxLength: 16 + readOnly: true + type: string + required: + - role + - status + type: object + id: + type: string + relationships: + properties: + camp: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + user: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + required: + - camp + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' items: - example: 'https://example.com/' - format: iri-reference - type: string + anyOf: + - + $ref: '#/components/schemas/User.jsonapi' + - + $ref: '#/components/schemas/User.jsonapi' readOnly: true type: array - coachName: - description: 'The name of the Y+S coach who is in charge of the camp.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - courseKind: - description: 'The official name for the type of this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - courseNumber: - description: 'The official course number, identifying this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - creator: - description: |- - The person that created the camp. This value never changes, even when the person - leaves the camp. - example: 'https://example.com/' + type: object + CampCollaboration.jsonhal-read: + deprecated: false + description: 'A user participating in some way in the planning or realization of a camp.' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + camp: + description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + example: /camps/1a2b3c4d format: iri-reference - readOnly: true type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' @@ -5946,207 +5715,69 @@ components: maxLength: 16 readOnly: true type: string - isPrototype: - description: 'Whether this camp may serve as a template for creating other camps.' - example: true - readOnly: true - type: boolean - kind: - description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' - example: Zeltlager - maxLength: 64 - type: - - 'null' - - string - materialLists: + inviteEmail: description: |- - Lists for collecting the required materials needed for carrying out the programme. Each collaborator - has a material list, and there may be more, such as shopping lists. - example: '["/material_lists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - motto: - description: "The thematic topic (if any) of the camp's programme and storyline." - example: Piraten + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email maxLength: 128 + minLength: 1 type: - 'null' - string - name: - description: 'A short name for the camp.' - example: 'SoLa 2022' - maxLength: 32 + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 type: string - organizer: - description: 'The name of the organization which plans and carries out the camp.' - example: 'Pfadi Luftig' - maxLength: 64 - type: - - 'null' - - string - periods: - items: - $ref: '#/components/schemas/Period.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - readOnly: true - type: array - printYSLogoOnPicasso: - description: 'Whether the Y+S logo should be printed on the picasso of this camp.' - example: true - type: boolean - profiles: - description: 'All profiles of the users collaborating in this camp.' - example: '/profiles?user.collaborations.camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - progressLabels: - description: 'All the progress labels within this camp.' - example: '["/progress_labels/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - title: - description: 'The full title of the camp.' - example: 'Abteilungs-Sommerlager 2022' - maxLength: 32 + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: + - established + - inactive + - invited + example: inactive + maxLength: 16 type: string - trainingAdvisorName: - description: 'The name of the training advisor who is in charge of the course.' - example: 'Albert Anderegg' - maxLength: 64 + user: + description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' + example: /users/1a2b3c4d + format: iri-reference type: - 'null' - string required: - - activities - - campCollaborations - - categories - - materialLists - - name - - periods - - printYSLogoOnPicasso - - progressLabels - - title + - camp + - role + - status type: object - Camp.jsonld-read_CampCollaboration.Camp_CampCollaboration.User: + CampCollaboration.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: deprecated: false description: '' properties: - '@context': - oneOf: - - - additionalProperties: true + _links: + properties: + self: properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] + href: + format: iri-reference type: string - required: - - '@vocab' - - hydra type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - activities: - description: |- - All the programme that will be carried out during the camp. An activity may be carried out - multiple times in the same camp. - example: '/activities?camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - addressCity: - description: 'The name of the town where the camp will take place.' - example: Hintertüpfingen - maxLength: 128 - type: - - 'null' - - string - addressName: - description: 'A textual description of the location of the camp.' - example: 'Wiese hinter der alten Mühle' - maxLength: 128 - type: - - 'null' - - string - addressStreet: - description: 'The street name and number (if any) of the location of the camp.' - example: 'Schönriedweg 23' - maxLength: 128 - type: - - 'null' - - string - addressZipcode: - description: 'The zipcode of the location of the camp.' - example: '1234' - maxLength: 128 - type: - - 'null' - - string - campCollaborations: - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - categories: - description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - coachName: - description: 'The name of the Y+S coach who is in charge of the camp.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - courseKind: - description: 'The official name for the type of this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - courseNumber: - description: 'The official course number, identifying this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - creator: - description: |- - The person that created the camp. This value never changes, even when the person - leaves the camp. - example: 'https://example.com/' + type: object + camp: + description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + example: /camps/1a2b3c4d format: iri-reference - readOnly: true type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' @@ -6154,108 +5785,180 @@ components: maxLength: 16 readOnly: true type: string - isPrototype: - description: 'Whether this camp may serve as a template for creating other camps.' - example: true - readOnly: true - type: boolean - kind: - description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' - example: Zeltlager - maxLength: 64 - type: - - 'null' - - string - materialLists: + inviteEmail: description: |- - Lists for collecting the required materials needed for carrying out the programme. Each collaborator - has a material list, and there may be more, such as shopping lists. - example: '["/material_lists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - motto: - description: "The thematic topic (if any) of the camp's programme and storyline." - example: Piraten + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email maxLength: 128 + minLength: 1 type: - 'null' - string - name: - description: 'A short name for the camp.' - example: 'SoLa 2022' - maxLength: 32 + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 type: string - organizer: - description: 'The name of the organization which plans and carries out the camp.' - example: 'Pfadi Luftig' - maxLength: 64 + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: + - established + - inactive + - invited + example: inactive + maxLength: 16 + type: string + user: + anyOf: + - + $ref: '#/components/schemas/User.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + - + type: 'null' + readOnly: true + required: + - camp + - role + - status + type: object + CampCollaboration.jsonhal-read_CampCollaboration.Camp_CampCollaboration.User: + deprecated: false + description: 'A user participating in some way in the planning or realization of a camp.' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + camp: + anyOf: + - + $ref: '#/components/schemas/Camp.jsonhal-read_CampCollaboration.Camp_CampCollaboration.User' + - + type: 'null' + readOnly: true + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + inviteEmail: + description: |- + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email + maxLength: 128 + minLength: 1 type: - 'null' - string - periods: - description: 'The time periods of the camp, there must be at least one. Periods in a camp may not overlap.' - example: + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 + type: string + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: + - established + - inactive + - invited + example: inactive + maxLength: 16 + type: string + user: + anyOf: - - description: Hauptlager - end: '2022-01-08' - start: '2022-01-01' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - printYSLogoOnPicasso: - description: 'Whether the Y+S logo should be printed on the picasso of this camp.' - example: true - type: boolean - profiles: - description: 'All profiles of the users collaborating in this camp.' - example: '/profiles?user.collaborations.camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - progressLabels: - description: 'All the progress labels within this camp.' - example: '["/progress_labels/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + $ref: '#/components/schemas/User.jsonhal-read_CampCollaboration.Camp_CampCollaboration.User' + - + type: 'null' readOnly: true - type: array - title: - description: 'The full title of the camp.' - example: 'Abteilungs-Sommerlager 2022' - maxLength: 32 + required: + - camp + - role + - status + type: object + CampCollaboration.jsonhal-write_create: + deprecated: false + description: 'A user participating in some way in the planning or realization of a camp.' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + camp: + description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + example: /camps/1a2b3c4d + format: iri-reference type: string - trainingAdvisorName: - description: 'The name of the training advisor who is in charge of the course.' - example: 'Albert Anderegg' - maxLength: 64 + inviteEmail: + description: |- + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email + maxLength: 128 + minLength: 1 + type: + - 'null' + - string + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 + type: string + user: + description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' + example: /users/1a2b3c4d + format: iri-reference type: - 'null' - string required: - - activities - - campCollaborations - - categories - - materialLists - - name - - periods - - printYSLogoOnPicasso - - progressLabels - - title + - camp + - role type: object - Camp.jsonld-read_Period.Camp_Period.Days: + CampCollaboration.jsonld-read: deprecated: false - description: '' + description: 'A user participating in some way in the planning or realization of a camp.' properties: '@context': oneOf: @@ -6280,341 +5983,197 @@ components: '@type': readOnly: true type: string - activities: - description: |- - All the programme that will be carried out during the camp. An activity may be carried out - multiple times in the same camp. - example: '/activities?camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string + camp: + description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 readOnly: true - type: array - addressCity: - description: 'The name of the town where the camp will take place.' - example: Hintertüpfingen + type: string + inviteEmail: + description: |- + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email maxLength: 128 + minLength: 1 type: - 'null' - string - addressName: - description: 'A textual description of the location of the camp.' - example: 'Wiese hinter der alten Mühle' - maxLength: 128 + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 + type: string + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: + - established + - inactive + - invited + example: inactive + maxLength: 16 + type: string + user: + description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' + example: /users/1a2b3c4d + format: iri-reference type: - 'null' - string - addressStreet: - description: 'The street name and number (if any) of the location of the camp.' - example: 'Schönriedweg 23' - maxLength: 128 - type: - - 'null' - - string - addressZipcode: - description: 'The zipcode of the location of the camp.' - example: '1234' - maxLength: 128 - type: - - 'null' - - string - campCollaborations: - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - categories: - description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + required: + - camp + - role + - status + type: object + CampCollaboration.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + deprecated: false + description: '' + properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string readOnly: true - type: array - coachName: - description: 'The name of the Y+S coach who is in charge of the camp.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - courseKind: - description: 'The official name for the type of this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - courseNumber: - description: 'The official course number, identifying this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - creator: - description: |- - The person that created the camp. This value never changes, even when the person - leaves the camp. - example: 'https://example.com/' - format: iri-reference + '@id': + readOnly: true + type: string + '@type': readOnly: true type: string + camp: + description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + example: /camps/1a2b3c4d + format: iri-reference + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - isPrototype: - description: 'Whether this camp may serve as a template for creating other camps.' - example: true - readOnly: true - type: boolean - kind: - description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' - example: Zeltlager - maxLength: 64 - type: - - 'null' - - string - materialLists: + inviteEmail: description: |- - Lists for collecting the required materials needed for carrying out the programme. Each collaborator - has a material list, and there may be more, such as shopping lists. - example: '["/material_lists/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - motto: - description: "The thematic topic (if any) of the camp's programme and storyline." - example: Piraten + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email maxLength: 128 + minLength: 1 type: - 'null' - string - name: - description: 'A short name for the camp.' - example: 'SoLa 2022' - maxLength: 32 + role: + description: |- + The role that this person has in the camp. Depending on the role, the collaborator might have + different access rights. There must always be at least one manager in a camp. + enum: + - guest + - manager + - member + example: member + maxLength: 16 type: string - organizer: - description: 'The name of the organization which plans and carries out the camp.' - example: 'Pfadi Luftig' - maxLength: 64 - type: - - 'null' - - string - periods: - description: 'The time periods of the camp, there must be at least one. Periods in a camp may not overlap.' - example: + status: + default: invited + description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' + enum: + - established + - inactive + - invited + example: inactive + maxLength: 16 + type: string + user: + anyOf: - - description: Hauptlager - end: '2022-01-08' - start: '2022-01-01' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - printYSLogoOnPicasso: - description: 'Whether the Y+S logo should be printed on the picasso of this camp.' - example: true - type: boolean - profiles: - description: 'All profiles of the users collaborating in this camp.' - example: '/profiles?user.collaborations.camp=%2Fcamps%2F1a2b3c4d' - items: - example: 'https://example.com/' - format: iri-reference - type: string + $ref: '#/components/schemas/User.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + - + type: 'null' readOnly: true - type: array - progressLabels: - description: 'All the progress labels within this camp.' - example: '["/progress_labels/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + required: + - camp + - role + - status + type: object + CampCollaboration.jsonld-read_CampCollaboration.Camp_CampCollaboration.User: + deprecated: false + description: 'A user participating in some way in the planning or realization of a camp.' + properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': readOnly: true - type: array - title: - description: 'The full title of the camp.' - example: 'Abteilungs-Sommerlager 2022' - maxLength: 32 type: string - trainingAdvisorName: - description: 'The name of the training advisor who is in charge of the course.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - required: - - activities - - campCollaborations - - categories - - materialLists - - name - - periods - - printYSLogoOnPicasso - - progressLabels - - title - type: object - Camp.jsonld-write_create: - deprecated: false - description: |- - The main entity that eCamp is designed to manage. Contains programme which may be - distributed across multiple time periods. - properties: - addressCity: - description: 'The name of the town where the camp will take place.' - example: Hintertüpfingen - maxLength: 128 - type: - - 'null' - - string - addressName: - description: 'A textual description of the location of the camp.' - example: 'Wiese hinter der alten Mühle' - maxLength: 128 - type: - - 'null' - - string - addressStreet: - description: 'The street name and number (if any) of the location of the camp.' - example: 'Schönriedweg 23' - maxLength: 128 - type: - - 'null' - - string - addressZipcode: - description: 'The zipcode of the location of the camp.' - example: '1234' - maxLength: 128 - type: - - 'null' - - string - campPrototype: - description: 'The prototype camp that will be used as a template to create this camp.' - example: /camps/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - writeOnly: true - coachName: - description: 'The name of the Y+S coach who is in charge of the camp.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - courseKind: - description: 'The official name for the type of this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - courseNumber: - description: 'The official course number, identifying this course.' - example: 'PBS AG 123-23' - maxLength: 64 - type: - - 'null' - - string - kind: - description: 'Rough categorization of the camp (house, tent, traveling, summer, autumn).' - example: Zeltlager - maxLength: 64 - type: - - 'null' - - string - motto: - description: "The thematic topic (if any) of the camp's programme and storyline." - example: Piraten - maxLength: 128 - type: - - 'null' - - string - name: - description: 'A short name for the camp.' - example: 'SoLa 2022' - maxLength: 32 - type: string - organizer: - description: 'The name of the organization which plans and carries out the camp.' - example: 'Pfadi Luftig' - maxLength: 64 - type: - - 'null' - - string - periods: - description: 'The time periods of the camp, there must be at least one. Periods in a camp may not overlap.' - example: - - - description: Hauptlager - end: '2022-01-08' - start: '2022-01-01' - items: - $ref: '#/components/schemas/Period.jsonld-write_create' - minItems: 1 - type: array - printYSLogoOnPicasso: - description: 'Whether the Y+S logo should be printed on the picasso of this camp.' - example: true - type: boolean - title: - description: 'The full title of the camp.' - example: 'Abteilungs-Sommerlager 2022' - maxLength: 32 - type: string - trainingAdvisorName: - description: 'The name of the training advisor who is in charge of the course.' - example: 'Albert Anderegg' - maxLength: 64 - type: - - 'null' - - string - required: - - name - - periods - - printYSLogoOnPicasso - - title - type: object - CampCollaboration-read: - deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' - properties: - camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 + '@type': + readOnly: true + type: string + camp: + anyOf: + - + $ref: '#/components/schemas/Camp.jsonld-read_CampCollaboration.Camp_CampCollaboration.User' + - + type: 'null' + readOnly: true + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + inviteEmail: + description: |- + The receiver email address of the invitation email, in case the collaboration does not yet have + a user account. Either this field or the user field should be null. + example: some-email@example.com + externalDocs: + url: 'https://schema.org/email' + format: email + maxLength: 128 + minLength: 1 type: - 'null' - string @@ -6640,32 +6199,26 @@ components: maxLength: 16 type: string user: - description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' - example: /users/1a2b3c4d - format: iri-reference - type: - - 'null' - - string + anyOf: + - + $ref: '#/components/schemas/User.jsonld-read_CampCollaboration.Camp_CampCollaboration.User' + - + type: 'null' + readOnly: true required: - camp - role - status type: object - CampCollaboration-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + CampCollaboration.jsonld-write_create: deprecated: false - description: '' + description: 'A user participating in some way in the planning or realization of a camp.' properties: camp: description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' example: /camps/1a2b3c4d format: iri-reference type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string inviteEmail: description: |- The receiver email address of the invitation email, in case the collaboration does not yet have @@ -6690,433 +6243,483 @@ components: example: member maxLength: 16 type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive - maxLength: 16 - type: string user: - anyOf: - - - $ref: '#/components/schemas/User-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - - - type: 'null' - readOnly: true + description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' + example: /users/1a2b3c4d + format: iri-reference + type: + - 'null' + - string required: - camp - role - - status type: object - CampCollaboration-read_CampCollaboration.Camp_CampCollaboration.User: + Category-read: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: camp: - anyOf: - - - $ref: '#/components/schemas/Camp-read_CampCollaboration.Camp_CampCollaboration.User' - - - type: 'null' + description: 'The camp to which this category belongs. May not be changed once the category is created.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + contentNodes: + description: 'All the content nodes that make up the tree of programme content.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member - maxLength: 16 - type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive - maxLength: 16 - type: string - user: - anyOf: - - - $ref: '#/components/schemas/User-read_CampCollaboration.Camp_CampCollaboration.User' - - - type: 'null' - readOnly: true - required: - - camp - - role - - status - type: object - CampCollaboration-resend_invitation: - deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' - type: object - CampCollaboration-write_create: - deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' - properties: - camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' - example: /camps/1a2b3c4d - format: iri-reference + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 type: string - inviteEmail: + preferredContentTypes: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/content_types/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + rootContentNode: + $ref: '#/components/schemas/ColumnLayout-read' description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + The programme contents, organized as a tree of content nodes. The root content node cannot be + exchanged, but all the contents attached to it can. + example: /content_nodes/1a2b3c4d + readOnly: true + short: description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. - enum: - - guest - - manager - - member - example: member + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' - example: /users/1a2b3c4d - format: iri-reference - type: - - 'null' - - string required: - camp - - role - type: object - CampCollaboration-write_update: - deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' - properties: - role: - description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. - enum: - - guest - - manager - - member - example: member - maxLength: 16 - type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive - maxLength: 16 - type: string - required: - - role - - status + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonapi-read: + Category-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + description: '' properties: camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + description: 'The camp to which this category belongs. May not be changed once the category is created.' example: /camps/1a2b3c4d format: iri-reference type: string + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + contentNodes: + description: 'All the content nodes that make up the tree of programme content.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member - maxLength: 16 + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive + preferredContentTypes: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/content_types/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + rootContentNode: + $ref: '#/components/schemas/ColumnLayout-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + description: |- + The programme contents, organized as a tree of content nodes. The root content node cannot be + exchanged, but all the contents attached to it can. + example: /content_nodes/1a2b3c4d + readOnly: true + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' - example: /users/1a2b3c4d - format: iri-reference - type: - - 'null' - - string required: - camp - - role - - status + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + Category-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false - description: '' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + description: 'The camp to which this category belongs. May not be changed once the category is created.' example: /camps/1a2b3c4d format: iri-reference type: string + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + contentNodes: + description: 'All the content nodes that make up the tree of programme content.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member - maxLength: 16 + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive + preferredContentTypes: + items: + $ref: '#/components/schemas/ContentType-read_Category.PreferredContentTypes_Category.ContentNodes' + readOnly: true + type: array + rootContentNode: + $ref: '#/components/schemas/ColumnLayout-read_Category.PreferredContentTypes_Category.ContentNodes' + description: |- + The programme contents, organized as a tree of content nodes. The root content node cannot be + exchanged, but all the contents attached to it can. + example: /content_nodes/1a2b3c4d + readOnly: true + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - anyOf: - - - $ref: '#/components/schemas/User.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - - - type: 'null' - readOnly: true required: - camp - - role - - status + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonapi-read_CampCollaboration.Camp_CampCollaboration.User: + Category-write_create: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: camp: - anyOf: - - - $ref: '#/components/schemas/Camp.jsonapi-read_CampCollaboration.Camp_CampCollaboration.User' - - - type: 'null' - readOnly: true - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true + description: 'The camp to which this category belongs. May not be changed once the category is created.' + example: /camps/1a2b3c4d + format: iri-reference type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + copyCategorySource: + description: 'Copy contents from this source category or activity.' + example: /categories/1a2b3c4d + format: iri-reference type: - 'null' - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member - maxLength: 16 + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive + preferredContentTypes: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/content_types/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - anyOf: - - - $ref: '#/components/schemas/User.jsonapi-read_CampCollaboration.Camp_CampCollaboration.User' - - - type: 'null' - readOnly: true required: - camp - - role - - status - type: object - CampCollaboration.jsonapi-resend_invitation: - deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonapi-write_create: + Category-write_update: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: - camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' - example: /camps/1a2b3c4d - format: iri-reference + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 + type: string + preferredContentTypes: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/content_types/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' - example: /users/1a2b3c4d - format: iri-reference - type: - - 'null' - - string required: - - camp - - role + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonapi-write_update: + Category.jsonapi: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' - properties: - role: - description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. - enum: - - guest - - manager - - member - example: member - maxLength: 16 - type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive - maxLength: 16 - type: string - required: - - role - - status + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. + properties: + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' + description: |- + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. + enum: ['-', '1', A, I, a, i] + example: '1' + maxLength: 1 + type: string + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS + maxLength: 16 + type: string + required: + - color + - name + - numberingStyle + - short + type: object + id: + type: string + relationships: + properties: + camp: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + contentNodes: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + preferredContentTypes: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + rootContentNode: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + required: + - camp + - preferredContentTypes + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - [] + - + $ref: '#/components/schemas/Camp.jsonapi' + - + $ref: '#/components/schemas/Camp.jsonapi' + - + $ref: '#/components/schemas/Camp.jsonapi' + readOnly: true + type: array type: object - CampCollaboration.jsonhal-read: + Category.jsonhal-read: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: _links: properties: @@ -7128,63 +6731,82 @@ components: type: object type: object camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + description: 'The camp to which this category belongs. May not be changed once the category is created.' example: /camps/1a2b3c4d format: iri-reference type: string + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + contentNodes: + description: 'All the content nodes that make up the tree of programme content.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member - maxLength: 16 + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive + preferredContentTypes: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/content_types/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + rootContentNode: + $ref: '#/components/schemas/ColumnLayout.jsonhal-read' + description: |- + The programme contents, organized as a tree of content nodes. The root content node cannot be + exchanged, but all the contents attached to it can. + example: /content_nodes/1a2b3c4d + readOnly: true + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' - example: /users/1a2b3c4d - format: iri-reference - type: - - 'null' - - string required: - camp - - role - - status + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + Category.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: @@ -7198,65 +6820,88 @@ components: type: object type: object camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + description: 'The camp to which this category belongs. May not be changed once the category is created.' example: /camps/1a2b3c4d format: iri-reference type: string + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + contentNodes: + description: 'All the content nodes that make up the tree of programme content.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member - maxLength: 16 + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive + preferredContentTypes: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/content_types/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + rootContentNode: + $ref: '#/components/schemas/ColumnLayout.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + description: |- + The programme contents, organized as a tree of content nodes. The root content node cannot be + exchanged, but all the contents attached to it can. + example: /content_nodes/1a2b3c4d + readOnly: true + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - anyOf: - - - $ref: '#/components/schemas/User.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - - - type: 'null' - readOnly: true required: - camp - - role - - status + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonhal-read_CampCollaboration.Camp_CampCollaboration.User: + Category.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: _links: properties: @@ -7268,67 +6913,85 @@ components: type: object type: object camp: - anyOf: - - - $ref: '#/components/schemas/Camp.jsonhal-read_CampCollaboration.Camp_CampCollaboration.User' - - - type: 'null' + description: 'The camp to which this category belongs. May not be changed once the category is created.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + contentNodes: + description: 'All the content nodes that make up the tree of programme content.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member - maxLength: 16 + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive + preferredContentTypes: + items: + $ref: '#/components/schemas/ContentType.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' + readOnly: true + type: array + rootContentNode: + $ref: '#/components/schemas/ColumnLayout.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' + description: |- + The programme contents, organized as a tree of content nodes. The root content node cannot be + exchanged, but all the contents attached to it can. + example: /content_nodes/1a2b3c4d + readOnly: true + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - anyOf: - - - $ref: '#/components/schemas/User.jsonhal-read_CampCollaboration.Camp_CampCollaboration.User' - - - type: 'null' - readOnly: true required: - camp - - role - - status + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonhal-write_create: + Category.jsonhal-write_create: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: _links: properties: @@ -7340,48 +7003,73 @@ components: type: object type: object camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + description: 'The camp to which this category belongs. May not be changed once the category is created.' example: /camps/1a2b3c4d format: iri-reference type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + copyCategorySource: + description: 'Copy contents from this source category or activity.' + example: /categories/1a2b3c4d + format: iri-reference type: - 'null' - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 + type: string + preferredContentTypes: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/content_types/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' - example: /users/1a2b3c4d - format: iri-reference - type: - - 'null' - - string required: - camp - - role + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonld-read: + Category.jsonld-read: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: '@context': oneOf: @@ -7407,63 +7095,82 @@ components: readOnly: true type: string camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + description: 'The camp to which this category belongs. May not be changed once the category is created.' example: /camps/1a2b3c4d format: iri-reference type: string + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + contentNodes: + description: 'All the content nodes that make up the tree of programme content.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member - maxLength: 16 + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive + preferredContentTypes: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/content_types/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + rootContentNode: + $ref: '#/components/schemas/ColumnLayout.jsonld-read' + description: |- + The programme contents, organized as a tree of content nodes. The root content node cannot be + exchanged, but all the contents attached to it can. + example: /content_nodes/1a2b3c4d + readOnly: true + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' - example: /users/1a2b3c4d - format: iri-reference - type: - - 'null' - - string required: - camp - - role - - status + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + Category.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: @@ -7491,65 +7198,88 @@ components: readOnly: true type: string camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' + description: 'The camp to which this category belongs. May not be changed once the category is created.' example: /camps/1a2b3c4d format: iri-reference type: string + color: + description: 'The color of the activities in this category, as a hex color string.' + example: '#4DBB52' + maxLength: 8 + pattern: '^(#[0-9a-zA-Z]{6})$' + type: string + contentNodes: + description: 'All the content nodes that make up the tree of programme content.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: + name: + description: 'The full name of the category.' + example: Lagersport + maxLength: 32 + type: string + numberingStyle: + default: '1' description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. + Specifies whether the schedule entries of the activities in this category should be numbered + using arabic numbers, roman numerals or letters. enum: - - guest - - manager - - member - example: member - maxLength: 16 + - '-' + - '1' + - A + - I + - a + - i + example: '1' + maxLength: 1 type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive + preferredContentTypes: + description: 'The content types that are most likely to be useful for planning programme of this category.' + example: '["/content_types/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + type: array + rootContentNode: + $ref: '#/components/schemas/ColumnLayout.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + description: |- + The programme contents, organized as a tree of content nodes. The root content node cannot be + exchanged, but all the contents attached to it can. + example: /content_nodes/1a2b3c4d + readOnly: true + short: + description: |- + An abbreviated name of the category, for display in tight spaces, often together with the day and + schedule entry number, e.g. LS 3.a, where LS is the category's short name. + example: LS maxLength: 16 type: string - user: - anyOf: - - - $ref: '#/components/schemas/User.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - - - type: 'null' - readOnly: true required: - camp - - role - - status + - color + - name + - numberingStyle + - preferredContentTypes + - short type: object - CampCollaboration.jsonld-read_CampCollaboration.Camp_CampCollaboration.User: + Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: '@context': oneOf: @@ -7574,117 +7304,6 @@ components: '@type': readOnly: true type: string - camp: - anyOf: - - - $ref: '#/components/schemas/Camp.jsonld-read_CampCollaboration.Camp_CampCollaboration.User' - - - type: 'null' - readOnly: true - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: - description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. - enum: - - guest - - manager - - member - example: member - maxLength: 16 - type: string - status: - default: invited - description: 'Indicates whether the collaborator is still invited, has left the camp, or is participating normally.' - enum: - - established - - inactive - - invited - example: inactive - maxLength: 16 - type: string - user: - anyOf: - - - $ref: '#/components/schemas/User.jsonld-read_CampCollaboration.Camp_CampCollaboration.User' - - - type: 'null' - readOnly: true - required: - - camp - - role - - status - type: object - CampCollaboration.jsonld-write_create: - deprecated: false - description: 'A user participating in some way in the planning or realization of a camp.' - properties: - camp: - description: 'The camp that the collaborator is part of. Cannot be changed once the campCollaboration is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - inviteEmail: - description: |- - The receiver email address of the invitation email, in case the collaboration does not yet have - a user account. Either this field or the user field should be null. - example: some-email@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 128 - minLength: 1 - type: - - 'null' - - string - role: - description: |- - The role that this person has in the camp. Depending on the role, the collaborator might have - different access rights. There must always be at least one manager in a camp. - enum: - - guest - - manager - - member - example: member - maxLength: 16 - type: string - user: - description: 'The person that is collaborating in the camp. Cannot be changed once the campCollaboration is established.' - example: /users/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - required: - - camp - - role - type: object - Category-read: - deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. - properties: camp: description: 'The camp to which this category belongs. May not be changed once the category is created.' example: /camps/1a2b3c4d @@ -7732,15 +7351,12 @@ components: maxLength: 1 type: string preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' items: - example: 'https://example.com/' - format: iri-reference - type: string + $ref: '#/components/schemas/ContentType.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' + readOnly: true type: array rootContentNode: - $ref: '#/components/schemas/ColumnLayout-read' + $ref: '#/components/schemas/ColumnLayout.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' description: |- The programme contents, organized as a tree of content nodes. The root content node cannot be exchanged, but all the contents attached to it can. @@ -7761,9 +7377,13 @@ components: - preferredContentTypes - short type: object - Category-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + Category.jsonld-write_create: deprecated: false - description: '' + description: |- + A type of programme, such as sports activities or meal times, is called a category. A category + determines color and numbering scheme of the associated activities, and is used for marking + "similar" activities. A category may contain some skeleton programme which is used as a blueprint + when creating a new activity in the category. properties: camp: description: 'The camp to which this category belongs. May not be changed once the category is created.' @@ -7776,21 +7396,13 @@ components: maxLength: 8 pattern: '^(#[0-9a-zA-Z]{6})$' type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string + copyCategorySource: + description: 'Copy contents from this source category or activity.' + example: /categories/1a2b3c4d + format: iri-reference + type: + - 'null' + - string name: description: 'The full name of the category.' example: Lagersport @@ -7819,13 +7431,6 @@ components: format: iri-reference type: string type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' - description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. - example: /content_nodes/1a2b3c4d - readOnly: true short: description: |- An abbreviated name of the category, for display in tight spaces, often together with the day and @@ -7841,27 +7446,12 @@ components: - preferredContentTypes - short type: object - Category-read_Category.PreferredContentTypes_Category.ContentNodes: + ColumnLayout-read: deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. + description: '' properties: - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' + children: + description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' @@ -7869,285 +7459,195 @@ components: type: string readOnly: true type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 + items: + type: string + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' + instanceName: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - items: - $ref: '#/components/schemas/ContentType-read_Category.PreferredContentTypes_Category.ContentNodes' - readOnly: true - type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout-read_Category.PreferredContentTypes_Category.ContentNodes' + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. example: /content_nodes/1a2b3c4d - readOnly: true - short: - description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string - required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short - type: object - Category-write_create: - deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. - properties: - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - copyCategorySource: - description: 'Copy contents from this source category or activity.' - example: /categories/1a2b3c4d + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d format: iri-reference + readOnly: true type: - 'null' - string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' + slot: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' + maxLength: 32 + type: + - 'null' + - string + required: + - children + - contentType + - data + - position + type: object + ColumnLayout-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + deprecated: false + description: '' + properties: + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string + readOnly: true type: array - short: + contentType: description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference type: string - required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short - type: object - Category-write_update: - deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. - properties: - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' - description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - short: + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string - required: - - color - - name - - numberingStyle - - preferredContentTypes - - short - type: object - Category.jsonapi-read: - deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. - properties: - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 items: - example: 'https://example.com/' - format: iri-reference type: string - readOnly: true - type: array + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: - description: 'The full name of the category.' - example: Lagersport + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm maxLength: 32 - type: string - numberingStyle: - default: '1' + type: + - 'null' + - string + parent: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read' + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. example: /content_nodes/1a2b3c4d + format: iri-reference readOnly: true - short: + type: + - 'null' + - string + slot: description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short + - children + - contentType + - data + - position type: object - Category.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ColumnLayout-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' + children: + description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' @@ -8155,83 +7655,97 @@ components: type: string readOnly: true type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 + items: + type: string + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' + instanceName: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. example: /content_nodes/1a2b3c4d + format: iri-reference readOnly: true - short: + type: + - 'null' + - string + slot: description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short + - children + - contentType + - data + - position type: object - Category.jsonapi-read_Category.PreferredContentTypes_Category.ContentNodes: + ColumnLayout-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. + description: '' properties: - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' + children: + description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' @@ -8239,373 +7753,417 @@ components: type: string readOnly: true type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 + items: + type: string + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' + instanceName: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - items: - $ref: '#/components/schemas/ContentType.jsonapi-read_Category.PreferredContentTypes_Category.ContentNodes' - readOnly: true - type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read_Category.PreferredContentTypes_Category.ContentNodes' + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. example: /content_nodes/1a2b3c4d - readOnly: true - short: - description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string - required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short - type: object - Category.jsonapi-write_create: - deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. - properties: - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - copyCategorySource: - description: 'Copy contents from this source category or activity.' - example: /categories/1a2b3c4d + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d format: iri-reference + readOnly: true type: - 'null' - string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' + slot: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' + maxLength: 32 + type: + - 'null' + - string + required: + - children + - contentType + - data + - position + type: object + ColumnLayout-read_ScheduleEntry.Activity: + deprecated: false + description: '' + properties: + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string + readOnly: true type: array - short: + contentType: description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference type: string - required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short - type: object - Category.jsonapi-write_update: - deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. - properties: - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' - description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - short: + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string - required: - - color - - name - - numberingStyle - - preferredContentTypes - - short - type: object - Category.jsonhal-read: - deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 items: - example: 'https://example.com/' - format: iri-reference type: string - readOnly: true - type: array + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: - description: 'The full name of the category.' - example: Lagersport + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm maxLength: 32 - type: string - numberingStyle: - default: '1' + type: + - 'null' + - string + parent: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonhal-read' + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. example: /content_nodes/1a2b3c4d + format: iri-reference readOnly: true - short: + type: + - 'null' + - string + slot: description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short + - children + - contentType + - data + - position type: object - Category.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ColumnLayout-write_create: deprecated: false description: '' properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 items: - example: 'https://example.com/' - format: iri-reference type: string - readOnly: true - type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - name: - description: 'The full name of the category.' - example: Lagersport + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm maxLength: 32 - type: string - numberingStyle: - default: '1' + type: + - 'null' + - string + parent: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' + maxLength: 32 + type: + - 'null' + - string + required: + - contentType + - data + - parent + - position + type: object + ColumnLayout-write_update: + deprecated: false + description: '' + properties: + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 + items: + type: string + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - data + - position + type: object + ColumnLayout.jsonapi: + deprecated: false + description: '' + properties: + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string + readOnly: true type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + contentType: description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. - example: /content_nodes/1a2b3c4d + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference readOnly: true - short: + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 + items: + type: string + type: + - array + - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d maxLength: 16 + readOnly: true type: string + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short + - children + - contentType + - data + - position type: object - Category.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes: + ColumnLayout.jsonhal-read: deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. + description: '' properties: _links: properties: @@ -8616,19 +8174,8 @@ components: type: string type: object type: object - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' + children: + description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' @@ -8636,66 +8183,94 @@ components: type: string readOnly: true type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 + items: + type: string + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' + instanceName: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - items: - $ref: '#/components/schemas/ContentType.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' - readOnly: true - type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes' + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. example: /content_nodes/1a2b3c4d + format: iri-reference readOnly: true - short: + type: + - 'null' + - string + slot: description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short + - children + - contentType + - data + - position type: object - Category.jsonhal-write_create: + ColumnLayout.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. + description: '' properties: _links: properties: @@ -8706,214 +8281,115 @@ components: type: string type: object type: object - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - copyCategorySource: - description: 'Copy contents from this source category or activity.' - example: /categories/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' - description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string + readOnly: true type: array - short: + contentType: description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string - required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short - type: object - Category.jsonld-read: - deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 items: - example: 'https://example.com/' - format: iri-reference type: string - readOnly: true - type: array + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' + instanceName: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonld-read' + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. example: /content_nodes/1a2b3c4d - readOnly: true - short: - description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short + - children + - contentType + - data + - position type: object - Category.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ColumnLayout.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: - '@context': - oneOf: - - - additionalProperties: true + _links: + properties: + self: properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] + href: + format: iri-reference type: string - required: - - '@vocab' - - hydra type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' + type: object + children: + description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' @@ -8921,241 +8397,106 @@ components: type: string readOnly: true type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string + data: + default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + columns: + - + slot: '1' + width: 12 + items: + type: string + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: - description: 'The full name of the category.' - example: Lagersport + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm maxLength: 32 - type: string - numberingStyle: - default: '1' + type: + - 'null' + - string + parent: description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. example: /content_nodes/1a2b3c4d + format: iri-reference readOnly: true - short: + type: + - 'null' + - string + slot: description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short + - children + - contentType + - data + - position type: object - Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes: + ColumnLayout.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. + description: '' properties: - '@context': - oneOf: - - - additionalProperties: true + _links: + properties: + self: properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] + href: + format: iri-reference type: string - required: - - '@vocab' - - hydra type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - contentNodes: - description: 'All the content nodes that make up the tree of programme content.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' - description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - items: - $ref: '#/components/schemas/ContentType.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' - readOnly: true - type: array - rootContentNode: - $ref: '#/components/schemas/ColumnLayout.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' - description: |- - The programme contents, organized as a tree of content nodes. The root content node cannot be - exchanged, but all the contents attached to it can. - example: /content_nodes/1a2b3c4d - readOnly: true - short: - description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string - required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short - type: object - Category.jsonld-write_create: - deprecated: false - description: |- - A type of programme, such as sports activities or meal times, is called a category. A category - determines color and numbering scheme of the associated activities, and is used for marking - "similar" activities. A category may contain some skeleton programme which is used as a blueprint - when creating a new activity in the category. - properties: - camp: - description: 'The camp to which this category belongs. May not be changed once the category is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - color: - description: 'The color of the activities in this category, as a hex color string.' - example: '#4DBB52' - maxLength: 8 - pattern: '^(#[0-9a-zA-Z]{6})$' - type: string - copyCategorySource: - description: 'Copy contents from this source category or activity.' - example: /categories/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - name: - description: 'The full name of the category.' - example: Lagersport - maxLength: 32 - type: string - numberingStyle: - default: '1' - description: |- - Specifies whether the schedule entries of the activities in this category should be numbered - using arabic numbers, roman numerals or letters. - enum: - - '-' - - '1' - - A - - I - - a - - i - example: '1' - maxLength: 1 - type: string - preferredContentTypes: - description: 'The content types that are most likely to be useful for planning programme of this category.' - example: '["/content_types/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - type: array - short: - description: |- - An abbreviated name of the category, for display in tight spaces, often together with the day and - schedule entry number, e.g. LS 3.a, where LS is the category's short name. - example: LS - maxLength: 16 - type: string - required: - - camp - - color - - name - - numberingStyle - - preferredContentTypes - - short - type: object - ColumnLayout-read: - deprecated: false - description: '' - properties: - children: - description: 'All content nodes that are direct children of this content node.' + type: object + children: + description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' @@ -9248,10 +8589,19 @@ components: - data - position type: object - ColumnLayout-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ColumnLayout.jsonhal-read_ScheduleEntry.Activity: deprecated: false description: '' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object children: description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' @@ -9346,19 +8696,19 @@ components: - data - position type: object - ColumnLayout-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ColumnLayout.jsonhal-write_create: deprecated: false description: '' properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object contentType: description: |- Defines the type of this content node. There is a fixed list of types that are implemented @@ -9367,11 +8717,6 @@ components: example: /content_types/1a2b3c4d format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string data: default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' description: |- @@ -9387,12 +8732,6 @@ components: type: - array - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string instanceName: description: |- An optional name for this content node. This is useful when planning e.g. an alternative @@ -9419,16 +8758,6 @@ components: same parent. The API does not guarantee the uniqueness of parent+slot+position. example: -1 type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string slot: description: |- The name of the slot in the parent in which this content node resides. The valid slot names @@ -9439,15 +8768,38 @@ components: - 'null' - string required: - - children - contentType - data + - parent - position type: object - ColumnLayout-read_Category.PreferredContentTypes_Category.ContentNodes: + ColumnLayout.jsonld-read: deprecated: false description: '' properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string children: description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' @@ -9542,10 +8894,33 @@ components: - data - position type: object - ColumnLayout-read_ScheduleEntry.Activity: + ColumnLayout.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false description: '' properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string children: description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' @@ -9640,78 +9015,55 @@ components: - data - position type: object - ColumnLayout-write_create: + ColumnLayout.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - columns: - - - slot: '1' - width: 12 - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - data - - parent - - position - type: object - ColumnLayout-write_update: - deprecated: false - description: '' - properties: + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string data: default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' description: |- @@ -9727,6 +9079,12 @@ components: type: - array - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string instanceName: description: |- An optional name for this content node. This is useful when planning e.g. an alternative @@ -9753,6 +9111,16 @@ components: same parent. The API does not guarantee the uniqueness of parent+slot+position. example: -1 type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string slot: description: |- The name of the slot in the parent in which this content node resides. The valid slot names @@ -9763,13 +9131,38 @@ components: - 'null' - string required: + - children + - contentType - data - position type: object - ColumnLayout.jsonapi-read: + ColumnLayout.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false description: '' properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string children: description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' @@ -9864,10 +9257,33 @@ components: - data - position type: object - ColumnLayout.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ColumnLayout.jsonld-read_ScheduleEntry.Activity: deprecated: false description: '' properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string children: description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' @@ -9962,19 +9378,10 @@ components: - data - position type: object - ColumnLayout.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ColumnLayout.jsonld-write_create: deprecated: false description: '' properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array contentType: description: |- Defines the type of this content node. There is a fixed list of types that are implemented @@ -9983,11 +9390,6 @@ components: example: /content_types/1a2b3c4d format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string data: default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' description: |- @@ -10003,12 +9405,6 @@ components: type: - array - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string instanceName: description: |- An optional name for this content node. This is useful when planning e.g. an alternative @@ -10035,16 +9431,6 @@ components: same parent. The API does not guarantee the uniqueness of parent+slot+position. example: -1 type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string slot: description: |- The name of the slot in the parent in which this content node resides. The valid slot names @@ -10055,14 +9441,19 @@ components: - 'null' - string required: - - children - contentType - data + - parent - position type: object - ColumnLayout.jsonapi-read_Category.PreferredContentTypes_Category.ContentNodes: + ContentNode-read: deprecated: false - description: '' + description: |- + A piece of information that is part of a programme. ContentNodes may store content such as + one or multiple free text fields, or any other necessary data. Content nodes may also be used + to define layouts. For this purpose, a content node may offer so-called slots, into which other + content nodes may be inserted. In return, a content node may be nested inside a slot in a parent + container content node. This way, a tree of content nodes makes up a complete programme. properties: children: description: 'All content nodes that are direct children of this content node.' @@ -10087,15 +9478,9 @@ components: readOnly: true type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: @@ -10155,10 +9540,9 @@ components: required: - children - contentType - - data - position type: object - ColumnLayout.jsonapi-read_ScheduleEntry.Activity: + ContentNode-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: @@ -10185,15 +9569,9 @@ components: readOnly: true type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: @@ -10253,13 +9631,21 @@ components: required: - children - contentType - - data - position type: object - ColumnLayout.jsonapi-write_create: + ContentNode-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false description: '' properties: + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array contentType: description: |- Defines the type of this content node. There is a fixed list of types that are implemented @@ -10268,21 +9654,26 @@ components: example: /content_types/1a2b3c4d format: iri-reference type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: - array - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string instanceName: description: |- An optional name for this content node. This is useful when planning e.g. an alternative @@ -10309,66 +9700,16 @@ components: same parent. The API does not guarantee the uniqueness of parent+slot+position. example: -1 type: integer - slot: + root: description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - data - - parent - - position - type: object - ColumnLayout.jsonapi-write_update: - deprecated: false - description: '' - properties: - data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - columns: - - - slot: '1' - width: 12 - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. example: /content_nodes/1a2b3c4d format: iri-reference + readOnly: true type: - 'null' - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer slot: description: |- The name of the slot in the parent in which this content node resides. The valid slot names @@ -10379,22 +9720,19 @@ components: - 'null' - string required: - - data + - children + - contentType - position type: object - ColumnLayout.jsonhal-read: + ContentNode.jsonapi: deprecated: false - description: '' + description: |- + A piece of information that is part of a programme. ContentNodes may store content such as + one or multiple free text fields, or any other necessary data. Content nodes may also be used + to define layouts. For this purpose, a content node may offer so-called slots, into which other + content nodes may be inserted. In return, a content node may be nested inside a slot in a parent + container content node. This way, a tree of content nodes makes up a complete programme. properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object children: description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' @@ -10411,6 +9749,7 @@ components: in a content node. The content type may not be changed once the content node is created. example: /content_types/1a2b3c4d format: iri-reference + readOnly: true type: string contentTypeName: description: 'The name of the content type of this content node. Read-only, for convenience.' @@ -10418,15 +9757,9 @@ components: readOnly: true type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: @@ -10486,12 +9819,16 @@ components: required: - children - contentType - - data - position type: object - ColumnLayout.jsonhal-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ContentNode.jsonhal-read: deprecated: false - description: '' + description: |- + A piece of information that is part of a programme. ContentNodes may store content such as + one or multiple free text fields, or any other necessary data. Content nodes may also be used + to define layouts. For this purpose, a content node may offer so-called slots, into which other + content nodes may be inserted. In return, a content node may be nested inside a slot in a parent + container content node. This way, a tree of content nodes makes up a complete programme. properties: _links: properties: @@ -10525,15 +9862,9 @@ components: readOnly: true type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: @@ -10593,10 +9924,9 @@ components: required: - children - contentType - - data - position type: object - ColumnLayout.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ContentNode.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: @@ -10632,15 +9962,9 @@ components: readOnly: true type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: @@ -10700,10 +10024,9 @@ components: required: - children - contentType - - data - position type: object - ColumnLayout.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes: + ContentNode.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false description: '' properties: @@ -10739,15 +10062,9 @@ components: readOnly: true type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: @@ -10807,22 +10124,23 @@ components: required: - children - contentType - - data - position type: object - ColumnLayout.jsonhal-read_ScheduleEntry.Activity: + ContentNode.jsonld-read: deprecated: false - description: '' + description: |- + A piece of information that is part of a programme. ContentNodes may store content such as + one or multiple free text fields, or any other necessary data. Content nodes may also be used + to define layouts. For this purpose, a content node may offer so-called slots, into which other + content nodes may be inserted. In return, a content node may be nested inside a slot in a parent + container content node. This way, a tree of content nodes makes up a complete programme. properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string children: description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' @@ -10846,15 +10164,9 @@ components: readOnly: true type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: @@ -10914,22 +10226,44 @@ components: required: - children - contentType - - data - position type: object - ColumnLayout.jsonhal-write_create: + ContentNode.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: - _links: - properties: - self: + '@context': + oneOf: + - + additionalProperties: true properties: - href: - format: iri-reference + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] type: string + required: + - '@vocab' + - hydra type: object - type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array contentType: description: |- Defines the type of this content node. There is a fixed list of types that are implemented @@ -10938,21 +10272,26 @@ components: example: /content_types/1a2b3c4d format: iri-reference type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: - array - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string instanceName: description: |- An optional name for this content node. This is useful when planning e.g. an alternative @@ -10979,6 +10318,16 @@ components: same parent. The API does not guarantee the uniqueness of parent+slot+position. example: -1 type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string slot: description: |- The name of the slot in the parent in which this content node resides. The valid slot names @@ -10989,12 +10338,11 @@ components: - 'null' - string required: + - children - contentType - - data - - parent - position type: object - ColumnLayout.jsonld-read: + ContentNode.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false description: '' properties: @@ -11044,15 +10392,9 @@ components: readOnly: true type: string data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + description: 'Holds the actual data of the content node.' example: - columns: - - - slot: '1' - width: 12 + text: 'dummy text' items: type: string type: @@ -11112,133 +10454,264 @@ components: required: - children - contentType - - data - position type: object - ColumnLayout.jsonld-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ContentType-read: deprecated: false - description: '' + description: |- + Defines a type of content that can be present in a content node tree. A content type + determines what data can be stored in content nodes of this type, as well as validation, + available slots and jsonConfig settings. properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string + active: + default: true + description: 'Whether this content type is still maintained and recommended for use in new camps.' + example: 'true' readOnly: true - '@id': + type: boolean + contentNodes: + description: 'API endpoint link for creating new entities of type entityClass.' + example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' + format: iri-reference + items: + type: string + readOnly: true + type: array + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 readOnly: true type: string - '@type': + name: + description: |- + A name in UpperCamelCase of the content type. This value may be used as a technical + identifier of this content type, it is guaranteed to stay fixed. + example: SafetyConcept + maxLength: 32 readOnly: true type: string - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + required: + - active + - name + type: object + ContentType-read_Category.PreferredContentTypes_Category.ContentNodes: + deprecated: false + description: '' + properties: + active: + default: true + description: 'Whether this content type is still maintained and recommended for use in new camps.' + example: 'true' + readOnly: true + type: boolean + contentNodes: + description: 'API endpoint link for creating new entities of type entityClass.' + example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' + format: iri-reference items: - example: 'https://example.com/' - format: iri-reference type: string readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' + name: + description: |- + A name in UpperCamelCase of the content type. This value may be used as a technical + identifier of this content type, it is guaranteed to stay fixed. example: SafetyConcept + maxLength: 32 readOnly: true type: string + required: + - active + - name + type: object + ContentType.jsonapi: + deprecated: false + description: |- + Defines a type of content that can be present in a content node tree. A content type + determines what data can be stored in content nodes of this type, as well as validation, + available slots and jsonConfig settings. + properties: data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - columns: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + active: + default: true + description: 'Whether this content type is still maintained and recommended for use in new camps.' + example: 'true' + readOnly: true + type: boolean + contentNodes: + description: 'API endpoint link for creating new entities of type entityClass.' + example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' + format: iri-reference + items: { type: string } + readOnly: true + type: array + entityClass: + description: |- + The name of the internal PHP class that implements all custom behaviour of content nodes + of this type. + readOnly: true + type: string + jsonConfig: + description: |- + Internal configuration for the entityClass, in case the same entityClass is reused + for different content types. + items: { type: string } + readOnly: true + type: [array, 'null'] + name: + description: |- + A name in UpperCamelCase of the content type. This value may be used as a technical + identifier of this content type, it is guaranteed to stay fixed. + example: SafetyConcept + maxLength: 32 + readOnly: true + type: string + required: + - active + - entityClass + - name + type: object + id: + type: string + relationships: + properties: + categories: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + required: + - categories + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: - - slot: '1' - width: 12 + $ref: '#/components/schemas/Category.jsonapi' + readOnly: true + type: array + type: object + ContentType.jsonhal-read: + deprecated: false + description: |- + Defines a type of content that can be present in a content node tree. A content type + determines what data can be stored in content nodes of this type, as well as validation, + available slots and jsonConfig settings. + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + active: + default: true + description: 'Whether this content type is still maintained and recommended for use in new camps.' + example: 'true' + readOnly: true + type: boolean + contentNodes: + description: 'API endpoint link for creating new entities of type entityClass.' + example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' + format: iri-reference items: type: string - type: - - array - - 'null' + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: + name: description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm + A name in UpperCamelCase of the content type. This value may be used as a technical + identifier of this content type, it is guaranteed to stay fixed. + example: SafetyConcept maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d + readOnly: true + type: string + required: + - active + - name + type: object + ContentType.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes: + deprecated: false + description: '' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + active: + default: true + description: 'Whether this content type is still maintained and recommended for use in new camps.' + example: 'true' + readOnly: true + type: boolean + contentNodes: + description: 'API endpoint link for creating new entities of type entityClass.' + example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' format: iri-reference + items: + type: string readOnly: true - type: - - 'null' - - string - slot: + type: array + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + name: description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' + A name in UpperCamelCase of the content type. This value may be used as a technical + identifier of this content type, it is guaranteed to stay fixed. + example: SafetyConcept maxLength: 32 - type: - - 'null' - - string + readOnly: true + type: string required: - - children - - contentType - - data - - position + - active + - name type: object - ColumnLayout.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ContentType.jsonld-read: deprecated: false - description: '' + description: |- + Defines a type of content that can be present in a content node tree. A content type + determines what data can be stored in content nodes of this type, as well as validation, + available slots and jsonConfig settings. properties: '@context': oneOf: @@ -11263,101 +10736,39 @@ components: '@type': readOnly: true type: string - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + active: + default: true + description: 'Whether this content type is still maintained and recommended for use in new camps.' + example: 'true' readOnly: true - type: string - data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - columns: - - - slot: '1' - width: 12 + type: boolean + contentNodes: + description: 'API endpoint link for creating new entities of type entityClass.' + example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' + format: iri-reference items: type: string - type: - - array - - 'null' + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: + name: description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm + A name in UpperCamelCase of the content type. This value may be used as a technical + identifier of this content type, it is guaranteed to stay fixed. + example: SafetyConcept maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string + type: string required: - - children - - contentType - - data - - position + - active + - name type: object - ColumnLayout.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes: + ContentType.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes: deprecated: false description: '' properties: @@ -11384,953 +10795,912 @@ components: '@type': readOnly: true type: string - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + active: + default: true + description: 'Whether this content type is still maintained and recommended for use in new camps.' + example: 'true' readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d + type: boolean + contentNodes: + description: 'API endpoint link for creating new entities of type entityClass.' + example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string - data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - columns: - - - slot: '1' - width: 12 items: type: string - type: - - array - - 'null' + readOnly: true + type: array id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: + name: description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm + A name in UpperCamelCase of the content type. This value may be used as a technical + identifier of this content type, it is guaranteed to stay fixed. + example: SafetyConcept maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string + type: string required: - - children - - contentType - - data - - position + - active + - name type: object - ColumnLayout.jsonld-read_ScheduleEntry.Activity: + Credentials: + properties: + identifier: + example: test@example.com + type: string + password: + example: test + type: string + type: object + Day-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: deprecated: false description: '' properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' readOnly: true - '@id': + type: integer + dayResponsibles: + description: 'The list of people who have a whole-day responsibility on this day.' + example: '["/day_responsibles/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date + readOnly: true + type: + - 'null' + - string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 readOnly: true type: string - '@type': + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' readOnly: true + type: integer + period: + description: 'The time period that this day belongs to.' + example: /periods/1a2b3c4d + format: iri-reference type: string - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + scheduleEntries: + description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." + example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date readOnly: true - type: string - data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - columns: - - - slot: '1' - width: 12 + type: + - 'null' + - string + required: + - dayOffset + - dayResponsibles + - period + type: object + Day-read_Day.DayResponsibles: + deprecated: false + description: |- + A day in a time period of a camp. This is represented as a reference to the time period + along with a number of days offset from the period's starting date. This is to make it + easier to move the whole periods to different dates. Days are created automatically when + creating or updating periods, and are not writable through the API directly. + properties: + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' + readOnly: true + type: integer + dayResponsibles: items: - type: string + $ref: '#/components/schemas/DayResponsible-read_Day.DayResponsibles' + readOnly: true + type: array + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date + readOnly: true type: - - array - 'null' + - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - children - - contentType - - data - - position - type: object - ColumnLayout.jsonld-write_create: - deprecated: false - description: '' - properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d + type: integer + period: + description: 'The time period that this day belongs to.' + example: /periods/1a2b3c4d format: iri-reference type: string - data: - default: '{"columns":[{"slot":"1","width":6},{"slot":"2","width":6}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - columns: - - - slot: '1' - width: 12 + scheduleEntries: + description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." + example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' items: + example: 'https://example.com/' + format: iri-reference type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 + readOnly: true + type: array + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date + readOnly: true type: - 'null' - string required: - - contentType - - data - - parent - - position + - dayOffset + - dayResponsibles + - period type: object - ContentNode-read: + Day-read_Period.Camp_Period.Days: deprecated: false - description: |- - A piece of information that is part of a programme. ContentNodes may store content such as - one or multiple free text fields, or any other necessary data. Content nodes may also be used - to define layouts. For this purpose, a content node may offer so-called slots, into which other - content nodes may be inserted. In return, a content node may be nested inside a slot in a parent - container content node. This way, a tree of content nodes makes up a complete programme. + description: '' properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' + readOnly: true + type: integer + dayResponsibles: + description: 'The list of people who have a whole-day responsibility on this day.' + example: '["/day_responsibles/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date readOnly: true - type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string type: - - array - 'null' + - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' + readOnly: true + type: integer + period: + description: 'The time period that this day belongs to.' + example: /periods/1a2b3c4d format: iri-reference + type: string + scheduleEntries: + description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." + example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 type: - 'null' - string required: - - children - - contentType - - position + - dayOffset + - dayResponsibles + - period type: object - ContentNode-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + Day.jsonapi: + deprecated: false + description: |- + A day in a time period of a camp. This is represented as a reference to the time period + along with a number of days offset from the period's starting date. This is to make it + easier to move the whole periods to different dates. Days are created automatically when + creating or updating periods, and are not writable through the API directly. + properties: + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' + readOnly: true + type: integer + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date + readOnly: true + type: ['null', string] + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' + readOnly: true + type: integer + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date + readOnly: true + type: ['null', string] + required: + - dayOffset + type: object + id: + type: string + relationships: + properties: + dayResponsibles: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + period: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + scheduleEntries: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + required: + - dayResponsibles + - period + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - + $ref: '#/components/schemas/DayResponsible.jsonapi' + - + $ref: '#/components/schemas/DayResponsible.jsonapi' + - + $ref: '#/components/schemas/DayResponsible.jsonapi' + readOnly: true + type: array + type: object + Day.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: deprecated: false description: '' properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' + readOnly: true + type: integer + dayResponsibles: + description: 'The list of people who have a whole-day responsibility on this day.' + example: '["/day_responsibles/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date readOnly: true - type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string type: - - array - 'null' + - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' + readOnly: true type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d + period: + description: 'The time period that this day belongs to.' + example: /periods/1a2b3c4d format: iri-reference + type: string + scheduleEntries: + description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." + example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 type: - 'null' - string required: - - children - - contentType - - position + - dayOffset + - dayResponsibles + - period type: object - ContentNode-read_Category.PreferredContentTypes_Category.ContentNodes: + Day.jsonhal-read_Day.DayResponsibles: deprecated: false - description: '' + description: |- + A day in a time period of a camp. This is represented as a reference to the time period + along with a number of days offset from the period's starting date. This is to make it + easier to move the whole periods to different dates. Days are created automatically when + creating or updating periods, and are not writable through the API directly. properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' + readOnly: true + type: integer + dayResponsibles: items: - example: 'https://example.com/' - format: iri-reference - type: string + $ref: '#/components/schemas/DayResponsible.jsonhal-read_Day.DayResponsibles' readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date readOnly: true - type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string type: - - array - 'null' + - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' + readOnly: true type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d + period: + description: 'The time period that this day belongs to.' + example: /periods/1a2b3c4d format: iri-reference + type: string + scheduleEntries: + description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." + example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 type: - 'null' - string required: - - children - - contentType - - position + - dayOffset + - dayResponsibles + - period type: object - ContentNode.jsonapi-read: + Day.jsonhal-read_Period.Camp_Period.Days: deprecated: false - description: |- - A piece of information that is part of a programme. ContentNodes may store content such as - one or multiple free text fields, or any other necessary data. Content nodes may also be used - to define layouts. For this purpose, a content node may offer so-called slots, into which other - content nodes may be inserted. In return, a content node may be nested inside a slot in a parent - container content node. This way, a tree of content nodes makes up a complete programme. + description: '' properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' + readOnly: true + type: integer + dayResponsibles: + description: 'The list of people who have a whole-day responsibility on this day.' + example: '["/day_responsibles/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date readOnly: true - type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string type: - - array - 'null' + - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' + readOnly: true type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d + period: + description: 'The time period that this day belongs to.' + example: /periods/1a2b3c4d format: iri-reference + type: string + scheduleEntries: + description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." + example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 type: - 'null' - string required: - - children - - contentType - - position + - dayOffset + - dayResponsibles + - period type: object - ContentNode.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + Day.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: deprecated: false description: '' properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' + readOnly: true + type: integer + dayResponsibles: + description: 'The list of people who have a whole-day responsibility on this day.' + example: '["/day_responsibles/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date readOnly: true - type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string type: - - array - 'null' + - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' + readOnly: true + type: integer + period: + description: 'The time period that this day belongs to.' + example: /periods/1a2b3c4d + format: iri-reference + type: string + scheduleEntries: + description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." + example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date + readOnly: true type: - 'null' - string required: - - children - - contentType - - position + - dayOffset + - dayResponsibles + - period type: object - ContentNode.jsonapi-read_Category.PreferredContentTypes_Category.ContentNodes: + Day.jsonld-read_Day.DayResponsibles: deprecated: false - description: '' + description: |- + A day in a time period of a camp. This is represented as a reference to the time period + along with a number of days offset from the period's starting date. This is to make it + easier to move the whole periods to different dates. Days are created automatically when + creating or updating periods, and are not writable through the API directly. properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + '@type': readOnly: true type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' + readOnly: true + type: integer + dayResponsibles: items: - type: string + $ref: '#/components/schemas/DayResponsible.jsonld-read_Day.DayResponsibles' + readOnly: true + type: array + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date + readOnly: true type: - - array - 'null' + - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' + readOnly: true type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d + period: + description: 'The time period that this day belongs to.' + example: /periods/1a2b3c4d format: iri-reference + type: string + scheduleEntries: + description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." + example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 type: - 'null' - string required: - - children - - contentType - - position + - dayOffset + - dayResponsibles + - period type: object - ContentNode.jsonhal-read: + Day.jsonld-read_Period.Camp_Period.Days: deprecated: false - description: |- - A piece of information that is part of a programme. ContentNodes may store content such as - one or multiple free text fields, or any other necessary data. Content nodes may also be used - to define layouts. For this purpose, a content node may offer so-called slots, into which other - content nodes may be inserted. In return, a content node may be nested inside a slot in a parent - container content node. This way, a tree of content nodes makes up a complete programme. + description: '' properties: - _links: - properties: - self: + '@context': + oneOf: + - + additionalProperties: true properties: - href: - format: iri-reference + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] type: string + required: + - '@vocab' + - hydra type: object - type: object - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + - + type: string + readOnly: true + '@id': + readOnly: true + type: string + '@type': + readOnly: true + type: string + dayOffset: + description: "The 0-based offset in days from the period's start date when this day starts." + example: '1' + readOnly: true + type: integer + dayResponsibles: + description: 'The list of people who have a whole-day responsibility on this day.' + example: '["/day_responsibles/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + end: + description: 'The end date and time of the day. This is a read-only convenience property.' + example: '2022-01-03T00:00:00+00:00' + format: date readOnly: true - type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string type: - - array - 'null' + - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 + number: + description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' + example: '2' + readOnly: true type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d + period: + description: 'The time period that this day belongs to.' + example: /periods/1a2b3c4d format: iri-reference + type: string + scheduleEntries: + description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." + example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The start date and time of the day. This is a read-only convenience property.' + example: '2022-01-02T00:00:00+00:00' + format: date readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 type: - 'null' - string required: - - children - - contentType - - position + - dayOffset + - dayResponsibles + - period type: object - ContentNode.jsonhal-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + DayResponsible-read: + deprecated: false + description: 'A person that has some whole-day responsibility on a day in the camp.' + properties: + campCollaboration: + description: "The person that is responsible. Must belong to the same camp as the day's period." + example: /camp_collaborations/1a2b3c4d + format: iri-reference + type: string + day: + description: 'The day on which the person is responsible.' + example: /days/1a2b3c4d + format: iri-reference + type: string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + required: + - campCollaboration + - day + type: object + DayResponsible-read_Day.DayResponsibles: deprecated: false description: '' + properties: + campCollaboration: + description: "The person that is responsible. Must belong to the same camp as the day's period." + example: /camp_collaborations/1a2b3c4d + format: iri-reference + type: string + day: + description: 'The day on which the person is responsible.' + example: /days/1a2b3c4d + format: iri-reference + type: string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + required: + - campCollaboration + - day + type: object + DayResponsible-write: + deprecated: false + description: 'A person that has some whole-day responsibility on a day in the camp.' + properties: + campCollaboration: + description: "The person that is responsible. Must belong to the same camp as the day's period." + example: /camp_collaborations/1a2b3c4d + format: iri-reference + type: string + day: + description: 'The day on which the person is responsible.' + example: /days/1a2b3c4d + format: iri-reference + type: string + required: + - campCollaboration + - day + type: object + DayResponsible.jsonapi: + deprecated: false + description: 'A person that has some whole-day responsibility on a day in the camp.' + properties: + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + type: object + id: + type: string + relationships: + properties: + campCollaboration: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + day: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + required: + - campCollaboration + - day + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - + $ref: '#/components/schemas/Day.jsonapi' + - + $ref: '#/components/schemas/Day.jsonapi' + readOnly: true + type: array + type: object + DayResponsible.jsonhal-read: + deprecated: false + description: 'A person that has some whole-day responsibility on a day in the camp.' properties: _links: properties: @@ -12341,94 +11711,27 @@ components: type: string type: object type: object - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d + campCollaboration: + description: "The person that is responsible. Must belong to the same camp as the day's period." + example: /camp_collaborations/1a2b3c4d format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true + day: + description: 'The day on which the person is responsible.' + example: /days/1a2b3c4d + format: iri-reference type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string - type: - - array - - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string required: - - children - - contentType - - position + - campCollaboration + - day type: object - ContentNode.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes: + DayResponsible.jsonhal-read_Day.DayResponsibles: deprecated: false description: '' properties: @@ -12441,196 +11744,101 @@ components: type: string type: object type: object - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d + campCollaboration: + description: "The person that is responsible. Must belong to the same camp as the day's period." + example: /camp_collaborations/1a2b3c4d format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true + day: + description: 'The day on which the person is responsible.' + example: /days/1a2b3c4d + format: iri-reference type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string - type: - - array - - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d + required: + - campCollaboration + - day + type: object + DayResponsible.jsonhal-write: + deprecated: false + description: 'A person that has some whole-day responsibility on a day in the camp.' + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + campCollaboration: + description: "The person that is responsible. Must belong to the same camp as the day's period." + example: /camp_collaborations/1a2b3c4d format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d + type: string + day: + description: 'The day on which the person is responsible.' + example: /days/1a2b3c4d format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string + type: string required: - - children - - contentType - - position + - campCollaboration + - day type: object - ContentNode.jsonld-read: + DayResponsible.jsonld-read: deprecated: false - description: |- - A piece of information that is part of a programme. ContentNodes may store content such as - one or multiple free text fields, or any other necessary data. Content nodes may also be used - to define layouts. For this purpose, a content node may offer so-called slots, into which other - content nodes may be inserted. In return, a content node may be nested inside a slot in a parent - container content node. This way, a tree of content nodes makes up a complete programme. + description: 'A person that has some whole-day responsibility on a day in the camp.' properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true '@id': readOnly: true type: string '@type': readOnly: true type: string - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d + campCollaboration: + description: "The person that is responsible. Must belong to the same camp as the day's period." + example: /camp_collaborations/1a2b3c4d format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true + day: + description: 'The day on which the person is responsible.' + example: /days/1a2b3c4d + format: iri-reference type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string - type: - - array - - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string required: - - children - - contentType - - position + - campCollaboration + - day type: object - ContentNode.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + DayResponsible.jsonld-read_Day.DayResponsibles: deprecated: false description: '' properties: @@ -12657,96 +11865,175 @@ components: '@type': readOnly: true type: string - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d + campCollaboration: + description: "The person that is responsible. Must belong to the same camp as the day's period." + example: /camp_collaborations/1a2b3c4d format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true + day: + description: 'The day on which the person is responsible.' + example: /days/1a2b3c4d + format: iri-reference type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string - type: - - array - - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d + required: + - campCollaboration + - day + type: object + DayResponsible.jsonld-write: + deprecated: false + description: 'A person that has some whole-day responsibility on a day in the camp.' + properties: + campCollaboration: + description: "The person that is responsible. Must belong to the same camp as the day's period." + example: /camp_collaborations/1a2b3c4d format: iri-reference - type: - - 'null' - - string - position: - default: -1 + type: string + day: + description: 'The day on which the person is responsible.' + example: /days/1a2b3c4d + format: iri-reference + type: string + required: + - campCollaboration + - day + type: object + Invitation-read: + deprecated: false + description: |- + An invitation for a person to collaborate in a camp. The person may or may not + already have an account. + properties: + campId: description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: + The id of the camp for which this invitation is valid. This is useful for + redirecting the user to the correct place after they accept. + example: 1a2b3c4d + type: string + campTitle: description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true + The full title of the camp for which this invitation is valid. This should help + the user to decide whether to accept or reject the invitation. + example: 'Abteilungs-Sommerlager 2022' + type: string + userAlreadyInCamp: + description: |- + Indicates whether the logged in user is already collaborating in the camp, and + can therefore not accept the invitation. + type: + - boolean + - 'null' + userDisplayName: + description: |- + The display name of the user that is invited. May be null in case the user does + not already have an account. + example: 'Robert Baden-Powell' type: - 'null' - string - slot: + type: object + Invitation-write: + deprecated: false + description: |- + An invitation for a person to collaborate in a camp. The person may or may not + already have an account. + type: object + Invitation.jsonapi: + deprecated: false + description: |- + An invitation for a person to collaborate in a camp. The person may or may not + already have an account. + properties: + data: + properties: + attributes: + properties: + campId: + description: |- + The id of the camp for which this invitation is valid. This is useful for + redirecting the user to the correct place after they accept. + example: 1a2b3c4d + type: string + campTitle: + description: |- + The full title of the camp for which this invitation is valid. This should help + the user to decide whether to accept or reject the invitation. + example: 'Abteilungs-Sommerlager 2022' + type: string + userAlreadyInCamp: + description: |- + Indicates whether the logged in user is already collaborating in the camp, and + can therefore not accept the invitation. + type: [boolean, 'null'] + userDisplayName: + description: |- + The display name of the user that is invited. May be null in case the user does + not already have an account. + example: 'Robert Baden-Powell' + type: ['null', string] + type: object + id: + type: string + type: + type: string + required: + - id + - type + type: object + type: object + Invitation.jsonhal-read: + deprecated: false + description: |- + An invitation for a person to collaborate in a camp. The person may or may not + already have an account. + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + campId: description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 + The id of the camp for which this invitation is valid. This is useful for + redirecting the user to the correct place after they accept. + example: 1a2b3c4d + type: string + campTitle: + description: |- + The full title of the camp for which this invitation is valid. This should help + the user to decide whether to accept or reject the invitation. + example: 'Abteilungs-Sommerlager 2022' + type: string + userAlreadyInCamp: + description: |- + Indicates whether the logged in user is already collaborating in the camp, and + can therefore not accept the invitation. + type: + - boolean + - 'null' + userDisplayName: + description: |- + The display name of the user that is invited. May be null in case the user does + not already have an account. + example: 'Robert Baden-Powell' type: - 'null' - string - required: - - children - - contentType - - position type: object - ContentNode.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes: + Invitation.jsonld-read: deprecated: false - description: '' + description: |- + An invitation for a person to collaborate in a camp. The person may or may not + already have an account. properties: '@context': oneOf: @@ -12771,249 +12058,197 @@ components: '@type': readOnly: true type: string - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: + campId: description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference + The id of the camp for which this invitation is valid. This is useful for + redirecting the user to the correct place after they accept. + example: 1a2b3c4d type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true + campTitle: + description: |- + The full title of the camp for which this invitation is valid. This should help + the user to decide whether to accept or reject the invitation. + example: 'Abteilungs-Sommerlager 2022' type: string - data: - description: 'Holds the actual data of the content node.' - example: - text: 'dummy text' - items: - type: string + userAlreadyInCamp: + description: |- + Indicates whether the logged in user is already collaborating in the camp, and + can therefore not accept the invitation. + type: + - boolean + - 'null' + userDisplayName: + description: |- + The display name of the user that is invited. May be null in case the user does + not already have an account. + example: 'Robert Baden-Powell' type: - - array - 'null' + - string + type: object + MaterialItem-read: + deprecated: false + description: '' + properties: + article: + description: 'The name of the item that is required.' + example: Volleyball + maxLength: 64 + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: + materialList: description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 + The list to which this item belongs. Lists are used to keep track of who is + responsible to prepare and bring the item to the camp. + example: /material_lists/1a2b3c4d + format: iri-reference + type: string + materialNode: + description: 'The content node to which this item belongs, if it does not belong to a period.' + example: /content_node/material_nodes/1a2b3c4d + format: iri-reference type: - 'null' - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d + period: + description: 'The period to which this item belongs, if it does not belong to a content node.' + example: /periods/1a2b3c4d format: iri-reference type: - 'null' - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true + quantity: + description: 'The number of items or the amount in the unit of items that are required.' + example: 1.5 type: - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' + - number + unit: + description: 'An optional unit for measuring the amount of items required.' + example: kg maxLength: 32 type: - 'null' - string required: - - children - - contentType - - position + - article + - materialList type: object - ContentType-read: + MaterialItem-write: deprecated: false - description: |- - Defines a type of content that can be present in a content node tree. A content type - determines what data can be stored in content nodes of this type, as well as validation, - available slots and jsonConfig settings. + description: 'A physical item that is needed for carrying out a programme or camp.' properties: - active: - default: true - description: 'Whether this content type is still maintained and recommended for use in new camps.' - example: 'true' - readOnly: true - type: boolean - contentNodes: - description: 'API endpoint link for creating new entities of type entityClass.' - example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' - format: iri-reference - items: - type: string - readOnly: true - type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true + article: + description: 'The name of the item that is required.' + example: Volleyball type: string - name: + materialList: description: |- - A name in UpperCamelCase of the content type. This value may be used as a technical - identifier of this content type, it is guaranteed to stay fixed. - example: SafetyConcept - maxLength: 32 - readOnly: true - type: string - required: - - active - - name - type: object - ContentType-read_Category.PreferredContentTypes_Category.ContentNodes: - deprecated: false - description: '' - properties: - active: - default: true - description: 'Whether this content type is still maintained and recommended for use in new camps.' - example: 'true' - readOnly: true - type: boolean - contentNodes: - description: 'API endpoint link for creating new entities of type entityClass.' - example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' + The list to which this item belongs. Lists are used to keep track of who is + responsible to prepare and bring the item to the camp. + example: /material_lists/1a2b3c4d format: iri-reference - items: - type: string - readOnly: true - type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - name: - description: |- - A name in UpperCamelCase of the content type. This value may be used as a technical - identifier of this content type, it is guaranteed to stay fixed. - example: SafetyConcept - maxLength: 32 - readOnly: true type: string - required: - - active - - name - type: object - ContentType.jsonapi-read: - deprecated: false - description: |- - Defines a type of content that can be present in a content node tree. A content type - determines what data can be stored in content nodes of this type, as well as validation, - available slots and jsonConfig settings. - properties: - active: - default: true - description: 'Whether this content type is still maintained and recommended for use in new camps.' - example: 'true' - readOnly: true - type: boolean - contentNodes: - description: 'API endpoint link for creating new entities of type entityClass.' - example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' + materialNode: + description: 'The content node to which this item belongs, if it does not belong to a period.' + example: /content_node/material_nodes/1a2b3c4d format: iri-reference - items: - type: string - readOnly: true - type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - name: - description: |- - A name in UpperCamelCase of the content type. This value may be used as a technical - identifier of this content type, it is guaranteed to stay fixed. - example: SafetyConcept - maxLength: 32 - readOnly: true - type: string - required: - - active - - name + type: + - 'null' + - string + period: + description: 'The period to which this item belongs, if it does not belong to a content node.' + example: /periods/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + quantity: + description: 'The number of items or the amount in the unit of items that are required.' + example: 1.5 + type: + - 'null' + - number + unit: + description: 'An optional unit for measuring the amount of items required.' + example: kg + type: + - 'null' + - string type: object - ContentType.jsonapi-read_Category.PreferredContentTypes_Category.ContentNodes: + MaterialItem.jsonapi: deprecated: false - description: '' + description: 'A physical item that is needed for carrying out a programme or camp.' properties: - active: - default: true - description: 'Whether this content type is still maintained and recommended for use in new camps.' - example: 'true' - readOnly: true - type: boolean - contentNodes: - description: 'API endpoint link for creating new entities of type entityClass.' - example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' - format: iri-reference + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + article: + description: 'The name of the item that is required.' + example: Volleyball + maxLength: 64 + type: string + quantity: + description: 'The number of items or the amount in the unit of items that are required.' + example: 1.5 + type: ['null', number] + unit: + description: 'An optional unit for measuring the amount of items required.' + example: kg + maxLength: 32 + type: ['null', string] + required: + - article + type: object + id: + type: string + relationships: + properties: + materialList: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + materialNode: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + period: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + required: + - materialList + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' items: - type: string + anyOf: + - + $ref: '#/components/schemas/MaterialList.jsonapi' + - + $ref: '#/components/schemas/MaterialList.jsonapi' + - + $ref: '#/components/schemas/MaterialList.jsonapi' readOnly: true type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - name: - description: |- - A name in UpperCamelCase of the content type. This value may be used as a technical - identifier of this content type, it is guaranteed to stay fixed. - example: SafetyConcept - maxLength: 32 - readOnly: true - type: string - required: - - active - - name type: object - ContentType.jsonhal-read: + MaterialItem.jsonhal-read: deprecated: false - description: |- - Defines a type of content that can be present in a content node tree. A content type - determines what data can be stored in content nodes of this type, as well as validation, - available slots and jsonConfig settings. + description: '' properties: _links: properties: @@ -13024,41 +12259,58 @@ components: type: string type: object type: object - active: - default: true - description: 'Whether this content type is still maintained and recommended for use in new camps.' - example: 'true' - readOnly: true - type: boolean - contentNodes: - description: 'API endpoint link for creating new entities of type entityClass.' - example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' - format: iri-reference - items: - type: string - readOnly: true - type: array + article: + description: 'The name of the item that is required.' + example: Volleyball + maxLength: 64 + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: + materialList: description: |- - A name in UpperCamelCase of the content type. This value may be used as a technical - identifier of this content type, it is guaranteed to stay fixed. - example: SafetyConcept - maxLength: 32 - readOnly: true + The list to which this item belongs. Lists are used to keep track of who is + responsible to prepare and bring the item to the camp. + example: /material_lists/1a2b3c4d + format: iri-reference type: string + materialNode: + description: 'The content node to which this item belongs, if it does not belong to a period.' + example: /content_node/material_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + period: + description: 'The period to which this item belongs, if it does not belong to a content node.' + example: /periods/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + quantity: + description: 'The number of items or the amount in the unit of items that are required.' + example: 1.5 + type: + - 'null' + - number + unit: + description: 'An optional unit for measuring the amount of items required.' + example: kg + maxLength: 32 + type: + - 'null' + - string required: - - active - - name + - article + - materialList type: object - ContentType.jsonhal-read_Category.PreferredContentTypes_Category.ContentNodes: + MaterialItem.jsonhal-write: deprecated: false - description: '' + description: 'A physical item that is needed for carrying out a programme or camp.' properties: _links: properties: @@ -13069,44 +12321,52 @@ components: type: string type: object type: object - active: - default: true - description: 'Whether this content type is still maintained and recommended for use in new camps.' - example: 'true' - readOnly: true - type: boolean - contentNodes: - description: 'API endpoint link for creating new entities of type entityClass.' - example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' - format: iri-reference - items: - type: string - readOnly: true - type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true + article: + description: 'The name of the item that is required.' + example: Volleyball + maxLength: 64 type: string - name: + materialList: description: |- - A name in UpperCamelCase of the content type. This value may be used as a technical - identifier of this content type, it is guaranteed to stay fixed. - example: SafetyConcept - maxLength: 32 - readOnly: true + The list to which this item belongs. Lists are used to keep track of who is + responsible to prepare and bring the item to the camp. + example: /material_lists/1a2b3c4d + format: iri-reference type: string + materialNode: + description: 'The content node to which this item belongs, if it does not belong to a period.' + example: /content_node/material_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + period: + description: 'The period to which this item belongs, if it does not belong to a content node.' + example: /periods/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + quantity: + description: 'The number of items or the amount in the unit of items that are required.' + example: 1.5 + type: + - 'null' + - number + unit: + description: 'An optional unit for measuring the amount of items required.' + example: kg + maxLength: 32 + type: + - 'null' + - string required: - - active - - name + - article + - materialList type: object - ContentType.jsonld-read: + MaterialItem.jsonld-read: deprecated: false - description: |- - Defines a type of content that can be present in a content node tree. A content type - determines what data can be stored in content nodes of this type, as well as validation, - available slots and jsonConfig settings. + description: '' properties: '@context': oneOf: @@ -13131,128 +12391,117 @@ components: '@type': readOnly: true type: string - active: - default: true - description: 'Whether this content type is still maintained and recommended for use in new camps.' - example: 'true' - readOnly: true - type: boolean - contentNodes: - description: 'API endpoint link for creating new entities of type entityClass.' - example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' - format: iri-reference - items: - type: string - readOnly: true - type: array + article: + description: 'The name of the item that is required.' + example: Volleyball + maxLength: 64 + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - name: + materialList: description: |- - A name in UpperCamelCase of the content type. This value may be used as a technical - identifier of this content type, it is guaranteed to stay fixed. - example: SafetyConcept - maxLength: 32 - readOnly: true + The list to which this item belongs. Lists are used to keep track of who is + responsible to prepare and bring the item to the camp. + example: /material_lists/1a2b3c4d + format: iri-reference type: string + materialNode: + description: 'The content node to which this item belongs, if it does not belong to a period.' + example: /content_node/material_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + period: + description: 'The period to which this item belongs, if it does not belong to a content node.' + example: /periods/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + quantity: + description: 'The number of items or the amount in the unit of items that are required.' + example: 1.5 + type: + - 'null' + - number + unit: + description: 'An optional unit for measuring the amount of items required.' + example: kg + maxLength: 32 + type: + - 'null' + - string required: - - active - - name + - article + - materialList type: object - ContentType.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes: + MaterialItem.jsonld-write: deprecated: false - description: '' + description: 'A physical item that is needed for carrying out a programme or camp.' properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true + article: + description: 'The name of the item that is required.' + example: Volleyball + maxLength: 64 type: string - active: - default: true - description: 'Whether this content type is still maintained and recommended for use in new camps.' - example: 'true' - readOnly: true - type: boolean - contentNodes: - description: 'API endpoint link for creating new entities of type entityClass.' - example: '/content_node/column_layouts?contentType=%2Fcontent_types%2F1a2b3c4d' + materialList: + description: |- + The list to which this item belongs. Lists are used to keep track of who is + responsible to prepare and bring the item to the camp. + example: /material_lists/1a2b3c4d format: iri-reference - items: - type: string - readOnly: true - type: array - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true type: string - name: - description: |- - A name in UpperCamelCase of the content type. This value may be used as a technical - identifier of this content type, it is guaranteed to stay fixed. - example: SafetyConcept + materialNode: + description: 'The content node to which this item belongs, if it does not belong to a period.' + example: /content_node/material_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + period: + description: 'The period to which this item belongs, if it does not belong to a content node.' + example: /periods/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + quantity: + description: 'The number of items or the amount in the unit of items that are required.' + example: 1.5 + type: + - 'null' + - number + unit: + description: 'An optional unit for measuring the amount of items required.' + example: kg maxLength: 32 - readOnly: true - type: string + type: + - 'null' + - string required: - - active - - name - type: object - Credentials: - properties: - identifier: - example: test@example.com - type: string - password: - example: test - type: string + - article + - materialList type: object - Day-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + MaterialList-read: deprecated: false - description: '' + description: |- + A list of material items that someone needs to bring to the camp. A material list + is automatically created for each person collaborating on the camp. properties: - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true - type: integer - dayResponsibles: - description: 'The list of people who have a whole-day responsibility on this day.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date + camp: + description: 'The camp this material list belongs to.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The campCollaboration this material list belongs to.' + example: /camp_collaborations/1a2b3c4d + format: iri-reference readOnly: true type: - 'null' @@ -13263,60 +12512,156 @@ components: maxLength: 16 readOnly: true type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' + itemCount: + example: 3 readOnly: true type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + materialItems: + description: 'The items that are part of this list.' + example: '["/material_items/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date - readOnly: true + name: + description: 'The human readable name of the material list.' + example: Lebensmittel + maxLength: 32 type: - 'null' - string required: - - dayOffset - - dayResponsibles - - period + - camp + - materialItems + - name type: object - Day-read_Day.DayResponsibles: + MaterialList-write: deprecated: false description: |- - A day in a time period of a camp. This is represented as a reference to the time period - along with a number of days offset from the period's starting date. This is to make it - easier to move the whole periods to different dates. Days are created automatically when - creating or updating periods, and are not writable through the API directly. + A list of material items that someone needs to bring to the camp. A material list + is automatically created for each person collaborating on the camp. properties: - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true - type: integer - dayResponsibles: + name: + description: 'The human readable name of the material list.' + example: Lebensmittel + maxLength: 32 + type: + - 'null' + - string + required: + - name + type: object + MaterialList-write_create: + deprecated: false + description: |- + A list of material items that someone needs to bring to the camp. A material list + is automatically created for each person collaborating on the camp. + properties: + camp: + description: 'The camp this material list belongs to.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + name: + description: 'The human readable name of the material list.' + example: Lebensmittel + maxLength: 32 + type: + - 'null' + - string + required: + - camp + - name + type: object + MaterialList.jsonapi: + deprecated: false + description: |- + A list of material items that someone needs to bring to the camp. A material list + is automatically created for each person collaborating on the camp. + properties: + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + itemCount: + example: 3 + readOnly: true + type: integer + name: + description: 'The human readable name of the material list.' + example: Lebensmittel + maxLength: 32 + type: ['null', string] + required: + - name + type: object + id: + type: string + relationships: + properties: + camp: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + campCollaboration: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + materialItems: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + required: + - camp + - materialItems + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' items: - $ref: '#/components/schemas/DayResponsible-read_Day.DayResponsibles' + anyOf: + - + $ref: '#/components/schemas/MaterialList.jsonapi' + - + $ref: '#/components/schemas/MaterialList.jsonapi' + - + $ref: '#/components/schemas/MaterialList.jsonapi' readOnly: true type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date + type: object + MaterialList.jsonhal-read: + deprecated: false + description: |- + A list of material items that someone needs to bring to the camp. A material list + is automatically created for each person collaborating on the camp. + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + camp: + description: 'The camp this material list belongs to.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The campCollaboration this material list belongs to.' + example: /camp_collaborations/1a2b3c4d + format: iri-reference readOnly: true type: - 'null' @@ -13327,60 +12672,100 @@ components: maxLength: 16 readOnly: true type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' + itemCount: + example: 3 readOnly: true type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + materialItems: + description: 'The items that are part of this list.' + example: '["/material_items/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date - readOnly: true + name: + description: 'The human readable name of the material list.' + example: Lebensmittel + maxLength: 32 type: - 'null' - string required: - - dayOffset - - dayResponsibles - - period + - camp + - materialItems + - name type: object - Day-read_Period.Camp_Period.Days: + MaterialList.jsonhal-write_create: deprecated: false - description: '' + description: |- + A list of material items that someone needs to bring to the camp. A material list + is automatically created for each person collaborating on the camp. properties: - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + camp: + description: 'The camp this material list belongs to.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + name: + description: 'The human readable name of the material list.' + example: Lebensmittel + maxLength: 32 + type: + - 'null' + - string + required: + - camp + - name + type: object + MaterialList.jsonld-read: + deprecated: false + description: |- + A list of material items that someone needs to bring to the camp. A material list + is automatically created for each person collaborating on the camp. + properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string readOnly: true - type: integer - dayResponsibles: - description: 'The list of people who have a whole-day responsibility on this day.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + '@id': readOnly: true - type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date + type: string + '@type': + readOnly: true + type: string + camp: + description: 'The camp this material list belongs to.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + campCollaboration: + description: 'The campCollaboration this material list belongs to.' + example: /camp_collaborations/1a2b3c4d + format: iri-reference readOnly: true type: - 'null' @@ -13391,310 +12776,362 @@ components: maxLength: 16 readOnly: true type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' + itemCount: + example: 3 readOnly: true type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + materialItems: + description: 'The items that are part of this list.' + example: '["/material_items/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date - readOnly: true + name: + description: 'The human readable name of the material list.' + example: Lebensmittel + maxLength: 32 type: - 'null' - string required: - - dayOffset - - dayResponsibles - - period + - camp + - materialItems + - name + type: object + MaterialList.jsonld-write_create: + deprecated: false + description: |- + A list of material items that someone needs to bring to the camp. A material list + is automatically created for each person collaborating on the camp. + properties: + camp: + description: 'The camp this material list belongs to.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + name: + description: 'The human readable name of the material list.' + example: Lebensmittel + maxLength: 32 + type: + - 'null' + - string + required: + - camp + - name type: object - Day.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + MaterialNode-read: deprecated: false description: '' properties: - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true - type: integer - dayResponsibles: - description: 'The list of people who have a whole-day responsibility on this day.' - example: '["/day_responsibles/1a2b3c4d"]' + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept readOnly: true + type: string + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + items: + type: string type: + - array - 'null' - - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' - readOnly: true - type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date - readOnly: true + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 type: - 'null' - string - required: - - dayOffset - - dayResponsibles - - period - type: object - Day.jsonapi-read_Day.DayResponsibles: - deprecated: false - description: |- - A day in a time period of a camp. This is represented as a reference to the time period - along with a number of days offset from the period's starting date. This is to make it - easier to move the whole periods to different dates. Days are created automatically when - creating or updating periods, and are not writable through the API directly. - properties: - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true - type: integer - dayResponsibles: + materialItems: items: - $ref: '#/components/schemas/DayResponsible.jsonapi-read_Day.DayResponsibles' - readOnly: true + $ref: '#/components/schemas/MaterialItem-read' type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date - readOnly: true + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference type: - 'null' - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' - readOnly: true + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date readOnly: true type: - 'null' - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - dayOffset - - dayResponsibles - - period + - children + - contentType + - materialItems + - position type: object - Day.jsonapi-read_Period.Camp_Period.Days: + MaterialNode-write_create: deprecated: false description: '' properties: - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true - type: integer - dayResponsibles: - description: 'The list of people who have a whole-day responsibility on this day.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date - readOnly: true - type: - - 'null' - - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' - readOnly: true - type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). items: - example: 'https://example.com/' - format: iri-reference type: string - readOnly: true - type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date - readOnly: true + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 type: - 'null' - string - required: - - dayOffset - - dayResponsibles - - period + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - contentType + - parent + - position type: object - Day.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + MaterialNode-write_update: deprecated: false description: '' properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + items: + type: string + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 type: integer - dayResponsibles: - description: 'The list of people who have a whole-day responsibility on this day.' - example: '["/day_responsibles/1a2b3c4d"]' + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string + required: + - position + type: object + MaterialNode.jsonapi: + deprecated: false + description: '' + properties: + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + readOnly: true + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept readOnly: true + type: string + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + items: + type: string type: + - array - 'null' - - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' - readOnly: true - type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + materialItems: items: - example: 'https://example.com/' - format: iri-reference - type: string + $ref: '#/components/schemas/MaterialItem.jsonapi' readOnly: true type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference readOnly: true type: - 'null' - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - dayOffset - - dayResponsibles - - period + - children + - contentType + - materialItems + - position type: object - Day.jsonhal-read_Day.DayResponsibles: + MaterialNode.jsonhal-read: deprecated: false - description: |- - A day in a time period of a camp. This is represented as a reference to the time period - along with a number of days offset from the period's starting date. This is to make it - easier to move the whole periods to different dates. Days are created automatically when - creating or updating periods, and are not writable through the API directly. + description: '' properties: _links: properties: @@ -13705,63 +13142,99 @@ components: type: string type: object type: object - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true - type: integer - dayResponsibles: + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' items: - $ref: '#/components/schemas/DayResponsible.jsonhal-read_Day.DayResponsibles' + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept readOnly: true + type: string + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + items: + type: string type: + - array - 'null' - - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' - readOnly: true - type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + materialItems: + items: + $ref: '#/components/schemas/MaterialItem.jsonhal-read' type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference readOnly: true type: - 'null' - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - dayOffset - - dayResponsibles - - period + - children + - contentType + - materialItems + - position type: object - Day.jsonhal-read_Period.Camp_Period.Days: + MaterialNode.jsonhal-write_create: deprecated: false description: '' properties: @@ -13774,67 +13247,64 @@ components: type: string type: object type: object - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true - type: integer - dayResponsibles: - description: 'The list of people who have a whole-day responsibility on this day.' - example: '["/day_responsibles/1a2b3c4d"]' + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). items: - example: 'https://example.com/' - format: iri-reference type: string - readOnly: true - type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date - readOnly: true + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 type: - 'null' - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' - readOnly: true - type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date - readOnly: true + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 type: - 'null' - string required: - - dayOffset - - dayResponsibles - - period + - contentType + - parent + - position type: object - Day.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + MaterialNode.jsonld-read: deprecated: false description: '' properties: @@ -13861,406 +13331,480 @@ components: '@type': readOnly: true type: string - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true - type: integer - dayResponsibles: - description: 'The list of people who have a whole-day responsibility on this day.' - example: '["/day_responsibles/1a2b3c4d"]' + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept readOnly: true + type: string + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + items: + type: string type: + - array - 'null' - - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' - readOnly: true - type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + materialItems: items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true + $ref: '#/components/schemas/MaterialItem.jsonld-read' type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference readOnly: true type: - 'null' - string - required: - - dayOffset - - dayResponsibles - - period - type: object - Day.jsonld-read_Day.DayResponsibles: - deprecated: false - description: |- - A day in a time period of a camp. This is represented as a reference to the time period - along with a number of days offset from the period's starting date. This is to make it - easier to move the whole periods to different dates. Days are created automatically when - creating or updating periods, and are not writable through the API directly. - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. example: '1' - readOnly: true - type: integer - dayResponsibles: - items: - $ref: '#/components/schemas/DayResponsible.jsonld-read_Day.DayResponsibles' - readOnly: true - type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date - readOnly: true + maxLength: 32 type: - 'null' - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' - readOnly: true - type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d + required: + - children + - contentType + - materialItems + - position + type: object + MaterialNode.jsonld-write_create: + deprecated: false + description: '' + properties: + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). items: - example: 'https://example.com/' - format: iri-reference type: string - readOnly: true - type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date - readOnly: true + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 type: - 'null' - string required: - - dayOffset - - dayResponsibles - - period + - contentType + - parent + - position type: object - Day.jsonld-read_Period.Camp_Period.Days: + MultiSelect-read: deprecated: false description: '' properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - dayOffset: - description: "The 0-based offset in days from the period's start date when this day starts." - example: '1' - readOnly: true - type: integer - dayResponsibles: - description: 'The list of people who have a whole-day responsibility on this day.' - example: '["/day_responsibles/1a2b3c4d"]' + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - end: - description: 'The end date and time of the day. This is a read-only convenience property.' - example: '2022-01-03T00:00:00+00:00' - format: date + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept readOnly: true + type: string + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + options: + natureAndEnvironment: + checked: true + outdoorTechnique: + checked: false + items: + type: string type: + - array - 'null' - - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - number: - description: 'The 1-based cardinal number of the day in the period. Not unique within the camp.' - example: '2' - readOnly: true + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 type: integer - period: - description: 'The time period that this day belongs to.' - example: /periods/1a2b3c4d + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - scheduleEntries: - description: "All scheduleEntries in this day's period which overlap with this day (using midnight as cutoff)." - example: '/schedule_entries?period=%2Fperiods%2F1a2b3c4d&start%5Bstrictly_before%5D=2022-01-03T00%3A00%3A00%2B00%3A00&end%5Bafter%5D=2022-01-02T00%3A00%3A00%2B00%3A00' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The start date and time of the day. This is a read-only convenience property.' - example: '2022-01-02T00:00:00+00:00' - format: date readOnly: true type: - 'null' - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - dayOffset - - dayResponsibles - - period - type: object - DayResponsible-read: - deprecated: false - description: 'A person that has some whole-day responsibility on a day in the camp.' - properties: - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - required: - - campCollaboration - - day + - children + - contentType + - position type: object - DayResponsible-read_Day.DayResponsibles: + MultiSelect-write_create: deprecated: false description: '' properties: - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - required: - - campCollaboration - - day - type: object - DayResponsible-write: - deprecated: false - description: 'A person that has some whole-day responsibility on a day in the camp.' - properties: - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + options: + natureAndEnvironment: + checked: true + outdoorTechnique: + checked: false + items: + type: string + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - campCollaboration - - day + - contentType + - parent + - position type: object - DayResponsible.jsonapi-read: + MultiSelect-write_update: deprecated: false - description: 'A person that has some whole-day responsibility on a day in the camp.' + description: '' properties: - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + options: + natureAndEnvironment: + checked: true + outdoorTechnique: + checked: false + items: + type: string + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - campCollaboration - - day + - data + - position type: object - DayResponsible.jsonapi-read_Day.DayResponsibles: + MultiSelect.jsonapi: deprecated: false description: '' properties: - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference + readOnly: true type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d - format: iri-reference + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true type: string + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + options: + natureAndEnvironment: + checked: true + outdoorTechnique: + checked: false + items: + type: string + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - required: - - campCollaboration - - day - type: object - DayResponsible.jsonapi-write: - deprecated: false - description: 'A person that has some whole-day responsibility on a day in the camp.' - properties: - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d - format: iri-reference - type: string - required: - - campCollaboration - - day - type: object - DayResponsible.jsonhal-read: - deprecated: false - description: 'A person that has some whole-day responsibility on a day in the camp.' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 readOnly: true - type: string + type: + - 'null' + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - campCollaboration - - day + - children + - contentType + - position type: object - DayResponsible.jsonhal-read_Day.DayResponsibles: + MultiSelect.jsonhal-read: deprecated: false description: '' properties: @@ -14273,29 +13817,102 @@ components: type: string type: object type: object - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d - format: iri-reference + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true type: string + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + options: + natureAndEnvironment: + checked: true + outdoorTechnique: + checked: false + items: + type: string + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - campCollaboration - - day + - children + - contentType + - position type: object - DayResponsible.jsonhal-write: + MultiSelect.jsonhal-write_create: deprecated: false - description: 'A person that has some whole-day responsibility on a day in the camp.' + description: '' properties: _links: properties: @@ -14306,23 +13923,72 @@ components: type: string type: object type: object - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + options: + natureAndEnvironment: + checked: true + outdoorTechnique: + checked: false + items: + type: string + type: + - array + - 'null' + instanceName: + description: |- + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 + type: + - 'null' + - string + parent: + description: |- + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string + type: + - 'null' + - string + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - campCollaboration - - day + - contentType + - parent + - position type: object - DayResponsible.jsonld-read: + MultiSelect.jsonld-read: deprecated: false - description: 'A person that has some whole-day responsibility on a day in the camp.' + description: '' properties: '@context': oneOf: @@ -14347,374 +14013,217 @@ components: '@type': readOnly: true type: string - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d - format: iri-reference - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - required: - - campCollaboration - - day - type: object - DayResponsible.jsonld-read_Day.DayResponsibles: - deprecated: false - description: '' - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true - type: string - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d - format: iri-reference + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true type: string + data: + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + options: + natureAndEnvironment: + checked: true + outdoorTechnique: + checked: false + items: + type: string + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - required: - - campCollaboration - - day - type: object - DayResponsible.jsonld-write: - deprecated: false - description: 'A person that has some whole-day responsibility on a day in the camp.' - properties: - campCollaboration: - description: "The person that is responsible. Must belong to the same camp as the day's period." - example: /camp_collaborations/1a2b3c4d - format: iri-reference - type: string - day: - description: 'The day on which the person is responsible.' - example: /days/1a2b3c4d - format: iri-reference - type: string - required: - - campCollaboration - - day - type: object - Invitation-read: - deprecated: false - description: |- - An invitation for a person to collaborate in a camp. The person may or may not - already have an account. - properties: - campId: - description: |- - The id of the camp for which this invitation is valid. This is useful for - redirecting the user to the correct place after they accept. - example: 1a2b3c4d - type: string - campTitle: - description: |- - The full title of the camp for which this invitation is valid. This should help - the user to decide whether to accept or reject the invitation. - example: 'Abteilungs-Sommerlager 2022' - type: string - userAlreadyInCamp: + instanceName: description: |- - Indicates whether the logged in user is already collaborating in the camp, and - can therefore not accept the invitation. + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 type: - - boolean - 'null' - userDisplayName: + - string + parent: description: |- - The display name of the user that is invited. May be null in case the user does - not already have an account. - example: 'Robert Baden-Powell' + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d + format: iri-reference type: - 'null' - string - type: object - Invitation-write: - deprecated: false - description: |- - An invitation for a person to collaborate in a camp. The person may or may not - already have an account. - type: object - Invitation.jsonapi-read: - deprecated: false - description: |- - An invitation for a person to collaborate in a camp. The person may or may not - already have an account. - properties: - campId: - description: |- - The id of the camp for which this invitation is valid. This is useful for - redirecting the user to the correct place after they accept. - example: 1a2b3c4d - type: string - campTitle: + position: + default: -1 description: |- - The full title of the camp for which this invitation is valid. This should help - the user to decide whether to accept or reject the invitation. - example: 'Abteilungs-Sommerlager 2022' - type: string - userAlreadyInCamp: + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + root: description: |- - Indicates whether the logged in user is already collaborating in the camp, and - can therefore not accept the invitation. + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true type: - - boolean - 'null' - userDisplayName: + - string + slot: description: |- - The display name of the user that is invited. May be null in case the user does - not already have an account. - example: 'Robert Baden-Powell' + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 type: - 'null' - string + required: + - children + - contentType + - position type: object - Invitation.jsonapi-write: - deprecated: false - description: |- - An invitation for a person to collaborate in a camp. The person may or may not - already have an account. - type: object - Invitation.jsonhal-read: + MultiSelect.jsonld-write_create: deprecated: false - description: |- - An invitation for a person to collaborate in a camp. The person may or may not - already have an account. + description: '' properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - campId: - description: |- - The id of the camp for which this invitation is valid. This is useful for - redirecting the user to the correct place after they accept. - example: 1a2b3c4d - type: string - campTitle: + contentType: description: |- - The full title of the camp for which this invitation is valid. This should help - the user to decide whether to accept or reject the invitation. - example: 'Abteilungs-Sommerlager 2022' + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference type: string - userAlreadyInCamp: + data: description: |- - Indicates whether the logged in user is already collaborating in the camp, and - can therefore not accept the invitation. + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + options: + natureAndEnvironment: + checked: true + outdoorTechnique: + checked: false + items: + type: string type: - - boolean + - array - 'null' - userDisplayName: + instanceName: description: |- - The display name of the user that is invited. May be null in case the user does - not already have an account. - example: 'Robert Baden-Powell' + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 type: - 'null' - string - type: object - Invitation.jsonld-read: - deprecated: false - description: |- - An invitation for a person to collaborate in a camp. The person may or may not - already have an account. - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - campId: - description: |- - The id of the camp for which this invitation is valid. This is useful for - redirecting the user to the correct place after they accept. - example: 1a2b3c4d - type: string - campTitle: - description: |- - The full title of the camp for which this invitation is valid. This should help - the user to decide whether to accept or reject the invitation. - example: 'Abteilungs-Sommerlager 2022' - type: string - userAlreadyInCamp: - description: |- - Indicates whether the logged in user is already collaborating in the camp, and - can therefore not accept the invitation. - type: - - boolean - - 'null' - userDisplayName: - description: |- - The display name of the user that is invited. May be null in case the user does - not already have an account. - example: 'Robert Baden-Powell' - type: - - 'null' - - string - type: object - MaterialItem-read: - deprecated: false - description: '' - properties: - article: - description: 'The name of the item that is required.' - example: Volleyball - maxLength: 64 - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialList: + parent: description: |- - The list to which this item belongs. Lists are used to keep track of who is - responsible to prepare and bring the item to the camp. - example: /material_lists/1a2b3c4d - format: iri-reference - type: string - materialNode: - description: 'The content node to which this item belongs, if it does not belong to a period.' - example: /content_node/material_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - period: - description: 'The period to which this item belongs, if it does not belong to a content node.' - example: /periods/1a2b3c4d + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d format: iri-reference type: - 'null' - string - quantity: - description: 'The number of items or the amount in the unit of items that are required.' - example: 1.5 - type: - - 'null' - - number - unit: - description: 'An optional unit for measuring the amount of items required.' - example: kg + position: + default: -1 + description: |- + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' maxLength: 32 type: - 'null' - string required: - - article - - materialList + - contentType + - parent + - position type: object - MaterialItem-write: + Period-read: deprecated: false - description: 'A physical item that is needed for carrying out a programme or camp.' + description: |- + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. properties: - article: - description: 'The name of the item that is required.' - example: Volleyball - type: string - materialList: - description: |- - The list to which this item belongs. Lists are used to keep track of who is - responsible to prepare and bring the item to the camp. - example: /material_lists/1a2b3c4d + camp: + description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' + example: /camps/1a2b3c4d format: iri-reference type: string - materialNode: - description: 'The content node to which this item belongs, if it does not belong to a period.' - example: /content_node/material_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - period: - description: 'The period to which this item belongs, if it does not belong to a content node.' - example: /periods/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - quantity: - description: 'The number of items or the amount in the unit of items that are required.' - example: 1.5 - type: - - 'null' - - number - unit: - description: 'An optional unit for measuring the amount of items required.' - example: kg + contentNodes: + description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + dayResponsibles: + description: 'A link to all the DayResponsibles in this period.' + example: '["/day_responsibles/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + days: + description: 'The days in this time period. These are generated automatically.' + example: '["/days?period=/periods/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager + maxLength: 32 type: - 'null' - string - type: object - MaterialItem.jsonapi-read: - deprecated: false - description: '' - properties: - article: - description: 'The name of the item that is required.' - example: Volleyball - maxLength: 64 + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' @@ -14722,103 +14231,179 @@ components: maxLength: 16 readOnly: true type: string - materialList: + materialItems: description: |- - The list to which this item belongs. Lists are used to keep track of who is - responsible to prepare and bring the item to the camp. - example: /material_lists/1a2b3c4d - format: iri-reference + Material items that are assigned directly to the period, as opposed to individual + activities. + example: '["/material_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + scheduleEntries: + description: |- + All time slots for programme that are part of this time period. A schedule entry + may span over multiple days, but may not end later than the period. + example: '["/schedule_entries/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date type: string - materialNode: - description: 'The content node to which this item belongs, if it does not belong to a period.' - example: /content_node/material_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - period: - description: 'The period to which this item belongs, if it does not belong to a content node.' - example: /periods/1a2b3c4d + required: + - camp + - days + - description + - end + - materialItems + - scheduleEntries + - start + type: object + Period-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + deprecated: false + description: '' + properties: + camp: + description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' + example: /camps/1a2b3c4d format: iri-reference - type: - - 'null' - - string - quantity: - description: 'The number of items or the amount in the unit of items that are required.' - example: 1.5 - type: - - 'null' - - number - unit: - description: 'An optional unit for measuring the amount of items required.' - example: kg + type: string + contentNodes: + description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + dayResponsibles: + description: 'A link to all the DayResponsibles in this period.' + example: '["/day_responsibles/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + days: + items: + $ref: '#/components/schemas/Day-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + readOnly: true + type: array + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager maxLength: 32 type: - 'null' - string + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date + type: string + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + materialItems: + description: |- + Material items that are assigned directly to the period, as opposed to individual + activities. + example: '["/material_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + scheduleEntries: + description: |- + All time slots for programme that are part of this time period. A schedule entry + may span over multiple days, but may not end later than the period. + example: '["/schedule_entries/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date + type: string required: - - article - - materialList + - camp + - days + - description + - end + - materialItems + - scheduleEntries + - start type: object - MaterialItem.jsonapi-write: + Period-read_Period.Camp_Period.Days: deprecated: false - description: 'A physical item that is needed for carrying out a programme or camp.' + description: |- + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. properties: - article: - description: 'The name of the item that is required.' - example: Volleyball - type: string - materialList: - description: |- - The list to which this item belongs. Lists are used to keep track of who is - responsible to prepare and bring the item to the camp. - example: /material_lists/1a2b3c4d - format: iri-reference - type: string - materialNode: - description: 'The content node to which this item belongs, if it does not belong to a period.' - example: /content_node/material_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - period: - description: 'The period to which this item belongs, if it does not belong to a content node.' - example: /periods/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - quantity: - description: 'The number of items or the amount in the unit of items that are required.' - example: 1.5 - type: - - 'null' - - number - unit: - description: 'An optional unit for measuring the amount of items required.' - example: kg + camp: + anyOf: + - + $ref: '#/components/schemas/Camp-read_Period.Camp_Period.Days' + - + type: 'null' + readOnly: true + contentNodes: + description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + dayResponsibles: + description: 'A link to all the DayResponsibles in this period.' + example: '["/day_responsibles/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + days: + items: + $ref: '#/components/schemas/Day-read_Period.Camp_Period.Days' + readOnly: true + type: array + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager + maxLength: 32 type: - 'null' - string - type: object - MaterialItem.jsonhal-read: - deprecated: false - description: '' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - article: - description: 'The name of the item that is required.' - example: Volleyball - maxLength: 64 + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' @@ -14826,131 +14411,279 @@ components: maxLength: 16 readOnly: true type: string - materialList: + materialItems: description: |- - The list to which this item belongs. Lists are used to keep track of who is - responsible to prepare and bring the item to the camp. - example: /material_lists/1a2b3c4d - format: iri-reference + Material items that are assigned directly to the period, as opposed to individual + activities. + example: '["/material_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + scheduleEntries: + description: |- + All time slots for programme that are part of this time period. A schedule entry + may span over multiple days, but may not end later than the period. + example: '["/schedule_entries/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date type: string - materialNode: - description: 'The content node to which this item belongs, if it does not belong to a period.' - example: /content_node/material_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - period: - description: 'The period to which this item belongs, if it does not belong to a content node.' - example: /periods/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - quantity: - description: 'The number of items or the amount in the unit of items that are required.' - example: 1.5 - type: - - 'null' - - number - unit: - description: 'An optional unit for measuring the amount of items required.' - example: kg + required: + - camp + - days + - description + - end + - materialItems + - scheduleEntries + - start + type: object + Period-write: + deprecated: false + description: |- + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. + properties: + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager maxLength: 32 type: - 'null' - string + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date + type: string + moveScheduleEntries: + default: true + description: |- + If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule + entries in the period. + example: true + type: boolean + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date + type: string required: - - article - - materialList + - description + - end + - moveScheduleEntries + - start type: object - MaterialItem.jsonhal-write: + Period-write_create: deprecated: false - description: 'A physical item that is needed for carrying out a programme or camp.' + description: |- + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - article: - description: 'The name of the item that is required.' - example: Volleyball - maxLength: 64 - type: string - materialList: - description: |- - The list to which this item belongs. Lists are used to keep track of who is - responsible to prepare and bring the item to the camp. - example: /material_lists/1a2b3c4d + camp: + description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' + example: /camps/1a2b3c4d format: iri-reference type: string - materialNode: - description: 'The content node to which this item belongs, if it does not belong to a period.' - example: /content_node/material_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - period: - description: 'The period to which this item belongs, if it does not belong to a content node.' - example: /periods/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - quantity: - description: 'The number of items or the amount in the unit of items that are required.' - example: 1.5 - type: - - 'null' - - number - unit: - description: 'An optional unit for measuring the amount of items required.' - example: kg + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager maxLength: 32 type: - 'null' - string + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date + type: string + moveScheduleEntries: + default: true + description: |- + If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule + entries in the period. + example: true + type: boolean + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date + type: string required: - - article - - materialList + - camp + - description + - end + - moveScheduleEntries + - start type: object - MaterialItem.jsonld-read: + Period.jsonapi: deprecated: false - description: '' + description: |- + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. properties: - '@context': - oneOf: - - - additionalProperties: true + data: + properties: + attributes: properties: - '@vocab': + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager + maxLength: 32 + type: ['null', string] + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date + type: string + moveScheduleEntries: + default: true + description: |- + If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule + entries in the period. + example: true + type: boolean + writeOnly: true + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date type: string required: - - '@vocab' - - hydra + - description + - end + - moveScheduleEntries + - start type: object - - + id: type: string + relationships: + properties: + camp: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + contentNodes: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + dayResponsibles: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + days: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + materialItems: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + scheduleEntries: + properties: { data: { items: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object }, type: array } } + required: + - camp + - days + - materialItems + - scheduleEntries + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - [] + - + $ref: '#/components/schemas/Day.jsonapi' + - + $ref: '#/components/schemas/Day.jsonapi' + - + $ref: '#/components/schemas/Day.jsonapi' + - + $ref: '#/components/schemas/Day.jsonapi' + - + $ref: '#/components/schemas/Day.jsonapi' readOnly: true - '@id': - readOnly: true + type: array + type: object + Period.jsonhal-read: + deprecated: false + description: |- + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. + properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + camp: + description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' + example: /camps/1a2b3c4d + format: iri-reference type: string - '@type': + contentNodes: + description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true - type: string - article: - description: 'The name of the item that is required.' - example: Volleyball - maxLength: 64 + type: array + dayResponsibles: + description: 'A link to all the DayResponsibles in this period.' + example: '["/day_responsibles/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + days: + description: 'The days in this time period. These are generated automatically.' + example: '["/days?period=/periods/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager + maxLength: 32 + type: + - 'null' + - string + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' @@ -14958,122 +14691,107 @@ components: maxLength: 16 readOnly: true type: string - materialList: + materialItems: description: |- - The list to which this item belongs. Lists are used to keep track of who is - responsible to prepare and bring the item to the camp. - example: /material_lists/1a2b3c4d - format: iri-reference + Material items that are assigned directly to the period, as opposed to individual + activities. + example: '["/material_items/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + scheduleEntries: + description: |- + All time slots for programme that are part of this time period. A schedule entry + may span over multiple days, but may not end later than the period. + example: '["/schedule_entries/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date type: string - materialNode: - description: 'The content node to which this item belongs, if it does not belong to a period.' - example: /content_node/material_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - period: - description: 'The period to which this item belongs, if it does not belong to a content node.' - example: /periods/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - quantity: - description: 'The number of items or the amount in the unit of items that are required.' - example: 1.5 - type: - - 'null' - - number - unit: - description: 'An optional unit for measuring the amount of items required.' - example: kg - maxLength: 32 - type: - - 'null' - - string - required: - - article - - materialList - type: object - MaterialItem.jsonld-write: - deprecated: false - description: 'A physical item that is needed for carrying out a programme or camp.' - properties: - article: - description: 'The name of the item that is required.' - example: Volleyball - maxLength: 64 - type: string - materialList: - description: |- - The list to which this item belongs. Lists are used to keep track of who is - responsible to prepare and bring the item to the camp. - example: /material_lists/1a2b3c4d - format: iri-reference - type: string - materialNode: - description: 'The content node to which this item belongs, if it does not belong to a period.' - example: /content_node/material_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - period: - description: 'The period to which this item belongs, if it does not belong to a content node.' - example: /periods/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - quantity: - description: 'The number of items or the amount in the unit of items that are required.' - example: 1.5 - type: - - 'null' - - number - unit: - description: 'An optional unit for measuring the amount of items required.' - example: kg - maxLength: 32 - type: - - 'null' - - string required: - - article - - materialList + - camp + - days + - description + - end + - materialItems + - scheduleEntries + - start type: object - MaterialList-read: + Period.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: deprecated: false - description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. + description: '' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object camp: - description: 'The camp this material list belongs to.' + description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' example: /camps/1a2b3c4d format: iri-reference type: string - campCollaboration: - description: 'The campCollaboration this material list belongs to.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference + contentNodes: + description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + dayResponsibles: + description: 'A link to all the DayResponsibles in this period.' + example: '["/day_responsibles/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + days: + items: + $ref: '#/components/schemas/Day.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' readOnly: true + type: array + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager + maxLength: 32 type: - 'null' - string + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - itemCount: - example: 3 - readOnly: true - type: integer materialItems: - description: 'The items that are part of this list.' + description: |- + Material items that are assigned directly to the period, as opposed to individual + activities. example: '["/material_items/1a2b3c4d"]' items: example: 'https://example.com/' @@ -15081,87 +14799,100 @@ components: type: string readOnly: true type: array - name: - description: 'The human readable name of the material list.' - example: Lebensmittel - maxLength: 32 - type: - - 'null' - - string + scheduleEntries: + description: |- + All time slots for programme that are part of this time period. A schedule entry + may span over multiple days, but may not end later than the period. + example: '["/schedule_entries/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date + type: string required: - camp + - days + - description + - end - materialItems - - name - type: object - MaterialList-write: - deprecated: false - description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. - properties: - name: - description: 'The human readable name of the material list.' - example: Lebensmittel - maxLength: 32 - type: - - 'null' - - string - required: - - name + - scheduleEntries + - start type: object - MaterialList-write_create: + Period.jsonhal-read_Period.Camp_Period.Days: deprecated: false description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object camp: - description: 'The camp this material list belongs to.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - name: - description: 'The human readable name of the material list.' - example: Lebensmittel + anyOf: + - + $ref: '#/components/schemas/Camp.jsonhal-read_Period.Camp_Period.Days' + - + type: 'null' + readOnly: true + contentNodes: + description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + dayResponsibles: + description: 'A link to all the DayResponsibles in this period.' + example: '["/day_responsibles/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + days: + items: + $ref: '#/components/schemas/Day.jsonhal-read_Period.Camp_Period.Days' + readOnly: true + type: array + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager maxLength: 32 type: - 'null' - string - required: - - camp - - name - type: object - MaterialList.jsonapi-read: - deprecated: false - description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. - properties: - camp: - description: 'The camp this material list belongs to.' - example: /camps/1a2b3c4d - format: iri-reference + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date type: string - campCollaboration: - description: 'The campCollaboration this material list belongs to.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - itemCount: - example: 3 - readOnly: true - type: integer materialItems: - description: 'The items that are part of this list.' + description: |- + Material items that are assigned directly to the period, as opposed to individual + activities. example: '["/material_items/1a2b3c4d"]' items: example: 'https://example.com/' @@ -15169,120 +14900,36 @@ components: type: string readOnly: true type: array - name: - description: 'The human readable name of the material list.' - example: Lebensmittel - maxLength: 32 - type: - - 'null' - - string - required: - - camp - - materialItems - - name - type: object - MaterialList.jsonapi-write: - deprecated: false - description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. - properties: - name: - description: 'The human readable name of the material list.' - example: Lebensmittel - maxLength: 32 - type: - - 'null' - - string - required: - - name - type: object - MaterialList.jsonapi-write_create: - deprecated: false - description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. - properties: - camp: - description: 'The camp this material list belongs to.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - name: - description: 'The human readable name of the material list.' - example: Lebensmittel - maxLength: 32 - type: - - 'null' - - string - required: - - camp - - name - type: object - MaterialList.jsonhal-read: - deprecated: false - description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - description: 'The camp this material list belongs to.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - campCollaboration: - description: 'The campCollaboration this material list belongs to.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - itemCount: - example: 3 - readOnly: true - type: integer - materialItems: - description: 'The items that are part of this list.' - example: '["/material_items/1a2b3c4d"]' + scheduleEntries: + description: |- + All time slots for programme that are part of this time period. A schedule entry + may span over multiple days, but may not end later than the period. + example: '["/schedule_entries/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - name: - description: 'The human readable name of the material list.' - example: Lebensmittel - maxLength: 32 - type: - - 'null' - - string + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date + type: string required: - camp + - days + - description + - end - materialItems - - name + - scheduleEntries + - start type: object - MaterialList.jsonhal-write_create: + Period.jsonhal-write_create: deprecated: false description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. properties: _links: properties: @@ -15294,26 +14941,48 @@ components: type: object type: object camp: - description: 'The camp this material list belongs to.' + description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' example: /camps/1a2b3c4d format: iri-reference type: string - name: - description: 'The human readable name of the material list.' - example: Lebensmittel + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager maxLength: 32 type: - 'null' - string + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date + type: string + moveScheduleEntries: + default: true + description: |- + If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule + entries in the period. + example: true + type: boolean + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date + type: string required: - camp - - name + - description + - end + - moveScheduleEntries + - start type: object - MaterialList.jsonld-read: + Period.jsonld-read: deprecated: false description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. properties: '@context': oneOf: @@ -15339,30 +15008,61 @@ components: readOnly: true type: string camp: - description: 'The camp this material list belongs to.' + description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' example: /camps/1a2b3c4d format: iri-reference type: string - campCollaboration: - description: 'The campCollaboration this material list belongs to.' - example: /camp_collaborations/1a2b3c4d - format: iri-reference + contentNodes: + description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + dayResponsibles: + description: 'A link to all the DayResponsibles in this period.' + example: '["/day_responsibles/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + days: + description: 'The days in this time period. These are generated automatically.' + example: '["/days?period=/periods/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true + type: array + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager + maxLength: 32 type: - 'null' - string + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - itemCount: - example: 3 - readOnly: true - type: integer materialItems: - description: 'The items that are part of this list.' + description: |- + Material items that are assigned directly to the period, as opposed to individual + activities. example: '["/material_items/1a2b3c4d"]' items: example: 'https://example.com/' @@ -15370,564 +15070,361 @@ components: type: string readOnly: true type: array - name: - description: 'The human readable name of the material list.' - example: Lebensmittel - maxLength: 32 - type: - - 'null' - - string - required: - - camp - - materialItems - - name - type: object - MaterialList.jsonld-write_create: - deprecated: false - description: |- - A list of material items that someone needs to bring to the camp. A material list - is automatically created for each person collaborating on the camp. - properties: - camp: - description: 'The camp this material list belongs to.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - name: - description: 'The human readable name of the material list.' - example: Lebensmittel - maxLength: 32 - type: - - 'null' - - string - required: - - camp - - name - type: object - MaterialNode-read: - deprecated: false - description: '' - properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' + scheduleEntries: + description: |- + All time slots for programme that are part of this time period. A schedule entry + may span over multiple days, but may not end later than the period. + example: '["/schedule_entries/1a2b3c4d"]' items: example: 'https://example.com/' format: iri-reference type: string readOnly: true type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept + required: + - camp + - days + - description + - end + - materialItems + - scheduleEntries + - start + type: object + Period.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: + deprecated: false + description: '' + properties: + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': readOnly: true type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + '@type': + readOnly: true + type: string + camp: + description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' + example: /camps/1a2b3c4d + format: iri-reference + type: string + contentNodes: + description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + dayResponsibles: + description: 'A link to all the DayResponsibles in this period.' + example: '["/day_responsibles/1a2b3c4d"]' items: + example: 'https://example.com/' + format: iri-reference type: string + readOnly: true + type: array + days: + items: + $ref: '#/components/schemas/Day.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + readOnly: true + type: array + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager + maxLength: 32 type: - - array - 'null' + - string + end: + description: |- + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date + type: string id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string materialItems: + description: |- + Material items that are assigned directly to the period, as opposed to individual + activities. + example: '["/material_items/1a2b3c4d"]' items: - $ref: '#/components/schemas/MaterialItem-read' + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true type: array - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: + scheduleEntries: description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference + All time slots for programme that are part of this time period. A schedule entry + may span over multiple days, but may not end later than the period. + example: '["/schedule_entries/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string + type: array + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date + type: string required: - - children - - contentType + - camp + - days + - description + - end - materialItems - - position + - scheduleEntries + - start type: object - MaterialNode-write_create: + Period.jsonld-read_Period.Camp_Period.Days: deprecated: false - description: '' + description: |- + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference + '@context': + oneOf: + - + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object + - + type: string + readOnly: true + '@id': + readOnly: true type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). + '@type': + readOnly: true + type: string + camp: + anyOf: + - + $ref: '#/components/schemas/Camp.jsonld-read_Period.Camp_Period.Days' + - + type: 'null' + readOnly: true + contentNodes: + description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' + example: '["/content_nodes/1a2b3c4d"]' items: + example: 'https://example.com/' + format: iri-reference type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm + readOnly: true + type: array + dayResponsibles: + description: 'A link to all the DayResponsibles in this period.' + example: '["/day_responsibles/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + days: + items: + $ref: '#/components/schemas/Day.jsonld-read_Period.Camp_Period.Days' + readOnly: true + type: array + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager maxLength: 32 type: - 'null' - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 + end: description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - parent - - position - type: object - MaterialNode-write_update: - deprecated: false - description: '' - properties: - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - position - type: object - MaterialNode.jsonapi-read: - deprecated: false - description: '' - properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - items: - type: string - type: - - array - - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string materialItems: + description: |- + Material items that are assigned directly to the period, as opposed to individual + activities. + example: '["/material_items/1a2b3c4d"]' items: - $ref: '#/components/schemas/MaterialItem.jsonapi-read' + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true type: array - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: + scheduleEntries: description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference + All time slots for programme that are part of this time period. A schedule entry + may span over multiple days, but may not end later than the period. + example: '["/schedule_entries/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string + type: array + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date + type: string required: - - children - - contentType + - camp + - days + - description + - end - materialItems - - position + - scheduleEntries + - start type: object - MaterialNode.jsonapi-write_create: + Period.jsonld-write_create: deprecated: false - description: '' + description: |- + A time period in which the programme of a camp will take place. There may be multiple + periods in a camp, but they may not overlap. A period is made up of one or more full days. properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d + camp: + description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' + example: /camps/1a2b3c4d format: iri-reference type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm + description: + description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' + example: Hauptlager maxLength: 32 type: - 'null' - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 + end: description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: + The (inclusive) day at the end of which the period ends, as an ISO date string. Should + not be before "start". + example: '2022-01-08' + format: date + type: string + moveScheduleEntries: + default: true description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string + If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule + entries in the period. + example: true + type: boolean + start: + description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' + example: '2022-01-01' + format: date + type: string required: - - contentType - - parent - - position - type: object - MaterialNode.jsonapi-write_update: - deprecated: false - description: '' + - camp + - description + - end + - moveScheduleEntries + - start + type: object + PersonalInvitation-read: + deprecated: false + description: 'An invitation for a person who already has an account to collaborate in a camp.' properties: - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 + campId: description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: + The id of the camp for which this invitation is valid. This is useful for + redirecting the user to the correct place after they accept. + example: 1a2b3c4d + type: string + campTitle: description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - position + The full title of the camp for which this invitation is valid. This should help + the user to decide whether to accept or reject the invitation. + example: 'Abteilungs-Sommerlager 2022' + type: string + id: + example: 1a2b3c4d + type: string type: object - MaterialNode.jsonhal-read: + PersonalInvitation-write: deprecated: false - description: '' + description: 'An invitation for a person who already has an account to collaborate in a camp.' + type: object + PersonalInvitation.jsonapi: + deprecated: false + description: 'An invitation for a person who already has an account to collaborate in a camp.' properties: - _links: + data: properties: - self: + attributes: properties: - href: - format: iri-reference + _id: + example: 1a2b3c4d + type: string + campId: + description: |- + The id of the camp for which this invitation is valid. This is useful for + redirecting the user to the correct place after they accept. + example: 1a2b3c4d + type: string + campTitle: + description: |- + The full title of the camp for which this invitation is valid. This should help + the user to decide whether to accept or reject the invitation. + example: 'Abteilungs-Sommerlager 2022' type: string type: object + id: + type: string + type: + type: string + required: + - id + - type type: object - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - items: - type: string - type: - - array - - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - materialItems: - items: - $ref: '#/components/schemas/MaterialItem.jsonhal-read' - type: array - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - children - - contentType - - materialItems - - position type: object - MaterialNode.jsonhal-write_create: + PersonalInvitation.jsonhal-read: deprecated: false - description: '' + description: 'An invitation for a person who already has an account to collaborate in a camp.' properties: _links: properties: @@ -15938,66 +15435,25 @@ components: type: string type: object type: object - contentType: + campId: description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference + The id of the camp for which this invitation is valid. This is useful for + redirecting the user to the correct place after they accept. + example: 1a2b3c4d type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: + campTitle: description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - parent - - position + The full title of the camp for which this invitation is valid. This should help + the user to decide whether to accept or reject the invitation. + example: 'Abteilungs-Sommerlager 2022' + type: string + id: + example: 1a2b3c4d + type: string type: object - MaterialNode.jsonld-read: + PersonalInvitation.jsonld-read: deprecated: false - description: '' + description: 'An invitation for a person who already has an account to collaborate in a camp.' properties: '@context': oneOf: @@ -16022,2829 +15478,23 @@ components: '@type': readOnly: true type: string - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string - data: + campId: description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - items: - type: string - type: - - array - - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' + The id of the camp for which this invitation is valid. This is useful for + redirecting the user to the correct place after they accept. example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - materialItems: - items: - $ref: '#/components/schemas/MaterialItem.jsonld-read' - type: array - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - children - - contentType - - materialItems - - position - type: object - MaterialNode.jsonld-write_create: - deprecated: false - description: '' - properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - parent - - position - type: object - MultiSelect-read: - deprecated: false - description: '' - properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: + campTitle: description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true + The full title of the camp for which this invitation is valid. This should help + the user to decide whether to accept or reject the invitation. + example: 'Abteilungs-Sommerlager 2022' type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' id: - description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d - maxLength: 16 - readOnly: true type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - children - - contentType - - position type: object - MultiSelect-write_create: - deprecated: false - description: '' - properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - parent - - position - type: object - MultiSelect-write_update: - deprecated: false - description: '' - properties: - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - data - - position - type: object - MultiSelect.jsonapi-read: - deprecated: false - description: '' - properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - children - - contentType - - position - type: object - MultiSelect.jsonapi-write_create: - deprecated: false - description: '' - properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - parent - - position - type: object - MultiSelect.jsonapi-write_update: - deprecated: false - description: '' - properties: - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - data - - position - type: object - MultiSelect.jsonhal-read: - deprecated: false - description: '' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - children - - contentType - - position - type: object - MultiSelect.jsonhal-write_create: - deprecated: false - description: '' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - parent - - position - type: object - MultiSelect.jsonld-read: - deprecated: false - description: '' - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - children - - contentType - - position - type: object - MultiSelect.jsonld-write_create: - deprecated: false - description: '' - properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - options: - natureAndEnvironment: - checked: true - outdoorTechnique: - checked: false - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - parent - - position - type: object - Period-read: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - description: 'The days in this time period. These are generated automatically.' - example: '["/days?period=/periods/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: - deprecated: false - description: '' - properties: - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - items: - $ref: '#/components/schemas/Day-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period-read_Period.Camp_Period.Days: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - camp: - anyOf: - - - $ref: '#/components/schemas/Camp-read_Period.Camp_Period.Days' - - - type: 'null' - readOnly: true - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - items: - $ref: '#/components/schemas/Day-read_Period.Camp_Period.Days' - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period-write: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - moveScheduleEntries: - default: true - description: |- - If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule - entries in the period. - example: true - type: boolean - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - description - - end - - moveScheduleEntries - - start - type: object - Period-write_create: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - moveScheduleEntries: - default: true - description: |- - If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule - entries in the period. - example: true - type: boolean - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - description - - end - - moveScheduleEntries - - start - type: object - Period.jsonapi-read: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - description: 'The days in this time period. These are generated automatically.' - example: '["/days?period=/periods/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: - deprecated: false - description: '' - properties: - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - items: - $ref: '#/components/schemas/Day.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period.jsonapi-read_Period.Camp_Period.Days: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - camp: - anyOf: - - - $ref: '#/components/schemas/Camp.jsonapi-read_Period.Camp_Period.Days' - - - type: 'null' - readOnly: true - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - items: - $ref: '#/components/schemas/Day.jsonapi-read_Period.Camp_Period.Days' - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period.jsonapi-write: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - moveScheduleEntries: - default: true - description: |- - If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule - entries in the period. - example: true - type: boolean - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - description - - end - - moveScheduleEntries - - start - type: object - Period.jsonapi-write_create: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - moveScheduleEntries: - default: true - description: |- - If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule - entries in the period. - example: true - type: boolean - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - description - - end - - moveScheduleEntries - - start - type: object - Period.jsonhal-read: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - description: 'The days in this time period. These are generated automatically.' - example: '["/days?period=/periods/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: - deprecated: false - description: '' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - items: - $ref: '#/components/schemas/Day.jsonhal-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period.jsonhal-read_Period.Camp_Period.Days: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - anyOf: - - - $ref: '#/components/schemas/Camp.jsonhal-read_Period.Camp_Period.Days' - - - type: 'null' - readOnly: true - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - items: - $ref: '#/components/schemas/Day.jsonhal-read_Period.Camp_Period.Days' - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period.jsonhal-write_create: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - moveScheduleEntries: - default: true - description: |- - If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule - entries in the period. - example: true - type: boolean - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - description - - end - - moveScheduleEntries - - start - type: object - Period.jsonld-read: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - description: 'The days in this time period. These are generated automatically.' - example: '["/days?period=/periods/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: - deprecated: false - description: '' - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - items: - $ref: '#/components/schemas/Day.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period.jsonld-read_Period.Camp_Period.Days: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - camp: - anyOf: - - - $ref: '#/components/schemas/Camp.jsonld-read_Period.Camp_Period.Days' - - - type: 'null' - readOnly: true - contentNodes: - description: 'All the content nodes used in some activity which is carried out (has a schedule entry) in this period.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - dayResponsibles: - description: 'A link to all the DayResponsibles in this period.' - example: '["/day_responsibles/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - days: - items: - $ref: '#/components/schemas/Day.jsonld-read_Period.Camp_Period.Days' - readOnly: true - type: array - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - materialItems: - description: |- - Material items that are assigned directly to the period, as opposed to individual - activities. - example: '["/material_items/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - scheduleEntries: - description: |- - All time slots for programme that are part of this time period. A schedule entry - may span over multiple days, but may not end later than the period. - example: '["/schedule_entries/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - days - - description - - end - - materialItems - - scheduleEntries - - start - type: object - Period.jsonld-write_create: - deprecated: false - description: |- - A time period in which the programme of a camp will take place. There may be multiple - periods in a camp, but they may not overlap. A period is made up of one or more full days. - properties: - camp: - description: 'The camp that this time period belongs to. Cannot be changed once the period is created.' - example: /camps/1a2b3c4d - format: iri-reference - type: string - description: - description: 'A free text name for the period. Useful to distinguish multiple periods in the same camp.' - example: Hauptlager - maxLength: 32 - type: - - 'null' - - string - end: - description: |- - The (inclusive) day at the end of which the period ends, as an ISO date string. Should - not be before "start". - example: '2022-01-08' - format: date - type: string - moveScheduleEntries: - default: true - description: |- - If the start date of the period is changing, moveScheduleEntries defines what happens with the schedule - entries in the period. - example: true - type: boolean - start: - description: 'The day on which the period starts, as an ISO date string. Should not be after "end".' - example: '2022-01-01' - format: date - type: string - required: - - camp - - description - - end - - moveScheduleEntries - - start - type: object - PersonalInvitation-read: - deprecated: false - description: 'An invitation for a person who already has an account to collaborate in a camp.' - properties: - campId: - description: |- - The id of the camp for which this invitation is valid. This is useful for - redirecting the user to the correct place after they accept. - example: 1a2b3c4d - type: string - campTitle: - description: |- - The full title of the camp for which this invitation is valid. This should help - the user to decide whether to accept or reject the invitation. - example: 'Abteilungs-Sommerlager 2022' - type: string - id: - example: 1a2b3c4d - type: string - type: object - PersonalInvitation-write: - deprecated: false - description: 'An invitation for a person who already has an account to collaborate in a camp.' - type: object - PersonalInvitation.jsonapi-read: - deprecated: false - description: 'An invitation for a person who already has an account to collaborate in a camp.' - properties: - campId: - description: |- - The id of the camp for which this invitation is valid. This is useful for - redirecting the user to the correct place after they accept. - example: 1a2b3c4d - type: string - campTitle: - description: |- - The full title of the camp for which this invitation is valid. This should help - the user to decide whether to accept or reject the invitation. - example: 'Abteilungs-Sommerlager 2022' - type: string - id: - example: 1a2b3c4d - type: string - type: object - PersonalInvitation.jsonapi-write: - deprecated: false - description: 'An invitation for a person who already has an account to collaborate in a camp.' - type: object - PersonalInvitation.jsonhal-read: - deprecated: false - description: 'An invitation for a person who already has an account to collaborate in a camp.' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - campId: - description: |- - The id of the camp for which this invitation is valid. This is useful for - redirecting the user to the correct place after they accept. - example: 1a2b3c4d - type: string - campTitle: - description: |- - The full title of the camp for which this invitation is valid. This should help - the user to decide whether to accept or reject the invitation. - example: 'Abteilungs-Sommerlager 2022' - type: string - id: - example: 1a2b3c4d - type: string - type: object - PersonalInvitation.jsonld-read: - deprecated: false - description: 'An invitation for a person who already has an account to collaborate in a camp.' - properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - campId: - description: |- - The id of the camp for which this invitation is valid. This is useful for - redirecting the user to the correct place after they accept. - example: 1a2b3c4d - type: string - campTitle: - description: |- - The full title of the camp for which this invitation is valid. This should help - the user to decide whether to accept or reject the invitation. - example: 'Abteilungs-Sommerlager 2022' - type: string - id: - example: 1a2b3c4d - type: string - type: object - Profile-read: - deprecated: false - description: |- - The profile of a person using eCamp. - The properties available to related eCamp users are here. - Related means that they were or are collaborators in the same camp. - properties: - email: - description: 'Unique email of the user.' - example: bi-pi@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 64 - type: string - firstname: - description: "The user's (optional) first name." - example: Robert - maxLength: 64 - type: - - 'null' - - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - language: - description: 'The optional preferred language of the user, as an ICU language code.' - enum: - - de - - de-CH-scout - - en - - en-CH-scout - - fr - - fr-CH-scout - - it - - it-CH-scout - - rm - - rm-CH-scout - example: en - maxLength: 20 - type: - - 'null' - - string - legalName: - description: |- - The legal name of the user, for printing on legally relevant - documents. Falls back to the nickname if not complete. - example: 'Robert Baden-Powell' - readOnly: true - type: - - 'null' - - string - nickname: - description: "The user's (optional) nickname or scout name." - example: Bi-Pi - maxLength: 32 - type: - - 'null' - - string - surname: - description: "The user's (optional) last name." - example: Baden-Powell - maxLength: 64 - type: - - 'null' - - string - user: - example: /users/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - required: - - email - type: object - Profile-read_User.create: - deprecated: false - description: '' - properties: - email: - description: 'Unique email of the user.' - example: bi-pi@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 64 - type: string - firstname: - description: "The user's (optional) first name." - example: Robert - maxLength: 64 - type: - - 'null' - - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - language: - description: 'The optional preferred language of the user, as an ICU language code.' - enum: - - de - - de-CH-scout - - en - - en-CH-scout - - fr - - fr-CH-scout - - it - - it-CH-scout - - rm - - rm-CH-scout - example: en - maxLength: 20 - type: - - 'null' - - string - legalName: - description: |- - The legal name of the user, for printing on legally relevant - documents. Falls back to the nickname if not complete. - example: 'Robert Baden-Powell' - readOnly: true - type: - - 'null' - - string - nickname: - description: "The user's (optional) nickname or scout name." - example: Bi-Pi - maxLength: 32 - type: - - 'null' - - string - surname: - description: "The user's (optional) last name." - example: Baden-Powell - maxLength: 64 - type: - - 'null' - - string - user: - example: /users/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - required: - - email - type: object - Profile-write_create: - deprecated: false - description: |- - The profile of a person using eCamp. - The properties available to related eCamp users are here. - Related means that they were or are collaborators in the same camp. - properties: - email: - description: 'Unique email of the user.' - example: bi-pi@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - maxLength: 64 - type: string - firstname: - description: "The user's (optional) first name." - example: Robert - maxLength: 64 - type: - - 'null' - - string - language: - description: 'The optional preferred language of the user, as an ICU language code.' - enum: - - de - - de-CH-scout - - en - - en-CH-scout - - fr - - fr-CH-scout - - it - - it-CH-scout - - rm - - rm-CH-scout - example: en - maxLength: 20 - type: - - 'null' - - string - newEmail: - description: 'New email.' - example: bi-pi@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - type: - - 'null' - - string - nickname: - description: "The user's (optional) nickname or scout name." - example: Bi-Pi - maxLength: 32 - type: - - 'null' - - string - surname: - description: "The user's (optional) last name." - example: Baden-Powell - maxLength: 64 - type: - - 'null' - - string - required: - - email - type: object - Profile-write_update: - deprecated: false - description: |- - The profile of a person using eCamp. - The properties available to related eCamp users are here. - Related means that they were or are collaborators in the same camp. - properties: - firstname: - description: "The user's (optional) first name." - example: Robert - maxLength: 64 - type: - - 'null' - - string - language: - description: 'The optional preferred language of the user, as an ICU language code.' - enum: - - de - - de-CH-scout - - en - - en-CH-scout - - fr - - fr-CH-scout - - it - - it-CH-scout - - rm - - rm-CH-scout - example: en - maxLength: 20 - type: - - 'null' - - string - newEmail: - description: 'New email.' - example: bi-pi@example.com - externalDocs: - url: 'https://schema.org/email' - format: email - type: - - 'null' - - string - nickname: - description: "The user's (optional) nickname or scout name." - example: Bi-Pi - maxLength: 32 - type: - - 'null' - - string - surname: - description: "The user's (optional) last name." - example: Baden-Powell - maxLength: 64 - type: - - 'null' - - string - untrustedEmailKey: - description: 'User input for email verification.' - type: - - 'null' - - string - writeOnly: true - type: object - Profile.jsonapi-read: + Profile-read: deprecated: false description: |- The profile of a person using eCamp. @@ -18923,7 +15573,7 @@ components: required: - email type: object - Profile.jsonapi-read_User.create: + Profile-read_User.create: deprecated: false description: '' properties: @@ -18999,7 +15649,7 @@ components: required: - email type: object - Profile.jsonapi-write_create: + Profile-write_create: deprecated: false description: |- The profile of a person using eCamp. @@ -19065,7 +15715,7 @@ components: required: - email type: object - Profile.jsonapi-write_update: + Profile-write_update: deprecated: false description: |- The profile of a person using eCamp. @@ -19127,6 +15777,97 @@ components: - string writeOnly: true type: object + Profile.jsonapi: + deprecated: false + description: |- + The profile of a person using eCamp. + The properties available to related eCamp users are here. + Related means that they were or are collaborators in the same camp. + properties: + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + email: + description: 'Unique email of the user.' + example: bi-pi@example.com + externalDocs: { url: 'https://schema.org/email' } + format: email + maxLength: 64 + readOnly: true + type: string + firstname: + description: "The user's (optional) first name." + example: Robert + maxLength: 64 + type: ['null', string] + language: + description: 'The optional preferred language of the user, as an ICU language code.' + enum: [de, de-CH-scout, en, en-CH-scout, fr, fr-CH-scout, it, it-CH-scout, rm, rm-CH-scout] + example: en + maxLength: 20 + type: ['null', string] + legalName: + description: |- + The legal name of the user, for printing on legally relevant + documents. Falls back to the nickname if not complete. + example: 'Robert Baden-Powell' + readOnly: true + type: ['null', string] + newEmail: + description: 'New email.' + example: bi-pi@example.com + externalDocs: { url: 'https://schema.org/email' } + format: email + type: ['null', string] + writeOnly: true + nickname: + description: "The user's (optional) nickname or scout name." + example: Bi-Pi + maxLength: 32 + type: ['null', string] + surname: + description: "The user's (optional) last name." + example: Baden-Powell + maxLength: 64 + type: ['null', string] + untrustedEmailKey: + description: 'User input for email verification.' + type: ['null', string] + writeOnly: true + required: + - email + type: object + id: + type: string + relationships: + properties: + user: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - + $ref: '#/components/schemas/Profile.jsonapi' + readOnly: true + type: array + type: object Profile.jsonhal-read: deprecated: false description: |- @@ -19704,59 +16445,33 @@ components: deprecated: false description: '' properties: - email: - type: - - 'null' - - string - id: - description: "$id: base64_encode($email . '#' . $resetKey)." - readOnly: true - type: - - 'null' - - string - password: - maxLength: 128 - minLength: 12 - type: - - 'null' - - string - writeOnly: true - recaptchaToken: - type: - - 'null' - - string - writeOnly: true - type: object - ResetPassword.jsonapi-create: - deprecated: false - description: '' - properties: - email: - type: - - 'null' - - string - recaptchaToken: - type: - - 'null' - - string - writeOnly: true - type: object - ResetPassword.jsonapi-update: - deprecated: false - description: '' - properties: - password: - maxLength: 128 - minLength: 12 - type: - - 'null' - - string - writeOnly: true - recaptchaToken: - type: - - 'null' - - string - writeOnly: true + data: + properties: + attributes: + properties: + _id: + description: "$id: base64_encode($email . '#' . $resetKey)." + readOnly: true + type: ['null', string] + email: + type: ['null', string] + password: + maxLength: 128 + minLength: 12 + type: ['null', string] + writeOnly: true + recaptchaToken: + type: ['null', string] + writeOnly: true + type: object + id: + type: string + type: + type: string + required: + - id + - type + type: object type: object ResetPassword.jsonhal: deprecated: false @@ -19839,282 +16554,49 @@ components: type: string readOnly: true '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string - email: - type: - - 'null' - - string - id: - description: "$id: base64_encode($email . '#' . $resetKey)." - readOnly: true - type: - - 'null' - - string - password: - maxLength: 128 - minLength: 12 - type: - - 'null' - - string - writeOnly: true - recaptchaToken: - type: - - 'null' - - string - writeOnly: true - type: object - ResetPassword.jsonld-create: - deprecated: false - description: '' - properties: - email: - type: - - 'null' - - string - recaptchaToken: - type: - - 'null' - - string - writeOnly: true - type: object - ResponsiveLayout-read: - deprecated: false - description: '' - properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string - data: - default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - items: - - - slot: aside-bottom - - - slot: aside-top - - - slot: main - items: - type: string - type: - - array - - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - children - - contentType - - data - - position - type: object - ResponsiveLayout-write_create: - deprecated: false - description: '' - properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - data: - default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - items: - - - slot: aside-bottom - - - slot: aside-top - - - slot: main - items: - type: string + readOnly: true + type: string + '@type': + readOnly: true + type: string + email: type: - - array - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 + - string + id: + description: "$id: base64_encode($email . '#' . $resetKey)." + readOnly: true type: - 'null' - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference + password: + maxLength: 128 + minLength: 12 type: - 'null' - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 + writeOnly: true + recaptchaToken: type: - 'null' - string - required: - - contentType - - data - - parent - - position + writeOnly: true type: object - ResponsiveLayout-write_update: + ResetPassword.jsonld-create: deprecated: false description: '' properties: - data: - default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - items: - - - slot: aside-bottom - - - slot: aside-top - - - slot: main - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference + email: type: - 'null' - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 + recaptchaToken: type: - 'null' - string - required: - - data - - position + writeOnly: true type: object - ResponsiveLayout.jsonapi-read: + ResponsiveLayout-read: deprecated: false description: '' properties: @@ -20215,160 +16697,10 @@ components: - data - position type: object - ResponsiveLayout.jsonapi-write_create: - deprecated: false - description: '' - properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - data: - default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - items: - - - slot: aside-bottom - - - slot: aside-top - - - slot: main - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - data - - parent - - position - type: object - ResponsiveLayout.jsonapi-write_update: - deprecated: false - description: '' - properties: - data: - default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - items: - - - slot: aside-bottom - - - slot: aside-top - - - slot: main - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - data - - position - type: object - ResponsiveLayout.jsonhal-read: - deprecated: false - description: '' - properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array + ResponsiveLayout-write_create: + deprecated: false + description: '' + properties: contentType: description: |- Defines the type of this content node. There is a fixed list of types that are implemented @@ -20377,11 +16709,6 @@ components: example: /content_types/1a2b3c4d format: iri-reference type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string data: default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' description: |- @@ -20400,12 +16727,6 @@ components: type: - array - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string instanceName: description: |- An optional name for this content node. This is useful when planning e.g. an alternative @@ -20432,16 +16753,6 @@ components: same parent. The API does not guarantee the uniqueness of parent+slot+position. example: -1 type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string slot: description: |- The name of the slot in the parent in which this content node resides. The valid slot names @@ -20452,32 +16763,15 @@ components: - 'null' - string required: - - children - contentType - data + - parent - position type: object - ResponsiveLayout.jsonhal-write_create: + ResponsiveLayout-write_update: deprecated: false description: '' properties: - _links: - properties: - self: - properties: - href: - format: iri-reference - type: string - type: object - type: object - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string data: default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' description: |- @@ -20532,38 +16826,13 @@ components: - 'null' - string required: - - contentType - data - - parent - position type: object - ResponsiveLayout.jsonld-read: + ResponsiveLayout.jsonapi: deprecated: false description: '' properties: - '@context': - oneOf: - - - additionalProperties: true - properties: - '@vocab': - type: string - hydra: - enum: ['http://www.w3.org/ns/hydra/core#'] - type: string - required: - - '@vocab' - - hydra - type: object - - - type: string - readOnly: true - '@id': - readOnly: true - type: string - '@type': - readOnly: true - type: string children: description: 'All content nodes that are direct children of this content node.' example: '["/content_nodes/1a2b3c4d"]' @@ -20580,6 +16849,7 @@ components: in a content node. The content type may not be changed once the content node is created. example: /content_types/1a2b3c4d format: iri-reference + readOnly: true type: string contentTypeName: description: 'The name of the content type of this content node. Read-only, for convenience.' @@ -20661,10 +16931,28 @@ components: - data - position type: object - ResponsiveLayout.jsonld-write_create: + ResponsiveLayout.jsonhal-read: deprecated: false description: '' properties: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array contentType: description: |- Defines the type of this content node. There is a fixed list of types that are implemented @@ -20673,6 +16961,11 @@ components: example: /content_types/1a2b3c4d format: iri-reference type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string data: default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' description: |- @@ -20691,6 +16984,12 @@ components: type: - array - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string instanceName: description: |- An optional name for this content node. This is useful when planning e.g. an alternative @@ -20717,482 +17016,307 @@ components: same parent. The API does not guarantee the uniqueness of parent+slot+position. example: -1 type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - data - - parent - - position - type: object - ScheduleEntry-read: - deprecated: false - description: |- - A calendar event in a period of the camp, at which some activity will take place. The start time - is specified as an offset in minutes from the period's start time. - properties: - activity: - description: |- - The activity that will take place at the time defined by this schedule entry. Can not be changed - once the schedule entry is created. - example: /activities/1a2b3c4d - format: iri-reference - type: string - day: - description: 'The day on which this schedule entry starts.' - example: /days/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - dayNumber: - description: 'The day number of the day on which this schedule entry starts.' - example: '1' - readOnly: true - type: integer - end: - description: 'End date and time of the schedule entry.' - example: '2022-01-02T01:30:00+00:00' - format: date-time - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - readOnly: true - type: string - left: - description: |- - When rendering a period in a calendar view: Specifies how far offset the rendered calendar event - should be from the left border of the day column, as a fractional amount of the width of the whole - day. This is useful to arrange multiple overlapping schedule entries such that all of them are - visible. Should be a decimal number between 0 and 1, and left+width should not exceed 1, but the - API currently does not enforce this. - example: 0.6 - type: - - 'null' - - number - number: - description: |- - Uniquely identifies this schedule entry in the period. This uses the day number, followed - by a period, followed by the cardinal number of the schedule entry in the numbering scheme - defined by the activity's category. - example: 1.b - readOnly: true - type: string - period: - description: 'The time period which this schedule entry is part of. Must belong to the same camp as the activity.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - scheduleEntryNumber: - description: |- - The cardinal number of this schedule entry, when chronologically ordering all - schedule entries WITH THE SAME NUMBERING STYLE that start on the same day. I.e. if - the schedule entry is the second entry with roman numbering on a given day, its - number will be 2. - example: '2' - readOnly: true - type: integer - start: - description: 'Start date and time of the schedule entry.' - example: '2022-01-02T00:00:00+00:00' - format: date-time - type: string - width: - default: 1 - description: |- - When rendering a period in a calendar view: Specifies how wide the rendered calendar event should - be, as a fractional amount of the width of the whole day. This is useful to arrange multiple - overlapping schedule entries such that all of them are visible. Should be a decimal number - between 0 and 1, and left+width should not exceed 1, but the API currently does not enforce this. - example: 0.4 - type: - - 'null' - - number - required: - - end - - start - type: object - ScheduleEntry-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: - deprecated: false - description: '' - properties: - activity: - description: |- - The activity that will take place at the time defined by this schedule entry. Can not be changed - once the schedule entry is created. - example: /activities/1a2b3c4d - format: iri-reference - type: string - day: - description: 'The day on which this schedule entry starts.' - example: /days/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - dayNumber: - description: 'The day number of the day on which this schedule entry starts.' - example: '1' - readOnly: true - type: integer - end: - description: 'End date and time of the schedule entry.' - example: '2022-01-02T01:30:00+00:00' - format: date-time - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - left: - description: |- - When rendering a period in a calendar view: Specifies how far offset the rendered calendar event - should be from the left border of the day column, as a fractional amount of the width of the whole - day. This is useful to arrange multiple overlapping schedule entries such that all of them are - visible. Should be a decimal number between 0 and 1, and left+width should not exceed 1, but the - API currently does not enforce this. - example: 0.6 - type: - - 'null' - - number - number: + root: description: |- - Uniquely identifies this schedule entry in the period. This uses the day number, followed - by a period, followed by the cardinal number of the schedule entry in the numbering scheme - defined by the activity's category. - example: 1.b - readOnly: true - type: string - period: - description: 'The time period which this schedule entry is part of. Must belong to the same camp as the activity.' - example: /periods/1a2b3c4d + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - scheduleEntryNumber: - description: |- - The cardinal number of this schedule entry, when chronologically ordering all - schedule entries WITH THE SAME NUMBERING STYLE that start on the same day. I.e. if - the schedule entry is the second entry with roman numbering on a given day, its - number will be 2. - example: '2' readOnly: true - type: integer - start: - description: 'Start date and time of the schedule entry.' - example: '2022-01-02T00:00:00+00:00' - format: date-time - type: string - width: - default: 1 + type: + - 'null' + - string + slot: description: |- - When rendering a period in a calendar view: Specifies how wide the rendered calendar event should - be, as a fractional amount of the width of the whole day. This is useful to arrange multiple - overlapping schedule entries such that all of them are visible. Should be a decimal number - between 0 and 1, and left+width should not exceed 1, but the API currently does not enforce this. - example: 0.4 + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 type: - 'null' - - number + - string required: - - activity - - end - - start + - children + - contentType + - data + - position type: object - ScheduleEntry-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ResponsiveLayout.jsonhal-write_create: deprecated: false description: '' properties: - activity: + _links: + properties: + self: + properties: + href: + format: iri-reference + type: string + type: object + type: object + contentType: description: |- - The activity that will take place at the time defined by this schedule entry. Can not be changed - once the schedule entry is created. - example: /activities/1a2b3c4d + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d format: iri-reference type: string - day: - description: 'The day on which this schedule entry starts.' - example: /days/1a2b3c4d - format: iri-reference - readOnly: true + data: + default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + items: + - + slot: aside-bottom + - + slot: aside-top + - + slot: main + items: + type: string type: + - array - 'null' - - string - dayNumber: - description: 'The day number of the day on which this schedule entry starts.' - example: '1' - readOnly: true - type: integer - end: - description: 'End date and time of the schedule entry.' - example: '2022-01-02T01:30:00+00:00' - format: date-time - type: string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - left: + instanceName: description: |- - When rendering a period in a calendar view: Specifies how far offset the rendered calendar event - should be from the left border of the day column, as a fractional amount of the width of the whole - day. This is useful to arrange multiple overlapping schedule entries such that all of them are - visible. Should be a decimal number between 0 and 1, and left+width should not exceed 1, but the - API currently does not enforce this. - example: 0.6 + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 type: - 'null' - - number - number: + - string + parent: description: |- - Uniquely identifies this schedule entry in the period. This uses the day number, followed - by a period, followed by the cardinal number of the schedule entry in the numbering scheme - defined by the activity's category. - example: 1.b - readOnly: true - type: string - period: - description: 'The time period which this schedule entry is part of. Must belong to the same camp as the activity.' - example: /periods/1a2b3c4d + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - scheduleEntryNumber: + type: + - 'null' + - string + position: + default: -1 description: |- - The cardinal number of this schedule entry, when chronologically ordering all - schedule entries WITH THE SAME NUMBERING STYLE that start on the same day. I.e. if - the schedule entry is the second entry with roman numbering on a given day, its - number will be 2. - example: '2' - readOnly: true + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 type: integer - start: - description: 'Start date and time of the schedule entry.' - example: '2022-01-02T00:00:00+00:00' - format: date-time - type: string - width: - default: 1 + slot: description: |- - When rendering a period in a calendar view: Specifies how wide the rendered calendar event should - be, as a fractional amount of the width of the whole day. This is useful to arrange multiple - overlapping schedule entries such that all of them are visible. Should be a decimal number - between 0 and 1, and left+width should not exceed 1, but the API currently does not enforce this. - example: 0.4 + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 type: - 'null' - - number + - string required: - - activity - - end - - start + - contentType + - data + - parent + - position type: object - ScheduleEntry-read_ScheduleEntry.Activity: + ResponsiveLayout.jsonld-read: deprecated: false - description: |- - A calendar event in a period of the camp, at which some activity will take place. The start time - is specified as an offset in minutes from the period's start time. + description: '' properties: - activity: - anyOf: + '@context': + oneOf: - - $ref: '#/components/schemas/Activity-read_ScheduleEntry.Activity' + additionalProperties: true + properties: + '@vocab': + type: string + hydra: + enum: ['http://www.w3.org/ns/hydra/core#'] + type: string + required: + - '@vocab' + - hydra + type: object - - type: 'null' + type: string readOnly: true - day: - description: 'The day on which this schedule entry starts.' - example: /days/1a2b3c4d - format: iri-reference + '@id': readOnly: true - type: - - 'null' - - string - dayNumber: - description: 'The day number of the day on which this schedule entry starts.' - example: '1' + type: string + '@type': + readOnly: true + type: string + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept readOnly: true - type: integer - end: - description: 'End date and time of the schedule entry.' - example: '2022-01-02T01:30:00+00:00' - format: date-time type: string + data: + default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' + description: |- + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + items: + - + slot: aside-bottom + - + slot: aside-top + - + slot: main + items: + type: string + type: + - array + - 'null' id: description: 'An internal, unique, randomly generated identifier of this entity.' example: 1a2b3c4d maxLength: 16 readOnly: true type: string - left: + instanceName: description: |- - When rendering a period in a calendar view: Specifies how far offset the rendered calendar event - should be from the left border of the day column, as a fractional amount of the width of the whole - day. This is useful to arrange multiple overlapping schedule entries such that all of them are - visible. Should be a decimal number between 0 and 1, and left+width should not exceed 1, but the - API currently does not enforce this. - example: 0.6 + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 type: - 'null' - - number - number: + - string + parent: description: |- - Uniquely identifies this schedule entry in the period. This uses the day number, followed - by a period, followed by the cardinal number of the schedule entry in the numbering scheme - defined by the activity's category. - example: 1.b - readOnly: true - type: string - period: - description: 'The time period which this schedule entry is part of. Must belong to the same camp as the activity.' - example: /periods/1a2b3c4d + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - scheduleEntryNumber: + type: + - 'null' + - string + position: + default: -1 description: |- - The cardinal number of this schedule entry, when chronologically ordering all - schedule entries WITH THE SAME NUMBERING STYLE that start on the same day. I.e. if - the schedule entry is the second entry with roman numbering on a given day, its - number will be 2. - example: '2' - readOnly: true + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 type: integer - start: - description: 'Start date and time of the schedule entry.' - example: '2022-01-02T00:00:00+00:00' - format: date-time - type: string - width: - default: 1 + root: description: |- - When rendering a period in a calendar view: Specifies how wide the rendered calendar event should - be, as a fractional amount of the width of the whole day. This is useful to arrange multiple - overlapping schedule entries such that all of them are visible. Should be a decimal number - between 0 and 1, and left+width should not exceed 1, but the API currently does not enforce this. - example: 0.4 + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true type: - 'null' - - number + - string + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 + type: + - 'null' + - string required: - - activity - - end - - start + - children + - contentType + - data + - position type: object - ScheduleEntry-write: + ResponsiveLayout.jsonld-write_create: deprecated: false - description: |- - A calendar event in a period of the camp, at which some activity will take place. The start time - is specified as an offset in minutes from the period's start time. + description: '' properties: - end: - description: 'End date and time of the schedule entry.' - example: '2022-01-02T01:30:00+00:00' - format: date-time + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference type: string - left: + data: + default: '{"items":[{"slot":"main"},{"slot":"aside-top"},{"slot":"aside-bottom"}]}' description: |- - When rendering a period in a calendar view: Specifies how far offset the rendered calendar event - should be from the left border of the day column, as a fractional amount of the width of the whole - day. This is useful to arrange multiple overlapping schedule entries such that all of them are - visible. Should be a decimal number between 0 and 1, and left+width should not exceed 1, but the - API currently does not enforce this. - example: 0.6 + Holds the actual data of the content node + (overridden from abstract class in order to add specific validation). + example: + items: + - + slot: aside-bottom + - + slot: aside-top + - + slot: main + items: + type: string type: + - array - 'null' - - number - period: - description: 'The time period which this schedule entry is part of. Must belong to the same camp as the activity.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - start: - description: 'Start date and time of the schedule entry.' - example: '2022-01-02T00:00:00+00:00' - format: date-time - type: string - width: - default: 1 + instanceName: description: |- - When rendering a period in a calendar view: Specifies how wide the rendered calendar event should - be, as a fractional amount of the width of the whole day. This is useful to arrange multiple - overlapping schedule entries such that all of them are visible. Should be a decimal number - between 0 and 1, and left+width should not exceed 1, but the API currently does not enforce this. - example: 0.4 + An optional name for this content node. This is useful when planning e.g. an alternative + version of the programme suited for bad weather, in addition to the normal version. + example: Schlechtwetterprogramm + maxLength: 32 type: - 'null' - - number - required: - - end - - start - type: object - ScheduleEntry-write_create: - deprecated: false - description: |- - A calendar event in a period of the camp, at which some activity will take place. The start time - is specified as an offset in minutes from the period's start time. - properties: - activity: + - string + parent: description: |- - The activity that will take place at the time defined by this schedule entry. Can not be changed - once the schedule entry is created. - example: /activities/1a2b3c4d + The parent to which this content node belongs. Is null in case this content node is the + root of a content node tree. For non-root content nodes, the parent can be changed, as long + as the new parent is in the same camp as the old one. + example: /content_nodes/1a2b3c4d format: iri-reference - type: string - end: - description: 'End date and time of the schedule entry.' - example: '2022-01-02T01:30:00+00:00' - format: date-time - type: string - left: - description: |- - When rendering a period in a calendar view: Specifies how far offset the rendered calendar event - should be from the left border of the day column, as a fractional amount of the width of the whole - day. This is useful to arrange multiple overlapping schedule entries such that all of them are - visible. Should be a decimal number between 0 and 1, and left+width should not exceed 1, but the - API currently does not enforce this. - example: 0.6 type: - 'null' - - number - period: - description: 'The time period which this schedule entry is part of. Must belong to the same camp as the activity.' - example: /periods/1a2b3c4d - format: iri-reference - type: string - start: - description: 'Start date and time of the schedule entry.' - example: '2022-01-02T00:00:00+00:00' - format: date-time - type: string - width: - default: 1 + - string + position: + default: -1 description: |- - When rendering a period in a calendar view: Specifies how wide the rendered calendar event should - be, as a fractional amount of the width of the whole day. This is useful to arrange multiple - overlapping schedule entries such that all of them are visible. Should be a decimal number - between 0 and 1, and left+width should not exceed 1, but the API currently does not enforce this. - example: 0.4 + A whole number used for ordering multiple content nodes that are in the same slot of the + same parent. The API does not guarantee the uniqueness of parent+slot+position. + example: -1 + type: integer + slot: + description: |- + The name of the slot in the parent in which this content node resides. The valid slot names + are defined by the content type of the parent. + example: '1' + maxLength: 32 type: - 'null' - - number + - string required: - - activity - - end - - start + - contentType + - data + - parent + - position type: object - ScheduleEntry.jsonapi-read: + ScheduleEntry-read: deprecated: false description: |- A calendar event in a period of the camp, at which some activity will take place. The start time @@ -21281,7 +17405,7 @@ components: - end - start type: object - ScheduleEntry.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: + ScheduleEntry-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries: deprecated: false description: '' properties: @@ -21370,7 +17494,7 @@ components: - end - start type: object - ScheduleEntry.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: + ScheduleEntry-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes: deprecated: false description: '' properties: @@ -21459,7 +17583,7 @@ components: - end - start type: object - ScheduleEntry.jsonapi-read_ScheduleEntry.Activity: + ScheduleEntry-read_ScheduleEntry.Activity: deprecated: false description: |- A calendar event in a period of the camp, at which some activity will take place. The start time @@ -21468,7 +17592,7 @@ components: activity: anyOf: - - $ref: '#/components/schemas/Activity.jsonapi-read_ScheduleEntry.Activity' + $ref: '#/components/schemas/Activity-read_ScheduleEntry.Activity' - type: 'null' readOnly: true @@ -21550,7 +17674,7 @@ components: - end - start type: object - ScheduleEntry.jsonapi-write: + ScheduleEntry-write: deprecated: false description: |- A calendar event in a period of the camp, at which some activity will take place. The start time @@ -21597,7 +17721,7 @@ components: - end - start type: object - ScheduleEntry.jsonapi-write_create: + ScheduleEntry-write_create: deprecated: false description: |- A calendar event in a period of the camp, at which some activity will take place. The start time @@ -21652,6 +17776,110 @@ components: - end - start type: object + ScheduleEntry.jsonapi: + deprecated: false + description: |- + A calendar event in a period of the camp, at which some activity will take place. The start time + is specified as an offset in minutes from the period's start time. + properties: + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + dayNumber: + description: 'The day number of the day on which this schedule entry starts.' + example: '1' + readOnly: true + type: integer + end: + description: 'End date and time of the schedule entry.' + example: '2022-01-02T01:30:00+00:00' + format: date-time + type: string + left: + description: |- + When rendering a period in a calendar view: Specifies how far offset the rendered calendar event + should be from the left border of the day column, as a fractional amount of the width of the whole + day. This is useful to arrange multiple overlapping schedule entries such that all of them are + visible. Should be a decimal number between 0 and 1, and left+width should not exceed 1, but the + API currently does not enforce this. + example: 0.6 + type: ['null', number] + number: + description: |- + Uniquely identifies this schedule entry in the period. This uses the day number, followed + by a period, followed by the cardinal number of the schedule entry in the numbering scheme + defined by the activity's category. + example: 1.b + readOnly: true + type: string + scheduleEntryNumber: + description: |- + The cardinal number of this schedule entry, when chronologically ordering all + schedule entries WITH THE SAME NUMBERING STYLE that start on the same day. I.e. if + the schedule entry is the second entry with roman numbering on a given day, its + number will be 2. + example: '2' + readOnly: true + type: integer + start: + description: 'Start date and time of the schedule entry.' + example: '2022-01-02T00:00:00+00:00' + format: date-time + type: string + width: + default: 1 + description: |- + When rendering a period in a calendar view: Specifies how wide the rendered calendar event should + be, as a fractional amount of the width of the whole day. This is useful to arrange multiple + overlapping schedule entries such that all of them are visible. Should be a decimal number + between 0 and 1, and left+width should not exceed 1, but the API currently does not enforce this. + example: 0.4 + type: ['null', number] + required: + - end + - start + type: object + id: + type: string + relationships: + properties: + activity: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + day: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + period: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + required: + - activity + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - + $ref: '#/components/schemas/Period.jsonapi' + - + $ref: '#/components/schemas/Period.jsonapi' + - + $ref: '#/components/schemas/Period.jsonapi' + readOnly: true + type: array + type: object ScheduleEntry.jsonhal-read: deprecated: false description: |- @@ -22830,7 +19058,7 @@ components: - data - position type: object - SingleText.jsonapi-read: + SingleText.jsonapi: deprecated: false description: '' properties: @@ -22850,6 +19078,7 @@ components: in a content node. The content type may not be changed once the content node is created. example: /content_types/1a2b3c4d format: iri-reference + readOnly: true type: string contentTypeName: description: 'The name of the content type of this content node. Read-only, for convenience.' @@ -22925,126 +19154,6 @@ components: - data - position type: object - SingleText.jsonapi-write_create: - deprecated: false - description: '' - properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - data: - default: '{"html":""}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - html: 'my example text' - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - contentType - - data - - parent - - position - type: object - SingleText.jsonapi-write_update: - deprecated: false - description: '' - properties: - data: - default: '{"html":""}' - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - html: 'my example text' - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - data - - position - type: object SingleText.jsonhal-read: deprecated: false description: '' @@ -23569,178 +19678,13 @@ components: - string required: - contentType - - parent - - position - type: object - Storyboard-write_update: - deprecated: false - description: '' - properties: - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - sections: - 186b7ff2-7470-4de4-8783-082c2c189fcd: - column1: '' - column2Html: '' - column3: '' - position: 0 - items: - type: string - type: - - array - - 'null' - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - data - - position - type: object - Storyboard.jsonapi-read: - deprecated: false - description: '' - properties: - children: - description: 'All content nodes that are direct children of this content node.' - example: '["/content_nodes/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string - readOnly: true - type: array - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string - contentTypeName: - description: 'The name of the content type of this content node. Read-only, for convenience.' - example: SafetyConcept - readOnly: true - type: string - data: - description: |- - Holds the actual data of the content node - (overridden from abstract class in order to add specific validation). - example: - sections: - 186b7ff2-7470-4de4-8783-082c2c189fcd: - column1: '' - column2Html: '' - column3: '' - position: 0 - items: - type: string - type: - - array - - 'null' - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - instanceName: - description: |- - An optional name for this content node. This is useful when planning e.g. an alternative - version of the programme suited for bad weather, in addition to the normal version. - example: Schlechtwetterprogramm - maxLength: 32 - type: - - 'null' - - string - parent: - description: |- - The parent to which this content node belongs. Is null in case this content node is the - root of a content node tree. For non-root content nodes, the parent can be changed, as long - as the new parent is in the same camp as the old one. - example: /content_nodes/1a2b3c4d - format: iri-reference - type: - - 'null' - - string - position: - default: -1 - description: |- - A whole number used for ordering multiple content nodes that are in the same slot of the - same parent. The API does not guarantee the uniqueness of parent+slot+position. - example: -1 - type: integer - root: - description: |- - The content node that is the root of the content node tree. Refers to itself in case this - content node is the root. - example: /content_nodes/1a2b3c4d - format: iri-reference - readOnly: true - type: - - 'null' - - string - slot: - description: |- - The name of the slot in the parent in which this content node resides. The valid slot names - are defined by the content type of the parent. - example: '1' - maxLength: 32 - type: - - 'null' - - string - required: - - children - - contentType + - parent - position type: object - Storyboard.jsonapi-write_create: + Storyboard-write_update: deprecated: false description: '' properties: - contentType: - description: |- - Defines the type of this content node. There is a fixed list of types that are implemented - in eCamp. Depending on the type, different content data and different slots may be allowed - in a content node. The content type may not be changed once the content node is created. - example: /content_types/1a2b3c4d - format: iri-reference - type: string data: description: |- Holds the actual data of the content node @@ -23793,14 +19737,36 @@ components: - 'null' - string required: - - contentType - - parent + - data - position type: object - Storyboard.jsonapi-write_update: + Storyboard.jsonapi: deprecated: false description: '' properties: + children: + description: 'All content nodes that are direct children of this content node.' + example: '["/content_nodes/1a2b3c4d"]' + items: + example: 'https://example.com/' + format: iri-reference + type: string + readOnly: true + type: array + contentType: + description: |- + Defines the type of this content node. There is a fixed list of types that are implemented + in eCamp. Depending on the type, different content data and different slots may be allowed + in a content node. The content type may not be changed once the content node is created. + example: /content_types/1a2b3c4d + format: iri-reference + readOnly: true + type: string + contentTypeName: + description: 'The name of the content type of this content node. Read-only, for convenience.' + example: SafetyConcept + readOnly: true + type: string data: description: |- Holds the actual data of the content node @@ -23817,6 +19783,12 @@ components: type: - array - 'null' + id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string instanceName: description: |- An optional name for this content node. This is useful when planning e.g. an alternative @@ -23843,6 +19815,16 @@ components: same parent. The API does not guarantee the uniqueness of parent+slot+position. example: -1 type: integer + root: + description: |- + The content node that is the root of the content node tree. Refers to itself in case this + content node is the root. + example: /content_nodes/1a2b3c4d + format: iri-reference + readOnly: true + type: + - 'null' + - string slot: description: |- The name of the slot in the parent in which this content node resides. The valid slot names @@ -23853,7 +19835,8 @@ components: - 'null' - string required: - - data + - children + - contentType - position type: object Storyboard.jsonhal-read: @@ -24405,181 +20388,69 @@ components: - password - profile type: object - User.jsonapi-activate: - deprecated: false - description: |- - A person using eCamp. - The properties available for all other eCamp users are here. - properties: - activationKey: - description: 'User-Input for activation.' - type: - - 'null' - - string - writeOnly: true - type: object - User.jsonapi-read: - deprecated: false - description: |- - A person using eCamp. - The properties available for all other eCamp users are here. - properties: - displayName: - description: 'A displayable name of the user.' - example: 'Robert Baden-Powell' - readOnly: true - type: - - 'null' - - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - profile: - example: - email: bi-pi@example.com - firstname: Robert - language: en - nickname: Bi-Pi - surname: Baden-Powell - format: iri-reference - type: string - type: object - User.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User: - deprecated: false - description: '' - properties: - displayName: - description: 'A displayable name of the user.' - example: 'Robert Baden-Powell' - readOnly: true - type: - - 'null' - - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - profile: - example: - email: bi-pi@example.com - firstname: Robert - language: en - nickname: Bi-Pi - surname: Baden-Powell - format: iri-reference - type: string - type: object - User.jsonapi-read_CampCollaboration.Camp_CampCollaboration.User: - deprecated: false - description: '' - properties: - displayName: - description: 'A displayable name of the user.' - example: 'Robert Baden-Powell' - readOnly: true - type: - - 'null' - - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - profile: - example: - email: bi-pi@example.com - firstname: Robert - language: en - nickname: Bi-Pi - surname: Baden-Powell - format: iri-reference - type: string - type: object - User.jsonapi-read_User.create: + User.jsonapi: deprecated: false description: |- A person using eCamp. The properties available for all other eCamp users are here. properties: - displayName: - description: 'A displayable name of the user.' - example: 'Robert Baden-Powell' - readOnly: true - type: - - 'null' - - string - id: - description: 'An internal, unique, randomly generated identifier of this entity.' - example: 1a2b3c4d - maxLength: 16 - readOnly: true - type: string - profile: - $ref: '#/components/schemas/Profile.jsonapi-read_User.create' + data: + properties: + attributes: + properties: + _id: + description: 'An internal, unique, randomly generated identifier of this entity.' + example: 1a2b3c4d + maxLength: 16 + readOnly: true + type: string + activationKey: + description: 'User-Input for activation.' + type: ['null', string] + writeOnly: true + displayName: + description: 'A displayable name of the user.' + example: 'Robert Baden-Powell' + readOnly: true + type: ['null', string] + password: + description: |- + A new password for this user. At least 12 characters, as is explicitly recommended by OWASP: + https://github.com/OWASP/ASVS/blob/master/4.0/en/0x11-V2-Authentication.md#v21-password-security + 2.1.1: Verify that user set passwords are at least 12 characters in length (after multiple spaces are combined). + example: learning-by-doing-101 + maxLength: 128 + minLength: 12 + type: ['null', string] + writeOnly: true + recaptchaToken: + description: 'ReCaptchaToken used on Register-View.' + type: ['null', string] + writeOnly: true + type: object + id: + type: string + relationships: + properties: + profile: + properties: { data: { properties: { id: { format: iri-reference, type: string }, type: { type: string } }, type: object } } + type: object + type: + type: string + required: + - id + - type + type: object + included: + description: 'Related resources requested via the "include" query parameter.' + externalDocs: + url: 'https://jsonapi.org/format/#fetching-includes' + items: + anyOf: + - + $ref: '#/components/schemas/Profile.jsonapi' readOnly: true - required: - - profile - type: object - User.jsonapi-write: - deprecated: false - description: |- - A person using eCamp. - The properties available for all other eCamp users are here. - properties: - password: - description: |- - A new password for this user. At least 12 characters, as is explicitly recommended by OWASP: - https://github.com/OWASP/ASVS/blob/master/4.0/en/0x11-V2-Authentication.md#v21-password-security - 2.1.1: Verify that user set passwords are at least 12 characters in length (after multiple spaces are combined). - example: learning-by-doing-101 - maxLength: 128 - minLength: 12 - type: - - 'null' - - string - writeOnly: true - type: object - User.jsonapi-write_create: - deprecated: false - description: |- - A person using eCamp. - The properties available for all other eCamp users are here. - properties: - password: - description: |- - A new password for this user. At least 12 characters, as is explicitly recommended by OWASP: - https://github.com/OWASP/ASVS/blob/master/4.0/en/0x11-V2-Authentication.md#v21-password-security - 2.1.1: Verify that user set passwords are at least 12 characters in length (after multiple spaces are combined). - example: learning-by-doing-101 - maxLength: 128 - minLength: 12 - type: - - 'null' - - string - writeOnly: true - profile: - $ref: '#/components/schemas/Profile.jsonapi-write_create' - example: - email: bi-pi@example.com - firstname: Robert - language: en - nickname: Bi-Pi - surname: Baden-Powell - recaptchaToken: - description: 'ReCaptchaToken used on Register-View.' - type: - - 'null' - - string - writeOnly: true - required: - - password - - profile + type: array type: object User.jsonhal-read: deprecated: false @@ -25081,7 +20952,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/Activity.jsonapi-read_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries' + $ref: '#/components/schemas/Activity.jsonapi' type: array text/html: schema: @@ -25111,7 +20982,7 @@ paths: $ref: '#/components/schemas/Activity.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/Activity.jsonapi-write_create' + $ref: '#/components/schemas/Activity.jsonapi' text/html: schema: $ref: '#/components/schemas/Activity-write_create' @@ -25131,7 +21002,7 @@ paths: $ref: '#/components/schemas/Activity.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' application/vnd.api+json: schema: - $ref: '#/components/schemas/Activity.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/Activity.jsonapi' text/html: schema: $ref: '#/components/schemas/Activity-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' @@ -25201,7 +21072,7 @@ paths: $ref: '#/components/schemas/Activity.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' application/vnd.api+json: schema: - $ref: '#/components/schemas/Activity.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/Activity.jsonapi' text/html: schema: $ref: '#/components/schemas/Activity-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' @@ -25236,7 +21107,7 @@ paths: $ref: '#/components/schemas/Activity-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/Activity.jsonapi-write' + $ref: '#/components/schemas/Activity.jsonapi' description: 'The updated Activity resource' required: true responses: @@ -25253,7 +21124,7 @@ paths: $ref: '#/components/schemas/Activity.jsonld-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' application/vnd.api+json: schema: - $ref: '#/components/schemas/Activity.jsonapi-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' + $ref: '#/components/schemas/Activity.jsonapi' text/html: schema: $ref: '#/components/schemas/Activity-read_Activity.Category_Activity.ActivityProgressLabel_Activity.ActivityResponsibles_Activity.ScheduleEntries_Activity.ContentNodes' @@ -25332,7 +21203,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/ActivityProgressLabel.jsonapi-read' + $ref: '#/components/schemas/ActivityProgressLabel.jsonapi' type: array text/html: schema: @@ -25362,7 +21233,7 @@ paths: $ref: '#/components/schemas/ActivityProgressLabel.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/ActivityProgressLabel.jsonapi-write_create' + $ref: '#/components/schemas/ActivityProgressLabel.jsonapi' text/html: schema: $ref: '#/components/schemas/ActivityProgressLabel-write_create' @@ -25382,7 +21253,7 @@ paths: $ref: '#/components/schemas/ActivityProgressLabel.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ActivityProgressLabel.jsonapi-read' + $ref: '#/components/schemas/ActivityProgressLabel.jsonapi' text/html: schema: $ref: '#/components/schemas/ActivityProgressLabel-read' @@ -25452,7 +21323,7 @@ paths: $ref: '#/components/schemas/ActivityProgressLabel.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ActivityProgressLabel.jsonapi-read' + $ref: '#/components/schemas/ActivityProgressLabel.jsonapi' text/html: schema: $ref: '#/components/schemas/ActivityProgressLabel-read' @@ -25487,7 +21358,7 @@ paths: $ref: '#/components/schemas/ActivityProgressLabel-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/ActivityProgressLabel.jsonapi-write' + $ref: '#/components/schemas/ActivityProgressLabel.jsonapi' description: 'The updated ActivityProgressLabel resource' required: true responses: @@ -25504,7 +21375,7 @@ paths: $ref: '#/components/schemas/ActivityProgressLabel.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ActivityProgressLabel.jsonapi-read' + $ref: '#/components/schemas/ActivityProgressLabel.jsonapi' text/html: schema: $ref: '#/components/schemas/ActivityProgressLabel-read' @@ -25609,7 +21480,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/ActivityResponsible.jsonapi-read' + $ref: '#/components/schemas/ActivityResponsible.jsonapi' type: array text/html: schema: @@ -25639,7 +21510,7 @@ paths: $ref: '#/components/schemas/ActivityResponsible.jsonld-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/ActivityResponsible.jsonapi-write' + $ref: '#/components/schemas/ActivityResponsible.jsonapi' text/html: schema: $ref: '#/components/schemas/ActivityResponsible-write' @@ -25659,7 +21530,7 @@ paths: $ref: '#/components/schemas/ActivityResponsible.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ActivityResponsible.jsonapi-read' + $ref: '#/components/schemas/ActivityResponsible.jsonapi' text/html: schema: $ref: '#/components/schemas/ActivityResponsible-read' @@ -25729,7 +21600,7 @@ paths: $ref: '#/components/schemas/ActivityResponsible.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ActivityResponsible.jsonapi-read' + $ref: '#/components/schemas/ActivityResponsible.jsonapi' text/html: schema: $ref: '#/components/schemas/ActivityResponsible-read' @@ -25856,7 +21727,7 @@ paths: $ref: '#/components/schemas/ResetPassword.jsonld-create' application/vnd.api+json: schema: - $ref: '#/components/schemas/ResetPassword.jsonapi-create' + $ref: '#/components/schemas/ResetPassword.jsonapi' text/html: schema: $ref: '#/components/schemas/ResetPassword-create' @@ -25951,7 +21822,7 @@ paths: $ref: '#/components/schemas/ResetPassword-update' application/vnd.api+json: schema: - $ref: '#/components/schemas/ResetPassword.jsonapi-update' + $ref: '#/components/schemas/ResetPassword.jsonapi' description: 'The updated ResetPassword resource' required: true responses: @@ -26109,7 +21980,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/CampCollaboration.jsonapi-read_CampCollaboration.Camp_CampCollaboration.User' + $ref: '#/components/schemas/CampCollaboration.jsonapi' type: array text/html: schema: @@ -26139,7 +22010,7 @@ paths: $ref: '#/components/schemas/CampCollaboration.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/CampCollaboration.jsonapi-write_create' + $ref: '#/components/schemas/CampCollaboration.jsonapi' text/html: schema: $ref: '#/components/schemas/CampCollaboration-write_create' @@ -26159,7 +22030,7 @@ paths: $ref: '#/components/schemas/CampCollaboration.jsonld-read_CampCollaboration.Camp_CampCollaboration.User' application/vnd.api+json: schema: - $ref: '#/components/schemas/CampCollaboration.jsonapi-read_CampCollaboration.Camp_CampCollaboration.User' + $ref: '#/components/schemas/CampCollaboration.jsonapi' text/html: schema: $ref: '#/components/schemas/CampCollaboration-read_CampCollaboration.Camp_CampCollaboration.User' @@ -26229,7 +22100,7 @@ paths: $ref: '#/components/schemas/CampCollaboration.jsonld-read_CampCollaboration.Camp_CampCollaboration.User' application/vnd.api+json: schema: - $ref: '#/components/schemas/CampCollaboration.jsonapi-read_CampCollaboration.Camp_CampCollaboration.User' + $ref: '#/components/schemas/CampCollaboration.jsonapi' text/html: schema: $ref: '#/components/schemas/CampCollaboration-read_CampCollaboration.Camp_CampCollaboration.User' @@ -26264,7 +22135,7 @@ paths: $ref: '#/components/schemas/CampCollaboration-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/CampCollaboration.jsonapi-write_update' + $ref: '#/components/schemas/CampCollaboration.jsonapi' description: 'The updated CampCollaboration resource' required: true responses: @@ -26281,7 +22152,7 @@ paths: $ref: '#/components/schemas/CampCollaboration.jsonld-read_CampCollaboration.Camp_CampCollaboration.User' application/vnd.api+json: schema: - $ref: '#/components/schemas/CampCollaboration.jsonapi-read_CampCollaboration.Camp_CampCollaboration.User' + $ref: '#/components/schemas/CampCollaboration.jsonapi' text/html: schema: $ref: '#/components/schemas/CampCollaboration-read_CampCollaboration.Camp_CampCollaboration.User' @@ -26322,7 +22193,7 @@ paths: $ref: '#/components/schemas/CampCollaboration-resend_invitation' application/vnd.api+json: schema: - $ref: '#/components/schemas/CampCollaboration.jsonapi-resend_invitation' + $ref: '#/components/schemas/CampCollaboration.jsonapi' description: 'The updated CampCollaboration resource' required: true responses: @@ -26339,7 +22210,7 @@ paths: $ref: '#/components/schemas/CampCollaboration.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/CampCollaboration.jsonapi-read' + $ref: '#/components/schemas/CampCollaboration.jsonapi' text/html: schema: $ref: '#/components/schemas/CampCollaboration-read' @@ -26418,7 +22289,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/Camp.jsonapi-read' + $ref: '#/components/schemas/Camp.jsonapi' type: array text/html: schema: @@ -26448,7 +22319,7 @@ paths: $ref: '#/components/schemas/Camp.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/Camp.jsonapi-write_create' + $ref: '#/components/schemas/Camp.jsonapi' text/html: schema: $ref: '#/components/schemas/Camp-write_create' @@ -26468,7 +22339,7 @@ paths: $ref: '#/components/schemas/Camp.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' application/vnd.api+json: schema: - $ref: '#/components/schemas/Camp.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Camp.jsonapi' text/html: schema: $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' @@ -26491,7 +22362,7 @@ paths: allowEmptyValue: false allowReserved: false deprecated: false - description: 'Camp identifier' + description: 'Category identifier' explode: false in: path name: campId @@ -26531,7 +22402,7 @@ paths: application/hal+json: schema: properties: - _embedded: { items: { $ref: '#/components/schemas/Category.jsonhal-read' }, type: array } + _embedded: { anyOf: [{ properties: { item: { items: { $ref: '#/components/schemas/Category.jsonhal-read' }, type: array } }, type: object }, { type: object }] } _links: { properties: { first: { properties: { href: { format: iri-reference, type: string } }, type: object }, last: { properties: { href: { format: iri-reference, type: string } }, type: object }, next: { properties: { href: { format: iri-reference, type: string } }, type: object }, previous: { properties: { href: { format: iri-reference, type: string } }, type: object }, self: { properties: { href: { format: iri-reference, type: string } }, type: object } }, type: object } itemsPerPage: { minimum: 0, type: integer } totalItems: { minimum: 0, type: integer } @@ -26557,7 +22428,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/Category-read' + $ref: '#/components/schemas/Category.jsonapi' type: array text/html: schema: @@ -26626,7 +22497,7 @@ paths: $ref: '#/components/schemas/Camp.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' application/vnd.api+json: schema: - $ref: '#/components/schemas/Camp.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Camp.jsonapi' text/html: schema: $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' @@ -26661,7 +22532,7 @@ paths: $ref: '#/components/schemas/Camp-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/Camp.jsonapi-write_update' + $ref: '#/components/schemas/Camp.jsonapi' description: 'The updated Camp resource' required: true responses: @@ -26678,7 +22549,7 @@ paths: $ref: '#/components/schemas/Camp.jsonld-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' application/vnd.api+json: schema: - $ref: '#/components/schemas/Camp.jsonapi-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' + $ref: '#/components/schemas/Camp.jsonapi' text/html: schema: $ref: '#/components/schemas/Camp-read_Camp.Periods_Period.Days_Camp.CampCollaborations_CampCollaboration.User' @@ -26757,7 +22628,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/Category.jsonapi-read' + $ref: '#/components/schemas/Category.jsonapi' type: array text/html: schema: @@ -26787,7 +22658,7 @@ paths: $ref: '#/components/schemas/Category.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi-write_create' + $ref: '#/components/schemas/Category.jsonapi' text/html: schema: $ref: '#/components/schemas/Category-write_create' @@ -26807,7 +22678,7 @@ paths: $ref: '#/components/schemas/Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/Category.jsonapi' text/html: schema: $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' @@ -26877,7 +22748,7 @@ paths: $ref: '#/components/schemas/Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/Category.jsonapi' text/html: schema: $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' @@ -26912,7 +22783,7 @@ paths: $ref: '#/components/schemas/Category-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi-write_update' + $ref: '#/components/schemas/Category.jsonapi' description: 'The updated Category resource' required: true responses: @@ -26929,7 +22800,7 @@ paths: $ref: '#/components/schemas/Category.jsonld-read_Category.PreferredContentTypes_Category.ContentNodes' application/vnd.api+json: schema: - $ref: '#/components/schemas/Category.jsonapi-read_Category.PreferredContentTypes_Category.ContentNodes' + $ref: '#/components/schemas/Category.jsonapi' text/html: schema: $ref: '#/components/schemas/Category-read_Category.PreferredContentTypes_Category.ContentNodes' @@ -27046,7 +22917,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read' + $ref: '#/components/schemas/ColumnLayout.jsonapi' type: array text/html: schema: @@ -27076,7 +22947,7 @@ paths: $ref: '#/components/schemas/ColumnLayout.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/ColumnLayout.jsonapi-write_create' + $ref: '#/components/schemas/ColumnLayout.jsonapi' text/html: schema: $ref: '#/components/schemas/ColumnLayout-write_create' @@ -27096,7 +22967,7 @@ paths: $ref: '#/components/schemas/ColumnLayout.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read' + $ref: '#/components/schemas/ColumnLayout.jsonapi' text/html: schema: $ref: '#/components/schemas/ColumnLayout-read' @@ -27166,7 +23037,7 @@ paths: $ref: '#/components/schemas/ColumnLayout.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read' + $ref: '#/components/schemas/ColumnLayout.jsonapi' text/html: schema: $ref: '#/components/schemas/ColumnLayout-read' @@ -27201,7 +23072,7 @@ paths: $ref: '#/components/schemas/ColumnLayout-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/ColumnLayout.jsonapi-write_update' + $ref: '#/components/schemas/ColumnLayout.jsonapi' description: 'The updated ColumnLayout resource' required: true responses: @@ -27218,7 +23089,7 @@ paths: $ref: '#/components/schemas/ColumnLayout.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ColumnLayout.jsonapi-read' + $ref: '#/components/schemas/ColumnLayout.jsonapi' text/html: schema: $ref: '#/components/schemas/ColumnLayout-read' @@ -27335,7 +23206,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/MaterialNode.jsonapi-read' + $ref: '#/components/schemas/MaterialNode.jsonapi' type: array text/html: schema: @@ -27365,7 +23236,7 @@ paths: $ref: '#/components/schemas/MaterialNode.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialNode.jsonapi-write_create' + $ref: '#/components/schemas/MaterialNode.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialNode-write_create' @@ -27385,7 +23256,7 @@ paths: $ref: '#/components/schemas/MaterialNode.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialNode.jsonapi-read' + $ref: '#/components/schemas/MaterialNode.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialNode-read' @@ -27455,7 +23326,7 @@ paths: $ref: '#/components/schemas/MaterialNode.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialNode.jsonapi-read' + $ref: '#/components/schemas/MaterialNode.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialNode-read' @@ -27490,7 +23361,7 @@ paths: $ref: '#/components/schemas/MaterialNode-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialNode.jsonapi-write_update' + $ref: '#/components/schemas/MaterialNode.jsonapi' description: 'The updated MaterialNode resource' required: true responses: @@ -27507,7 +23378,7 @@ paths: $ref: '#/components/schemas/MaterialNode.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialNode.jsonapi-read' + $ref: '#/components/schemas/MaterialNode.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialNode-read' @@ -27624,7 +23495,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/MultiSelect.jsonapi-read' + $ref: '#/components/schemas/MultiSelect.jsonapi' type: array text/html: schema: @@ -27654,7 +23525,7 @@ paths: $ref: '#/components/schemas/MultiSelect.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/MultiSelect.jsonapi-write_create' + $ref: '#/components/schemas/MultiSelect.jsonapi' text/html: schema: $ref: '#/components/schemas/MultiSelect-write_create' @@ -27674,7 +23545,7 @@ paths: $ref: '#/components/schemas/MultiSelect.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MultiSelect.jsonapi-read' + $ref: '#/components/schemas/MultiSelect.jsonapi' text/html: schema: $ref: '#/components/schemas/MultiSelect-read' @@ -27744,7 +23615,7 @@ paths: $ref: '#/components/schemas/MultiSelect.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MultiSelect.jsonapi-read' + $ref: '#/components/schemas/MultiSelect.jsonapi' text/html: schema: $ref: '#/components/schemas/MultiSelect-read' @@ -27779,7 +23650,7 @@ paths: $ref: '#/components/schemas/MultiSelect-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/MultiSelect.jsonapi-write_update' + $ref: '#/components/schemas/MultiSelect.jsonapi' description: 'The updated MultiSelect resource' required: true responses: @@ -27796,7 +23667,7 @@ paths: $ref: '#/components/schemas/MultiSelect.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MultiSelect.jsonapi-read' + $ref: '#/components/schemas/MultiSelect.jsonapi' text/html: schema: $ref: '#/components/schemas/MultiSelect-read' @@ -27913,7 +23784,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/ResponsiveLayout.jsonapi-read' + $ref: '#/components/schemas/ResponsiveLayout.jsonapi' type: array text/html: schema: @@ -27943,7 +23814,7 @@ paths: $ref: '#/components/schemas/ResponsiveLayout.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/ResponsiveLayout.jsonapi-write_create' + $ref: '#/components/schemas/ResponsiveLayout.jsonapi' text/html: schema: $ref: '#/components/schemas/ResponsiveLayout-write_create' @@ -27963,7 +23834,7 @@ paths: $ref: '#/components/schemas/ResponsiveLayout.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ResponsiveLayout.jsonapi-read' + $ref: '#/components/schemas/ResponsiveLayout.jsonapi' text/html: schema: $ref: '#/components/schemas/ResponsiveLayout-read' @@ -28033,7 +23904,7 @@ paths: $ref: '#/components/schemas/ResponsiveLayout.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ResponsiveLayout.jsonapi-read' + $ref: '#/components/schemas/ResponsiveLayout.jsonapi' text/html: schema: $ref: '#/components/schemas/ResponsiveLayout-read' @@ -28068,7 +23939,7 @@ paths: $ref: '#/components/schemas/ResponsiveLayout-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/ResponsiveLayout.jsonapi-write_update' + $ref: '#/components/schemas/ResponsiveLayout.jsonapi' description: 'The updated ResponsiveLayout resource' required: true responses: @@ -28085,7 +23956,7 @@ paths: $ref: '#/components/schemas/ResponsiveLayout.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ResponsiveLayout.jsonapi-read' + $ref: '#/components/schemas/ResponsiveLayout.jsonapi' text/html: schema: $ref: '#/components/schemas/ResponsiveLayout-read' @@ -28202,7 +24073,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/SingleText.jsonapi-read' + $ref: '#/components/schemas/SingleText.jsonapi' type: array text/html: schema: @@ -28232,7 +24103,7 @@ paths: $ref: '#/components/schemas/SingleText.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/SingleText.jsonapi-write_create' + $ref: '#/components/schemas/SingleText.jsonapi' text/html: schema: $ref: '#/components/schemas/SingleText-write_create' @@ -28252,7 +24123,7 @@ paths: $ref: '#/components/schemas/SingleText.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/SingleText.jsonapi-read' + $ref: '#/components/schemas/SingleText.jsonapi' text/html: schema: $ref: '#/components/schemas/SingleText-read' @@ -28322,7 +24193,7 @@ paths: $ref: '#/components/schemas/SingleText.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/SingleText.jsonapi-read' + $ref: '#/components/schemas/SingleText.jsonapi' text/html: schema: $ref: '#/components/schemas/SingleText-read' @@ -28357,7 +24228,7 @@ paths: $ref: '#/components/schemas/SingleText-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/SingleText.jsonapi-write_update' + $ref: '#/components/schemas/SingleText.jsonapi' description: 'The updated SingleText resource' required: true responses: @@ -28374,7 +24245,7 @@ paths: $ref: '#/components/schemas/SingleText.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/SingleText.jsonapi-read' + $ref: '#/components/schemas/SingleText.jsonapi' text/html: schema: $ref: '#/components/schemas/SingleText-read' @@ -28491,7 +24362,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/Storyboard.jsonapi-read' + $ref: '#/components/schemas/Storyboard.jsonapi' type: array text/html: schema: @@ -28521,7 +24392,7 @@ paths: $ref: '#/components/schemas/Storyboard.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/Storyboard.jsonapi-write_create' + $ref: '#/components/schemas/Storyboard.jsonapi' text/html: schema: $ref: '#/components/schemas/Storyboard-write_create' @@ -28541,7 +24412,7 @@ paths: $ref: '#/components/schemas/Storyboard.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Storyboard.jsonapi-read' + $ref: '#/components/schemas/Storyboard.jsonapi' text/html: schema: $ref: '#/components/schemas/Storyboard-read' @@ -28611,7 +24482,7 @@ paths: $ref: '#/components/schemas/Storyboard.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Storyboard.jsonapi-read' + $ref: '#/components/schemas/Storyboard.jsonapi' text/html: schema: $ref: '#/components/schemas/Storyboard-read' @@ -28646,7 +24517,7 @@ paths: $ref: '#/components/schemas/Storyboard-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/Storyboard.jsonapi-write_update' + $ref: '#/components/schemas/Storyboard.jsonapi' description: 'The updated Storyboard resource' required: true responses: @@ -28663,7 +24534,7 @@ paths: $ref: '#/components/schemas/Storyboard.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Storyboard.jsonapi-read' + $ref: '#/components/schemas/Storyboard.jsonapi' text/html: schema: $ref: '#/components/schemas/Storyboard-read' @@ -28780,7 +24651,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/ContentNode.jsonapi-read' + $ref: '#/components/schemas/ContentNode.jsonapi' type: array text/html: schema: @@ -28856,7 +24727,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/ContentType.jsonapi-read' + $ref: '#/components/schemas/ContentType.jsonapi' type: array text/html: schema: @@ -28900,7 +24771,7 @@ paths: $ref: '#/components/schemas/ContentType.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/ContentType.jsonapi-read' + $ref: '#/components/schemas/ContentType.jsonapi' text/html: schema: $ref: '#/components/schemas/ContentType-read' @@ -29001,7 +24872,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/DayResponsible.jsonapi-read' + $ref: '#/components/schemas/DayResponsible.jsonapi' type: array text/html: schema: @@ -29031,7 +24902,7 @@ paths: $ref: '#/components/schemas/DayResponsible.jsonld-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/DayResponsible.jsonapi-write' + $ref: '#/components/schemas/DayResponsible.jsonapi' text/html: schema: $ref: '#/components/schemas/DayResponsible-write' @@ -29051,7 +24922,7 @@ paths: $ref: '#/components/schemas/DayResponsible.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/DayResponsible.jsonapi-read' + $ref: '#/components/schemas/DayResponsible.jsonapi' text/html: schema: $ref: '#/components/schemas/DayResponsible-read' @@ -29121,7 +24992,7 @@ paths: $ref: '#/components/schemas/DayResponsible.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/DayResponsible.jsonapi-read' + $ref: '#/components/schemas/DayResponsible.jsonapi' text/html: schema: $ref: '#/components/schemas/DayResponsible-read' @@ -29222,7 +25093,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/Day.jsonapi-read_Day.DayResponsibles' + $ref: '#/components/schemas/Day.jsonapi' type: array text/html: schema: @@ -29266,7 +25137,7 @@ paths: $ref: '#/components/schemas/Day.jsonld-read_Day.DayResponsibles' application/vnd.api+json: schema: - $ref: '#/components/schemas/Day.jsonapi-read_Day.DayResponsibles' + $ref: '#/components/schemas/Day.jsonapi' text/html: schema: $ref: '#/components/schemas/Day-read_Day.DayResponsibles' @@ -29303,7 +25174,7 @@ paths: $ref: '#/components/schemas/Invitation-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/Invitation.jsonapi-write' + $ref: '#/components/schemas/Invitation.jsonapi' description: 'The updated Invitation resource' required: true responses: @@ -29320,7 +25191,7 @@ paths: $ref: '#/components/schemas/Invitation.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Invitation.jsonapi-read' + $ref: '#/components/schemas/Invitation.jsonapi' text/html: schema: $ref: '#/components/schemas/Invitation-read' @@ -29367,7 +25238,7 @@ paths: $ref: '#/components/schemas/Invitation.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Invitation.jsonapi-read' + $ref: '#/components/schemas/Invitation.jsonapi' text/html: schema: $ref: '#/components/schemas/Invitation-read' @@ -29404,7 +25275,7 @@ paths: $ref: '#/components/schemas/Invitation-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/Invitation.jsonapi-write' + $ref: '#/components/schemas/Invitation.jsonapi' description: 'The updated Invitation resource' required: true responses: @@ -29421,7 +25292,7 @@ paths: $ref: '#/components/schemas/Invitation.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Invitation.jsonapi-read' + $ref: '#/components/schemas/Invitation.jsonapi' text/html: schema: $ref: '#/components/schemas/Invitation-read' @@ -29538,7 +25409,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/MaterialItem.jsonapi-read' + $ref: '#/components/schemas/MaterialItem.jsonapi' type: array text/html: schema: @@ -29568,7 +25439,7 @@ paths: $ref: '#/components/schemas/MaterialItem.jsonld-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialItem.jsonapi-write' + $ref: '#/components/schemas/MaterialItem.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialItem-write' @@ -29588,7 +25459,7 @@ paths: $ref: '#/components/schemas/MaterialItem.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialItem.jsonapi-read' + $ref: '#/components/schemas/MaterialItem.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialItem-read' @@ -29658,7 +25529,7 @@ paths: $ref: '#/components/schemas/MaterialItem.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialItem.jsonapi-read' + $ref: '#/components/schemas/MaterialItem.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialItem-read' @@ -29693,7 +25564,7 @@ paths: $ref: '#/components/schemas/MaterialItem-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialItem.jsonapi-write' + $ref: '#/components/schemas/MaterialItem.jsonapi' description: 'The updated MaterialItem resource' required: true responses: @@ -29710,7 +25581,7 @@ paths: $ref: '#/components/schemas/MaterialItem.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialItem.jsonapi-read' + $ref: '#/components/schemas/MaterialItem.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialItem-read' @@ -29789,7 +25660,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/MaterialList.jsonapi-read' + $ref: '#/components/schemas/MaterialList.jsonapi' type: array text/html: schema: @@ -29819,7 +25690,7 @@ paths: $ref: '#/components/schemas/MaterialList.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialList.jsonapi-write_create' + $ref: '#/components/schemas/MaterialList.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialList-write_create' @@ -29839,7 +25710,7 @@ paths: $ref: '#/components/schemas/MaterialList.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialList.jsonapi-read' + $ref: '#/components/schemas/MaterialList.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialList-read' @@ -29909,7 +25780,7 @@ paths: $ref: '#/components/schemas/MaterialList.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialList.jsonapi-read' + $ref: '#/components/schemas/MaterialList.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialList-read' @@ -29944,7 +25815,7 @@ paths: $ref: '#/components/schemas/MaterialList-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialList.jsonapi-write' + $ref: '#/components/schemas/MaterialList.jsonapi' description: 'The updated MaterialList resource' required: true responses: @@ -29961,7 +25832,7 @@ paths: $ref: '#/components/schemas/MaterialList.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/MaterialList.jsonapi-read' + $ref: '#/components/schemas/MaterialList.jsonapi' text/html: schema: $ref: '#/components/schemas/MaterialList-read' @@ -30040,7 +25911,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/Period.jsonapi-read' + $ref: '#/components/schemas/Period.jsonapi' type: array text/html: schema: @@ -30070,7 +25941,7 @@ paths: $ref: '#/components/schemas/Period.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/Period.jsonapi-write_create' + $ref: '#/components/schemas/Period.jsonapi' text/html: schema: $ref: '#/components/schemas/Period-write_create' @@ -30090,7 +25961,7 @@ paths: $ref: '#/components/schemas/Period.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Period.jsonapi-read' + $ref: '#/components/schemas/Period.jsonapi' text/html: schema: $ref: '#/components/schemas/Period-read' @@ -30160,7 +26031,7 @@ paths: $ref: '#/components/schemas/Period.jsonld-read_Period.Camp_Period.Days' application/vnd.api+json: schema: - $ref: '#/components/schemas/Period.jsonapi-read_Period.Camp_Period.Days' + $ref: '#/components/schemas/Period.jsonapi' text/html: schema: $ref: '#/components/schemas/Period-read_Period.Camp_Period.Days' @@ -30195,7 +26066,7 @@ paths: $ref: '#/components/schemas/Period-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/Period.jsonapi-write' + $ref: '#/components/schemas/Period.jsonapi' description: 'The updated Period resource' required: true responses: @@ -30212,7 +26083,7 @@ paths: $ref: '#/components/schemas/Period.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Period.jsonapi-read' + $ref: '#/components/schemas/Period.jsonapi' text/html: schema: $ref: '#/components/schemas/Period-read' @@ -30265,7 +26136,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/PersonalInvitation.jsonapi-read' + $ref: '#/components/schemas/PersonalInvitation.jsonapi' type: array text/html: schema: @@ -30309,7 +26180,7 @@ paths: $ref: '#/components/schemas/PersonalInvitation.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/PersonalInvitation.jsonapi-read' + $ref: '#/components/schemas/PersonalInvitation.jsonapi' text/html: schema: $ref: '#/components/schemas/PersonalInvitation-read' @@ -30346,7 +26217,7 @@ paths: $ref: '#/components/schemas/PersonalInvitation-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/PersonalInvitation.jsonapi-write' + $ref: '#/components/schemas/PersonalInvitation.jsonapi' description: 'The updated PersonalInvitation resource' required: true responses: @@ -30363,7 +26234,7 @@ paths: $ref: '#/components/schemas/PersonalInvitation.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/PersonalInvitation.jsonapi-read' + $ref: '#/components/schemas/PersonalInvitation.jsonapi' text/html: schema: $ref: '#/components/schemas/PersonalInvitation-read' @@ -30404,7 +26275,7 @@ paths: $ref: '#/components/schemas/PersonalInvitation-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/PersonalInvitation.jsonapi-write' + $ref: '#/components/schemas/PersonalInvitation.jsonapi' description: 'The updated PersonalInvitation resource' required: true responses: @@ -30421,7 +26292,7 @@ paths: $ref: '#/components/schemas/PersonalInvitation.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/PersonalInvitation.jsonapi-read' + $ref: '#/components/schemas/PersonalInvitation.jsonapi' text/html: schema: $ref: '#/components/schemas/PersonalInvitation-read' @@ -30500,7 +26371,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/Profile.jsonapi-read' + $ref: '#/components/schemas/Profile.jsonapi' type: array text/html: schema: @@ -30544,7 +26415,7 @@ paths: $ref: '#/components/schemas/Profile.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Profile.jsonapi-read' + $ref: '#/components/schemas/Profile.jsonapi' text/html: schema: $ref: '#/components/schemas/Profile-read' @@ -30579,7 +26450,7 @@ paths: $ref: '#/components/schemas/Profile-write_update' application/vnd.api+json: schema: - $ref: '#/components/schemas/Profile.jsonapi-write_update' + $ref: '#/components/schemas/Profile.jsonapi' description: 'The updated Profile resource' required: true responses: @@ -30596,7 +26467,7 @@ paths: $ref: '#/components/schemas/Profile.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/Profile.jsonapi-read' + $ref: '#/components/schemas/Profile.jsonapi' text/html: schema: $ref: '#/components/schemas/Profile-read' @@ -30797,7 +26668,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/ScheduleEntry.jsonapi-read' + $ref: '#/components/schemas/ScheduleEntry.jsonapi' type: array text/html: schema: @@ -30827,7 +26698,7 @@ paths: $ref: '#/components/schemas/ScheduleEntry.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/ScheduleEntry.jsonapi-write_create' + $ref: '#/components/schemas/ScheduleEntry.jsonapi' text/html: schema: $ref: '#/components/schemas/ScheduleEntry-write_create' @@ -30847,7 +26718,7 @@ paths: $ref: '#/components/schemas/ScheduleEntry.jsonld-read_ScheduleEntry.Activity' application/vnd.api+json: schema: - $ref: '#/components/schemas/ScheduleEntry.jsonapi-read_ScheduleEntry.Activity' + $ref: '#/components/schemas/ScheduleEntry.jsonapi' text/html: schema: $ref: '#/components/schemas/ScheduleEntry-read_ScheduleEntry.Activity' @@ -30917,7 +26788,7 @@ paths: $ref: '#/components/schemas/ScheduleEntry.jsonld-read_ScheduleEntry.Activity' application/vnd.api+json: schema: - $ref: '#/components/schemas/ScheduleEntry.jsonapi-read_ScheduleEntry.Activity' + $ref: '#/components/schemas/ScheduleEntry.jsonapi' text/html: schema: $ref: '#/components/schemas/ScheduleEntry-read_ScheduleEntry.Activity' @@ -30952,7 +26823,7 @@ paths: $ref: '#/components/schemas/ScheduleEntry-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/ScheduleEntry.jsonapi-write' + $ref: '#/components/schemas/ScheduleEntry.jsonapi' description: 'The updated ScheduleEntry resource' required: true responses: @@ -30969,7 +26840,7 @@ paths: $ref: '#/components/schemas/ScheduleEntry.jsonld-read_ScheduleEntry.Activity' application/vnd.api+json: schema: - $ref: '#/components/schemas/ScheduleEntry.jsonapi-read_ScheduleEntry.Activity' + $ref: '#/components/schemas/ScheduleEntry.jsonapi' text/html: schema: $ref: '#/components/schemas/ScheduleEntry-read_ScheduleEntry.Activity' @@ -31022,7 +26893,7 @@ paths: application/vnd.api+json: schema: items: - $ref: '#/components/schemas/User.jsonapi-read' + $ref: '#/components/schemas/User.jsonapi' type: array text/html: schema: @@ -31052,7 +26923,7 @@ paths: $ref: '#/components/schemas/User.jsonld-write_create' application/vnd.api+json: schema: - $ref: '#/components/schemas/User.jsonapi-write_create' + $ref: '#/components/schemas/User.jsonapi' text/html: schema: $ref: '#/components/schemas/User-write_create' @@ -31072,7 +26943,7 @@ paths: $ref: '#/components/schemas/User.jsonld-read_User.create' application/vnd.api+json: schema: - $ref: '#/components/schemas/User.jsonapi-read_User.create' + $ref: '#/components/schemas/User.jsonapi' text/html: schema: $ref: '#/components/schemas/User-read_User.create' @@ -31142,7 +27013,7 @@ paths: $ref: '#/components/schemas/User.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/User.jsonapi-read' + $ref: '#/components/schemas/User.jsonapi' text/html: schema: $ref: '#/components/schemas/User-read' @@ -31177,7 +27048,7 @@ paths: $ref: '#/components/schemas/User-write' application/vnd.api+json: schema: - $ref: '#/components/schemas/User.jsonapi-write' + $ref: '#/components/schemas/User.jsonapi' description: 'The updated User resource' required: true responses: @@ -31194,7 +27065,7 @@ paths: $ref: '#/components/schemas/User.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/User.jsonapi-read' + $ref: '#/components/schemas/User.jsonapi' text/html: schema: $ref: '#/components/schemas/User-read' @@ -31235,7 +27106,7 @@ paths: $ref: '#/components/schemas/User-activate' application/vnd.api+json: schema: - $ref: '#/components/schemas/User.jsonapi-activate' + $ref: '#/components/schemas/User.jsonapi' description: 'The updated User resource' required: true responses: @@ -31252,7 +27123,7 @@ paths: $ref: '#/components/schemas/User.jsonld-read' application/vnd.api+json: schema: - $ref: '#/components/schemas/User.jsonapi-read' + $ref: '#/components/schemas/User.jsonapi' text/html: schema: $ref: '#/components/schemas/User-read' @@ -31273,3 +27144,4 @@ servers: description: '' url: / tags: [] +webhooks: [] From 1aaab392cc0f4ede34b0f3e788b841f12f08cf26 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 08:14:57 +0200 Subject: [PATCH 59/81] take over changes from upstream PurgeHttpCacheListener (compatibility with doctrine/orm v3) --- api/src/HttpCache/PurgeHttpCacheListener.php | 18 ++++++++++++++++-- .../HttpCache/PurgeHttpCacheListenerTest.php | 4 ++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index 39b0da0a04..f4f1c74419 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -30,6 +30,7 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; use Doctrine\ORM\Event\PreUpdateEventArgs; +use Doctrine\ORM\Mapping\AssociationMapping; use Doctrine\ORM\PersistentCollection; use FOS\HttpCacheBundle\CacheManager; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -123,8 +124,21 @@ private function gatherResourceTags(object $entity): void { private function gatherRelationTags(EntityManagerInterface $em, object $entity): void { $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings(); - foreach ($associationMappings as $property => $mappings) { - $relatedProperty = $mappings['isOwningSide'] ? $mappings['inversedBy'] : $mappings['mappedBy']; + + foreach ($associationMappings as $property => $associationMapping) { + // @phpstan-ignore-next-line + if ($associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) { + return; + } + + // @phpstan-ignore-next-line + if (\is_array($associationMapping) + && \array_key_exists('targetEntity', $associationMapping) + && !$this->resourceClassResolver->isResourceClass($associationMapping['targetEntity'])) { + return; + } + + $relatedProperty = $associationMapping['isOwningSide'] ? $associationMapping['inversedBy'] : $associationMapping['mappedBy']; if (!$relatedProperty) { continue; } diff --git a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php index 4b3932eda1..6de4b1d3ad 100644 --- a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php +++ b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php @@ -230,8 +230,8 @@ public function testNotAResourceClass(): void { $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'notAResource')->willReturn(true); $propertyAccessorProphecy->isReadable(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->willReturn(true); - $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'notAResource')->shouldBeCalled()->willReturn($nonResource); - $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldBeCalled()->willReturn([$nonResource]); + $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'notAResource')->shouldNotBeCalled(); + $propertyAccessorProphecy->getValue(Argument::type(ContainNonResource::class), 'collectionOfNotAResource')->shouldNotBeCalled(); $listener = new PurgeHttpCacheListener($iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), $metadataFactoryProphecy->reveal(), $cacheManagerProphecy->reveal()); $listener->onFlush($eventArgs); From 75ad2285a1fa3314f61b0ac1659988887ef730e5 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 10:09:50 +0200 Subject: [PATCH 60/81] extend PurgeHttpCacheListenerTest --- .../HttpCache/PurgeHttpCacheListenerTest.php | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php index 6de4b1d3ad..b4509d3a40 100644 --- a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php +++ b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php @@ -38,6 +38,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; +use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; /** @@ -48,6 +49,61 @@ class PurgeHttpCacheListenerTest extends TestCase { use ProphecyTrait; + private ObjectProphecy $cacheManagerProphecy; + private ObjectProphecy $resourceClassResolverProphecy; + private ObjectProphecy $uowProphecy; + private ObjectProphecy $emProphecy; + private ObjectProphecy $propertyAccessorProphecy; + private ObjectProphecy $iriConverterProphecy; + private ObjectProphecy $metadataFactoryProphecy; + + protected function setUp(): void { + $this->cacheManagerProphecy = $this->prophesize(CacheManager::class); + $this->cacheManagerProphecy->flush(Argument::any())->willReturn(0); + + $this->resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $this->resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true); + $this->resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class); + + $this->uowProphecy = $this->prophesize(UnitOfWork::class); + + $this->emProphecy = $this->prophesize(EntityManagerInterface::class); + $this->emProphecy->getUnitOfWork()->willReturn($this->uowProphecy->reveal()); + $dummyClassMetadata = new ClassMetadata(Dummy::class); + $dummyClassMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']); + $dummyClassMetadata->mapOneToOne(['fieldName' => 'relatedOwningDummy', 'targetEntity' => RelatedOwningDummy::class, 'inversedBy' => 'ownedDummy']); + + $this->emProphecy->getClassMetadata(Dummy::class)->willReturn($dummyClassMetadata); + + $this->propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $this->propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedDummy')->willReturn(true); + $this->propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(false); + $this->propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedDummy')->willReturn(null); + $this->propertyAccessorProphecy->getValue(Argument::type(Dummy::class), 'relatedOwningDummy')->willReturn(null); + + $this->metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $operation = (new GetCollection())->withShortName('Dummy')->withClass(Dummy::class); + $operation2 = (new GetCollection())->withShortName('DummyAsSubresource')->withClass(Dummy::class); + $this->metadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [ + (new ApiResource('Dummy')) + ->withShortName('Dummy') + ->withOperations(new Operations([ + 'get_collection' => $operation, + 'related_dummies/{id}/dummmies_get_collection' => $operation2, + ])), + ])); + + $this->iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $this->iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, $operation)->willReturn('/dummies'); + $this->iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class), UrlGeneratorInterface::ABS_PATH, $operation2)->will(function ($args) { return '/related_dummies/'.$args[0]->getRelatedDummy()->getId().'/dummies'; }); + $this->iriConverterProphecy->getIriFromResource(Argument::type(Dummy::class))->will(function ($args) { return '/dummies/'.$args[0]->getId(); }); + } + + /** + * the following tests are copied from upstream PurgeHttpCacheListenerTest + * only adjusted to passt the tests with adjusted logic from PurgeHttpCacheListener. + * Other than that, kept changes to a minimum, in order to simplify copying changes to upstream test. + */ public function testOnFlush(): void { $toInsert1 = new Dummy(); $toInsert2 = new Dummy(); @@ -236,4 +292,50 @@ public function testNotAResourceClass(): void { $listener = new PurgeHttpCacheListener($iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), $metadataFactoryProphecy->reveal(), $cacheManagerProphecy->reveal()); $listener->onFlush($eventArgs); } + + /** + * the following tests are additional tests, created to test specific new behavior of PurgeHttpCacheListener. + */ + public function testInsertingShouldPurgeSubresourceCollections(): void { + // given + $toInsert1 = new Dummy(); + $toInsert1->setId(1); + $relatedDummy = new RelatedDummy(); + $relatedDummy->setId(100); + $toInsert1->setRelatedDummy($relatedDummy); + + $this->uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1]); + $this->uowProphecy->getScheduledEntityDeletions()->willReturn([]); + + // then + $this->cacheManagerProphecy->invalidateTags(['/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + $this->cacheManagerProphecy->invalidateTags(['/related_dummies/100/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + + // when + $listener = new PurgeHttpCacheListener($this->iriConverterProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->propertyAccessorProphecy->reveal(), $this->metadataFactoryProphecy->reveal(), $this->cacheManagerProphecy->reveal()); + $listener->onFlush(new OnFlushEventArgs($this->emProphecy->reveal())); + $listener->postFlush(); + } + + public function testDeleteShouldPurgeSubresourceCollections(): void { + // given + $toDelete1 = new Dummy(); + $toDelete1->setId(1); + $relatedDummy = new RelatedDummy(); + $relatedDummy->setId(100); + $toDelete1->setRelatedDummy($relatedDummy); + + $this->uowProphecy->getScheduledEntityInsertions()->willReturn([]); + $this->uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete1]); + + // then + $this->cacheManagerProphecy->invalidateTags(['/dummies/1'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + $this->cacheManagerProphecy->invalidateTags(['/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + $this->cacheManagerProphecy->invalidateTags(['/related_dummies/100/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + + // when + $listener = new PurgeHttpCacheListener($this->iriConverterProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->propertyAccessorProphecy->reveal(), $this->metadataFactoryProphecy->reveal(), $this->cacheManagerProphecy->reveal()); + $listener->onFlush(new OnFlushEventArgs($this->emProphecy->reveal())); + $listener->postFlush(); + } } From 4d8c949e5a118051d8191e95386e24e69bc5be51 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 10:51:52 +0200 Subject: [PATCH 61/81] add tests for TagCollector --- api/src/Entity/BaseEntity.php | 2 +- api/src/Entity/HasId.php | 7 + api/src/HttpCache/TagCollector.php | 4 +- api/tests/HttpCache/Entity/BaseEntity.php | 13 +- .../HttpCache/PurgeHttpCacheListenerTest.php | 22 +-- api/tests/HttpCache/TagCollectorTest.php | 135 ++++++++++++++++++ 6 files changed, 163 insertions(+), 20 deletions(-) create mode 100644 api/src/Entity/HasId.php create mode 100644 api/tests/HttpCache/TagCollectorTest.php diff --git a/api/src/Entity/BaseEntity.php b/api/src/Entity/BaseEntity.php index 4d601bd499..58f4a8af42 100644 --- a/api/src/Entity/BaseEntity.php +++ b/api/src/Entity/BaseEntity.php @@ -11,7 +11,7 @@ #[ORM\MappedSuperclass] #[ORM\Index(columns: ['createTime'])] #[ORM\Index(columns: ['updateTime'])] -abstract class BaseEntity { +abstract class BaseEntity implements HasId { /** * An internal, unique, randomly generated identifier of this entity. */ diff --git a/api/src/Entity/HasId.php b/api/src/Entity/HasId.php new file mode 100644 index 0000000000..7bbf61c545 --- /dev/null +++ b/api/src/Entity/HasId.php @@ -0,0 +1,7 @@ +getId(); } diff --git a/api/tests/HttpCache/Entity/BaseEntity.php b/api/tests/HttpCache/Entity/BaseEntity.php index 453f3a6018..704fb88d62 100644 --- a/api/tests/HttpCache/Entity/BaseEntity.php +++ b/api/tests/HttpCache/Entity/BaseEntity.php @@ -4,22 +4,23 @@ namespace App\Tests\HttpCache\Entity; +use App\Entity\HasId; use Doctrine\ORM\Mapping as ORM; -abstract class BaseEntity { +abstract class BaseEntity implements HasId { /** - * @var null|int The id + * @var null|string The id */ - #[ORM\Column(type: 'integer', nullable: true)] + #[ORM\Column(type: 'string', nullable: true)] #[ORM\Id] #[ORM\GeneratedValue(strategy: 'AUTO')] - private $id; + private string $id; - public function getId() { + public function getId(): string { return $this->id; } - public function setId($id): void { + public function setId(string $id): void { $this->id = $id; } } diff --git a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php index b4509d3a40..fdee3d8b7c 100644 --- a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php +++ b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php @@ -109,12 +109,12 @@ public function testOnFlush(): void { $toInsert2 = new Dummy(); $toDelete1 = new Dummy(); - $toDelete1->setId(3); + $toDelete1->setId('3'); $toDelete2 = new Dummy(); - $toDelete2->setId(4); + $toDelete2->setId('4'); $toDeleteNoPurge = new DummyNoGetOperation(); - $toDeleteNoPurge->setId(5); + $toDeleteNoPurge->setId('5'); $cacheManagerProphecy = $this->prophesize(CacheManager::class); $cacheManagerProphecy->invalidateTags(['/dummies'])->willReturn($cacheManagerProphecy)->shouldBeCalled(); @@ -174,13 +174,13 @@ public function testOnFlush(): void { public function testPreUpdate(): void { $oldRelatedDummy = new RelatedDummy(); - $oldRelatedDummy->setId(1); + $oldRelatedDummy->setId('1'); $newRelatedDummy = new RelatedDummy(); - $newRelatedDummy->setId(2); + $newRelatedDummy->setId('2'); $dummy = new Dummy(); - $dummy->setId(1); + $dummy->setId('1'); $cacheManagerProphecy = $this->prophesize(CacheManager::class); $cacheManagerProphecy->invalidateTags(['/dummies/1'])->shouldBeCalled()->willReturn($cacheManagerProphecy); @@ -216,7 +216,7 @@ public function testPreUpdate(): void { public function testNothingToPurge(): void { $dummyNoGetOperation = new DummyNoGetOperation(); - $dummyNoGetOperation->setId(1); + $dummyNoGetOperation->setId('1'); $purgerProphecy = $this->prophesize(PurgerInterface::class); $purgerProphecy->purge([])->shouldNotBeCalled(); @@ -299,9 +299,9 @@ public function testNotAResourceClass(): void { public function testInsertingShouldPurgeSubresourceCollections(): void { // given $toInsert1 = new Dummy(); - $toInsert1->setId(1); + $toInsert1->setId('1'); $relatedDummy = new RelatedDummy(); - $relatedDummy->setId(100); + $relatedDummy->setId('100'); $toInsert1->setRelatedDummy($relatedDummy); $this->uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1]); @@ -320,9 +320,9 @@ public function testInsertingShouldPurgeSubresourceCollections(): void { public function testDeleteShouldPurgeSubresourceCollections(): void { // given $toDelete1 = new Dummy(); - $toDelete1->setId(1); + $toDelete1->setId('1'); $relatedDummy = new RelatedDummy(); - $relatedDummy->setId(100); + $relatedDummy->setId('100'); $toDelete1->setRelatedDummy($relatedDummy); $this->uowProphecy->getScheduledEntityInsertions()->willReturn([]); diff --git a/api/tests/HttpCache/TagCollectorTest.php b/api/tests/HttpCache/TagCollectorTest.php new file mode 100644 index 0000000000..a6a6cecabf --- /dev/null +++ b/api/tests/HttpCache/TagCollectorTest.php @@ -0,0 +1,135 @@ +responseTaggerProphecy = $this->prophesize(ResponseTagger::class); + $this->tagCollector = new TagCollector($this->responseTaggerProphecy->reveal()); + } + + public function testNoTagForEmptyContext() { + // then + $this->responseTaggerProphecy->addTags(Argument::any())->shouldNotBeCalled(); + + // when + $this->tagCollector->collect([]); + } + + public function testWithIri() { + // then + $this->responseTaggerProphecy->addTags(['/test-iri'])->shouldBeCalled(); + + // when + $this->tagCollector->collect(['iri' => '/test-iri']); + } + + public function testWithBaseEntity() { + // given + $object = new Dummy(); + $object->setId('123'); + + // then + $this->responseTaggerProphecy->addTags(['123'])->shouldBeCalled(); + + // when + $this->tagCollector->collect(['iri' => '/dummy/123', 'object' => $object]); + } + + public function testWithRelation() { + // given + $object = new Dummy(); + $object->setId('123'); + + // then + $this->responseTaggerProphecy->addTags(['123#propertyName'])->shouldBeCalled(); + + // when + $this->tagCollector->collect([ + 'iri' => '/dummy/123', + 'object' => $object, + 'property_metadata' => new ApiProperty(), + 'api_attribute' => 'propertyName', + ]); + } + + public function testWithExtraCacheDependency() { + // given + $object = new Dummy(); + $object->setId('123'); + + // then + $this->responseTaggerProphecy->addTags(['123#PROPERTY_NAME'])->shouldBeCalled(); + $this->responseTaggerProphecy->addTags(['123#OTHER_DEPENDENCY'])->shouldBeCalled(); + + // when + $this->tagCollector->collect([ + 'iri' => '/dummy/123', + 'object' => $object, + 'property_metadata' => new ApiProperty( + extraProperties: [ + 'cacheDependencies' => ['PROPERTY_NAME', 'OTHER_DEPENDENCY'], + ] + ), + 'api_attribute' => 'propertyName', + ]); + } + + public function testNoTagForHalLinks() { + // then + $this->responseTaggerProphecy->addTags(Argument::any())->shouldNotBeCalled(); + + // when + $this->tagCollector->collect([ + 'iri' => '/test-iri', + 'format' => 'jsonhal', + 'data' => '/test-iri', + ]); + } + + public function testNoTagForJsonLdLinks() { + // then + $this->responseTaggerProphecy->addTags(Argument::any())->shouldNotBeCalled(); + + // when + $this->tagCollector->collect([ + 'iri' => '/test-iri', + 'format' => 'jsonld', + 'data' => '/test-iri', + ]); + } + + public function testNoTagForJsonApiLinks() { + // then + $this->responseTaggerProphecy->addTags(Argument::any())->shouldNotBeCalled(); + + // when + $this->tagCollector->collect([ + 'iri' => '/test-iri', + 'format' => 'jsonapi', + 'data' => [ + 'data' => [ + 'type' => 'dummy', + 'id' => '/test-iri', + ], + ], + ]); + } +} From 9ada6e8a714a7f8a65b2f1614419855b270ff19c Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 10:58:32 +0200 Subject: [PATCH 62/81] fix eslint --- .../responses/categories_collection.json | 492 +++++++++--------- .../responses/content_types_collection.json | 344 ++++++------ e2e/specs/responses/content_types_entity.json | 22 +- 3 files changed, 429 insertions(+), 429 deletions(-) diff --git a/e2e/specs/responses/categories_collection.json b/e2e/specs/responses/categories_collection.json index b03855db2b..a6cdad1075 100644 --- a/e2e/specs/responses/categories_collection.json +++ b/e2e/specs/responses/categories_collection.json @@ -1,254 +1,254 @@ { - "_links": { - "self": { - "href": "/api/camps/3c79b99ab424/categories.jsonhal" + "_links": { + "self": { + "href": "/api/camps/3c79b99ab424/categories.jsonhal" + }, + "items": [ + { + "href": "/api/categories/ebfd46a1c181" + }, + { + "href": "/api/categories/1a869b162875" + }, + { + "href": "/api/categories/dfa531302823" + }, + { + "href": "/api/categories/a023e85227ac" + } + ] + }, + "totalItems": 4, + "_embedded": { + "items": [ + { + "_links": { + "self": { + "href": "/api/categories/ebfd46a1c181" + }, + "camp": { + "href": "/api/camps/3c79b99ab424" + }, + "preferredContentTypes": { + "href": "/api/content_types?categories=%2Fapi%2Fcategories%2Febfd46a1c181" + }, + "rootContentNode": { + "href": "/api/content_node/column_layouts/9d7b3a220fb4" + }, + "contentNodes": { + "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2F9d7b3a220fb4" + } }, - "items": [ - { - "href": "/api/categories/ebfd46a1c181" + "_embedded": { + "rootContentNode": { + "_links": { + "self": { + "href": "/api/content_node/column_layouts/9d7b3a220fb4" + }, + "root": { + "href": "/api/content_node/column_layouts/9d7b3a220fb4" + }, + "parent": null, + "children": [], + "contentType": { + "href": "/api/content_types/f17470519474" + } }, - { - "href": "/api/categories/1a869b162875" + "data": { + "columns": [ + { + "slot": "1", + "width": 12 + } + ] }, - { - "href": "/api/categories/dfa531302823" + "slot": null, + "position": 0, + "instanceName": null, + "id": "9d7b3a220fb4", + "contentTypeName": "ColumnLayout" + } + }, + "short": "ES", + "name": "Essen", + "color": "#BBBBBB", + "numberingStyle": "-", + "id": "ebfd46a1c181" + }, + { + "_links": { + "self": { + "href": "/api/categories/1a869b162875" + }, + "camp": { + "href": "/api/camps/3c79b99ab424" + }, + "preferredContentTypes": { + "href": "/api/content_types?categories=%2Fapi%2Fcategories%2F1a869b162875" + }, + "rootContentNode": { + "href": "/api/content_node/column_layouts/be9b6b7f23f6" + }, + "contentNodes": { + "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2Fbe9b6b7f23f6" + } + }, + "_embedded": { + "rootContentNode": { + "_links": { + "self": { + "href": "/api/content_node/column_layouts/be9b6b7f23f6" + }, + "root": { + "href": "/api/content_node/column_layouts/be9b6b7f23f6" + }, + "parent": null, + "children": [ + { + "href": "/api/content_node/responsive_layouts/179ba93a4bb9" + } + ], + "contentType": { + "href": "/api/content_types/f17470519474" + } }, - { - "href": "/api/categories/a023e85227ac" - } - ] - }, - "totalItems": 4, - "_embedded": { - "items": [ - { - "_links": { - "self": { - "href": "/api/categories/ebfd46a1c181" - }, - "camp": { - "href": "/api/camps/3c79b99ab424" - }, - "preferredContentTypes": { - "href": "/api/content_types?categories=%2Fapi%2Fcategories%2Febfd46a1c181" - }, - "rootContentNode": { - "href": "/api/content_node/column_layouts/9d7b3a220fb4" - }, - "contentNodes": { - "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2F9d7b3a220fb4" - } - }, - "_embedded": { - "rootContentNode": { - "_links": { - "self": { - "href": "/api/content_node/column_layouts/9d7b3a220fb4" - }, - "root": { - "href": "/api/content_node/column_layouts/9d7b3a220fb4" - }, - "parent": null, - "children": [], - "contentType": { - "href": "/api/content_types/f17470519474" - } - }, - "data": { - "columns": [ - { - "slot": "1", - "width": 12 - } - ] - }, - "slot": null, - "position": 0, - "instanceName": null, - "id": "9d7b3a220fb4", - "contentTypeName": "ColumnLayout" - } - }, - "short": "ES", - "name": "Essen", - "color": "#BBBBBB", - "numberingStyle": "-", - "id": "ebfd46a1c181" + "data": { + "columns": [ + { + "slot": "1", + "width": 12 + } + ] }, - { - "_links": { - "self": { - "href": "/api/categories/1a869b162875" - }, - "camp": { - "href": "/api/camps/3c79b99ab424" - }, - "preferredContentTypes": { - "href": "/api/content_types?categories=%2Fapi%2Fcategories%2F1a869b162875" - }, - "rootContentNode": { - "href": "/api/content_node/column_layouts/be9b6b7f23f6" - }, - "contentNodes": { - "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2Fbe9b6b7f23f6" - } - }, - "_embedded": { - "rootContentNode": { - "_links": { - "self": { - "href": "/api/content_node/column_layouts/be9b6b7f23f6" - }, - "root": { - "href": "/api/content_node/column_layouts/be9b6b7f23f6" - }, - "parent": null, - "children": [ - { - "href": "/api/content_node/responsive_layouts/179ba93a4bb9" - } - ], - "contentType": { - "href": "/api/content_types/f17470519474" - } - }, - "data": { - "columns": [ - { - "slot": "1", - "width": 12 - } - ] - }, - "slot": null, - "position": 0, - "instanceName": null, - "id": "be9b6b7f23f6", - "contentTypeName": "ColumnLayout" - } - }, - "short": "LA", - "name": "Lageraktivität", - "color": "#FF9800", - "numberingStyle": "A", - "id": "1a869b162875" + "slot": null, + "position": 0, + "instanceName": null, + "id": "be9b6b7f23f6", + "contentTypeName": "ColumnLayout" + } + }, + "short": "LA", + "name": "Lageraktivität", + "color": "#FF9800", + "numberingStyle": "A", + "id": "1a869b162875" + }, + { + "_links": { + "self": { + "href": "/api/categories/dfa531302823" + }, + "camp": { + "href": "/api/camps/3c79b99ab424" + }, + "preferredContentTypes": { + "href": "/api/content_types?categories=%2Fapi%2Fcategories%2Fdfa531302823" + }, + "rootContentNode": { + "href": "/api/content_node/column_layouts/63cbc734fa04" + }, + "contentNodes": { + "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2F63cbc734fa04" + } + }, + "_embedded": { + "rootContentNode": { + "_links": { + "self": { + "href": "/api/content_node/column_layouts/63cbc734fa04" + }, + "root": { + "href": "/api/content_node/column_layouts/63cbc734fa04" + }, + "parent": null, + "children": [ + { + "href": "/api/content_node/responsive_layouts/801027c511e6" + } + ], + "contentType": { + "href": "/api/content_types/f17470519474" + } }, - { - "_links": { - "self": { - "href": "/api/categories/dfa531302823" - }, - "camp": { - "href": "/api/camps/3c79b99ab424" - }, - "preferredContentTypes": { - "href": "/api/content_types?categories=%2Fapi%2Fcategories%2Fdfa531302823" - }, - "rootContentNode": { - "href": "/api/content_node/column_layouts/63cbc734fa04" - }, - "contentNodes": { - "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2F63cbc734fa04" - } - }, - "_embedded": { - "rootContentNode": { - "_links": { - "self": { - "href": "/api/content_node/column_layouts/63cbc734fa04" - }, - "root": { - "href": "/api/content_node/column_layouts/63cbc734fa04" - }, - "parent": null, - "children": [ - { - "href": "/api/content_node/responsive_layouts/801027c511e6" - } - ], - "contentType": { - "href": "/api/content_types/f17470519474" - } - }, - "data": { - "columns": [ - { - "slot": "1", - "width": 12 - } - ] - }, - "slot": null, - "position": 0, - "instanceName": null, - "id": "63cbc734fa04", - "contentTypeName": "ColumnLayout" - } - }, - "short": "LP", - "name": "Lagerprogramm", - "color": "#90B7E4", - "numberingStyle": "1", - "id": "dfa531302823" + "data": { + "columns": [ + { + "slot": "1", + "width": 12 + } + ] }, - { - "_links": { - "self": { - "href": "/api/categories/a023e85227ac" - }, - "camp": { - "href": "/api/camps/3c79b99ab424" - }, - "preferredContentTypes": { - "href": "/api/content_types?categories=%2Fapi%2Fcategories%2Fa023e85227ac" - }, - "rootContentNode": { - "href": "/api/content_node/column_layouts/2cce9e17a368" - }, - "contentNodes": { - "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2F2cce9e17a368" - } - }, - "_embedded": { - "rootContentNode": { - "_links": { - "self": { - "href": "/api/content_node/column_layouts/2cce9e17a368" - }, - "root": { - "href": "/api/content_node/column_layouts/2cce9e17a368" - }, - "parent": null, - "children": [ - { - "href": "/api/content_node/responsive_layouts/80d79bc8f484" - } - ], - "contentType": { - "href": "/api/content_types/f17470519474" - } - }, - "data": { - "columns": [ - { - "slot": "1", - "width": 12 - } - ] - }, - "slot": null, - "position": 0, - "instanceName": "est", - "id": "2cce9e17a368", - "contentTypeName": "ColumnLayout" - } - }, - "short": "LS", - "name": "Lagersport", - "color": "#4DBB52", - "numberingStyle": "1", - "id": "a023e85227ac" - } - ] - } -} \ No newline at end of file + "slot": null, + "position": 0, + "instanceName": null, + "id": "63cbc734fa04", + "contentTypeName": "ColumnLayout" + } + }, + "short": "LP", + "name": "Lagerprogramm", + "color": "#90B7E4", + "numberingStyle": "1", + "id": "dfa531302823" + }, + { + "_links": { + "self": { + "href": "/api/categories/a023e85227ac" + }, + "camp": { + "href": "/api/camps/3c79b99ab424" + }, + "preferredContentTypes": { + "href": "/api/content_types?categories=%2Fapi%2Fcategories%2Fa023e85227ac" + }, + "rootContentNode": { + "href": "/api/content_node/column_layouts/2cce9e17a368" + }, + "contentNodes": { + "href": "/api/content_nodes?root=%2Fapi%2Fcontent_node%2Fcolumn_layouts%2F2cce9e17a368" + } + }, + "_embedded": { + "rootContentNode": { + "_links": { + "self": { + "href": "/api/content_node/column_layouts/2cce9e17a368" + }, + "root": { + "href": "/api/content_node/column_layouts/2cce9e17a368" + }, + "parent": null, + "children": [ + { + "href": "/api/content_node/responsive_layouts/80d79bc8f484" + } + ], + "contentType": { + "href": "/api/content_types/f17470519474" + } + }, + "data": { + "columns": [ + { + "slot": "1", + "width": 12 + } + ] + }, + "slot": null, + "position": 0, + "instanceName": "est", + "id": "2cce9e17a368", + "contentTypeName": "ColumnLayout" + } + }, + "short": "LS", + "name": "Lagersport", + "color": "#4DBB52", + "numberingStyle": "1", + "id": "a023e85227ac" + } + ] + } +} diff --git a/e2e/specs/responses/content_types_collection.json b/e2e/specs/responses/content_types_collection.json index a1361e57ab..f209e6a3b5 100644 --- a/e2e/specs/responses/content_types_collection.json +++ b/e2e/specs/responses/content_types_collection.json @@ -1,174 +1,174 @@ { - "_links": { - "self": { - "href": "/api/content_types.jsonhal" - }, - "items": [ - { - "href": "/api/content_types/c462edd869f3" - }, - { - "href": "/api/content_types/5e2028c55ee4" - }, - { - "href": "/api/content_types/a4211c112939" - }, - { - "href": "/api/content_types/f17470519474" - }, - { - "href": "/api/content_types/1a0f84e322c8" - }, - { - "href": "/api/content_types/3ef17bd1df72" - }, - { - "href": "/api/content_types/4f0c657fecef" - }, - { - "href": "/api/content_types/44dcc7493c65" - }, - { - "href": "/api/content_types/cfccaecd4bad" - }, - { - "href": "/api/content_types/318e064ea0c9" - } - ] + "_links": { + "self": { + "href": "/api/content_types.jsonhal" }, - "totalItems": 10, - "_embedded": { - "items": [ - { - "_links": { - "self": { - "href": "/api/content_types/c462edd869f3" - }, - "contentNodes": { - "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2Fc462edd869f3" - } - }, - "name": "LearningObjectives", - "active": true, - "id": "c462edd869f3" - }, - { - "_links": { - "self": { - "href": "/api/content_types/5e2028c55ee4" - }, - "contentNodes": { - "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F5e2028c55ee4" - } - }, - "name": "LearningTopics", - "active": true, - "id": "5e2028c55ee4" - }, - { - "_links": { - "self": { - "href": "/api/content_types/a4211c112939" - }, - "contentNodes": { - "href": "/api/content_node/responsive_layouts?contentType=%2Fapi%2Fcontent_types%2Fa4211c112939" - } - }, - "name": "ResponsiveLayout", - "active": true, - "id": "a4211c112939" - }, - { - "_links": { - "self": { - "href": "/api/content_types/f17470519474" - }, - "contentNodes": { - "href": "/api/content_node/column_layouts?contentType=%2Fapi%2Fcontent_types%2Ff17470519474" - } - }, - "name": "ColumnLayout", - "active": true, - "id": "f17470519474" - }, - { - "_links": { - "self": { - "href": "/api/content_types/1a0f84e322c8" - }, - "contentNodes": { - "href": "/api/content_node/multi_selects?contentType=%2Fapi%2Fcontent_types%2F1a0f84e322c8" - } - }, - "name": "LAThematicArea", - "active": true, - "id": "1a0f84e322c8" - }, - { - "_links": { - "self": { - "href": "/api/content_types/3ef17bd1df72" - }, - "contentNodes": { - "href": "/api/content_node/material_nodes?contentType=%2Fapi%2Fcontent_types%2F3ef17bd1df72" - } - }, - "name": "Material", - "active": true, - "id": "3ef17bd1df72" - }, - { - "_links": { - "self": { - "href": "/api/content_types/4f0c657fecef" - }, - "contentNodes": { - "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F4f0c657fecef" - } - }, - "name": "Notes", - "active": true, - "id": "4f0c657fecef" - }, - { - "_links": { - "self": { - "href": "/api/content_types/44dcc7493c65" - }, - "contentNodes": { - "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F44dcc7493c65" - } - }, - "name": "SafetyConcept", - "active": true, - "id": "44dcc7493c65" - }, - { - "_links": { - "self": { - "href": "/api/content_types/cfccaecd4bad" - }, - "contentNodes": { - "href": "/api/content_node/storyboards?contentType=%2Fapi%2Fcontent_types%2Fcfccaecd4bad" - } - }, - "name": "Storyboard", - "active": true, - "id": "cfccaecd4bad" - }, - { - "_links": { - "self": { - "href": "/api/content_types/318e064ea0c9" - }, - "contentNodes": { - "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F318e064ea0c9" - } - }, - "name": "Storycontext", - "active": true, - "id": "318e064ea0c9" - } - ] - } -} \ No newline at end of file + "items": [ + { + "href": "/api/content_types/c462edd869f3" + }, + { + "href": "/api/content_types/5e2028c55ee4" + }, + { + "href": "/api/content_types/a4211c112939" + }, + { + "href": "/api/content_types/f17470519474" + }, + { + "href": "/api/content_types/1a0f84e322c8" + }, + { + "href": "/api/content_types/3ef17bd1df72" + }, + { + "href": "/api/content_types/4f0c657fecef" + }, + { + "href": "/api/content_types/44dcc7493c65" + }, + { + "href": "/api/content_types/cfccaecd4bad" + }, + { + "href": "/api/content_types/318e064ea0c9" + } + ] + }, + "totalItems": 10, + "_embedded": { + "items": [ + { + "_links": { + "self": { + "href": "/api/content_types/c462edd869f3" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2Fc462edd869f3" + } + }, + "name": "LearningObjectives", + "active": true, + "id": "c462edd869f3" + }, + { + "_links": { + "self": { + "href": "/api/content_types/5e2028c55ee4" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F5e2028c55ee4" + } + }, + "name": "LearningTopics", + "active": true, + "id": "5e2028c55ee4" + }, + { + "_links": { + "self": { + "href": "/api/content_types/a4211c112939" + }, + "contentNodes": { + "href": "/api/content_node/responsive_layouts?contentType=%2Fapi%2Fcontent_types%2Fa4211c112939" + } + }, + "name": "ResponsiveLayout", + "active": true, + "id": "a4211c112939" + }, + { + "_links": { + "self": { + "href": "/api/content_types/f17470519474" + }, + "contentNodes": { + "href": "/api/content_node/column_layouts?contentType=%2Fapi%2Fcontent_types%2Ff17470519474" + } + }, + "name": "ColumnLayout", + "active": true, + "id": "f17470519474" + }, + { + "_links": { + "self": { + "href": "/api/content_types/1a0f84e322c8" + }, + "contentNodes": { + "href": "/api/content_node/multi_selects?contentType=%2Fapi%2Fcontent_types%2F1a0f84e322c8" + } + }, + "name": "LAThematicArea", + "active": true, + "id": "1a0f84e322c8" + }, + { + "_links": { + "self": { + "href": "/api/content_types/3ef17bd1df72" + }, + "contentNodes": { + "href": "/api/content_node/material_nodes?contentType=%2Fapi%2Fcontent_types%2F3ef17bd1df72" + } + }, + "name": "Material", + "active": true, + "id": "3ef17bd1df72" + }, + { + "_links": { + "self": { + "href": "/api/content_types/4f0c657fecef" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F4f0c657fecef" + } + }, + "name": "Notes", + "active": true, + "id": "4f0c657fecef" + }, + { + "_links": { + "self": { + "href": "/api/content_types/44dcc7493c65" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F44dcc7493c65" + } + }, + "name": "SafetyConcept", + "active": true, + "id": "44dcc7493c65" + }, + { + "_links": { + "self": { + "href": "/api/content_types/cfccaecd4bad" + }, + "contentNodes": { + "href": "/api/content_node/storyboards?contentType=%2Fapi%2Fcontent_types%2Fcfccaecd4bad" + } + }, + "name": "Storyboard", + "active": true, + "id": "cfccaecd4bad" + }, + { + "_links": { + "self": { + "href": "/api/content_types/318e064ea0c9" + }, + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F318e064ea0c9" + } + }, + "name": "Storycontext", + "active": true, + "id": "318e064ea0c9" + } + ] + } +} diff --git a/e2e/specs/responses/content_types_entity.json b/e2e/specs/responses/content_types_entity.json index bf5dcd4311..8e76a52dcd 100644 --- a/e2e/specs/responses/content_types_entity.json +++ b/e2e/specs/responses/content_types_entity.json @@ -1,13 +1,13 @@ { - "_links": { - "self": { - "href": "/api/content_types/318e064ea0c9" - }, - "contentNodes": { - "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F318e064ea0c9" - } + "_links": { + "self": { + "href": "/api/content_types/318e064ea0c9" }, - "name": "Storycontext", - "active": true, - "id": "318e064ea0c9" -} \ No newline at end of file + "contentNodes": { + "href": "/api/content_node/single_texts?contentType=%2Fapi%2Fcontent_types%2F318e064ea0c9" + } + }, + "name": "Storycontext", + "active": true, + "id": "318e064ea0c9" +} From 3e38717cf5b863fa25e1a8f908853cfa95418fb3 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 11:42:19 +0200 Subject: [PATCH 63/81] varnishncsa: also log backend errors --- .helm/ecamp3/templates/api_cache_deployment.yaml | 2 ++ .helm/ecamp3/values.yaml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.helm/ecamp3/templates/api_cache_deployment.yaml b/.helm/ecamp3/templates/api_cache_deployment.yaml index 79abc053d8..4fc2a179b2 100644 --- a/.helm/ecamp3/templates/api_cache_deployment.yaml +++ b/.helm/ecamp3/templates/api_cache_deployment.yaml @@ -67,6 +67,8 @@ spec: imagePullPolicy: {{ .Values.apiCache.image.pullPolicy }} command: - varnishncsa + - -b + - -c {{- if .Values.apiCache.logging.customOutputJsonFormat }} - -j {{- end }} diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index 1de067627d..feec7cf7ac 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -243,7 +243,7 @@ apiCache: memory: 100Mi logging: enabled: true - customOutput: '{ "received_at": "%t", "method": "%m", "url": "%U", "query": "%q", "response_bytes": %b, "time_taken": %D, "status": %s, "handling": "%{Varnish:handling}x" }' + customOutput: '{ "received_at": "%t", "varnish_side": "%{Varnish:side}x", "method": "%m", "url": "%U", "query": "%q", "response_bytes": %b, "time_taken": %D, "status": %s, "handling": "%{Varnish:handling}x", "response_reason": "%{VSL:RespReason}x", "fetch_error": "%{VSL:FetchError}x" }' customOutputJsonFormat: true # Timeout before returning error on initial VSM connection. # If set the VSM connection is retried every 0.5 seconds for this many seconds. From 4ace13cd5db67ac4d566bab874243725d6f2ff6e Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 11:48:29 +0200 Subject: [PATCH 64/81] fix psalm --- api/src/HttpCache/PurgeHttpCacheListener.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index f4f1c74419..80ba100372 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -122,12 +122,15 @@ private function gatherResourceTags(object $entity): void { } } + /** + * @psalm-suppress UndefinedClass + */ private function gatherRelationTags(EntityManagerInterface $em, object $entity): void { $associationMappings = $em->getClassMetadata(ClassUtils::getClass($entity))->getAssociationMappings(); foreach ($associationMappings as $property => $associationMapping) { // @phpstan-ignore-next-line - if ($associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) { + if (class_exists(AssociationMapping::class) && $associationMapping instanceof AssociationMapping && ($associationMapping->targetEntity ?? null) && !$this->resourceClassResolver->isResourceClass($associationMapping->targetEntity)) { return; } From eb4d98cb5f9b701fb9487dacdd788197cf1272f2 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 12:44:34 +0200 Subject: [PATCH 65/81] utilize ApiProperty uriTemplate --- api/src/Entity/Camp.php | 6 +- api/src/Entity/Category.php | 4 +- .../Repository/CanFilterByCampInterface.php | 10 -- .../RelatedCollectionLinkNormalizer.php | 21 +--- ...est__testOpenApiSpecMatchesSnapshot__1.yml | 108 ++++++------------ ...manceDidNotChangeForStableEndpoints__1.yml | 14 +-- 6 files changed, 53 insertions(+), 110 deletions(-) delete mode 100644 api/src/Repository/CanFilterByCampInterface.php diff --git a/api/src/Entity/Camp.php b/api/src/Entity/Camp.php index 272b5822c0..eea7559c5e 100644 --- a/api/src/Entity/Camp.php +++ b/api/src/Entity/Camp.php @@ -101,7 +101,11 @@ class Camp extends BaseEntity implements BelongsToCampInterface, CopyFromPrototy /** * Types of programme, such as sports activities or meal times. */ - #[ApiProperty(writable: false, example: '["/categories/1a2b3c4d"]')] + #[ApiProperty( + writable: false, + uriTemplate: Category::CAMP_SUBRESOURCE_URI_TEMPLATE, + example: '"/camp/1a2b3c4d/categories"' + )] #[Groups(['read'])] #[ORM\OneToMany(targetEntity: Category::class, mappedBy: 'camp', orphanRemoval: true, cascade: ['persist'])] public Collection $categories; diff --git a/api/src/Entity/Category.php b/api/src/Entity/Category.php index 720529ea4d..6d274dc3a0 100644 --- a/api/src/Entity/Category.php +++ b/api/src/Entity/Category.php @@ -57,7 +57,7 @@ ), new GetCollection( name: 'BelongsToCamp_App\Entity\Category_get_collection', - uriTemplate: '/camps/{campId}/categories.{_format}', + uriTemplate: self::CAMP_SUBRESOURCE_URI_TEMPLATE, uriVariables: [ 'campId' => new Link( fromClass: Camp::class, @@ -77,6 +77,8 @@ class Category extends BaseEntity implements BelongsToCampInterface, CopyFromPro use ClassInfoTrait; use HasRootContentNodeTrait; + public const CAMP_SUBRESOURCE_URI_TEMPLATE = '/camps/{campId}/categories.{_format}'; + public const ITEM_NORMALIZATION_CONTEXT = [ 'groups' => [ 'read', diff --git a/api/src/Repository/CanFilterByCampInterface.php b/api/src/Repository/CanFilterByCampInterface.php deleted file mode 100644 index ddf31aeb44..0000000000 --- a/api/src/Repository/CanFilterByCampInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -resourceMetadataCollectionFactory->create($relatedResourceClass); - - $operation = null; - $parameters[$relatedFilterName] = urlencode($this->iriConverter->getIriFromResource($object)); - - try { - if (is_a($relatedResourceClass, BelongsToCampInterface::class, true) && is_a($object, BelongsToCampInterface::class, true)) { - $operation = $resourceMetadataCollection->getOperation("BelongsToCamp_{$relatedResourceClass}_get_collection"); - $parameters['campId'] = $object->getCamp()->getId(); - $parameters['camp'] = null; - } - } catch (OperationNotFoundException $e) { - } finally { - if (!$operation) { - $operation = OperationHelper::findOneByType($resourceMetadataCollection, GetCollection::class); - } - } + $operation = OperationHelper::findOneByType($resourceMetadataCollection, GetCollection::class); if (!$operation) { throw new UnsupportedRelationException('The resource '.$relatedResourceClass.' does not implement GetCollection() operation.'); @@ -201,7 +184,7 @@ public function getRelatedCollectionHref($object, $rel, array $context = []): st throw new UnsupportedRelationException('The resource '.$relatedResourceClass.' does not have a search filter for the relation '.$relatedFilterName.'.'); } - return $this->router->generate($operation->getName(), $parameters, UrlGeneratorInterface::ABS_PATH); + return $this->router->generate($operation->getName(), [$relatedFilterName => urlencode($this->iriConverter->getIriFromResource($object))], UrlGeneratorInterface::ABS_PATH); } protected function getRelatedCollectionLinkAnnotation(string $className, string $propertyName): ?RelatedCollectionLink { diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml index 4d64b77728..7f14702125 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testOpenApiSpecMatchesSnapshot__1.yml @@ -2329,13 +2329,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -2525,13 +2522,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -2710,13 +2704,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -2902,13 +2893,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3501,13 +3489,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3706,13 +3691,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -3900,13 +3882,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4101,13 +4080,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4449,13 +4425,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4668,13 +4641,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -4876,13 +4846,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' @@ -5091,13 +5058,10 @@ components: type: array categories: description: 'Types of programme, such as sports activities or meal times.' - example: '["/categories/1a2b3c4d"]' - items: - example: 'https://example.com/' - format: iri-reference - type: string + example: '"/camp/1a2b3c4d/categories"' + format: iri-reference readOnly: true - type: array + type: string coachName: description: 'The name of the Y+S coach who is in charge of the camp.' example: 'Albert Anderegg' diff --git a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml index 105a7aeeb0..c7c2e0be2d 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml @@ -4,10 +4,10 @@ /activity_progress_labels/item: 7 /activity_responsibles: 6 /activity_responsibles/item: 8 -/camps: 29 -/camps/item: 28 -/camp_collaborations: 25 -/camp_collaborations/item: 15 +/camps: 26 +/camps/item: 27 +/camp_collaborations: 22 +/camp_collaborations/item: 14 /categories: 11 /categories/item: 9 /content_types: 6 @@ -21,7 +21,7 @@ /material_lists: 6 /material_lists/item: 7 /periods: 6 -/periods/item: 19 +/periods/item: 18 /profiles: 6 /profiles/item: 6 /schedule_entries: 23 @@ -30,8 +30,8 @@ '/activities?camp=': 13 '/activity_progress_labels?camp=': 6 '/activity_responsibles?activity.camp=': 6 -'/camp_collaborations?camp=': 13 -'/camp_collaborations?activityResponsibles.activity=': 15 +'/camp_collaborations?camp=': 12 +'/camp_collaborations?activityResponsibles.activity=': 14 '/categories?camp=': 9 '/content_types?categories=': 6 '/day_responsibles?day.period=': 6 From 3a8b16b84784984552010f37294a51ba4843321c Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 20:57:49 +0200 Subject: [PATCH 66/81] upgrade http-cache and http-cache-bundle to 3.0.0 --- api/composer.json | 4 ++-- api/composer.lock | 38 +++++++++++++++++--------------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/api/composer.json b/api/composer.json index 99a7b3f3f7..8257449eb1 100644 --- a/api/composer.json +++ b/api/composer.json @@ -12,8 +12,8 @@ "doctrine/doctrine-migrations-bundle": "3.3.0", "doctrine/orm": "2.19.5", "exercise/htmlpurifier-bundle": "5.0", - "friendsofsymfony/http-cache": "^3.x-dev", - "friendsofsymfony/http-cache-bundle": "3.x-dev", + "friendsofsymfony/http-cache": "3.0.0", + "friendsofsymfony/http-cache-bundle": "3.0.0", "google/recaptcha": "1.3.0", "guzzlehttp/guzzle": "7.8.1", "knpuniversity/oauth2-client-bundle": "2.18.1", diff --git a/api/composer.lock b/api/composer.lock index b45b2ee16a..a0fb2f09ae 100644 --- a/api/composer.lock +++ b/api/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": "b735592cca4540bc0226dfa1ae871c24", + "content-hash": "3771cb07978fe149b1f4ea9f7b379862", "packages": [ { "name": "api-platform/core", @@ -2004,16 +2004,16 @@ }, { "name": "friendsofsymfony/http-cache", - "version": "3.x-dev", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/FriendsOfSymfony/FOSHttpCache.git", - "reference": "24a47d586b81a7a9a21fe8aad88b20948e6856cf" + "reference": "2b2ccae740c164c55ea43c6ccf5fca3011d00537" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCache/zipball/24a47d586b81a7a9a21fe8aad88b20948e6856cf", - "reference": "24a47d586b81a7a9a21fe8aad88b20948e6856cf", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCache/zipball/2b2ccae740c164c55ea43c6ccf5fca3011d00537", + "reference": "2b2ccae740c164c55ea43c6ccf5fca3011d00537", "shasum": "" }, "require": { @@ -2027,7 +2027,6 @@ "symfony/options-resolver": "^6.4 || ^7.0" }, "conflict": { - "guzzlehttp/psr7": "<2", "toflar/psr6-symfony-http-cache-store": "<2.2.1" }, "require-dev": { @@ -2044,7 +2043,6 @@ "monolog/monolog": "For logging issues while invalidating", "phpunit/phpunit": "To build tests with the WebServerSubscriber, ^10.0" }, - "default-branch": true, "type": "library", "autoload": { "psr-4": { @@ -2082,26 +2080,26 @@ ], "support": { "issues": "https://github.com/FriendsOfSymfony/FOSHttpCache/issues", - "source": "https://github.com/FriendsOfSymfony/FOSHttpCache/tree/3.x" + "source": "https://github.com/FriendsOfSymfony/FOSHttpCache/tree/3.0.0" }, - "time": "2024-04-08T12:10:48+00:00" + "time": "2024-05-04T18:09:55+00:00" }, { "name": "friendsofsymfony/http-cache-bundle", - "version": "3.x-dev", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle.git", - "reference": "9afe5ec19ff3cfbf480e97c0cf1821ccb6136155" + "reference": "d9606c656c0bc4d81ca8130c7fb3fca572dce93c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCacheBundle/zipball/9afe5ec19ff3cfbf480e97c0cf1821ccb6136155", - "reference": "9afe5ec19ff3cfbf480e97c0cf1821ccb6136155", + "url": "https://api.github.com/repos/FriendsOfSymfony/FOSHttpCacheBundle/zipball/d9606c656c0bc4d81ca8130c7fb3fca572dce93c", + "reference": "d9606c656c0bc4d81ca8130c7fb3fca572dce93c", "shasum": "" }, "require": { - "friendsofsymfony/http-cache": "^2.15 || 3.x-dev", + "friendsofsymfony/http-cache": "^2.15 || ^3.0", "php": "^8.1", "symfony/expression-language": "^6.4 || ^7.0", "symfony/framework-bundle": "^6.4 || ^7.0", @@ -2114,6 +2112,7 @@ "twig/twig": "<1.12.0" }, "require-dev": { + "friendsofphp/php-cs-fixer": "^3.54", "guzzlehttp/guzzle": "^7.2", "jean-beru/fos-http-cache-cloudfront": "^1.1", "matthiasnoback/symfony-config-test": "^4.3.0 || ^5.1", @@ -2181,9 +2180,9 @@ ], "support": { "issues": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle/issues", - "source": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle/tree/3.x" + "source": "https://github.com/FriendsOfSymfony/FOSHttpCacheBundle/tree/3.0.0" }, - "time": "2024-05-03T16:18:00+00:00" + "time": "2024-05-04T18:15:18+00:00" }, { "name": "gedmo/doctrine-extensions", @@ -14365,10 +14364,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "friendsofsymfony/http-cache": 20, - "friendsofsymfony/http-cache-bundle": 20 - }, + "stability-flags": [], "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -14378,4 +14374,4 @@ }, "platform-dev": [], "plugin-api-version": "2.6.0" -} \ No newline at end of file +} From df27b3308f91a9c056a385dd179e0c4eeea2a1ad Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 20:59:40 +0200 Subject: [PATCH 67/81] adjust number of queries --- api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php | 2 +- api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php b/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php index 8ce744e353..06975241fe 100644 --- a/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php +++ b/api/tests/Api/CampCollaborations/ListCampCollaborationsTest.php @@ -114,6 +114,6 @@ public function testSqlQueryCount() { $client->enableProfiler(); $client->request('GET', '/camp_collaborations'); - $this->assertSqlQueryCount($client, 25); + $this->assertSqlQueryCount($client, 22); } } diff --git a/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php b/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php index 8c9eaf027f..a9916e6211 100644 --- a/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php +++ b/api/tests/Api/CampCollaborations/ReadCampCollaborationTest.php @@ -126,6 +126,6 @@ public function testSqlQueryCount() { $client->enableProfiler(); $client->request('GET', '/camp_collaborations/'.$campCollaboration->getId()); - $this->assertSqlQueryCount($client, 15); + $this->assertSqlQueryCount($client, 14); } } From a532c219717b0e91b602a68700c4bae99b751051 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 4 May 2024 21:04:31 +0200 Subject: [PATCH 68/81] revert dry-run on deploy-to-cluster.sh --- .helm/deploy-to-cluster.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.helm/deploy-to-cluster.sh b/.helm/deploy-to-cluster.sh index e9b67f40c9..241d202e58 100755 --- a/.helm/deploy-to-cluster.sh +++ b/.helm/deploy-to-cluster.sh @@ -105,7 +105,7 @@ for i in 1; do values="$values --set postgresql.dbBackupRestoreImage.repository=docker.io/${docker_hub_account}/ecamp3-db-backup-restore" helm uninstall ecamp3-"$instance_name"-"$i" || true - helm upgrade --install --debug --dry-run ecamp3-"$instance_name"-"$i" $SCRIPT_DIR/ecamp3 $values + helm upgrade --install ecamp3-"$instance_name"-"$i" $SCRIPT_DIR/ecamp3 $values done rm -f private.pem From ae74c7bf46bd9f8bb53d96d6ca6d5f42790ce79c Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 13 May 2024 17:02:14 +0200 Subject: [PATCH 69/81] use MailDev REST Api --- e2e/specs/httpCache.cy.js | 67 +++++++++++---------------------------- 1 file changed, 19 insertions(+), 48 deletions(-) diff --git a/e2e/specs/httpCache.cy.js b/e2e/specs/httpCache.cy.js index 5fb034d0c8..4b0fc346be 100644 --- a/e2e/specs/httpCache.cy.js +++ b/e2e/specs/httpCache.cy.js @@ -173,34 +173,6 @@ describe('HTTP cache tests', () => { }) }) - const getIframeDocument = () => { - return ( - cy - .get('iframe.panel-html') - // Cypress yields jQuery element, which has the real - // DOM element under property "0". - // From the real DOM iframe element we can get - // the "document" element, it is stored in "contentDocument" property - // Cypress "its" command can access deep properties using dot notation - // https://on.cypress.io/its - .its('0.contentDocument') - .should('exist') - ) - } - - const getIframeBody = () => { - // get the document - return ( - getIframeDocument() - // automatically retries until body is loaded - .its('body') - .should('not.be.undefined') - // wraps "body" DOM element to allow - // chaining more Cypress commands, like ".find(...)" - .then(cy.wrap) - ) - } - it('invalidates cached data when user leaves a camp', () => { Cypress.session.clearAllSavedSessions() const uri = '/api/camps/3c79b99ab424/categories' @@ -231,11 +203,10 @@ describe('HTTP cache tests', () => { }) // delete old emails - cy.visit('localhost:3000/mail') - cy.get('a[title="Delete all emails"]').click() - /* eslint-disable cypress/no-unnecessary-waiting */ - cy.wait(50) - cy.get('a[title="Delete all emails"]').click() + cy.request({ + url: 'localhost:3000/mail/email/all', + method: 'DELETE', + }) // invite Castor cy.login('test@example.com') @@ -246,22 +217,22 @@ describe('HTTP cache tests', () => { // accept invitation as Castor cy.login('castor@example.com') - cy.visit('localhost:3000/mail') - cy.get('a') - .contains('[eCamp v3] Du wurdest ins Lager "Pfila 2023" eingeladen') - .click() - /* eslint-disable cypress/no-unnecessary-waiting */ - cy.wait(200) - getIframeBody() - .find('a') - .then(($a) => { - const href = $a.prop('href') - cy.visit(href) - cy.get('button').contains('Einladung mit aktuellem Account akzeptieren').click() - cy.wait('@invitations') - cy.visit('/camps') - cy.contains('GRGR') + + cy.request({ + url: 'localhost:3000/mail/email', + }).then((response) => { + const emailHtmlContent = response.body[0].html + cy.document().then((document) => { + document.documentElement.innerHTML = emailHtmlContent }) + + cy.get('a:contains("Einladung beantworten")').invoke('removeAttr', 'target').click() + + cy.get('button:contains("Einladung mit aktuellem Account akzeptieren")').click() + cy.wait('@invitations') + cy.visit('/camps') + cy.contains('GRGR') + }) }) it("doesn't cache /camps", () => { From a05fdec3237169dcf8c7003a77bc64921b9ca643 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 13 May 2024 20:27:41 +0200 Subject: [PATCH 70/81] revert changes to nuxtPrint.cy.js --- e2e/specs/nuxtPrint.cy.js | 55 +++++++++++++++++++-------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/e2e/specs/nuxtPrint.cy.js b/e2e/specs/nuxtPrint.cy.js index a4fc8ac74b..e9938c0b04 100644 --- a/e2e/specs/nuxtPrint.cy.js +++ b/e2e/specs/nuxtPrint.cy.js @@ -6,35 +6,34 @@ describe('Nuxt print test', () => { it('shows print preview', () => { cy.login('test@example.com') - cy.request(Cypress.env('API_ROOT_URL') + '/camps/3c79b99ab424.jsonhal').then( - (response) => { - const camp = response.body - const campUri = camp._links.self.href - - const printConfig = { - language: 'en', - documentName: 'camp', - camp: campUri, - contents: [ - { - type: 'Cover', - options: {}, - }, - ], - } - - cy.visit( - Cypress.env('PRINT_URL') + - '/?config=' + - encodeURIComponent(JSON.stringify(printConfig)) - ) - cy.contains(camp.name) - cy.contains(camp.title) - cy.contains(camp.motto) - - cy.get('#content_0_cover').should('have.css', 'font-size', '50px') // this ensures Tailwind is properly built and integrated + cy.request(Cypress.env('API_ROOT_URL') + '/camps.jsonhal').then((response) => { + const body = response.body + const campUri = body._links.items[1].href + const camp = body._embedded.items[1] + + const printConfig = { + language: 'en', + documentName: 'camp', + camp: campUri, + contents: [ + { + type: 'Cover', + options: {}, + }, + ], } - ) + + cy.visit( + Cypress.env('PRINT_URL') + + '/?config=' + + encodeURIComponent(JSON.stringify(printConfig)) + ) + cy.contains(camp.name) + cy.contains(camp.title) + cy.contains(camp.motto) + + cy.get('#content_0_cover').should('have.css', 'font-size', '50px') // this ensures Tailwind is properly built and integrated + }) }) it('downloads PDF', () => { From c04ebb75051ad38f842ee9e4cf8181b62bcd3543 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 13 May 2024 20:45:54 +0200 Subject: [PATCH 71/81] use parameter for path match regex --- api/config/packages/http_cache.yaml | 5 ++++- api/config/services.yaml | 4 ++++ api/src/HttpCache/ResponseTagger.php | 8 +++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/api/config/packages/http_cache.yaml b/api/config/packages/http_cache.yaml index e2b9162f62..63f8ee24fe 100644 --- a/api/config/packages/http_cache.yaml +++ b/api/config/packages/http_cache.yaml @@ -1,3 +1,6 @@ +parameters: + app.httpCache.matchPath: '^/(content_types|camps/[0-9a-f]*/categories)' + fos_http_cache: debug: enabled: true # this sets the X-Cache-Debug response header; can be removed later-on @@ -16,7 +19,7 @@ fos_http_cache: # matches /camps/133/categories endpoint - match: - path: ^/(content_types|camps/[0-9a-f]*/categories) + path: '%app.httpCache.matchPath%' headers: overwrite: true cache_control: { public: true, max_age: 0, s_maxage: 3600 } diff --git a/api/config/services.yaml b/api/config/services.yaml index 05bc292ce5..c82a494f16 100644 --- a/api/config/services.yaml +++ b/api/config/services.yaml @@ -183,6 +183,10 @@ services: $apiCacheEnabled: '%env(API_CACHE_ENABLED)%' $varnishApiUrl: '%env(VARNISH_API_URL)%' + App\HttpCache\ResponseTagger: + arguments: + $matchPath: '%app.httpCache.matchPath%' + # Entity Filter App\Doctrine\FilterByCurrentUserExtension: tags: diff --git a/api/src/HttpCache/ResponseTagger.php b/api/src/HttpCache/ResponseTagger.php index 6ae53c28df..73f6c34a56 100644 --- a/api/src/HttpCache/ResponseTagger.php +++ b/api/src/HttpCache/ResponseTagger.php @@ -8,15 +8,13 @@ use Symfony\Component\HttpFoundation\RequestStack; /** - * Wrapper around SymfonyResponseTagger which only adds tags for specific URIs. + * Wrapper around SymfonyResponseTagger which only adds tags for specific URIs, which match the regex $matchPath. * * @author Urban Suppiger */ class ResponseTagger { - // only add tags for request URI matching below regex - private const PATH_REGEX = '^/(content_types|camps/[0-9a-f]*/categories)'; - public function __construct( + private string $matchPath, private SymfonyResponseTagger $responseTagger, private RequestStack $requestStack ) {} @@ -43,6 +41,6 @@ private function isCacheable(): bool { $requestUri = $request->getRequestUri(); - return (bool) preg_match('{'.self::PATH_REGEX.'}', $requestUri); + return (bool) preg_match('{'.$this->matchPath.'}', $requestUri); } } From e851ff4d89a14ee9082413b617dac7be72dc1dd2 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 13 May 2024 21:20:22 +0200 Subject: [PATCH 72/81] improve header & license information --- .github/workflows/reusable-dev-deployment.yml | 2 +- .github/workflows/reusable-stage-prod-deployment.yml | 2 +- api/src/HttpCache/AddCollectionTagsListener.php | 9 +++++---- api/src/HttpCache/PurgeHttpCacheListener.php | 11 +++++------ api/tests/HttpCache/Entity/ContainNonResource.php | 9 --------- api/tests/HttpCache/Entity/Dummy.php | 9 --------- api/tests/HttpCache/Entity/DummyNoGetOperation.php | 9 --------- api/tests/HttpCache/Entity/NotAResource.php | 9 --------- api/tests/HttpCache/Entity/RelatedDummy.php | 9 --------- api/tests/HttpCache/Entity/RelatedOwningDummy.php | 9 --------- api/tests/HttpCache/PurgeHttpCacheListenerTest.php | 9 +++++---- 11 files changed, 17 insertions(+), 70 deletions(-) diff --git a/.github/workflows/reusable-dev-deployment.yml b/.github/workflows/reusable-dev-deployment.yml index b4602719a0..b64ecde35c 100644 --- a/.github/workflows/reusable-dev-deployment.yml +++ b/.github/workflows/reusable-dev-deployment.yml @@ -93,7 +93,7 @@ jobs: --set ingress.basicAuth.enabled=${{ vars.BASIC_AUTH_ENABLED || false }} \ --set ingress.basicAuth.username=${{ secrets.BASIC_AUTH_USERNAME }} \ --set ingress.basicAuth.password='${{ secrets.BASIC_AUTH_PASSWORD }}' \ - --set apiCache.enabled=${{ vars.API_CACHE_ENABLED || false }} \ + --set apiCache.enabled=${{ vars.API_CACHE_ENABLED || false }} \ --set mail.dummyEnabled=true \ --set postgresql.url='${{ secrets.POSTGRES_URL }}/ecamp3${{ inputs.name }}?sslmode=require' \ --set postgresql.adminUrl='${{ secrets.POSTGRES_ADMIN_URL }}/ecamp3${{ inputs.name }}?sslmode=require' \ diff --git a/.github/workflows/reusable-stage-prod-deployment.yml b/.github/workflows/reusable-stage-prod-deployment.yml index ba8152f1db..58c51822ab 100644 --- a/.github/workflows/reusable-stage-prod-deployment.yml +++ b/.github/workflows/reusable-stage-prod-deployment.yml @@ -52,7 +52,7 @@ jobs: --set ingress.basicAuth.enabled=${{ vars.BASIC_AUTH_ENABLED || false }} \ --set ingress.basicAuth.username=${{ secrets.BASIC_AUTH_USERNAME }} \ --set ingress.basicAuth.password='${{ secrets.BASIC_AUTH_PASSWORD }}' \ - --set apiCache.enabled=${{ vars.API_CACHE_ENABLED || false }} \ + --set apiCache.enabled=${{ vars.API_CACHE_ENABLED || false }} \ --set mail.dsn=${{ secrets.MAILER_DSN }} \ --set postgresql.url='${{ secrets.POSTGRES_URL }}/${{ secrets.DB_NAME }}?sslmode=require' \ --set postgresql.dropDBOnUninstall=false \ diff --git a/api/src/HttpCache/AddCollectionTagsListener.php b/api/src/HttpCache/AddCollectionTagsListener.php index 9142ba70d2..c89cd25908 100644 --- a/api/src/HttpCache/AddCollectionTagsListener.php +++ b/api/src/HttpCache/AddCollectionTagsListener.php @@ -1,12 +1,13 @@ + * For original author and license information see upstream file. * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * Upstream file (main branch): https://github.com/api-platform/core/blob/main/src/Symfony/EventListener/AddTagsListener.php + * Upstream file (last synchronized version): https://github.com/api-platform/core/blob/66e26729540c91a44730cde75f3272fa94db6572/src/Symfony/EventListener/AddTagsListener.php + * Last synchronized commit: 2024-02-03 / 66e26729540c91a44730cde75f3272fa94db6572 */ declare(strict_types=1); diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index 80ba100372..f46efc9313 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -1,12 +1,13 @@ + * For original author and license information see upstream file. * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * Upstream file (main branch): https://github.com/api-platform/core/blob/main/src/Doctrine/EventListener/PurgeHttpCacheListener.php + * Upstream file (last synchronized version): https://github.com/api-platform/core/blob/d8e2d0c5e9b48c15d60a734086b0102b6ecf95c8/src/Doctrine/EventListener/PurgeHttpCacheListener.php + * Last synchronized commit: 2024-03-05 / d8e2d0c5e9b48c15d60a734086b0102b6ecf95c8 */ declare(strict_types=1); @@ -37,8 +38,6 @@ /** * Purges responses containing modified entities from the proxy cache. - * - * @author Kévin Dunglas */ final class PurgeHttpCacheListener { use ClassInfoTrait; diff --git a/api/tests/HttpCache/Entity/ContainNonResource.php b/api/tests/HttpCache/Entity/ContainNonResource.php index 84eaf510b9..2063142446 100644 --- a/api/tests/HttpCache/Entity/ContainNonResource.php +++ b/api/tests/HttpCache/Entity/ContainNonResource.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - declare(strict_types=1); namespace App\Tests\HttpCache\Entity; diff --git a/api/tests/HttpCache/Entity/Dummy.php b/api/tests/HttpCache/Entity/Dummy.php index 5faf455df9..b780638580 100644 --- a/api/tests/HttpCache/Entity/Dummy.php +++ b/api/tests/HttpCache/Entity/Dummy.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - declare(strict_types=1); namespace App\Tests\HttpCache\Entity; diff --git a/api/tests/HttpCache/Entity/DummyNoGetOperation.php b/api/tests/HttpCache/Entity/DummyNoGetOperation.php index 1521623d86..831e7c8f20 100644 --- a/api/tests/HttpCache/Entity/DummyNoGetOperation.php +++ b/api/tests/HttpCache/Entity/DummyNoGetOperation.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - declare(strict_types=1); namespace App\Tests\HttpCache\Entity; diff --git a/api/tests/HttpCache/Entity/NotAResource.php b/api/tests/HttpCache/Entity/NotAResource.php index dc84939516..aa3be1e314 100644 --- a/api/tests/HttpCache/Entity/NotAResource.php +++ b/api/tests/HttpCache/Entity/NotAResource.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - declare(strict_types=1); namespace App\Tests\HttpCache\Entity; diff --git a/api/tests/HttpCache/Entity/RelatedDummy.php b/api/tests/HttpCache/Entity/RelatedDummy.php index a812f9c9f7..76dd522422 100644 --- a/api/tests/HttpCache/Entity/RelatedDummy.php +++ b/api/tests/HttpCache/Entity/RelatedDummy.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - declare(strict_types=1); namespace App\Tests\HttpCache\Entity; diff --git a/api/tests/HttpCache/Entity/RelatedOwningDummy.php b/api/tests/HttpCache/Entity/RelatedOwningDummy.php index afbac8b50a..b84370d904 100644 --- a/api/tests/HttpCache/Entity/RelatedOwningDummy.php +++ b/api/tests/HttpCache/Entity/RelatedOwningDummy.php @@ -1,14 +1,5 @@ - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - declare(strict_types=1); namespace App\Tests\HttpCache\Entity; diff --git a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php index fdee3d8b7c..27188cc703 100644 --- a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php +++ b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php @@ -1,12 +1,13 @@ + * For original author and license information see upstream file. * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. + * Upstream file (main branch): https://github.com/api-platform/core/blob/main/tests/Doctrine/EventListener/PurgeHttpCacheListenerTest.php + * Upstream file (last synchronized version): https://github.com/api-platform/core/blob/1821a05eebd107fd495376b43bfc9f64d72d6e7c/tests/Doctrine/EventListener/PurgeHttpCacheListenerTest.php + * Last synchronized commit: 2023-10-27 / 1821a05eebd107fd495376b43bfc9f64d72d6e7c */ declare(strict_types=1); From 73b4d1e568a777507378a9906afb3619b119dab8 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Mon, 13 May 2024 21:25:36 +0200 Subject: [PATCH 73/81] remove duplicate package symfony/http-client --- api/composer.json | 1 - api/composer.lock | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/api/composer.json b/api/composer.json index 7be0ab9c83..e05e4d2eb6 100644 --- a/api/composer.json +++ b/api/composer.json @@ -63,7 +63,6 @@ "symfony/browser-kit": "7.0.7", "symfony/css-selector": "7.0.7", "symfony/debug-bundle": "7.0.7", - "symfony/http-client": "7.0.7", "symfony/maker-bundle": "1.59.1", "symfony/phpunit-bridge": "7.0.7", "symfony/stopwatch": "7.0.7", diff --git a/api/composer.lock b/api/composer.lock index a56a3c627b..e2e53fcbd7 100644 --- a/api/composer.lock +++ b/api/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": "662d62251194e87ea3830f4acfebcb19", + "content-hash": "69a40ae66877e96dcec1292db4f99737", "packages": [ { "name": "api-platform/core", From d7212f3555ea824a310de90e11c34cc12d206110 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Wed, 15 May 2024 21:55:47 +0200 Subject: [PATCH 74/81] remove tmpfs --- docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 08c5274349..1d324d013e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -100,8 +100,6 @@ services: - caddy volumes: - ./api/docker/varnish/vcl/:/etc/varnish/:ro - tmpfs: - - /var/lib/varnish/varnishd:exec ports: - target: 8080 published: 3004 From 459f198716fc1e08ba4e52efd6777cb2a34168b1 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Wed, 15 May 2024 21:56:53 +0200 Subject: [PATCH 75/81] update snapshots --- ...apshotTest__testRootEndpointMatchesSnapshot__1.json | 2 +- ...estPerformanceDidNotChangeForStableEndpoints__1.yml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json index 4526835cd7..aa4cf669b9 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json +++ b/api/tests/Api/SnapshotTests/__snapshots__/ResponseSnapshotTest__testRootEndpointMatchesSnapshot__1.json @@ -21,7 +21,7 @@ "templated": true }, "categories": { - "href": "\/categories{\/id}{\/action}{?camp,camp[]}", + "href": "\/categories{\/id}{?camp,camp[]}", "templated": true }, "columnLayouts": { diff --git a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml index c8031ee651..1e0924898b 100644 --- a/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml +++ b/api/tests/Api/SnapshotTests/__snapshots__/test_EndpointPerformanceTest__testPerformanceDidNotChangeForStableEndpoints__1.yml @@ -4,10 +4,10 @@ /activity_progress_labels/item: 7 /activity_responsibles: 6 /activity_responsibles/item: 8 -/camps: 29 -/camps/item: 22 -/camp_collaborations: 25 -/camp_collaborations/item: 15 +/camps: 26 +/camps/item: 21 +/camp_collaborations: 22 +/camp_collaborations/item: 14 /categories: 11 /categories/item: 9 /content_types: 6 @@ -21,7 +21,7 @@ /material_lists: 6 /material_lists/item: 7 /periods: 6 -/periods/item: 18 +/periods/item: 17 /profiles: 6 /profiles/item: 6 /schedule_entries: 23 From 8e146b34fd0e43f01002e4c671e8a1b150c14a7f Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Wed, 15 May 2024 21:57:37 +0200 Subject: [PATCH 76/81] apiCache.enabled now defaults to false --- .helm/ecamp3/values.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.helm/ecamp3/values.yaml b/.helm/ecamp3/values.yaml index feec7cf7ac..72031c27d8 100644 --- a/.helm/ecamp3/values.yaml +++ b/.helm/ecamp3/values.yaml @@ -223,7 +223,7 @@ ingress: tls: apiCache: - enabled: true + enabled: false image: repository: "docker.io/ecamp/ecamp3-varnish" pullPolicy: IfNotPresent From 6805865e4f4aa48fab71b70c71ec29a724461d81 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Wed, 15 May 2024 21:59:02 +0200 Subject: [PATCH 77/81] composer update --- api/composer.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/composer.lock b/api/composer.lock index 2fb148d176..efe85c332a 100644 --- a/api/composer.lock +++ b/api/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": "69a40ae66877e96dcec1292db4f99737", + "content-hash": "43a05a2ab8dfa658946def712781e1a6", "packages": [ { "name": "api-platform/core", @@ -14378,4 +14378,4 @@ }, "platform-dev": [], "plugin-api-version": "2.6.0" -} \ No newline at end of file +} From 3f604dbe5929a66bd8359753880d42d6b079e9d8 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 18 May 2024 12:36:32 +0200 Subject: [PATCH 78/81] delete: use orignal entity data to generate collection rotues; update: also purge old and new collection routes --- api/src/HttpCache/PurgeHttpCacheListener.php | 76 ++++++++++++++--- .../HttpCache/PurgeHttpCacheListenerTest.php | 83 +++++++++++++++---- 2 files changed, 133 insertions(+), 26 deletions(-) diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index f46efc9313..219a4b961a 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -50,9 +50,6 @@ public function __construct(private readonly IriConverterInterface|LegacyIriConv * Collects tags from the previous and the current version of the updated entities to purge related documents. */ public function preUpdate(PreUpdateEventArgs $eventArgs): void { - $object = $eventArgs->getObject(); - $this->addTagForItem($object); - $changeSet = $eventArgs->getEntityChangeSet(); $objectManager = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); $associationMappings = $objectManager->getClassMetadata(ClassUtils::getClass($eventArgs->getObject()))->getAssociationMappings(); @@ -73,7 +70,7 @@ public function preUpdate(PreUpdateEventArgs $eventArgs): void { } /** - * Collects tags from inserted and deleted entities, including relations. + * Collects tags from inserted, updated and deleted entities, including relations. */ public function onFlush(OnFlushEventArgs $eventArgs): void { $em = method_exists($eventArgs, 'getObjectManager') ? $eventArgs->getObjectManager() : $eventArgs->getEntityManager(); @@ -84,10 +81,17 @@ public function onFlush(OnFlushEventArgs $eventArgs): void { $this->gatherRelationTags($em, $entity); } - foreach ($uow->getScheduledEntityDeletions() as $entity) { + foreach ($uow->getScheduledEntityUpdates() as $entity) { + $originalEntity = $this->getOriginalEntity($entity, $em); $this->addTagForItem($entity); - $this->gatherResourceTags($entity); - $this->gatherRelationTags($em, $entity); + $this->gatherResourceTags($entity, $originalEntity); + } + + foreach ($uow->getScheduledEntityDeletions() as $entity) { + $originalEntity = $this->getOriginalEntity($entity, $em); + $this->addTagForItem($originalEntity); + $this->gatherResourceTags($originalEntity); + $this->gatherRelationTags($em, $originalEntity); } } @@ -98,7 +102,30 @@ public function postFlush(): void { $this->cacheManager->flush(); } - private function gatherResourceTags(object $entity): void { + /** + * Computes the original state of the entity based on the current entity and on the changeset. + */ + private function getOriginalEntity($entity, $em) { + $uow = $em->getUnitOfWork(); + $changeSet = $uow->getEntityChangeSet($entity); + $classMetadata = $em->getClassMetadata(ClassUtils::getClass($entity)); + + $originalEntity = clone $entity; + $em->detach($originalEntity); + foreach ($changeSet as $key => $value) { + $classMetadata->setFieldValue($originalEntity, $key, $value[0]); + } + + return $originalEntity; + } + + /** + * Purges all collections (GetCollection operations), in which entity is listed on top level. + * + * If oldEntity is provided, purge is only done if the IRI of the collection has changed + * (e.g. for updating period on a ScheduleEntry and the IRI changes from /periods/1/schedule_entries to /periods/2/schedule_entries) + */ + private function gatherResourceTags(object $entity, ?object $oldEntity = null): void { try { $resourceClass = $this->resourceClassResolver->getResourceClass($entity); $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); @@ -109,10 +136,7 @@ private function gatherResourceTags(object $entity): void { foreach ($metadata->getOperations() ?? [] as $operation) { if ($operation instanceof GetCollection) { - $iri = $this->iriConverter->getIriFromResource($entity, UrlGeneratorInterface::ABS_PATH, $operation); - if ($iri) { - $this->cacheManager->invalidateTags([$iri]); - } + $this->invalidateCollection($operation, $entity, $oldEntity); } } $resourceIterator->next(); @@ -122,6 +146,34 @@ private function gatherResourceTags(object $entity): void { } /** + * Purges a single collection (GetCollection operation). + * + * If oldEntity is provided, purge is only done if the IRI of the collection has changed + * (e.g. for updating period on a ScheduleEntry and the IRI changes from /periods/1/schedule_entries to /periods/2/schedule_entries) + */ + private function invalidateCollection(GetCollection $operation, object $entity, ?object $oldEntity = null): void { + $iri = $this->iriConverter->getIriFromResource($entity, UrlGeneratorInterface::ABS_PATH, $operation); + + if (!$iri) { + return; + } + + if (!$oldEntity) { + $this->cacheManager->invalidateTags([$iri]); + + return; + } + + $oldIri = $this->iriConverter->getIriFromResource($oldEntity, UrlGeneratorInterface::ABS_PATH, $operation); + if ($iri !== $oldIri) { + $this->cacheManager->invalidateTags([$iri]); + $this->cacheManager->invalidateTags([$oldIri]); + } + } + + /** + * Invalidate all relation tags of foreign objects ($relatedObject), in which $entity appears. + * * @psalm-suppress UndefinedClass */ private function gatherRelationTags(EntityManagerInterface $em, object $entity): void { diff --git a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php index 27188cc703..6ad010fa4e 100644 --- a/api/tests/HttpCache/PurgeHttpCacheListenerTest.php +++ b/api/tests/HttpCache/PurgeHttpCacheListenerTest.php @@ -69,12 +69,31 @@ protected function setUp(): void { $this->uowProphecy = $this->prophesize(UnitOfWork::class); $this->emProphecy = $this->prophesize(EntityManagerInterface::class); + $this->emProphecy->detach(Argument::any())->willReturn(); $this->emProphecy->getUnitOfWork()->willReturn($this->uowProphecy->reveal()); - $dummyClassMetadata = new ClassMetadata(Dummy::class); - $dummyClassMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']); - $dummyClassMetadata->mapOneToOne(['fieldName' => 'relatedOwningDummy', 'targetEntity' => RelatedOwningDummy::class, 'inversedBy' => 'ownedDummy']); - $this->emProphecy->getClassMetadata(Dummy::class)->willReturn($dummyClassMetadata); + $classMetadataProphecy = $this->prophesize(ClassMetadata::class); + $classMetadataProphecy->getAssociationMappings()->willReturn([ + 'relatedDummy' => [ + 'targetEntity' => 'App\\Tests\\HttpCache\\Entity\\RelatedDummy', + 'isOwningSide' => true, + 'inversedBy' => 'dummies', + 'mappedBy' => null, + ], + 'relatedOwningDummy' => [ + 'targetEntity' => 'App\\Tests\\HttpCache\\Entity\\RelatedOwningDummy', + 'isOwningSide' => true, + 'inversedBy' => 'ownedDummy', + 'mappedBy' => null, + ], + ]); + $classMetadataProphecy->setFieldValue(Argument::any(), Argument::any(), Argument::any())->will(function ($args) { + $entity = $args[0]; + $field = $args[1]; + $value = $args[2]; + $entity->{$field} = $value; + }); + $this->emProphecy->getClassMetadata(Dummy::class)->willReturn($classMetadataProphecy->reveal()); $this->propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); $this->propertyAccessorProphecy->isReadable(Argument::type(Dummy::class), 'relatedDummy')->willReturn(true); @@ -148,12 +167,15 @@ public function testOnFlush(): void { $resourceClassResolverProphecy->getResourceClass(Argument::type(Dummy::class))->willReturn(Dummy::class)->shouldBeCalled(); $resourceClassResolverProphecy->getResourceClass(Argument::type(DummyNoGetOperation::class))->willReturn(DummyNoGetOperation::class)->shouldBeCalled(); - $uowProphecy = $this->prophesize(UnitOfWork::class); - $uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1, $toInsert2])->shouldBeCalled(); - $uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete1, $toDelete2, $toDeleteNoPurge])->shouldBeCalled(); + $uowMock = $this->createMock(UnitOfWork::class); + $uowMock->method('getScheduledEntityInsertions')->willReturn([$toInsert1, $toInsert2]); + $uowMock->method('getScheduledEntityUpdates')->willReturn([]); + $uowMock->method('getScheduledEntityDeletions')->willReturn([$toDelete1, $toDelete2, $toDeleteNoPurge]); + $uowMock->method('getEntityChangeSet')->willReturn([]); $emProphecy = $this->prophesize(EntityManagerInterface::class); - $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled(); + $emProphecy->getUnitOfWork()->willReturn($uowMock)->shouldBeCalled(); + $emProphecy->detach(Argument::any())->willReturn(); $dummyClassMetadata = new ClassMetadata(Dummy::class); $dummyClassMetadata->mapManyToOne(['fieldName' => 'relatedDummy', 'targetEntity' => RelatedDummy::class, 'inversedBy' => 'dummies']); $dummyClassMetadata->mapOneToOne(['fieldName' => 'relatedOwningDummy', 'targetEntity' => RelatedOwningDummy::class, 'inversedBy' => 'ownedDummy']); @@ -184,7 +206,6 @@ public function testPreUpdate(): void { $dummy->setId('1'); $cacheManagerProphecy = $this->prophesize(CacheManager::class); - $cacheManagerProphecy->invalidateTags(['/dummies/1'])->shouldBeCalled()->willReturn($cacheManagerProphecy); $cacheManagerProphecy->invalidateTags(['/related_dummies/old#dummies'])->shouldBeCalled()->willReturn($cacheManagerProphecy); $cacheManagerProphecy->invalidateTags(['/related_dummies/new#dummies'])->shouldBeCalled()->willReturn($cacheManagerProphecy); $cacheManagerProphecy->flush(Argument::any())->willReturn(0); @@ -192,7 +213,6 @@ public function testPreUpdate(): void { $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource($dummy)->willReturn('/dummies/1')->shouldBeCalled(); $iriConverterProphecy->getIriFromResource($oldRelatedDummy)->willReturn('/related_dummies/old')->shouldBeCalled(); $iriConverterProphecy->getIriFromResource($newRelatedDummy)->willReturn('/related_dummies/new')->shouldBeCalled(); @@ -229,10 +249,8 @@ public function testNothingToPurge(): void { $metadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromResource($dummyNoGetOperation)->willReturn(null)->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->isResourceClass(Argument::type('string'))->willReturn(true)->shouldBeCalled(); $emProphecy = $this->prophesize(EntityManagerInterface::class); @@ -274,6 +292,7 @@ public function testNotAResourceClass(): void { $uowProphecy = $this->prophesize(UnitOfWork::class); $uowProphecy->getScheduledEntityInsertions()->willReturn([$containNonResource])->shouldBeCalled(); $uowProphecy->getScheduledEntityDeletions()->willReturn([])->shouldBeCalled(); + $uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled(); $emProphecy = $this->prophesize(EntityManagerInterface::class); $emProphecy->getUnitOfWork()->willReturn($uowProphecy->reveal())->shouldBeCalled(); @@ -307,6 +326,7 @@ public function testInsertingShouldPurgeSubresourceCollections(): void { $this->uowProphecy->getScheduledEntityInsertions()->willReturn([$toInsert1]); $this->uowProphecy->getScheduledEntityDeletions()->willReturn([]); + $this->uowProphecy->getScheduledEntityUpdates()->willReturn([])->shouldBeCalled(); // then $this->cacheManagerProphecy->invalidateTags(['/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); @@ -326,8 +346,13 @@ public function testDeleteShouldPurgeSubresourceCollections(): void { $relatedDummy->setId('100'); $toDelete1->setRelatedDummy($relatedDummy); - $this->uowProphecy->getScheduledEntityInsertions()->willReturn([]); - $this->uowProphecy->getScheduledEntityDeletions()->willReturn([$toDelete1]); + $uowMock = $this->createMock(UnitOfWork::class); + $uowMock->method('getScheduledEntityInsertions')->willReturn([]); + $uowMock->method('getScheduledEntityUpdates')->willReturn([]); + $uowMock->method('getScheduledEntityDeletions')->willReturn([$toDelete1]); + $uowMock->method('getEntityChangeSet')->willReturn([]); + + $this->emProphecy->getUnitOfWork()->willReturn($uowMock)->shouldBeCalled(); // then $this->cacheManagerProphecy->invalidateTags(['/dummies/1'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); @@ -339,4 +364,34 @@ public function testDeleteShouldPurgeSubresourceCollections(): void { $listener->onFlush(new OnFlushEventArgs($this->emProphecy->reveal())); $listener->postFlush(); } + + public function testUpdateShouldPurgeSubresourceCollections(): void { + // given + $toUpdate1 = new Dummy(); + $toUpdate1->setId('1'); + $relatedDummy = new RelatedDummy(); + $relatedDummy->setId('100'); + $toUpdate1->setRelatedDummy($relatedDummy); + + $relatedDummyOld = new RelatedDummy(); + $relatedDummyOld->setId('99'); + + $uowMock = $this->createMock(UnitOfWork::class); + $uowMock->method('getScheduledEntityInsertions')->willReturn([]); + $uowMock->method('getScheduledEntityUpdates')->willReturn([$toUpdate1]); + $uowMock->method('getScheduledEntityDeletions')->willReturn([]); + $uowMock->method('getEntityChangeSet')->willReturn(['relatedDummy' => [$relatedDummyOld, $relatedDummy]]); + + $this->emProphecy->getUnitOfWork()->willReturn($uowMock)->shouldBeCalled(); + + // then + $this->cacheManagerProphecy->invalidateTags(['/dummies/1'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + $this->cacheManagerProphecy->invalidateTags(['/related_dummies/100/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + $this->cacheManagerProphecy->invalidateTags(['/related_dummies/99/dummies'])->willReturn($this->cacheManagerProphecy)->shouldBeCalled(); + + // when + $listener = new PurgeHttpCacheListener($this->iriConverterProphecy->reveal(), $this->resourceClassResolverProphecy->reveal(), $this->propertyAccessorProphecy->reveal(), $this->metadataFactoryProphecy->reveal(), $this->cacheManagerProphecy->reveal()); + $listener->onFlush(new OnFlushEventArgs($this->emProphecy->reveal())); + $listener->postFlush(); + } } From dd199d70b59eefc95ca7c73a705444972e45648e Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Tue, 21 May 2024 16:25:52 +0200 Subject: [PATCH 79/81] don't catch InvalidArgumentException|OperationNotFoundException in PurgeHttpCacheListener --- api/src/HttpCache/PurgeHttpCacheListener.php | 26 +++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index 219a4b961a..0e97120d1d 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -17,7 +17,6 @@ use ApiPlatform\Api\IriConverterInterface as LegacyIriConverterInterface; use ApiPlatform\Api\ResourceClassResolverInterface as LegacyResourceClassResolverInterface; use ApiPlatform\Exception\InvalidArgumentException; -use ApiPlatform\Exception\OperationNotFoundException; use ApiPlatform\Exception\RuntimeException; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; @@ -126,22 +125,19 @@ private function getOriginalEntity($entity, $em) { * (e.g. for updating period on a ScheduleEntry and the IRI changes from /periods/1/schedule_entries to /periods/2/schedule_entries) */ private function gatherResourceTags(object $entity, ?object $oldEntity = null): void { - try { - $resourceClass = $this->resourceClassResolver->getResourceClass($entity); - $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); - $resourceIterator = $resourceMetadataCollection->getIterator(); - while ($resourceIterator->valid()) { - /** @var ApiResource $metadata */ - $metadata = $resourceIterator->current(); - - foreach ($metadata->getOperations() ?? [] as $operation) { - if ($operation instanceof GetCollection) { - $this->invalidateCollection($operation, $entity, $oldEntity); - } + $resourceClass = $this->resourceClassResolver->getResourceClass($entity); + $resourceMetadataCollection = $this->resourceMetadataCollectionFactory->create($resourceClass); + $resourceIterator = $resourceMetadataCollection->getIterator(); + while ($resourceIterator->valid()) { + /** @var ApiResource $metadata */ + $metadata = $resourceIterator->current(); + + foreach ($metadata->getOperations() ?? [] as $operation) { + if ($operation instanceof GetCollection) { + $this->invalidateCollection($operation, $entity, $oldEntity); } - $resourceIterator->next(); } - } catch (InvalidArgumentException|OperationNotFoundException) { + $resourceIterator->next(); } } From 81509a72a50e2271d27beb62ed3550fb0a7582ec Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 25 May 2024 17:56:02 +0200 Subject: [PATCH 80/81] fix psalm --- api/src/HttpCache/PurgeHttpCacheListener.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index 0e97120d1d..9f9f2ca84f 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -154,15 +154,14 @@ private function invalidateCollection(GetCollection $operation, object $entity, return; } - if (!$oldEntity) { - $this->cacheManager->invalidateTags([$iri]); + $this->cacheManager->invalidateTags([$iri]); + if (!$oldEntity) { return; } $oldIri = $this->iriConverter->getIriFromResource($oldEntity, UrlGeneratorInterface::ABS_PATH, $operation); - if ($iri !== $oldIri) { - $this->cacheManager->invalidateTags([$iri]); + if ($oldIri && $iri !== $oldIri) { $this->cacheManager->invalidateTags([$oldIri]); } } From fac40cac44001503e83093322c459e50e49c3656 Mon Sep 17 00:00:00 2001 From: Urban Suppiger Date: Sat, 25 May 2024 18:29:26 +0200 Subject: [PATCH 81/81] fix test --- api/src/HttpCache/PurgeHttpCacheListener.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api/src/HttpCache/PurgeHttpCacheListener.php b/api/src/HttpCache/PurgeHttpCacheListener.php index 9f9f2ca84f..840351a559 100644 --- a/api/src/HttpCache/PurgeHttpCacheListener.php +++ b/api/src/HttpCache/PurgeHttpCacheListener.php @@ -154,14 +154,15 @@ private function invalidateCollection(GetCollection $operation, object $entity, return; } - $this->cacheManager->invalidateTags([$iri]); - if (!$oldEntity) { + $this->cacheManager->invalidateTags([$iri]); + return; } $oldIri = $this->iriConverter->getIriFromResource($oldEntity, UrlGeneratorInterface::ABS_PATH, $operation); if ($oldIri && $iri !== $oldIri) { + $this->cacheManager->invalidateTags([$iri]); $this->cacheManager->invalidateTags([$oldIri]); } }