diff --git a/projects/backend/src/Aggregator/Application/ReadModel/ArticleForExport.php b/projects/backend/src/Aggregator/Application/ReadModel/ArticleForExport.php index 046e4d1..2b96f5f 100644 --- a/projects/backend/src/Aggregator/Application/ReadModel/ArticleForExport.php +++ b/projects/backend/src/Aggregator/Application/ReadModel/ArticleForExport.php @@ -30,7 +30,7 @@ final readonly class ArticleForExport public static function create(array $item): self { return new self( - ArticleId::fromBinary($item['article_id']), + ArticleId::fromString(DataMapping::string($item, 'article_id')), DataMapping::string($item, 'article_title'), DataMapping::string($item, 'article_link'), DataMapping::string($item, 'article_categories'), diff --git a/projects/backend/src/Aggregator/Application/ReadModel/SourceStatistics.php b/projects/backend/src/Aggregator/Application/ReadModel/SourceStatistics.php index 430bff1..722a864 100644 --- a/projects/backend/src/Aggregator/Application/ReadModel/SourceStatistics.php +++ b/projects/backend/src/Aggregator/Application/ReadModel/SourceStatistics.php @@ -26,10 +26,10 @@ final readonly class SourceStatistics public static function create(array $item): self { return new self( - SourceId::fromBinary($item['source_id']), + SourceId::fromString(DataMapping::string($item, 'source_id')), DataMapping::string($item, 'source_name'), DataMapping::integer($item, 'articles_count'), - DataMapping::integer($item, 'article_metadata_available'), + DataMapping::integer($item, 'articles_metadata_available'), DataMapping::nullableDatetime($item, 'source_crawled_at') ); } diff --git a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetArticlesForExportDbalHandler.php b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetArticlesForExportDbalHandler.php index 5ea2369..cd6250a 100644 --- a/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetArticlesForExportDbalHandler.php +++ b/projects/backend/src/Aggregator/Infrastructure/Persistence/Doctrine/DBAL/GetArticlesForExportDbalHandler.php @@ -41,7 +41,8 @@ final readonly class GetArticlesForExportDbalHandler implements GetArticlesForEx ) ->from('article', 'a') ->innerJoin('a', 'source', 's', 'a.source_id = s.id') - ->orderBy('a.published_at', 'DESC'); + ->orderBy('a.published_at', 'DESC') + ->addOrderBy('a.id', 'DESC'); if ($query->source !== null) { $qb->andWhere('s.name = :source') diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/ArticleDetails.php b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleDetails.php index f5f1cf8..5e41d8e 100644 --- a/projects/backend/src/FeedManagement/Application/ReadModel/ArticleDetails.php +++ b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleDetails.php @@ -44,7 +44,7 @@ final readonly class ArticleDetails public static function create(array $item): self { return new self( - ArticleId::fromBinary($item['article_id']), + ArticleId::fromString(DataMapping::string($item, 'article_id')), DataMapping::string($item, 'article_title'), Link::from(DataMapping::string($item, 'article_link')), explode(',', DataMapping::string($item, 'article_categories')), diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverview.php b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverview.php index 23594b5..b06e163 100644 --- a/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverview.php +++ b/projects/backend/src/FeedManagement/Application/ReadModel/ArticleOverview.php @@ -33,7 +33,7 @@ final readonly class ArticleOverview public static function create(array $item): self { return new self( - ArticleId::fromBinary($item['article_id']), + ArticleId::fromString(DataMapping::string($item, 'article_id')), DataMapping::string($item, 'article_title'), Link::from(DataMapping::string($item, 'article_link')), explode(',', DataMapping::string($item, 'article_categories')), diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/Bookmark.php b/projects/backend/src/FeedManagement/Application/ReadModel/Bookmark.php index ef0ec8f..cb687d2 100644 --- a/projects/backend/src/FeedManagement/Application/ReadModel/Bookmark.php +++ b/projects/backend/src/FeedManagement/Application/ReadModel/Bookmark.php @@ -28,7 +28,7 @@ final readonly class Bookmark public static function create(array $item): self { return new self( - BookmarkId::fromBinary($item['bookmark_id']), + BookmarkId::fromString(DataMapping::string($item, 'bookmark_id')), DataMapping::string($item, 'bookmark_name'), DataMapping::datetime($item, 'bookmark_created_at'), DataMapping::nullableString($item, 'bookmark_description'), diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/Comment.php b/projects/backend/src/FeedManagement/Application/ReadModel/Comment.php index 1c0d21a..7f98b04 100644 --- a/projects/backend/src/FeedManagement/Application/ReadModel/Comment.php +++ b/projects/backend/src/FeedManagement/Application/ReadModel/Comment.php @@ -27,7 +27,7 @@ final readonly class Comment public static function create(array $item): self { return new self( - CommentId::fromBinary($item['comment_id']), + CommentId::fromString(DataMapping::string($item, 'comment_id')), UserReference::create($item), DataMapping::enum($item, 'comment_sentiment', Sentiment::class), DataMapping::string($item, 'comment_content'), diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/SourceDetails.php b/projects/backend/src/FeedManagement/Application/ReadModel/SourceDetails.php index 64149f6..8ae3b37 100644 --- a/projects/backend/src/FeedManagement/Application/ReadModel/SourceDetails.php +++ b/projects/backend/src/FeedManagement/Application/ReadModel/SourceDetails.php @@ -39,7 +39,7 @@ final readonly class SourceDetails public static function create(array $item, PublicationGraph $publicationGraph, CategoryShares $categoryShares): self { return new self( - SourceId::fromBinary($item['source_id']), + SourceId::fromString(DataMapping::string($item, 'source_id')), DataMapping::string($item, 'source_name'), DataMapping::string($item, 'source_url'), new Credibility( diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverview.php b/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverview.php index b36e3ca..1b59beb 100644 --- a/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverview.php +++ b/projects/backend/src/FeedManagement/Application/ReadModel/SourceOverview.php @@ -27,7 +27,7 @@ final readonly class SourceOverview public static function create(array $item): self { return new self( - SourceId::fromBinary($item['source_id']), + SourceId::fromString(DataMapping::string($item, 'source_id')), DataMapping::string($item, 'source_name'), DataMapping::string($item, 'source_url'), DataMapping::nullableString($item, 'source_display_name'), diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/SourceReference.php b/projects/backend/src/FeedManagement/Application/ReadModel/SourceReference.php index a83061e..45929f7 100644 --- a/projects/backend/src/FeedManagement/Application/ReadModel/SourceReference.php +++ b/projects/backend/src/FeedManagement/Application/ReadModel/SourceReference.php @@ -26,7 +26,7 @@ final readonly class SourceReference public static function create(array $item): self { return new self( - SourceId::fromBinary($item['source_id']), + SourceId::fromString(DataMapping::string($item, 'source_id')), DataMapping::string($item, 'source_name'), DataMapping::nullableString($item, 'source_display_name'), DataMapping::nullableString($item, 'source_image'), diff --git a/projects/backend/src/FeedManagement/Application/ReadModel/UserReference.php b/projects/backend/src/FeedManagement/Application/ReadModel/UserReference.php index 0952f48..cb1a190 100644 --- a/projects/backend/src/FeedManagement/Application/ReadModel/UserReference.php +++ b/projects/backend/src/FeedManagement/Application/ReadModel/UserReference.php @@ -23,7 +23,7 @@ final readonly class UserReference public static function create(array $item): self { return new self( - UserId::fromBinary($item['user_id']), + UserId::fromString(DataMapping::string($item, 'user_id')), DataMapping::string($item, 'user_name') ); } diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleCommentListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleCommentListDbalHandler.php index bd34d64..9c72fc4 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleCommentListDbalHandler.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleCommentListDbalHandler.php @@ -39,8 +39,7 @@ final readonly class GetArticleCommentListDbalHandler implements GetArticleComme ->from('comment', 'c') ->innerJoin('c', 'user', 'u', 'c.user_id = u.id') ->where('c.article_id = :articleId') - ->orderBy('c.created_at', 'DESC') - ->setParameter('articleId', $query->articleId->toRfc4122()); + ->setParameter('articleId', $query->articleId->toString()); $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('c.id', 'c.created_at')); diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleDetailsDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleDetailsDbalHandler.php index 562f278..b6024a7 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleDetailsDbalHandler.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleDetailsDbalHandler.php @@ -41,8 +41,8 @@ final readonly class GetArticleDetailsDbalHandler implements GetArticleDetailsHa $qb->innerJoin('a', 'source', 's', 'a.source_id = s.id') ->from('article', 'a') ->where('a.id = :articleId') - ->setParameter('articleId', $query->id->toRfc4122()) - ->setParameter('userId', $query->userId->toRfc4122()) + ->setParameter('articleId', $query->id->toString()) + ->setParameter('userId', $query->userId->toString()) ; try { diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleOverviewListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleOverviewListDbalHandler.php index 8ded803..1105190 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleOverviewListDbalHandler.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetArticleOverviewListDbalHandler.php @@ -43,11 +43,16 @@ final readonly class GetArticleOverviewListDbalHandler implements GetArticleOver $qb->from('article', 'a') ->innerJoin('a', 'source', 's', 'a.source_id = s.id') //->orderBy('a.published_at', $query->filters->sortDirection->value) - ->setParameter('userId', $query->userId->toRfc4122()) + ->setParameter('userId', $query->userId->toString()) ; $qb = $this->applyArticleFilters($qb, $query->filters); - $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('a.id', 'a.published_at')); + $qb = $this->applyCursorPagination( + $qb, + $query->page, + new PaginatorKeyset('a.id', 'a.published_at'), + $query->filters->sortDirection + ); try { $data = $qb->executeQuery()->fetchAllAssociative(); diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkListDbalHandler.php index 94ae06e..22bbdde 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkListDbalHandler.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkListDbalHandler.php @@ -37,8 +37,7 @@ final readonly class GetBookmarkListDbalHandler implements GetBookmarkListHandle ->leftJoin('b', 'bookmark_article', 'ba', 'ba.bookmark_id = b.id') ->where('b.user_id = :userId') ->groupBy('b.id') - ->orderBy('b.id', 'DESC') - ->setParameter('userId', $query->userId->toRfc4122()) + ->setParameter('userId', $query->userId->toString()) ; $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('b.id')); diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkedArticleListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkedArticleListDbalHandler.php index b66be25..157cd4d 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkedArticleListDbalHandler.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetBookmarkedArticleListDbalHandler.php @@ -43,12 +43,17 @@ final readonly class GetBookmarkedArticleListDbalHandler implements GetBookmarke ->innerJoin('ba', 'bookmark', 'b', 'b.id = ba.bookmark_id AND b.user_id = :userId') ->innerJoin('a', 'source', 's', 'a.source_id = s.id') ->where('b.id = :bookmarkId') - ->setParameter('bookmarkId', $query->bookmarkId->toRfc4122()) - ->setParameter('userId', $query->userId->toRfc4122()) + ->setParameter('bookmarkId', $query->bookmarkId->toString()) + ->setParameter('userId', $query->userId->toString()) ; $qb = $this->applyArticleFilters($qb, $query->filters); - $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('a.id', 'a.published_at')); + $qb = $this->applyCursorPagination( + $qb, + $query->page, + new PaginatorKeyset('a.id', 'a.published_at'), + $query->filters->sortDirection + ); try { $data = $qb->executeQuery()->fetchAllAssociative(); diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceArticleOverviewListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceArticleOverviewListDbalHandler.php index 75befd0..2cddd11 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceArticleOverviewListDbalHandler.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceArticleOverviewListDbalHandler.php @@ -43,13 +43,17 @@ final readonly class GetSourceArticleOverviewListDbalHandler implements GetSourc $qb->from('article', 'a') ->innerJoin('a', 'source', 's', 'a.source_id = s.id') ->where('s.id = :sourceId') - ->orderBy('a.published_at', $query->filters->sortDirection->value) - ->setParameter('userId', $query->userId->toRfc4122()) - ->setParameter('sourceId', $query->sourceId->toRfc4122()) + ->setParameter('userId', $query->userId->toString()) + ->setParameter('sourceId', $query->sourceId->toString()) ; $qb = $this->applyArticleFilters($qb, $query->filters); - $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('a.id', 'a.published_at')); + $qb = $this->applyCursorPagination( + $qb, + $query->page, + new PaginatorKeyset('a.id', 'a.published_at'), + $query->filters->sortDirection + ); try { $data = $qb->executeQuery()->fetchAllAssociative(); diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceDetailsDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceDetailsDbalHandler.php index 50b7901..5f01a88 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceDetailsDbalHandler.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceDetailsDbalHandler.php @@ -49,8 +49,8 @@ final readonly class GetSourceDetailsDbalHandler implements GetSourceDetailsHand $qb->from('source', 's') ->leftJoin('s', 'article', 'a', 'a.source_id = s.id') ->where('s.id = :sourceId') - ->setParameter('sourceId', $query->sourceId->toRfc4122()) - ->setParameter('userId', $query->userId->toRfc4122()); + ->setParameter('sourceId', $query->sourceId->toString()) + ->setParameter('userId', $query->userId->toString()); // Aggregate columns are selected; include non-aggregated columns in GROUP BY for PostgreSQL $qb->groupBy('s.id, s.name, s.description, s.url, s.updated_at, s.display_name, s.bias, s.reliability, s.transparency'); @@ -84,7 +84,7 @@ final readonly class GetSourceDetailsDbalHandler implements GetSourceDetailsHand ->andWhere('a.published_at BETWEEN to_timestamp(:start) AND to_timestamp(:end)') ->groupBy('day') ->orderBy('day', 'ASC') - ->setParameter('sourceId', $query->sourceId->toRfc4122()) + ->setParameter('sourceId', $query->sourceId->toString()) ->setParameter('start', $dateRange->start, ParameterType::INTEGER) ->setParameter('end', $dateRange->end, ParameterType::INTEGER) ->enableResultCache(new QueryCacheProfile(SourceCacheAttributes::CACHE_TTL, $cacheKey)); @@ -126,7 +126,7 @@ final readonly class GetSourceDetailsDbalHandler implements GetSourceDetailsHand ->from('article', 'a') ->innerJoin('a', 'source', 's', 'a.source_id = s.id') ->where('s.id = :sourceId') - ->setParameter('sourceId', $query->sourceId->toRfc4122()) + ->setParameter('sourceId', $query->sourceId->toString()) ->enableResultCache(new QueryCacheProfile(SourceCacheAttributes::CACHE_TTL, $cacheKey)); try { diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceOverviewListDbalHandler.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceOverviewListDbalHandler.php index 5f5ab1e..d215b86 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceOverviewListDbalHandler.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/GetSourceOverviewListDbalHandler.php @@ -36,7 +36,7 @@ final readonly class GetSourceOverviewListDbalHandler implements GetSourceOvervi $qb = $this->addFollowedSourceExistsQuery($qb); $qb->from('source', 's') - ->setParameter('userId', $query->userId->toRfc4122()) + ->setParameter('userId', $query->userId->toString()) ; $qb = $this->applyCursorPagination($qb, $query->page, new PaginatorKeyset('s.id', 's.created_at')); @@ -47,7 +47,7 @@ final readonly class GetSourceOverviewListDbalHandler implements GetSourceOvervi throw NoResult::forQuery($qb->getSQL(), $qb->getParameters(), $e); } - $pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('source_id')); + $pagination = $this->createPaginationInfo($data, $query->page, new PaginatorKeyset('source_id', 'source_created_at')); return SourceOverviewList::create($data, $pagination); } } diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/SourceQuery.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/SourceQuery.php index a7d97c6..6f9a472 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/SourceQuery.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/DBAL/Queries/SourceQuery.php @@ -22,6 +22,7 @@ trait SourceQuery "CONCAT('https://devscast.org/images/sources/', s.name, '.png') as source_image", 's.url as source_url', 's.name as source_name', + 's.created_at as source_created_at', ); } diff --git a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/FollowedSourceOrmRepository.php b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/FollowedSourceOrmRepository.php index e1cf086..e677977 100644 --- a/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/FollowedSourceOrmRepository.php +++ b/projects/backend/src/FeedManagement/Infrastructure/Persistence/Doctrine/ORM/FollowedSourceOrmRepository.php @@ -42,8 +42,8 @@ final class FollowedSourceOrmRepository extends ServiceEntityRepository implemen return $this->createQueryBuilder('fs') ->andWhere('IDENTITY(fs.follower) = :userId') ->andWhere('IDENTITY(fs.source) = :sourceId') - ->setParameter('sourceId', $sourceId->toRfc4122()) - ->setParameter('userId', $userId->toRfc4122()) + ->setParameter('sourceId', $sourceId->toString()) + ->setParameter('userId', $userId->toString()) ->getQuery() ->getOneOrNullResult(); } diff --git a/projects/backend/src/IdentityAndAccess/Application/ReadModel/UserProfile.php b/projects/backend/src/IdentityAndAccess/Application/ReadModel/UserProfile.php index 07c878a..07a795f 100644 --- a/projects/backend/src/IdentityAndAccess/Application/ReadModel/UserProfile.php +++ b/projects/backend/src/IdentityAndAccess/Application/ReadModel/UserProfile.php @@ -27,7 +27,7 @@ final readonly class UserProfile public static function create(array $item): self { return new self( - UserId::fromBinary($item['user_id']), + UserId::fromString(DataMapping::string($item, 'user_id')), DataMapping::string($item, 'user_name'), EmailAddress::from(DataMapping::string($item, 'user_email')), DataMapping::dateTime($item, 'user_created_at'), diff --git a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/GetUserProfileDbalHandler.php b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/GetUserProfileDbalHandler.php index 31143bc..4478e1b 100644 --- a/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/GetUserProfileDbalHandler.php +++ b/projects/backend/src/IdentityAndAccess/Infrastructure/Persistence/Doctrine/DBAL/GetUserProfileDbalHandler.php @@ -34,7 +34,7 @@ final readonly class GetUserProfileDbalHandler implements GetUserProfileHandler ) ->from('user', 'u') ->where('u.id = :userId') - ->setParameter('userId', $query->userId->toRfc4122()); + ->setParameter('userId', $query->userId->toString()); /** @var array|false $data */ $data = $qb->executeQuery()->fetchAssociative(); diff --git a/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationCursor.php b/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationCursor.php index fe4ba2b..13ad035 100644 --- a/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationCursor.php +++ b/projects/backend/src/SharedKernel/Domain/Model/Pagination/PaginationCursor.php @@ -16,7 +16,7 @@ final readonly class PaginationCursor { public function __construct( public UuidV7 $id, - public \DateTimeImmutable $date, + public ?\DateTimeImmutable $date = null, ) { } @@ -26,24 +26,15 @@ final readonly class PaginationCursor */ public static function encode(array $item, PaginatorKeyset $keyset): string { - $id = DataMapping::uuid($item, $keyset->id)->toString(); + $payload = [ + 'id' => DataMapping::string($item, $keyset->id), + ]; if ($keyset->date !== null) { - $date = DataMapping::dateTime($item, $keyset->date)->format('Y-m-d H:i:s'); - - return base64_encode( - json_encode([ - 'date' => $date, - 'id' => $id, - ], JSON_THROW_ON_ERROR) - ); + $payload['date'] = DataMapping::dateTime($item, $keyset->date)->format('Y-m-d H:i:s'); } - return base64_encode( - json_encode([ - 'id' => $id, - ], JSON_THROW_ON_ERROR) - ); + return base64_encode(json_encode($payload, JSON_THROW_ON_ERROR)); } @@ -58,15 +49,25 @@ final readonly class PaginationCursor } try { - $data = json_decode(base64_decode($cursor), true, 512, JSON_THROW_ON_ERROR); + $decoded = base64_decode($cursor, true); + if ($decoded === false) { + return null; + } - if (! is_array($data) || ! isset($data['date'], $data['id'])) { + $data = json_decode($decoded, true, 512, JSON_THROW_ON_ERROR); + + if (! is_array($data) || ! isset($data['id'])) { throw new \InvalidArgumentException('Invalid cursor format'); } + $date = null; + if (isset($data['date'])) { + $date = new \DateTimeImmutable($data['date']); + } + return new self( id: UuidV7::fromString($data['id']), - date: new \DateTimeImmutable($data['date']) + date: $date, ); } catch (\Throwable) { return null; diff --git a/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Features/PaginationQuery.php b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Features/PaginationQuery.php index f5654fa..54a5889 100644 --- a/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Features/PaginationQuery.php +++ b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/DBAL/Features/PaginationQuery.php @@ -8,6 +8,7 @@ use Basango\SharedKernel\Domain\Model\Pagination\Page; use Basango\SharedKernel\Domain\Model\Pagination\PaginationCursor; use Basango\SharedKernel\Domain\Model\Pagination\PaginationInfo; use Basango\SharedKernel\Domain\Model\Pagination\PaginatorKeyset; +use Basango\SharedKernel\Domain\Model\ValueObject\SortDirection; use Doctrine\DBAL\Query\QueryBuilder; /** @@ -17,35 +18,62 @@ use Doctrine\DBAL\Query\QueryBuilder; */ trait PaginationQuery { - public function createPaginationInfo(array $data, Page $page, PaginatorKeyset $keyset): PaginationInfo + public function createPaginationInfo(array &$data, Page $page, PaginatorKeyset $keyset): PaginationInfo { $paginationInfo = PaginationInfo::from($page); if ($data === []) { return $paginationInfo; } - $paginationInfo->cursor = PaginationCursor::encode(array_pop($data), $keyset); - $paginationInfo->hasNext = count($data) > $page->limit; + $hasNext = count($data) > $page->limit; + if ($hasNext) { + array_pop($data); + } + + $cursorSource = end($data); + + if (is_array($cursorSource)) { + $paginationInfo->cursor = PaginationCursor::encode($cursorSource, $keyset); + } + + $paginationInfo->hasNext = $hasNext; + reset($data); return $paginationInfo; } - public function applyCursorPagination(QueryBuilder $qb, Page $page, PaginatorKeyset $keyset): QueryBuilder - { + public function applyCursorPagination( + QueryBuilder $qb, + Page $page, + PaginatorKeyset $keyset, + SortDirection $direction = SortDirection::DESC + ): QueryBuilder { + $orderDirection = strtoupper($direction->value); + $comparisonOperator = $direction === SortDirection::ASC ? '>' : '<'; + + if ($keyset->date !== null) { + $qb->addOrderBy($keyset->date, $orderDirection); + } + $qb->addOrderBy($keyset->id, $orderDirection); + $cursor = PaginationCursor::decode($page->cursor); if (! $cursor instanceof PaginationCursor) { - return $this->applyOffsetPagination($qb, $page); + return $qb->setMaxResults($page->limit + 1); } if ($keyset->date === null) { $qb - ->andWhere(sprintf('%s <= :cursorLastId', $keyset->id)) - ->setParameter('cursorLastId', $cursor->id->toRfc4122()); + ->andWhere(sprintf('%s %s :cursorLastId', $keyset->id, $comparisonOperator)) + ->setParameter('cursorLastId', $cursor->id->toString()); } else { + if (! $cursor->date instanceof \DateTimeImmutable) { + return $qb->setMaxResults($page->limit + 1); + } + $qb - ->andWhere(sprintf('(%s, %s) <= (:cursorLastDate, :cursorLastId)', $keyset->date, $keyset->id)) + ->andWhere(sprintf('(%s, %s) %s (:cursorLastDate, :cursorLastId)', $keyset->date, $keyset->id, $comparisonOperator)) ->setParameter('cursorLastDate', $cursor->date->format('Y-m-d H:i:s')) - ->setParameter('cursorLastId', $cursor->id->toRfc4122()); + ->setParameter('cursorLastId', $cursor->id->toString()); } return $qb->setMaxResults($page->limit + 1); diff --git a/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/Importer/ImportEngine.php b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/Importer/ImportEngine.php index d77b3ba..d484753 100644 --- a/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/Importer/ImportEngine.php +++ b/projects/backend/src/SharedKernel/Infrastructure/Persistence/Doctrine/Importer/ImportEngine.php @@ -192,9 +192,8 @@ final readonly class ImportEngine $val = $row[$col] ?? null; if ($val !== null) { - // Convert BINARY(16) UUIDs to canonical RFC4122 if ($col === 'id' || str_ends_with((string) $col, '_id')) { - $params[$i++] = Uuid::fromBinary($val)->toRfc4122(); + $params[$i++] = $this->normalizeUuidValue($val); continue; } @@ -298,4 +297,25 @@ final readonly class ImportEngine return $converted !== false ? $converted : $value; } + + private function normalizeUuidValue(mixed $value): string + { + if ($value instanceof Uuid) { + return (string) $value; + } + + if (is_string($value)) { + if (strlen($value) === 16) { + $uuid = Uuid::fromBinary($value); + + return method_exists($uuid, 'toString') + ? $uuid->toString() + : $uuid->toRfc4122(); + } + + return $value; + } + + return (string) $value; + } }