diff --git a/config/notes.yaml b/config/notes.yaml new file mode 100644 index 000000000..4164d7e15 --- /dev/null +++ b/config/notes.yaml @@ -0,0 +1,28 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + # controllers are imported separately to make sure they're public + # and have a tag that allows actions to type-hint services + Pimcore\Bundle\StudioBackendBundle\Note\Controller\: + resource: '../src/Note/Controller' + public: true + tags: [ 'controller.service_arguments' ] + + # Hydrators + Pimcore\Bundle\StudioBackendBundle\Note\Hydrator\NoteHydratorInterface: + class: Pimcore\Bundle\StudioBackendBundle\Note\Hydrator\NoteHydrator + + Pimcore\Bundle\StudioBackendBundle\Note\Extractor\NoteDataExtractorInterface: + class: Pimcore\Bundle\StudioBackendBundle\Note\Extractor\NoteDataExtractor + + Pimcore\Bundle\StudioBackendBundle\Note\Repository\NoteRepositoryInterface: + class: Pimcore\Bundle\StudioBackendBundle\Note\Repository\NoteRepository + + Pimcore\Bundle\StudioBackendBundle\Note\Service\NoteServiceInterface: + class: Pimcore\Bundle\StudioBackendBundle\Note\Service\NoteService + + Pimcore\Bundle\StudioBackendBundle\Note\Service\FilterServiceInterface: + class: Pimcore\Bundle\StudioBackendBundle\Note\Service\FilterService diff --git a/src/DependencyInjection/PimcoreStudioBackendExtension.php b/src/DependencyInjection/PimcoreStudioBackendExtension.php index ed30670d9..57d0652b2 100644 --- a/src/DependencyInjection/PimcoreStudioBackendExtension.php +++ b/src/DependencyInjection/PimcoreStudioBackendExtension.php @@ -59,6 +59,7 @@ public function load(array $configs, ContainerBuilder $container): void $loader->load('factories.yaml'); $loader->load('filters.yaml'); $loader->load('icon.yaml'); + $loader->load('notes.yaml'); $loader->load('open_api.yaml'); $loader->load('properties.yaml'); $loader->load('security.yaml'); diff --git a/src/Exception/ElementNotFoundException.php b/src/Exception/ElementNotFoundException.php index 5beb6fa31..5cd915bb1 100644 --- a/src/Exception/ElementNotFoundException.php +++ b/src/Exception/ElementNotFoundException.php @@ -21,8 +21,8 @@ */ final class ElementNotFoundException extends AbstractApiException { - public function __construct(int $id) + public function __construct(int $id, string $type = 'Element') { - parent::__construct(404, 'Element with ID ' . $id . ' not found'); + parent::__construct(404, sprintf('%s with ID %d not found', $type, $id)); } } diff --git a/src/Exception/InvalidFilterException.php b/src/Exception/InvalidFilterException.php new file mode 100644 index 000000000..9a377b18b --- /dev/null +++ b/src/Exception/InvalidFilterException.php @@ -0,0 +1,31 @@ +value)] + #[Get( + path: self::API_PATH . '/notes', + operationId: 'getNotes', + summary: 'Get notes', + security: self::SECURITY_SCHEME, + tags: [Tags::Notes->name] + )] + #[PageParameter] + #[PageSizeParameter(50)] + #[NoteSortByParameter] + #[SortOrderParameter] + #[FilterParameter('notes')] + #[FieldFilterParameter] + #[SuccessResponse( + description: 'Paginated assets with total count as header param', + content: new CollectionJson(new NoteCollection()) + )] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED + ])] + public function getNotes( + #[MapQueryString] NoteParameters $parameters = new NoteParameters() + ): JsonResponse + { + $collection = $this->noteService->listNotes(new NoteElement(), $parameters); + + return $this->getPaginatedCollection( + $this->serializer, + $collection->getItems(), + $collection->getTotalItems() + ); + } +} diff --git a/src/Note/Controller/DeleteController.php b/src/Note/Controller/DeleteController.php new file mode 100644 index 000000000..ea3adcafc --- /dev/null +++ b/src/Note/Controller/DeleteController.php @@ -0,0 +1,74 @@ +value)] + #[Delete( + path: self::API_PATH . '/notes/{id}', + operationId: 'deleteNote', + summary: 'Deleting note by id', + security: self::SECURITY_SCHEME, + tags: [Tags::Notes->name] + )] + #[IdParameter] + #[SuccessResponse( + description: 'Id of the note that got deleted', + content: new IdJson('ID of deleted note') + )] + #[DefaultResponses([ + HttpResponseCodes::NOT_FOUND, + HttpResponseCodes::UNAUTHORIZED + ])] + public function deleteNote(int $id): JsonResponse + { + $this->noteService->deleteNote($id); + return $this->jsonResponse(['id' => $id]); + } +} diff --git a/src/Note/Controller/Element/CollectionController.php b/src/Note/Controller/Element/CollectionController.php new file mode 100644 index 000000000..e82dba34d --- /dev/null +++ b/src/Note/Controller/Element/CollectionController.php @@ -0,0 +1,99 @@ +value)] + #[Get( + path: self::API_PATH . '/notes/{elementType}/{id}', + operationId: 'getNotesForElementByTypeAndId', + summary: 'Get notes for an element', + security: self::SECURITY_SCHEME, + tags: [Tags::NotesForElement->name] + )] + #[ElementTypeParameter] + #[IdParameter(type: 'element')] + #[PageParameter] + #[PageSizeParameter(50)] + #[NoteSortByParameter] + #[SortOrderParameter] + #[FilterParameter('notes')] + #[FieldFilterParameter] + #[SuccessResponse( + description: 'Paginated assets with total count as header param', + content: new CollectionJson(new NoteCollection()) + )] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED + ])] + public function getNotes( + string $elementType, + int $id, + #[MapQueryString] NoteParameters $parameters = new NoteParameters() + ): JsonResponse + { + $collection = $this->noteService->listNotes(new NoteElement($elementType, $id), $parameters); + + return $this->getPaginatedCollection( + $this->serializer, + $collection->getItems(), + $collection->getTotalItems() + ); + } +} diff --git a/src/Note/Controller/Element/CreateController.php b/src/Note/Controller/Element/CreateController.php new file mode 100644 index 000000000..8552721aa --- /dev/null +++ b/src/Note/Controller/Element/CreateController.php @@ -0,0 +1,78 @@ +value)] + #[Post( + path: self::API_PATH . '/notes/{elementType}/{id}', + operationId: 'createNoteForElement', + summary: 'Creating new note for element', + security: self::SECURITY_SCHEME, + tags: [Tags::NotesForElement->name] + )] + #[ElementTypeParameter] + #[IdParameter(type: 'element')] + #[CreateNoteRequestBody] + #[DefaultResponses([ + HttpResponseCodes::UNAUTHORIZED + ])] + public function createNote( + string $elementType, + int $id, + #[MapRequestPayload] CreateNote $createNote + ): JsonResponse + { + $note = $this->noteService->createNote(new NoteElement($elementType, $id), $createNote); + return $this->jsonResponse(['id' => $note->getId()]); + } +} diff --git a/src/Note/Extractor/NoteDataExtractor.php b/src/Note/Extractor/NoteDataExtractor.php new file mode 100644 index 000000000..6ecaaabfa --- /dev/null +++ b/src/Note/Extractor/NoteDataExtractor.php @@ -0,0 +1,108 @@ +getCid() || !$note->getCtype()) { + return ''; + } + $element = $this->serviceResolver->getElementById($note->getCtype(), $note->getCid()); + + if (!$element) { + return ''; + } + + return $element->getRealFullPath(); + } + + public function extractUserData(CoreNote $note) : NoteUser + { + $emptyUser = new NoteUser(); + if (!$note->getUser()) { + return $emptyUser; + } + + $user = $this->userResolver->getById($note->getUser()); + + if (!$user) { + return $emptyUser; + } + + return new NoteUser( + $user->getId(), + $user->getName(), + ); + } + + public function extractData(CoreNote $note): array + { + // prepare key-values + $keyValues = []; + foreach ($note->getData() as $name => $d) { + + $type = $d['type']; + + $data = match($type) { + 'document', 'object', 'asset' => $this->extractElementData($d['data']), + 'date' => is_object($d['data']) ? $d['data']->getTimestamp() : $d['data'], + default => $d['data'], + }; + + $keyValue = [ + 'type' => $type, + 'name' => $name, + 'data' => $data, + ]; + + $keyValues[] = $keyValue; + } + + return $keyValues; + } + + private function extractElementData(?ElementInterface $element): array + { + if (!$element) { + return []; + } + + return [ + 'id' => $element->getId(), + 'path' => $element->getRealFullPath(), + 'type' => $element->getType(), + ]; + } +} diff --git a/src/Note/Extractor/NoteDataExtractorInterface.php b/src/Note/Extractor/NoteDataExtractorInterface.php new file mode 100644 index 000000000..9011cc8ca --- /dev/null +++ b/src/Note/Extractor/NoteDataExtractorInterface.php @@ -0,0 +1,32 @@ +extractor->extractUserData($note); + + return new Note( + $note->getId(), + $note->getType(), + $note->getCid(), + $note->getCtype(), + $this->extractor->extractCPath($note), + $note->getDate(), + $note->getTitle(), + $note->getDescription(), + $note->getLocked(), + $this->extractor->extractData($note), + $noteUser->getId(), + $noteUser->getName(), + ); + } +} diff --git a/src/Note/Hydrator/NoteHydratorInterface.php b/src/Note/Hydrator/NoteHydratorInterface.php new file mode 100644 index 000000000..e8a575d96 --- /dev/null +++ b/src/Note/Hydrator/NoteHydratorInterface.php @@ -0,0 +1,28 @@ +setCid($noteElement->getId()); + $note->setCtype($noteElement->getType()); + $note->setDate(time()); + $note->setTitle($createNote->getTitle()); + $note->setDescription($createNote->getDescription()); + $note->setType($createNote->getType()); + $note->setLocked(false); + + try { + $note->save(); + } catch (Exception $e) { + throw new ElementSavingFailedException(0, $e->getTraceAsString()); + } + + return $note; + } + + public function getNote(int $id): Note + { + return $this->noteResolver->getById($id); + } + + public function listNotes(NoteElement $noteElement, NoteParameters $parameters): NoteListing + { + $list = new NoteListing(); + + $list->setOrderKey(['date', 'id']); + $list->setOrder(['DESC', 'DESC']); + + $list->setLimit($parameters->getPageSize()); + $list->setOffset($parameters->getOffset()); + + if ($parameters->getSortBy() && $parameters->getSortOrder()) { + $list->setOrderKey($parameters->getSortBy()); + $list->setOrder($parameters->getSortOrder()); + } + + $this->filterService->applyFilter($list, $parameters); + + $this->filterService->applyFieldFilters($list, $parameters); + + $this->filterService->applyElementFilter($list, $noteElement); + + return $list; + } + + /** + * @throws ElementNotFoundException + */ + public function deleteNote(int $id): void + { + $note = $this->noteResolver->getById($id); + if (!$note) { + throw new ElementNotFoundException($id, 'Note'); + } + $note->delete(); + } +} diff --git a/src/Note/Repository/NoteRepositoryInterface.php b/src/Note/Repository/NoteRepositoryInterface.php new file mode 100644 index 000000000..34c53b986 --- /dev/null +++ b/src/Note/Repository/NoteRepositoryInterface.php @@ -0,0 +1,45 @@ +type; + } + + public function getId(): ?int + { + return $this->id; + } +} diff --git a/src/Note/Request/NoteParameters.php b/src/Note/Request/NoteParameters.php new file mode 100644 index 000000000..865e0a19c --- /dev/null +++ b/src/Note/Request/NoteParameters.php @@ -0,0 +1,66 @@ +sortBy; + } + + public function getSortOrder(): ?string + { + return $this->sortOrder; + } + + public function getFilter(): ?string + { + return $this->filter; + } + + /** + * @throws JsonException + */ + public function getFieldFilters(): ?array + { + return $this->fieldFilters === null ? null : + json_decode( + $this->fieldFilters, + true, + 512, + JSON_THROW_ON_ERROR + ); + } +} diff --git a/src/Note/Response/Collection.php b/src/Note/Response/Collection.php new file mode 100644 index 000000000..475d165a7 --- /dev/null +++ b/src/Note/Response/Collection.php @@ -0,0 +1,56 @@ +totalItems; + } + + /** + * @return array + */ + public function getItems(): array + { + return $this->items; + } + + public function getCurrentPage(): int + { + return $this->currentPage; + } + + public function getPageSize(): int + { + return $this->pageSize; + } +} diff --git a/src/Note/Schema/CreateNote.php b/src/Note/Schema/CreateNote.php new file mode 100644 index 000000000..86821ef20 --- /dev/null +++ b/src/Note/Schema/CreateNote.php @@ -0,0 +1,56 @@ +title; + } + + public function getDescription(): string + { + return $this->description; + } + + public function getType(): string + { + return $this->type; + } +} diff --git a/src/Note/Schema/Note.php b/src/Note/Schema/Note.php new file mode 100644 index 000000000..e0fc0d893 --- /dev/null +++ b/src/Note/Schema/Note.php @@ -0,0 +1,138 @@ +id; + } + + public function getType(): string + { + return $this->type; + } + + public function getCId(): int + { + return $this->cId; + } + + public function getCType(): string + { + return $this->cType; + } + + public function getCPath(): string + { + return $this->cPath; + } + + public function getDate(): int + { + return $this->date; + } + + public function getTitle(): string + { + return $this->title; + } + + public function getDescription(): string + { + return $this->description; + } + + public function isLocked(): bool + { + return $this->locked; + } + + public function getData(): array + { + return $this->data; + } + + public function getUserId(): ?int + { + return $this->userId; + } + + public function getUserName(): ?string + { + return $this->userName; + } +} diff --git a/src/Note/Schema/NoteUser.php b/src/Note/Schema/NoteUser.php new file mode 100644 index 000000000..9bc684f26 --- /dev/null +++ b/src/Note/Schema/NoteUser.php @@ -0,0 +1,41 @@ +id; + } + + public function getName(): ?string + { + return $this->name; + } +} diff --git a/src/Note/Service/FilterService.php b/src/Note/Service/FilterService.php new file mode 100644 index 000000000..2c21bb6f9 --- /dev/null +++ b/src/Note/Service/FilterService.php @@ -0,0 +1,136 @@ +getFilter()) { + $list->addConditionParam( + $this->createFilterCondition(), + ['filter' => '%' . $parameters->getFilter() . '%'] + ); + } + } + + public function applyFieldFilters(NoteListing $list, NoteParameters $parameters): void + { + try { + if (empty($parameters->getFieldFilters())) { + return; + } + + $propertyKey = 'field'; + + foreach ($parameters->getFieldFilters() as $filter) { + $operator = $this->findOperator($filter['type'], $filter['operator']); + $value = $this->prepareValue($filter['type'], $filter['operator'], $filter['value']); + + if ($operator === 'LIKE') { + $value = '%' . $value . '%'; + } + + if ($filter[$propertyKey] === 'user') { + $list->addConditionParam( + '`user` IN (SELECT `id` FROM `users` WHERE `name` ' . $operator . ' :user)', + ['user' => $value] + ); + } + + if ($filter['type'] === 'date' && $filter['operator'] === 'eq') { + $maxTime = $value + (86400 - 1); //specifies the top point of the range used in the condition + $dateCondition = '`' . $filter[$propertyKey] . '` ' . ' BETWEEN :minTime AND :maxTime'; + $list->addConditionParam($dateCondition, ['minTime' => $value, 'maxTime' => $maxTime]); + } else { + $list->addConditionParam( + '`' . $filter[$propertyKey] . '` ' . $operator . ' :' . $filter[$propertyKey], + [$filter[$propertyKey] => $value] + ); + } + } + } catch (Exception) { + throw new InvalidFilterException('fieldFilters'); + } + } + + public function applyElementFilter(NoteListing $list, NoteElement $noteElement): void + { + if ($noteElement->getId() && $noteElement->getType()) { + $list->addConditionParam( + '(cid = :id AND ctype = :type)', + ['id' => $noteElement->getId(), 'type' => $noteElement->getType()] + ); + } + } + + private function prepareValue(string $type, string $operator, mixed $value): mixed + { + return match ($type) { + 'date' => strtotime($value), + default => $this->matchValueOperator($operator, $value) + }; + } + + private function matchValueOperator(string $operator, mixed $value): mixed + { + return match ($operator) { + 'boolean' => (int)$value, + default => $value + }; + } + + private function createFilterCondition(): string + { + return '(' + . '`title` LIKE :filter' + . ' OR `description` LIKE :filter' + . ' OR `type` LIKE :filter' + . ' OR `user` IN (SELECT `id` FROM `users` WHERE `name` LIKE :filter)' + . " OR DATE_FORMAT(FROM_UNIXTIME(`date`), '%Y-%m-%d') LIKE :filter" + . ')'; + } + + private function findOperator(string $type, string $operator): string + { + return match ($type) { + 'string' => 'LIKE', + 'numeric', 'date' => $this->matchNumericOperator($operator), + default => '=' + }; + } + + private function matchNumericOperator(string $operator): string + { + return match ($operator) { + 'lt' => '<', + 'lte' => '<=', + 'gt' => '>', + 'gte' => '>=', + default => '=' + }; + } +} diff --git a/src/Note/Service/FilterServiceInterface.php b/src/Note/Service/FilterServiceInterface.php new file mode 100644 index 000000000..081ce15f2 --- /dev/null +++ b/src/Note/Service/FilterServiceInterface.php @@ -0,0 +1,33 @@ +noteRepository->createNote($noteElement, $createNote); + return $this->getNote($note->getId()); + } + + public function listNotes(NoteElement $noteElement, NoteParameters $parameters): Collection + { + $noteListing = $this->noteRepository->listNotes($noteElement, $parameters); + $notes = []; + foreach ($noteListing as $note) { + $notes[] = $this->noteHydrator->hydrate($note); + } + + return new Collection( + $notes, + $parameters->getPage(), + $parameters->getPageSize(), + $noteListing->getTotalCount() + ); + } + + /** + * @throws ElementNotFoundException + */ + public function deleteNote(int $id): void + { + $this->noteRepository->deleteNote($id); + } + + private function getNote(int $id): Note + { + return $this->noteHydrator->hydrate($this->noteRepository->getNote($id)); + } +} diff --git a/src/Note/Service/NoteServiceInterface.php b/src/Note/Service/NoteServiceInterface.php new file mode 100644 index 000000000..8512bd6b0 --- /dev/null +++ b/src/Note/Service/NoteServiceInterface.php @@ -0,0 +1,43 @@ +generateDescription($codes); + + parent::__construct( + response: 'default', + description: $description, + content: new JsonContent( + oneOf: array_map(static function ($class) { + return new Schema(ref: $class); + }, Schemas::ERRORS), + ) + ); + } + + private function generateDescription(array $errorCodes): string + { + // merge the default error codes with the provided ones + $errorCodes = array_merge($this->defaultErrorCodes, $errorCodes); + + // Sort the array of enums by http status code + usort($errorCodes, static function ($a, $b) { + return $a->value <=> $b->value; + }); + + // Generate description block of http codes + $errorCodes = array_map(function ($code) { + return sprintf('%s - %s', $code->value, $this->generateNiceName($code->name)); + }, $errorCodes); + + return implode('
', $errorCodes); + } + + private function generateNiceName(string $name): string + { + return ucwords(str_replace('_', ' ', strtolower($name))); + } +} diff --git a/src/OpenApi/Config/Tags.php b/src/OpenApi/Config/Tags.php index c1a8483cb..3c4babb42 100644 --- a/src/OpenApi/Config/Tags.php +++ b/src/OpenApi/Config/Tags.php @@ -37,6 +37,14 @@ name: Tags::Dependencies->name, description: 'Get dependencies for a single element.' )] +#[Tag( + name: Tags::Notes->name, + description: 'Note operations to list/delete notes' +)] +#[Tag( + name: Tags::NotesForElement->name, + description: 'Note operations to create/list notes for an element' +)] #[Tag( name: Tags::Properties->name, description: 'Property operations to get/update/create/delete properties' @@ -59,6 +67,9 @@ enum Tags: string case Authorization = 'Authorization'; case DataObjects = 'DataObjects'; case Dependencies = 'Dependencies'; + case Notes = 'Notes'; + + case NotesForElement = 'Notes for Element'; case Properties = 'Properties'; case PropertiesForElement = 'Properties for Element'; case Translation = 'Translation'; diff --git a/src/Property/Controller/CreateController.php b/src/Property/Controller/CreateController.php index c44cc9ed4..de00431ab 100644 --- a/src/Property/Controller/CreateController.php +++ b/src/Property/Controller/CreateController.php @@ -57,7 +57,7 @@ public function __construct( tags: [Tags::Properties->name] )] #[SuccessResponse( - description: 'Element Properties data as json', + description: 'Created predefined property', content: new JsonContent(ref: PredefinedProperty::class, type: 'object') )] #[BadRequestResponse] diff --git a/src/Property/Controller/DeleteController.php b/src/Property/Controller/DeleteController.php index d56682570..da3a777ea 100644 --- a/src/Property/Controller/DeleteController.php +++ b/src/Property/Controller/DeleteController.php @@ -60,7 +60,7 @@ public function __construct( )] #[IdParameter(type: 'property', schema: new Schema(type: 'string', example: 'alpha-numerical'))] #[SuccessResponse( - description: 'Element Properties data as json', + description: 'Id of deleted property', content: new IdJson('ID of deleted property') )] #[UnauthorizedResponse] diff --git a/src/Property/Controller/Element/UpdateController.php b/src/Property/Controller/Element/UpdateController.php index e066fef11..2f3af7d25 100644 --- a/src/Property/Controller/Element/UpdateController.php +++ b/src/Property/Controller/Element/UpdateController.php @@ -62,7 +62,7 @@ public function __construct( #[IdParameter(type: 'element')] #[ElementPropertyRequestBody] #[SuccessResponse( - description: 'Element Properties data as json', + description: 'Updated Element Properties data as json', content: new ItemsJson(ElementProperty::class) )] #[UnauthorizedResponse] diff --git a/src/Property/Controller/UpdateController.php b/src/Property/Controller/UpdateController.php index d5809d184..3a4ae6354 100644 --- a/src/Property/Controller/UpdateController.php +++ b/src/Property/Controller/UpdateController.php @@ -64,7 +64,7 @@ public function __construct( #[IdParameter(type: 'property', schema: new Schema(type: 'string', example: 'alpha-numerical'))] #[PredefinedPropertyRequestBody] #[SuccessResponse( - description: 'Updated property', + description: 'Updated predefined property', content: new JsonContent(ref: PredefinedProperty::class, type: 'object') )] #[BadRequestResponse] diff --git a/src/Request/CollectionParameters.php b/src/Request/CollectionParameters.php index 6d0a7233b..2ba86b1b2 100644 --- a/src/Request/CollectionParameters.php +++ b/src/Request/CollectionParameters.php @@ -46,6 +46,11 @@ public function getPageSize(): int return $this->pageSize; } + public function getOffset(): int + { + return ($this->page - 1) * $this->pageSize; + } + private function validate(): void { new PositiveInteger($this->page); diff --git a/src/Util/Constants/HttpResponseCodes.php b/src/Util/Constants/HttpResponseCodes.php new file mode 100644 index 000000000..04fb1c7da --- /dev/null +++ b/src/Util/Constants/HttpResponseCodes.php @@ -0,0 +1,33 @@ +filterService = new FilterService(); + } + + public function testApplyFilter(): void + { + $noteListing = $this->getNoteListing(); + $noteParameters = new NoteParameters( + filter: 'test' + ); + $this->filterService->applyFilter($noteListing, $noteParameters); + + $this->assertSame( + "((`title` LIKE :filter OR `description` LIKE :filter OR `type` LIKE :filter OR `user` IN (SELECT `id` FROM `users` WHERE `name` LIKE :filter) OR DATE_FORMAT(FROM_UNIXTIME(`date`), '%Y-%m-%d') LIKE :filter)) ", + $noteListing->getCondition()); + + $this->assertSame( + ['filter' => '%test%'], + $noteListing->getConditionVariables() + ); + } + + /** + * @throws JsonException + */ + public function testApplyFieldFiltersDate(): void + { + $noteListing = $this->getNoteListing(); + $noteParameters = new NoteParameters( + fieldFilters: json_encode([ + [ + 'field' => 'date', + 'type' => 'date', + 'operator' => 'eq', + 'value' => '05/04/2024', + ], + ], JSON_THROW_ON_ERROR) + ); + $this->filterService->applyFieldFilters($noteListing, $noteParameters); + + $this->assertSame('(`date` BETWEEN :minTime AND :maxTime) ', $noteListing->getCondition()); + $this->assertSame( + [ + 'minTime' => 1714780800, + 'maxTime' => 1714867199, + ], + $noteListing->getConditionVariables() + ); + } + + /** + * @throws JsonException + */ + public function testApplyFieldFiltersNumeric(): void + { + $noteListing = $this->getNoteListing(); + $noteParameters = new NoteParameters( + fieldFilters: json_encode([ + [ + 'field' => 'numeric', + 'type' => 'numeric', + 'operator' => 'eq', + 'value' => 10, + ], + ], JSON_THROW_ON_ERROR) + ); + $this->filterService->applyFieldFilters($noteListing, $noteParameters); + + $this->assertSame('(`numeric` = :numeric) ', $noteListing->getCondition()); + $this->assertSame( + [ + 'numeric' => 10, + ], + $noteListing->getConditionVariables() + ); + } + + /** + * @throws JsonException + */ + public function testApplyFieldFiltersBoolean(): void + { + $noteListing = $this->getNoteListing(); + $noteParameters = new NoteParameters( + fieldFilters: json_encode([ + [ + 'field' => 'boolean', + 'type' => 'boolean', + 'operator' => 'boolean', + 'value' => true, + ], + ], JSON_THROW_ON_ERROR) + ); + $this->filterService->applyFieldFilters($noteListing, $noteParameters); + + $this->assertSame('(`boolean` = :boolean) ', $noteListing->getCondition()); + $this->assertSame( + [ + 'boolean' => 1, + ], + $noteListing->getConditionVariables() + ); + } + + /** + * @throws JsonException + */ + public function testApplyFieldFiltersList(): void + { + $noteListing = $this->getNoteListing(); + $noteParameters = new NoteParameters( + fieldFilters: json_encode([ + [ + 'field' => 'list', + 'type' => 'list', + 'operator' => 'list', + 'value' => 'list', + ], + ], JSON_THROW_ON_ERROR) + ); + $this->filterService->applyFieldFilters($noteListing, $noteParameters); + + $this->assertSame('(`list` = :list) ', $noteListing->getCondition()); + $this->assertSame( + [ + 'list' => 'list', + ], + $noteListing->getConditionVariables() + ); + } + + /** + * @throws JsonException + */ + public function testApplyFieldFiltersUser(): void + { + $noteListing = $this->getNoteListing(); + $noteParameters = new NoteParameters( + fieldFilters: json_encode([ + [ + 'field' => 'user', + 'type' => 'user', + 'operator' => 'user', + 'value' => 'admin', + ], + ], JSON_THROW_ON_ERROR) + ); + $this->filterService->applyFieldFilters($noteListing, $noteParameters); + + $this->assertSame( + '(`user` IN (SELECT `id` FROM `users` WHERE `name` = :user)) AND (`user` = :user) ', + $noteListing->getCondition() + ); + $this->assertSame( + [ + 'user' => 'admin', + ], + $noteListing->getConditionVariables() + ); + } + + /** + * @throws JsonException + */ + public function testApplyFieldFiltersInvalidJson(): void + { + $noteListing = $this->getNoteListing(); + $noteParameters = new NoteParameters( + fieldFilters: 'invalid' + ); + + $this->expectException(InvalidFilterException::class); + $this->expectExceptionMessage('Invalid filter: fieldFilters'); + + $this->filterService->applyFieldFilters($noteListing, $noteParameters); + } + + private function getNoteListing(): NoteListing + { + return new NoteListing(); + } +}