diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ba171dd2..05d8cd52d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,8 @@ The format is almost based on [Keep a Changelog](https://keepachangelog.com/en/1
 ## [Unreleased]
 
 ### Changed
+- Remove outdated item DB code.
+- Stop returning all feeds after marking folder as read.
 
 ### Fixed
 
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php
index a262e2a03..079b32fe8 100644
--- a/lib/AppInfo/Application.php
+++ b/lib/AppInfo/Application.php
@@ -31,9 +31,6 @@ use OCP\AppFramework\App;
 use OCP\Files\IRootFolder;
 use OCP\Files\Node;
 
-
-use OCA\News\Db\MapperFactory;
-use OCA\News\Db\ItemMapper;
 use OCA\News\Fetcher\FeedFetcher;
 use OCA\News\Fetcher\Fetcher;
 use OCP\User\Events\BeforeUserDeletedEvent;
@@ -90,11 +87,6 @@ class Application extends App implements IBootstrap
         $context->registerParameter('exploreDir', __DIR__ . '/../Explore/feeds');
         $context->registerParameter('configFile', 'config.ini');
 
-        // factories
-        $context->registerService(ItemMapper::class, function (ContainerInterface $c): ItemMapper {
-            return $c->get(MapperFactory::class)->build();
-        });
-
         $context->registerService(HTMLPurifier::class, function (ContainerInterface $c): HTMLPurifier {
             $directory = $c->get(ITempManager::class)->getTempBaseDir() . '/news/cache/purifier';
 
diff --git a/lib/Command/Config/FolderDelete.php b/lib/Command/Config/FolderDelete.php
index a80875682..8d7722b3b 100644
--- a/lib/Command/Config/FolderDelete.php
+++ b/lib/Command/Config/FolderDelete.php
@@ -2,7 +2,7 @@
 
 namespace OCA\News\Command\Config;
 
-use OCA\News\Service\Exceptions\ServiceException;
+use OCA\News\Service\Exceptions\ServiceValidationException;
 use OCA\News\Service\FolderServiceV2;
 use Symfony\Component\Console\Command\Command;
 use Symfony\Component\Console\Input\InputArgument;
@@ -50,7 +50,7 @@ class FolderDelete extends Command
         $id = $input->getArgument('folder-id');
 
         if ($id === null) {
-            throw new ServiceException('Can not remove root folder!');
+            throw new ServiceValidationException('Can not remove root folder!');
         }
 
         $this->folderService->delete($user, intval($id));
diff --git a/lib/Controller/ApiController.php b/lib/Controller/ApiController.php
index a434f8de7..e6a83b21a 100644
--- a/lib/Controller/ApiController.php
+++ b/lib/Controller/ApiController.php
@@ -77,7 +77,7 @@ class ApiController extends BaseApiController
      *
      * @return array
      */
-    public function index()
+    public function index(): array
     {
         return [
             'apiLevels' => ['v1-2']
diff --git a/lib/Controller/FeedApiController.php b/lib/Controller/FeedApiController.php
index f247a4e66..43d92b7ca 100644
--- a/lib/Controller/FeedApiController.php
+++ b/lib/Controller/FeedApiController.php
@@ -19,12 +19,12 @@ use Exception;
 use OCA\News\Service\Exceptions\ServiceConflictException;
 use OCA\News\Service\Exceptions\ServiceNotFoundException;
 use OCA\News\Service\FeedServiceV2;
+use OCA\News\Service\ItemServiceV2;
 use OCP\AppFramework\Http\JSONResponse;
 use \OCP\IRequest;
 use \OCP\IUserSession;
 use \OCP\AppFramework\Http;
 
-use \OCA\News\Service\ItemService;
 use Psr\Log\LoggerInterface;
 
 class FeedApiController extends ApiController
@@ -32,10 +32,9 @@ class FeedApiController extends ApiController
     use JSONHttpErrorTrait, ApiPayloadTrait;
 
     /**
-     * TODO: Remove
-     * @var ItemService
+     * @var ItemServiceV2
      */
-    private $oldItemService;
+    private $itemService;
 
     /**
      * @var FeedServiceV2
@@ -51,12 +50,12 @@ class FeedApiController extends ApiController
         IRequest $request,
         ?IUserSession $userSession,
         FeedServiceV2 $feedService,
-        ItemService $oldItemService,
+        ItemServiceV2 $itemService,
         LoggerInterface $logger
     ) {
         parent::__construct($request, $userSession);
         $this->feedService = $feedService;
-        $this->oldItemService = $oldItemService;
+        $this->itemService = $itemService;
         $this->logger = $logger;
     }
 
@@ -70,12 +69,12 @@ class FeedApiController extends ApiController
     {
 
         $result = [
-            'starredCount' => $this->oldItemService->starredCount($this->getUserId()),
+            'starredCount' => count($this->itemService->starred($this->getUserId())),
             'feeds' => $this->serialize($this->feedService->findAllForUser($this->getUserId()))
         ];
 
         try {
-            $result['newestItemId'] = $this->oldItemService->getNewestItemId($this->getUserId());
+            $result['newestItemId'] = $this->itemService->newest($this->getUserId())->getId();
         } catch (ServiceNotFoundException $ex) {
             // in case there are no items, ignore
         }
@@ -96,9 +95,7 @@ class FeedApiController extends ApiController
      */
     public function create(string $url, ?int $folderId = null)
     {
-        if ($folderId === 0) {
-            $folderId = null;
-        }
+        $folderId = $folderId === 0 ? null : $folderId;
 
         try {
             $this->feedService->purgeDeleted($this->getUserId(), time() - 600);
@@ -109,7 +106,7 @@ class FeedApiController extends ApiController
             $this->feedService->fetch($feed);
 
             try {
-                $result['newestItemId'] = $this->oldItemService->getNewestItemId($this->getUserId());
+                $result['newestItemId'] = $this->itemService->newest($this->getUserId())->getId();
             } catch (ServiceNotFoundException $ex) {
                 // in case there are no items, ignore
             }
@@ -154,7 +151,7 @@ class FeedApiController extends ApiController
      */
     public function read(int $feedId, int $newestItemId): void
     {
-        $this->oldItemService->readFeed($feedId, $newestItemId, $this->getUserId());
+        $this->itemService->read($this->getUserId(), $feedId, $newestItemId);
     }
 
 
@@ -170,9 +167,7 @@ class FeedApiController extends ApiController
      */
     public function move(int $feedId, ?int $folderId)
     {
-        if ($folderId === 0) {
-            $folderId = null;
-        }
+        $folderId = $folderId === 0 ? null : $folderId;
 
         try {
             $feed = $this->feedService->find($this->getUserId(), $feedId);
diff --git a/lib/Controller/FeedController.php b/lib/Controller/FeedController.php
index 9f7c9b0d6..681dda4bc 100644
--- a/lib/Controller/FeedController.php
+++ b/lib/Controller/FeedController.php
@@ -18,12 +18,12 @@ use OCA\News\Service\Exceptions\ServiceNotFoundException;
 use OCA\News\Service\FeedServiceV2;
 use OCA\News\Service\FolderServiceV2;
 use OCA\News\Service\ImportService;
+use OCA\News\Service\ItemServiceV2;
 use OCP\AppFramework\Http\JSONResponse;
 use OCP\IRequest;
 use OCP\IConfig;
 use OCP\AppFramework\Http;
 
-use OCA\News\Service\ItemService;
 use OCA\News\Db\FeedType;
 use OCP\IUserSession;
 
@@ -35,7 +35,9 @@ class FeedController extends Controller
      * @var FeedServiceV2
      */
     private $feedService;
-    //TODO: Remove
+    /**
+     * @var ItemServiceV2
+     */
     private $itemService;
     /**
      * @var FolderServiceV2
@@ -54,7 +56,7 @@ class FeedController extends Controller
         IRequest $request,
         FolderServiceV2 $folderService,
         FeedServiceV2 $feedService,
-        ItemService $itemService,
+        ItemServiceV2 $itemService,
         ImportService $importService,
         IConfig $settings,
         ?IUserSession $userSession
@@ -79,11 +81,11 @@ class FeedController extends Controller
         // item id which will be used for marking feeds read
         $params = [
             'feeds' => $this->feedService->findAllForUser($this->getUserId()),
-            'starred' => $this->itemService->starredCount($this->getUserId())
+            'starred' => count($this->itemService->starred($this->getUserId()))
         ];
 
         try {
-            $id = $this->itemService->getNewestItemId($this->getUserId());
+            $id = $this->itemService->newest($this->getUserId())->getId();
 
             // An exception occurs if there is a newest item. If there is none,
             // simply ignore it and do not add the newestItemId
@@ -183,7 +185,7 @@ class FeedController extends Controller
             $this->feedService->fetch($feed);
 
             try {
-                $id = $this->itemService->getNewestItemId($this->getUserId());
+                $id = $this->itemService->newest($this->getUserId())->getId();
                 // An exception occurs if there is a newest item. If there is none,
                 // simply ignore it and do not add the newestItemId
                 $params['newestItemId'] = $id;
@@ -261,7 +263,7 @@ class FeedController extends Controller
         $feed = $this->importService->importArticles($this->getUserId(), $json);
 
         $params = [
-            'starred' => $this->itemService->starredCount($this->getUserId())
+            'starred' => count($this->itemService->starred($this->getUserId()))
         ];
 
         if ($feed) {
@@ -281,7 +283,7 @@ class FeedController extends Controller
      */
     public function read(int $feedId, int $highestItemId): array
     {
-        $this->itemService->readFeed($feedId, $highestItemId, $this->getUserId());
+        $this->feedService->read($this->getUserId(), $feedId, $highestItemId);
 
         return [
             'feeds' => [
diff --git a/lib/Controller/FolderApiController.php b/lib/Controller/FolderApiController.php
index 8de4b9e69..71fc503e2 100644
--- a/lib/Controller/FolderApiController.php
+++ b/lib/Controller/FolderApiController.php
@@ -20,7 +20,6 @@ use \OCP\IRequest;
 use \OCP\IUserSession;
 use \OCP\AppFramework\Http;
 
-use \OCA\News\Service\ItemService;
 use \OCA\News\Service\FolderServiceV2;
 use \OCA\News\Service\Exceptions\ServiceNotFoundException;
 use \OCA\News\Service\Exceptions\ServiceConflictException;
@@ -30,20 +29,19 @@ class FolderApiController extends ApiController
 {
     use JSONHttpErrorTrait, ApiPayloadTrait;
 
+    /**
+     * @var FolderServiceV2
+     */
     private $folderService;
-    //TODO: Remove
-    private $itemService;
 
     public function __construct(
         IRequest $request,
         ?IUserSession $userSession,
-        FolderServiceV2 $folderService,
-        ItemService $itemService
+        FolderServiceV2 $folderService
     ) {
         parent::__construct($request, $userSession);
 
         $this->folderService = $folderService;
-        $this->itemService = $itemService;
     }
 
 
@@ -52,7 +50,7 @@ class FolderApiController extends ApiController
      * @NoCSRFRequired
      * @CORS
      */
-    public function index()
+    public function index(): array
     {
         $folders = $this->folderService->findAllForUser($this->getUserId());
         return ['folders' => $this->serialize($folders)];
@@ -142,14 +140,13 @@ class FolderApiController extends ApiController
      * @NoCSRFRequired
      * @CORS
      *
-     * @param int|null $folderId
-     * @param int      $newestItemId
+     * @param int|null $folderId  ID of the folder
+     * @param int      $maxItemId The newest read item
      */
-    public function read(?int $folderId, int $newestItemId): void
+    public function read(?int $folderId, int $maxItemId): void
     {
-        if ($folderId === 0) {
-            $folderId = null;
-        }
-        $this->itemService->readFolder($folderId, $newestItemId, $this->getUserId());
+        $folderId = $folderId === 0 ? null : $folderId;
+
+        $this->folderService->read($this->getUserId(), $folderId, $maxItemId);
     }
 }
diff --git a/lib/Controller/FolderController.php b/lib/Controller/FolderController.php
index 9dc13b309..da03f9863 100644
--- a/lib/Controller/FolderController.php
+++ b/lib/Controller/FolderController.php
@@ -14,13 +14,11 @@
 namespace OCA\News\Controller;
 
 use OCA\News\Service\Exceptions\ServiceException;
-use OCA\News\Service\FeedServiceV2;
 use OCP\AppFramework\Http\JSONResponse;
 use \OCP\IRequest;
 use \OCP\AppFramework\Http;
 
 use \OCA\News\Service\FolderServiceV2;
-use \OCA\News\Service\ItemService;
 use \OCA\News\Service\Exceptions\ServiceNotFoundException;
 use \OCA\News\Service\Exceptions\ServiceConflictException;
 use OCP\IUserSession;
@@ -33,24 +31,14 @@ class FolderController extends Controller
      * @var FolderServiceV2
      */
     private $folderService;
-    /**
-     * @var FeedServiceV2
-     */
-    private $feedService;
-    //TODO: Remove
-    private $itemService;
 
     public function __construct(
         IRequest $request,
         FolderServiceV2 $folderService,
-        FeedServiceV2 $feedService,
-        ItemService $itemService,
         ?IUserSession $userSession
     ) {
         parent::__construct($request, $userSession);
         $this->folderService = $folderService;
-        $this->feedService = $feedService;
-        $this->itemService = $itemService;
     }
 
 
@@ -134,12 +122,12 @@ class FolderController extends Controller
     /**
      * @NoAdminRequired
      *
-     * @param string   $folderName
-     * @param int|null $folderId
+     * @param int|null $folderId   The ID of the folder
+     * @param string   $folderName The new name of the folder
      *
      * @return array|JSONResponse
      */
-    public function rename(string $folderName, ?int $folderId)
+    public function rename(?int $folderId, string $folderName)
     {
         if (empty($folderId)) {
             return new JSONResponse([], Http::STATUS_BAD_REQUEST);
@@ -159,21 +147,18 @@ class FolderController extends Controller
      * @NoAdminRequired
      *
      * @param int|null $folderId
-     * @param int      $highestItemId
+     * @param int      $maxItemId
      *
-     * @return array
+     * @return void
+     *
+     * @throws ServiceConflictException
+     * @throws ServiceNotFoundException
      */
-    public function read(?int $folderId, int $highestItemId): array
+    public function read(?int $folderId, int $maxItemId): void
     {
         $folderId = $folderId === 0 ? null : $folderId;
 
-        $this->itemService->readFolder(
-            $folderId,
-            $highestItemId,
-            $this->getUserId()
-        );
-        $feeds = $this->feedService->findAllForUser($this->getUserId());
-        return ['feeds' => $this->serialize($feeds)];
+        $this->folderService->read($this->getUserId(), $folderId, $maxItemId);
     }
 
 
diff --git a/lib/Controller/ItemApiController.php b/lib/Controller/ItemApiController.php
index 7ec43bc5c..003c61fa2 100644
--- a/lib/Controller/ItemApiController.php
+++ b/lib/Controller/ItemApiController.php
@@ -15,7 +15,9 @@
 
 namespace OCA\News\Controller;
 
-use OCA\News\Service\ItemService;
+use OCA\News\Db\FeedType;
+use OCA\News\Service\Exceptions\ServiceConflictException;
+use OCA\News\Service\Exceptions\ServiceValidationException;
 use OCA\News\Service\ItemServiceV2;
 use OCP\AppFramework\Http\JSONResponse;
 use \OCP\IRequest;
@@ -24,22 +26,27 @@ use \OCP\AppFramework\Http;
 
 use \OCA\News\Service\Exceptions\ServiceNotFoundException;
 
+/**
+ * Class ItemApiController
+ *
+ * @package OCA\News\Controller
+ */
 class ItemApiController extends ApiController
 {
     use JSONHttpErrorTrait, ApiPayloadTrait;
 
-    private $oldItemService;
+    /**
+     * @var ItemServiceV2
+     */
     private $itemService;
 
     public function __construct(
         IRequest $request,
         ?IUserSession $userSession,
-        ItemService $oldItemService,
         ItemServiceV2 $itemService
     ) {
         parent::__construct($request, $userSession);
 
-        $this->oldItemService = $oldItemService;
         $this->itemService = $itemService;
     }
 
@@ -64,16 +71,38 @@ class ItemApiController extends ApiController
         int $batchSize = -1,
         int $offset = 0,
         bool $oldestFirst = false
-    ) {
-        $items = $this->oldItemService->findAllItems(
-            $id,
-            $type,
-            $batchSize,
-            $offset,
-            $getRead,
-            $oldestFirst,
-            $this->getUserId()
-        );
+    ): array {
+        switch ($type) {
+            case FeedType::FEED:
+                $items = $this->itemService->findAllInFeedWithFilters(
+                    $this->getUserId(),
+                    $id,
+                    $batchSize,
+                    $offset,
+                    !$getRead,
+                    $oldestFirst
+                );
+                break;
+            case FeedType::FOLDER:
+                $items = $this->itemService->findAllInFolderWithFilters(
+                    $this->getUserId(),
+                    $id,
+                    $batchSize,
+                    $offset,
+                    !$getRead,
+                    $oldestFirst
+                );
+                break;
+            default:
+                $items = $this->itemService->findAllWithFilters(
+                    $this->getUserId(),
+                    $type,
+                    $batchSize,
+                    $offset,
+                    $oldestFirst
+                );
+                break;
+        }
 
         return ['items' => $this->serialize($items)];
     }
@@ -88,8 +117,10 @@ class ItemApiController extends ApiController
      * @param int $id
      * @param int $lastModified
      * @return array|JSONResponse
+     *
+     * @throws ServiceValidationException
      */
-    public function updated(int $type = 3, int $id = 0, int $lastModified = 0)
+    public function updated(int $type = 3, int $id = 0, int $lastModified = 0): array
     {
         // needs to be turned into a millisecond timestamp to work properly
         if (strlen((string) $lastModified) <= 10) {
@@ -97,27 +128,33 @@ class ItemApiController extends ApiController
         } else {
             $paddedLastModified = $lastModified;
         }
-        $items = $this->oldItemService->findAllNew(
-            $id,
-            $type,
-            (int) $paddedLastModified,
-            true,
-            $this->getUserId()
-        );
+
+        switch ($type) {
+            case FeedType::FEED:
+                $items = $this->itemService->findAllInFeedAfter($this->getUserId(), $id, $paddedLastModified, false);
+                break;
+            case FeedType::FOLDER:
+                $items = $this->itemService->findAllInFolderAfter($this->getUserId(), $id, $paddedLastModified, false);
+                break;
+            default:
+                $items = $this->itemService->findAllAfter($this->getUserId(), $type, $paddedLastModified);
+                break;
+        }
 
         return ['items' => $this->serialize($items)];
     }
 
-
     /**
-     * @return JSONResponse|array
+     * @param int  $itemId
+     * @param bool $isRead
      *
-     * @psalm-return JSONResponse|array<empty, empty>
+     * @return array|JSONResponse
+     * @throws ServiceConflictException
      */
-    private function setRead(bool $isRead, int $itemId)
+    private function setRead(int $itemId, bool $isRead)
     {
         try {
-            $this->oldItemService->read($itemId, $isRead, $this->getUserId());
+            $this->itemService->read($this->getUserId(), $itemId, $isRead);
         } catch (ServiceNotFoundException $ex) {
             return $this->error($ex, Http::STATUS_NOT_FOUND);
         }
@@ -134,10 +171,11 @@ class ItemApiController extends ApiController
      * @param int $itemId
      *
      * @return array|JSONResponse
+     * @throws ServiceConflictException
      */
     public function read(int $itemId)
     {
-        return $this->setRead(true, $itemId);
+        return $this->setRead($itemId, true);
     }
 
 
@@ -149,27 +187,25 @@ class ItemApiController extends ApiController
      * @param int $itemId
      *
      * @return array|JSONResponse
+     * @throws ServiceConflictException
      */
     public function unread(int $itemId)
     {
-        return $this->setRead(false, $itemId);
+        return $this->setRead($itemId, false);
     }
 
-
     /**
-     * @return JSONResponse|array
+     * @param int    $feedId
+     * @param string $guidHash
+     * @param bool   $isStarred
      *
-     * @psalm-return JSONResponse|array<empty, empty>
+     * @return array|JSONResponse
+     * @throws ServiceConflictException
      */
-    private function setStarred(bool $isStarred, int $feedId, string $guidHash)
+    private function setStarred(int $feedId, string $guidHash, bool $isStarred)
     {
         try {
-            $this->oldItemService->star(
-                $feedId,
-                $guidHash,
-                $isStarred,
-                $this->getUserId()
-            );
+            $this->itemService->starByGuid($this->getUserId(), $feedId, $guidHash, $isStarred);
         } catch (ServiceNotFoundException $ex) {
             return $this->error($ex, Http::STATUS_NOT_FOUND);
         }
@@ -187,10 +223,11 @@ class ItemApiController extends ApiController
      * @param string $guidHash
      *
      * @return array|JSONResponse
+     * @throws ServiceConflictException
      */
     public function star(int $feedId, string $guidHash)
     {
-        return $this->setStarred(true, $feedId, $guidHash);
+        return $this->setStarred($feedId, $guidHash, true);
     }
 
 
@@ -203,10 +240,11 @@ class ItemApiController extends ApiController
      * @param string $guidHash
      *
      * @return array|JSONResponse
+     * @throws ServiceConflictException
      */
     public function unstar(int $feedId, string $guidHash)
     {
-        return $this->setStarred(false, $feedId, $guidHash);
+        return $this->setStarred($feedId, $guidHash, false);
     }
 
 
@@ -223,15 +261,20 @@ class ItemApiController extends ApiController
      */
     public function readAll(int $newestItemId): void
     {
-        $this->oldItemService->readAll($newestItemId, $this->getUserId());
+        $this->itemService->readAll($this->getUserId(), $newestItemId);
     }
 
-
-    private function setMultipleRead(bool $isRead, array $items): void
+    /**
+     * @param array $items
+     * @param bool  $isRead
+     *
+     * @throws ServiceConflictException
+     */
+    private function setMultipleRead(array $items, bool $isRead): void
     {
         foreach ($items as $id) {
             try {
-                $this->oldItemService->read($id, $isRead, $this->getUserId());
+                $this->itemService->read($this->getUserId(), $id, $isRead);
             } catch (ServiceNotFoundException $ex) {
                 continue;
             }
@@ -249,10 +292,12 @@ class ItemApiController extends ApiController
      * @param int[] $items item ids
      *
      * @return void
+     *
+     * @throws ServiceConflictException
      */
     public function readMultiple(array $items): void
     {
-        $this->setMultipleRead(true, $items);
+        $this->setMultipleRead($items, true);
     }
 
 
@@ -266,30 +311,32 @@ class ItemApiController extends ApiController
      * @param int[] $items item ids
      *
      * @return void
+     *
+     * @throws ServiceConflictException
      */
     public function unreadMultiple(array $items): void
     {
-        $this->setMultipleRead(false, $items);
+        $this->setMultipleRead($items, false);
     }
 
 
     /**
-     * @param bool  $isStarred
      * @param array $items
+     * @param bool  $isStarred
      *
      * @return void
      */
-    private function setMultipleStarred(bool $isStarred, array $items): void
+    private function setMultipleStarred(array $items, bool $isStarred): void
     {
         foreach ($items as $item) {
             try {
-                $this->oldItemService->star(
+                $this->itemService->starByGuid(
+                    $this->getUserId(),
                     $item['feedId'],
                     $item['guidHash'],
-                    $isStarred,
-                    $this->getUserId()
+                    $isStarred
                 );
-            } catch (ServiceNotFoundException $ex) {
+            } catch (ServiceNotFoundException | ServiceConflictException $ex) {
                 continue;
             }
         }
@@ -309,7 +356,7 @@ class ItemApiController extends ApiController
      */
     public function starMultiple(array $items): void
     {
-        $this->setMultipleStarred(true, $items);
+        $this->setMultipleStarred($items, true);
     }
 
 
@@ -326,6 +373,6 @@ class ItemApiController extends ApiController
      */
     public function unstarMultiple(array $items): void
     {
-        $this->setMultipleStarred(false, $items);
+        $this->setMultipleStarred($items, false);
     }
 }
diff --git a/lib/Controller/ItemController.php b/lib/Controller/ItemController.php
index 96ebcbaec..02a308d87 100644
--- a/lib/Controller/ItemController.php
+++ b/lib/Controller/ItemController.php
@@ -13,20 +13,31 @@
 
 namespace OCA\News\Controller;
 
+use OCA\News\Db\FeedType;
+use OCA\News\Service\Exceptions\ServiceConflictException;
 use OCA\News\Service\FeedServiceV2;
+use OCP\AppFramework\Http\JSONResponse;
 use \OCP\IRequest;
 use \OCP\IConfig;
 use \OCP\AppFramework\Http;
 
 use \OCA\News\Service\Exceptions\ServiceException;
 use \OCA\News\Service\Exceptions\ServiceNotFoundException;
-use \OCA\News\Service\ItemService;
+use \OCA\News\Service\ItemServiceV2;
 use OCP\IUserSession;
 
+/**
+ * Class ItemController
+ *
+ * @package OCA\News\Controller
+ */
 class ItemController extends Controller
 {
     use JSONHttpErrorTrait;
 
+    /**
+     * @var ItemServiceV2
+     */
     private $itemService;
     /**
      * @var FeedServiceV2
@@ -40,7 +51,7 @@ class ItemController extends Controller
     public function __construct(
         IRequest $request,
         FeedServiceV2 $feedService,
-        ItemService $itemService,
+        ItemServiceV2 $itemService,
         IConfig $settings,
         ?IUserSession $userSession
     ) {
@@ -71,7 +82,7 @@ class ItemController extends Controller
         ?bool $showAll = null,
         ?bool $oldestFirst = null,
         string $search = ''
-    ) {
+    ): array {
 
         // in case this is called directly and not from the website use the
         // internal state
@@ -104,15 +115,14 @@ class ItemController extends Controller
             $type
         );
 
-        $params = [];
+        $return = [];
 
         // split search parameter on url space
-        $search = trim(urldecode($search));
-        $search = preg_replace('/\s+/', ' ', $search);  // remove multiple ws
-        if ($search === '') {
-            $search = [];
-        } else {
-            $search = explode(' ', $search);
+        $search_string = trim(urldecode($search));
+        $search_string = preg_replace('/\s+/', ' ', $search_string);  // remove multiple ws
+        $search_items = [];
+        if ($search !== '') {
+            $search_items = explode(' ', $search_string);
         }
 
         try {
@@ -120,30 +130,54 @@ class ItemController extends Controller
             // we need to pass the newest feeds to not let the unread count get
             // out of sync
             if ($offset === 0) {
-                $params['newestItemId'] =
-                    $this->itemService->getNewestItemId($this->getUserId());
-                $params['feeds'] = $this->feedService->findAllForUser($this->getUserId());
-                $params['starred'] =
-                    $this->itemService->starredCount($this->getUserId());
+                $return['newestItemId'] = $this->itemService->newest($this->getUserId())->getId();
+                $return['feeds'] = $this->feedService->findAllForUser($this->getUserId());
+                $return['starred'] = count($this->itemService->starred($this->getUserId()));
             }
 
-            $params['items'] = $this->itemService->findAllItems(
-                $id,
-                $type,
-                $limit,
-                $offset,
-                $showAll,
-                $oldestFirst,
-                $this->getUserId(),
-                $search
-            );
+            switch ($type) {
+                case FeedType::FEED:
+                    $items = $this->itemService->findAllInFeedWithFilters(
+                        $this->getUserId(),
+                        $id,
+                        $limit,
+                        $offset,
+                        !$showAll,
+                        $oldestFirst,
+                        $search_items
+                    );
+                    break;
+                case FeedType::FOLDER:
+                    $items = $this->itemService->findAllInFolderWithFilters(
+                        $this->getUserId(),
+                        $id,
+                        $limit,
+                        $offset,
+                        !$showAll,
+                        $oldestFirst,
+                        $search_items
+                    );
+                    break;
+                default:
+                    $items = $this->itemService->findAllWithFilters(
+                        $this->getUserId(),
+                        $type,
+                        $limit,
+                        $offset,
+                        $oldestFirst,
+                        $search_items
+                    );
+                    break;
+            }
+            $return['items'] = $items;
 
             // this gets thrown if there are no items
             // in that case just return an empty array
         } catch (ServiceException $ex) {
+            //NO-OP
         }
 
-        return $params;
+        return $return;
     }
 
 
@@ -155,7 +189,7 @@ class ItemController extends Controller
      * @param int $lastModified
      * @return array
      */
-    public function newItems($type, $id, $lastModified = 0)
+    public function newItems(int $type, int $id, $lastModified = 0): array
     {
         $showAll = $this->settings->getUserValue(
             $this->getUserId(),
@@ -163,28 +197,47 @@ class ItemController extends Controller
             'showAll'
         ) === '1';
 
-        $params = [];
+        $return = [];
 
         try {
-            $params['newestItemId'] =
-                $this->itemService->getNewestItemId($this->getUserId());
-            $params['feeds'] = $this->feedService->findAllForUser($this->getUserId());
-            $params['starred'] =
-                $this->itemService->starredCount($this->getUserId());
-            $params['items'] = $this->itemService->findAllNew(
-                $id,
-                $type,
-                $lastModified,
-                $showAll,
-                $this->getUserId()
-            );
+            switch ($type) {
+                case FeedType::FEED:
+                    $items = $this->itemService->findAllInFeedAfter(
+                        $this->getUserId(),
+                        $id,
+                        $lastModified,
+                        !$showAll
+                    );
+                    break;
+                case FeedType::FOLDER:
+                    $items = $this->itemService->findAllInFolderAfter(
+                        $this->getUserId(),
+                        $id,
+                        $lastModified,
+                        !$showAll
+                    );
+                    break;
+                default:
+                    $items = $this->itemService->findAllAfter(
+                        $this->getUserId(),
+                        $type,
+                        $lastModified
+                    );
+                    break;
+            }
+
+            $return['newestItemId'] = $this->itemService->newest($this->getUserId())->getId();
+            $return['feeds'] = $this->feedService->findAllForUser($this->getUserId());
+            $return['starred'] = count($this->itemService->starred($this->getUserId()));
+            $return['items'] = $items;
 
             // this gets thrown if there are no items
             // in that case just return an empty array
         } catch (ServiceException $ex) {
+            //NO-OP
         }
 
-        return $params;
+        return $return;
     }
 
 
@@ -194,16 +247,17 @@ class ItemController extends Controller
      * @param int    $feedId
      * @param string $guidHash
      * @param bool   $isStarred
-     * @return array|\OCP\AppFramework\Http\JSONResponse
+     *
+     * @return array|JSONResponse
      */
-    public function star($feedId, $guidHash, $isStarred)
+    public function star(int $feedId, string $guidHash, bool $isStarred)
     {
         try {
-            $this->itemService->star(
+            $this->itemService->starByGuid(
+                $this->getUserId(),
                 $feedId,
                 $guidHash,
-                $isStarred,
-                $this->getUserId()
+                $isStarred
             );
         } catch (ServiceException $ex) {
             return $this->error($ex, Http::STATUS_NOT_FOUND);
@@ -218,12 +272,13 @@ class ItemController extends Controller
      *
      * @param int  $itemId
      * @param bool $isRead
-     * @return array|\OCP\AppFramework\Http\JSONResponse
+     *
+     * @return array|JSONResponse
      */
-    public function read($itemId, $isRead = true)
+    public function read(int $itemId, $isRead = true)
     {
         try {
-            $this->itemService->read($itemId, $isRead, $this->getUserId());
+            $this->itemService->read($this->getUserId(), $itemId, $isRead);
         } catch (ServiceException $ex) {
             return $this->error($ex, Http::STATUS_NOT_FOUND);
         }
@@ -236,11 +291,12 @@ class ItemController extends Controller
      * @NoAdminRequired
      *
      * @param int $highestItemId
+     *
      * @return array
      */
-    public function readAll($highestItemId)
+    public function readAll(int $highestItemId): array
     {
-        $this->itemService->readAll($highestItemId, $this->getUserId());
+        $this->itemService->readAll($this->getUserId(), $highestItemId);
         return ['feeds' => $this->feedService->findAllForUser($this->getUserId())];
     }
 
@@ -252,12 +308,12 @@ class ItemController extends Controller
      *
      * @return void
      */
-    public function readMultiple($itemIds): void
+    public function readMultiple(array $itemIds): void
     {
         foreach ($itemIds as $id) {
             try {
-                $this->itemService->read($id, true, $this->getUserId());
-            } catch (ServiceNotFoundException $ex) {
+                $this->itemService->read($this->getUserId(), $id, true);
+            } catch (ServiceNotFoundException | ServiceConflictException $ex) {
                 continue;
             }
         }
diff --git a/lib/Controller/PageController.php b/lib/Controller/PageController.php
index 35924d10c..87ed91c73 100644
--- a/lib/Controller/PageController.php
+++ b/lib/Controller/PageController.php
@@ -14,6 +14,7 @@
 namespace OCA\News\Controller;
 
 use OCA\News\AppInfo\Application;
+use OCA\News\Explore\Exceptions\RecommendedSiteNotFoundException;
 use OCP\IRequest;
 use OCP\IConfig;
 use OCP\IL10N;
@@ -24,7 +25,6 @@ use OCP\AppFramework\Http\ContentSecurityPolicy;
 
 use OCA\News\Service\StatusService;
 use OCA\News\Explore\RecommendedSites;
-use OCA\News\Explore\RecommendedSiteNotFoundException;
 use OCA\News\Db\FeedType;
 use OCP\IUserSession;
 
diff --git a/lib/Db/FeedMapperV2.php b/lib/Db/FeedMapperV2.php
index b3d8879f3..5e346732c 100644
--- a/lib/Db/FeedMapperV2.php
+++ b/lib/Db/FeedMapperV2.php
@@ -153,4 +153,28 @@ class FeedMapperV2 extends NewsMapperV2
 
         return $this->findEntities($builder);
     }
+
+    /**
+     * @param string   $userId
+     * @param int      $id
+     * @param int|null $maxItemID
+     */
+    public function read(string $userId, int $id, ?int $maxItemID = null): void
+    {
+        $builder = $this->db->getQueryBuilder();
+        $builder->update(ItemMapperV2::TABLE_NAME, 'items')
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->setValue('unread', 0)
+            ->andWhere('feeds.user_id = :userId')
+            ->andWhere('feeds.id = :feedId')
+            ->setParameter('userId', $userId)
+            ->setParameter('feedId', $id);
+
+        if ($maxItemID !== null) {
+            $builder->andWhere('items.id =< :maxItemId')
+                ->setParameter('maxItemId', $maxItemID);
+        }
+
+        $this->db->executeUpdate($builder->getSQL());
+    }
 }
diff --git a/lib/Db/FeedType.php b/lib/Db/FeedType.php
index bf487992c..1ccd592a8 100644
--- a/lib/Db/FeedType.php
+++ b/lib/Db/FeedType.php
@@ -13,6 +13,11 @@
 
 namespace OCA\News\Db;
 
+/**
+ * Enum FeedType
+ *
+ * @package OCA\News\Db
+ */
 class FeedType
 {
     const FEED          = 0;
diff --git a/lib/Db/FolderMapperV2.php b/lib/Db/FolderMapperV2.php
index 85e07c07f..12fa26887 100644
--- a/lib/Db/FolderMapperV2.php
+++ b/lib/Db/FolderMapperV2.php
@@ -95,4 +95,30 @@ class FolderMapperV2 extends NewsMapperV2
 
         return $this->findEntity($builder);
     }
+
+    /**
+     * @param string   $userId
+     * @param int      $id
+     * @param int|null $maxItemID
+     *
+     * @return void
+     */
+    public function read(string $userId, int $id, ?int $maxItemID = null): void
+    {
+        $builder = $this->db->getQueryBuilder();
+        $builder->update(ItemMapperV2::TABLE_NAME, 'items')
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->setValue('unread', 0)
+            ->andWhere('feeds.user_id = :userId')
+            ->andWhere('feeds.folder_id = :folderId')
+            ->setParameter('userId', $userId)
+            ->setParameter('folderId', $id);
+
+        if ($maxItemID !== null) {
+            $builder->andWhere('items.id =< :maxItemId')
+                    ->setParameter('maxItemId', $maxItemID);
+        }
+
+        $this->db->executeUpdate($builder->getSQL());
+    }
 }
diff --git a/lib/Db/ItemMapper.php b/lib/Db/ItemMapper.php
deleted file mode 100644
index 17f6638de..000000000
--- a/lib/Db/ItemMapper.php
+++ /dev/null
@@ -1,586 +0,0 @@
-<?php
-/**
- * Nextcloud - News
- *
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
- *
- * @author    Alessandro Cosentino <cosenal@gmail.com>
- * @author    Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright 2012 Alessandro Cosentino
- * @copyright 2012-2014 Bernhard Posselt
- */
-
-namespace OCA\News\Db;
-
-use OCA\News\Utility\Time;
-use OCP\AppFramework\Db\DoesNotExistException;
-use OCP\AppFramework\Db\Entity;
-use OCP\AppFramework\Db\Mapper;
-use OCP\AppFramework\Db\MultipleObjectsReturnedException;
-use OCP\DB\QueryBuilder\IQueryBuilder;
-use OCP\IDBConnection;
-
-/**
- * Class LegacyItemMapper
- *
- * @package OCA\News\Db
- * @deprecated use ItemMapper
- */
-class ItemMapper extends Mapper
-{
-
-    const TABLE_NAME = 'news_items';
-    /**
-     * @var Time
-     */
-    private $time;
-
-    /**
-     * NewsMapper constructor.
-     *
-     * @param IDBConnection $db     Database connection
-     * @param Time          $time   Time class
-     */
-    public function __construct(IDBConnection $db, Time $time)
-    {
-        parent::__construct($db, static::TABLE_NAME, Item::class);
-        $this->time = $time;
-    }
-
-    private function makeSelectQuery(
-        string $prependTo = '',
-        bool $oldestFirst = false,
-        bool $distinctFingerprint = false
-    ): string {
-        if ($oldestFirst) {
-            $ordering = 'ASC';
-        } else {
-            $ordering = 'DESC';
-        }
-
-        return 'SELECT `items`.* FROM `*PREFIX*news_items` `items` ' .
-        'JOIN `*PREFIX*news_feeds` `feeds` ' .
-        'ON `feeds`.`id` = `items`.`feed_id` ' .
-        'AND `feeds`.`deleted_at` = 0 ' .
-        'AND `feeds`.`user_id` = ? ' .
-        $prependTo .
-        'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' .
-        'ON `folders`.`id` = `feeds`.`folder_id` ' .
-        'WHERE `feeds`.`folder_id` IS NULL ' .
-        'OR `folders`.`deleted_at` = 0 ' .
-        'ORDER BY `items`.`id` ' . $ordering;
-    }
-
-    /**
-     * check if type is feed or all items should be shown
-     *
-     * @param  bool     $showAll
-     * @param  int|null $type
-     * @return string
-     */
-    private function buildStatusQueryPart($showAll, $type = null)
-    {
-        $sql = '';
-
-        if (isset($type) && $type === FeedType::STARRED) {
-            $sql = 'AND `items`.`starred` = ';
-            $sql .= $this->db->quote(true, IQueryBuilder::PARAM_BOOL) . ' ';
-        } elseif (!$showAll || $type === FeedType::UNREAD) {
-            $sql .= 'AND `items`.`unread` = ';
-            $sql .= $this->db->quote(true, IQueryBuilder::PARAM_BOOL) . ' ';
-        }
-
-        return $sql;
-    }
-
-    private function buildSearchQueryPart(array $search = []): string
-    {
-        return str_repeat('AND `items`.`search_index` LIKE ? ', count($search));
-    }
-
-    /**
-     * wrap and escape search parameters in a like statement
-     *
-     * @param  string[] $search an array of strings that should be searched
-     * @return array with like parameters
-     */
-    private function buildLikeParameters($search = [])
-    {
-        return array_map(
-            function ($param) {
-                $param = addcslashes($param, '\\_%');
-                return '%' . mb_strtolower($param, 'UTF-8') . '%';
-            },
-            $search
-        );
-    }
-
-    /**
-     * @param int    $id
-     * @param string $userId
-     * @return \OCA\News\Db\Item|Entity
-     */
-    public function find(string $userId, int $id)
-    {
-        $sql = $this->makeSelectQuery('AND `items`.`id` = ? ');
-        return $this->findEntity($sql, [$userId, $id]);
-    }
-
-    public function starredCount(string $userId): int
-    {
-        $sql = 'SELECT COUNT(*) AS size FROM `*PREFIX*news_items` `items` ' .
-            'JOIN `*PREFIX*news_feeds` `feeds` ' .
-            'ON `feeds`.`id` = `items`.`feed_id` ' .
-            'AND `feeds`.`deleted_at` = 0 ' .
-            'AND `feeds`.`user_id` = ? ' .
-            'AND `items`.`starred` = ? ' .
-            'LEFT OUTER JOIN `*PREFIX*news_folders` `folders` ' .
-            'ON `folders`.`id` = `feeds`.`folder_id` ' .
-            'WHERE `feeds`.`folder_id` IS NULL ' .
-            'OR `folders`.`deleted_at` = 0';
-
-        $params = [$userId, true];
-
-        $result = $this->execute($sql, $params)->fetch();
-
-        return (int)$result['size'];
-    }
-
-
-    public function readAll(int $highestItemId, string $time, string $userId): void
-    {
-        $sql = 'UPDATE `*PREFIX*news_items` ' .
-            'SET unread = ? ' .
-            ', `last_modified` = ? ' .
-            'WHERE `feed_id` IN (' .
-            'SELECT `id` FROM `*PREFIX*news_feeds` ' .
-            'WHERE `user_id` = ? ' .
-            ') ' .
-            'AND `id` <= ?';
-        $params = [false, $time, $userId, $highestItemId];
-        $this->execute($sql, $params);
-    }
-
-
-    public function readFolder(?int $folderId, int $highestItemId, string $time, string $userId): void
-    {
-        $folderWhere = is_null($folderId) ? 'IS' : '=';
-        $sql = 'UPDATE `*PREFIX*news_items` ' .
-            'SET unread = ? ' .
-            ', `last_modified` = ? ' .
-            'WHERE `feed_id` IN (' .
-            'SELECT `id` FROM `*PREFIX*news_feeds` ' .
-            "WHERE `folder_id` ${folderWhere} ? " .
-            'AND `user_id` = ? ' .
-            ') ' .
-            'AND `id` <= ?';
-        $params = [false, $time, $folderId, $userId,
-            $highestItemId];
-        $this->execute($sql, $params);
-    }
-
-
-    public function readFeed(int $feedId, int $highestItemId, string $time, string $userId): void
-    {
-        $sql = 'UPDATE `*PREFIX*news_items` ' .
-            'SET unread = ? ' .
-            ', `last_modified` = ? ' .
-            'WHERE `feed_id` = ? ' .
-            'AND `id` <= ? ' .
-            'AND EXISTS (' .
-            'SELECT * FROM `*PREFIX*news_feeds` ' .
-            'WHERE `user_id` = ? ' .
-            'AND `id` = ? ) ';
-        $params = [false, $time, $feedId, $highestItemId,
-            $userId, $feedId];
-
-        $this->execute($sql, $params);
-    }
-
-
-    private function getOperator(bool $oldestFirst): string
-    {
-        if ($oldestFirst) {
-            return '>';
-        } else {
-            return '<';
-        }
-    }
-
-
-    public function findAllNew(int $updatedSince, int $type, bool $showAll, string $userId): array
-    {
-        $sql = $this->buildStatusQueryPart($showAll, $type);
-
-        $sql .= 'AND `items`.`last_modified` >= ? ';
-        $sql = $this->makeSelectQuery($sql);
-        $params = [$userId, $updatedSince];
-        return $this->findEntities($sql, $params);
-    }
-
-
-    public function findAllNewFolder(?int $id, int $updatedSince, bool $showAll, string $userId): array
-    {
-        $sql = $this->buildStatusQueryPart($showAll);
-
-        $folderWhere = is_null($id) ? 'IS' : '=';
-        $sql .= "AND `feeds`.`folder_id` ${folderWhere} ? " .
-            'AND `items`.`last_modified` >= ? ';
-        $sql = $this->makeSelectQuery($sql);
-        $params = [$userId, $id, $updatedSince];
-        return $this->findEntities($sql, $params);
-    }
-
-
-    public function findAllNewFeed(?int $id, int $updatedSince, bool $showAll, string $userId): array
-    {
-        $sql = $this->buildStatusQueryPart($showAll);
-
-        $sql .= 'AND `items`.`feed_id` = ? ' .
-            'AND `items`.`last_modified` >= ? ';
-        $sql = $this->makeSelectQuery($sql);
-        $params = [$userId, $id, $updatedSince];
-        return $this->findEntities($sql, $params);
-    }
-
-
-    /**
-     * @param (int|mixed|null)[] $params
-     */
-    private function findEntitiesIgnoringNegativeLimit(string $sql, array $params, int $limit): array
-    {
-        // ignore limit if negative to offer a way to return all feeds
-        if ($limit >= 0) {
-            return $this->findEntities($sql, $params, $limit);
-        } else {
-            return $this->findEntities($sql, $params);
-        }
-    }
-
-
-    public function findAllFeed(
-        ?int $id,
-        int $limit,
-        int $offset,
-        bool $showAll,
-        bool $oldestFirst,
-        string $userId,
-        array $search = []
-    ): array {
-        $params = [$userId];
-        $params = array_merge($params, $this->buildLikeParameters($search));
-        $params[] = $id;
-
-        $sql = $this->buildStatusQueryPart($showAll);
-        $sql .= $this->buildSearchQueryPart($search);
-
-        $sql .= 'AND `items`.`feed_id` = ? ';
-        if ($offset !== 0) {
-            $sql .= 'AND `items`.`id` ' .
-                $this->getOperator($oldestFirst) . ' ? ';
-            $params[] = $offset;
-        }
-        $sql = $this->makeSelectQuery($sql, $oldestFirst);
-        return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
-    }
-
-
-    public function findAllFolder(
-        ?int $id,
-        int $limit,
-        int $offset,
-        bool $showAll,
-        bool $oldestFirst,
-        string $userId,
-        array $search = []
-    ): array {
-        $params = [$userId];
-        $params = array_merge($params, $this->buildLikeParameters($search));
-        $params[] = $id;
-
-        $sql = $this->buildStatusQueryPart($showAll);
-        $sql .= $this->buildSearchQueryPart($search);
-
-        $folderWhere = is_null($id) ? 'IS' : '=';
-        $sql .= "AND `feeds`.`folder_id` ${folderWhere} ? ";
-        if ($offset !== 0) {
-            $sql .= 'AND `items`.`id` ' . $this->getOperator($oldestFirst) . ' ? ';
-            $params[] = $offset;
-        }
-        $sql = $this->makeSelectQuery($sql, $oldestFirst);
-        return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
-    }
-
-
-    /**
-     * @param string[] $search
-     */
-    public function findAllItems(
-        int $limit,
-        int $offset,
-        int $type,
-        bool $showAll,
-        bool $oldestFirst,
-        string $userId,
-        array $search = []
-    ): array {
-        $params = [$userId];
-        $params = array_merge($params, $this->buildLikeParameters($search));
-        $sql = $this->buildStatusQueryPart($showAll, $type);
-        $sql .= $this->buildSearchQueryPart($search);
-
-        if ($offset !== 0) {
-            $sql .= 'AND `items`.`id` ' .
-                $this->getOperator($oldestFirst) . ' ? ';
-            $params[] = $offset;
-        }
-
-        $sql = $this->makeSelectQuery($sql, $oldestFirst);
-
-        return $this->findEntitiesIgnoringNegativeLimit($sql, $params, $limit);
-    }
-
-
-    public function findAllUnreadOrStarred(string $userId): array
-    {
-        $params = [$userId, true, true];
-        $sql = 'AND (`items`.`unread` = ? OR `items`.`starred` = ?) ';
-        $sql = $this->makeSelectQuery($sql);
-        return $this->findEntities($sql, $params);
-    }
-
-    /**
-     * @param $guidHash
-     * @param $feedId
-     * @param $userId
-     *
-     * @return Entity|Item
-     * @throws DoesNotExistException
-     * @throws MultipleObjectsReturnedException
-     */
-    public function findByGuidHash($guidHash, $feedId, $userId)
-    {
-        $sql = $this->makeSelectQuery(
-            'AND `items`.`guid_hash` = ? ' .
-            'AND `feeds`.`id` = ? '
-        );
-
-        return $this->findEntity($sql, [$userId, $guidHash, $feedId]);
-    }
-
-
-    /**
-     * Delete all items for feeds that have over $threshold unread and not
-     * starred items
-     *
-     * @param int $threshold the number of items that should be deleted
-     *
-     * @return void
-     */
-    public function deleteReadOlderThanThreshold($threshold)
-    {
-        $params = [false, false, $threshold];
-
-        $sql = 'SELECT (COUNT(*) - `feeds`.`articles_per_update`) AS `size`, ' .
-            '`feeds`.`id` AS `feed_id`, `feeds`.`articles_per_update` ' .
-            'FROM `*PREFIX*news_items` `items` ' .
-            'JOIN `*PREFIX*news_feeds` `feeds` ' .
-            'ON `feeds`.`id` = `items`.`feed_id` ' .
-            'AND `items`.`unread` = ? ' .
-            'AND `items`.`starred` = ? ' .
-            'GROUP BY `feeds`.`id`, `feeds`.`articles_per_update` ' .
-            'HAVING COUNT(*) > ?';
-
-        $result = $this->execute($sql, $params);
-
-        while ($row = $result->fetch()) {
-            $size = (int)$row['size'];
-            $limit = $size - $threshold;
-            $feed_id = $row['feed_id'];
-
-            if ($limit > 0) {
-                $params = [false, false, $feed_id, $limit];
-                $sql = 'SELECT `id` FROM `*PREFIX*news_items` ' .
-                    'WHERE `unread` = ? ' .
-                    'AND `starred` = ? ' .
-                    'AND `feed_id` = ? ' .
-                    'ORDER BY `id` ASC ' .
-                    'LIMIT 1 ' .
-                    'OFFSET ? ';
-            }
-            $limit_result = $this->execute($sql, $params);
-            if ($limit_row = $limit_result->fetch()) {
-                $limit_id = (int)$limit_row['id'];
-                $params = [false, false, $feed_id, $limit_id];
-                $sql = 'DELETE FROM `*PREFIX*news_items` ' .
-                    'WHERE `unread` = ? ' .
-                    'AND `starred` = ? ' .
-                    'AND `feed_id` = ? ' .
-                    'AND `id` < ? ';
-                $this->execute($sql, $params);
-            }
-        }
-    }
-
-
-    public function getNewestItemId(string $userId): int
-    {
-        $sql = 'SELECT MAX(`items`.`id`) AS `max_id` ' .
-            'FROM `*PREFIX*news_items` `items` ' .
-            'JOIN `*PREFIX*news_feeds` `feeds` ' .
-            'ON `feeds`.`id` = `items`.`feed_id` ' .
-            'AND `feeds`.`user_id` = ?';
-        $params = [$userId];
-
-        $result = $this->findOneQuery($sql, $params);
-
-        return (int)$result['max_id'];
-    }
-
-
-    /**
-     * Returns a list of ids and userid of all items
-     *
-     * @param int|null $limit
-     * @param int|null $offset
-     *
-     * @return array|false
-     */
-    public function findAllIds(?int $limit = null, ?int $offset = null)
-    {
-        $sql = 'SELECT `id` FROM `*PREFIX*news_items`';
-        return $this->execute($sql, [], $limit, $offset)->fetchAll();
-    }
-
-    /**
-     * Update search indices of all items
-     *
-     * @return void
-     */
-    public function updateSearchIndices(): void
-    {
-        // update indices in steps to prevent memory issues on larger systems
-        $step = 1000;  // update 1000 items at a time
-        $itemCount = 1;
-        $offset = 0;
-
-        // stop condition if there are no previously fetched items
-        while ($itemCount > 0) {
-            $items = $this->findAllIds($step, $offset);
-            $itemCount = count($items);
-            $this->updateSearchIndex($items);
-            $offset += $step;
-        }
-    }
-
-    private function updateSearchIndex(array $items = []): void
-    {
-        foreach ($items as $row) {
-            $sql = 'SELECT * FROM `*PREFIX*news_items` WHERE `id` = ?';
-            $params = [$row['id']];
-            $item = $this->findEntity($sql, $params);
-            $item->generateSearchIndex();
-            $this->update($item);
-        }
-    }
-
-    /**
-     * @return void
-     */
-    public function readItem(int $itemId, bool $isRead, string $lastModified, string $userId)
-    {
-        $item = $this->find($userId, $itemId);
-
-        // reading an item should set all of the same items as read, whereas
-        // marking an item as unread should only mark the selected instance
-        // as unread
-        if ($isRead) {
-            $sql = 'UPDATE `*PREFIX*news_items`
-                SET `unread` = ?,
-                    `last_modified` = ?
-                WHERE `fingerprint` = ?
-                    AND `feed_id` IN (
-                        SELECT `f`.`id` FROM `*PREFIX*news_feeds` AS `f`
-                            WHERE `f`.`user_id` = ?
-                    )';
-            $params = [false, $lastModified, $item->getFingerprint(), $userId];
-            $this->execute($sql, $params);
-        } else {
-            $item->setLastModified($lastModified);
-            $item->setUnread(true);
-            $this->update($item);
-        }
-    }
-
-    public function update(Entity $entity): Entity
-    {
-        $entity->setLastModified($this->time->getMicroTime());
-        return parent::update($entity);
-    }
-
-    public function insert(Entity $entity): Entity
-    {
-        $entity->setLastModified($this->time->getMicroTime());
-        return parent::insert($entity);
-    }
-
-    /**
-     * Remove deleted items.
-     *
-     * @return void
-     */
-    public function purgeDeleted(): void
-    {
-        $builder = $this->db->getQueryBuilder();
-        $builder->delete($this->tableName)
-            ->where('deleted_at != 0')
-            ->execute();
-    }
-    /**
-     * Performs a SELECT query with all arguments appended to the WHERE clause
-     * The SELECT will be performed on the current table and takes the entity
-     * that is related for transforming the properties into column names
-     *
-     * Important: This method does not filter marked as deleted rows!
-     *
-     * @param array $search an assoc array from property to filter value
-     * @param int|null $limit  Output limit
-     * @param int|null $offset Output offset
-     *
-     * @depreacted Legacy function
-     *
-     * @return Entity[]
-     */
-    public function where(array $search = [], ?int $limit = null, ?int $offset = null)
-    {
-        $entity = new $this->entityClass();
-
-        // turn keys into sql query filter, e.g. feedId -> feed_id = :feedId
-        $filter = array_map(
-            function ($property) use ($entity) {
-                // check if the property actually exists on the entity to prevent
-                // accidental Sql injection
-                if (!property_exists($entity, $property)) {
-                    $msg = 'Property ' . $property . ' does not exist on '
-                        . $this->entityClass;
-                    throw new \BadFunctionCallException($msg);
-                }
-
-                $column = $entity->propertyToColumn($property);
-                return $column . ' = :' . $property;
-            },
-            array_keys($search)
-        );
-
-        $andStatement = implode(' AND ', $filter);
-
-        $sql = 'SELECT * FROM `' . $this->getTableName() . '`';
-
-        if (count($search) > 0) {
-            $sql .= 'WHERE ' . $andStatement;
-        }
-
-        return $this->findEntities($sql, $search, $limit, $offset);
-    }
-}
diff --git a/lib/Db/ItemMapperV2.php b/lib/Db/ItemMapperV2.php
index f51b7af4d..8cdf9c430 100644
--- a/lib/Db/ItemMapperV2.php
+++ b/lib/Db/ItemMapperV2.php
@@ -12,6 +12,7 @@
 
 namespace OCA\News\Db;
 
+use OCA\News\Service\Exceptions\ServiceValidationException;
 use Doctrine\DBAL\FetchMode;
 use OCA\News\Utility\Time;
 use OCP\AppFramework\Db\DoesNotExistException;
@@ -55,12 +56,11 @@ class ItemMapperV2 extends NewsMapperV2
                 ->from($this->tableName, 'items')
                 ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
                 ->where('feeds.user_id = :user_id')
-                ->andWhere('deleted_at = 0')
+                ->andWhere('feeds.deleted_at = 0')
                 ->setParameter('user_id', $userId, IQueryBuilder::PARAM_STR);
 
         foreach ($params as $key => $value) {
-            $builder->andWhere("${key} = :${key}")
-                    ->setParameter($key, $value);
+            $builder->andWhere("${key} = " . $builder->createNamedParameter($value));
         }
 
         return $this->findEntities($builder);
@@ -74,13 +74,17 @@ class ItemMapperV2 extends NewsMapperV2
     public function findAll(): array
     {
         $builder = $this->db->getQueryBuilder();
-        $builder->addSelect('*')
+        $builder->select('*')
             ->from($this->tableName)
-            ->andWhere('deleted_at = 0');
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->andWhere('feeds.deleted_at = 0');
 
         return $this->findEntities($builder);
     }
 
+    /**
+     * @inheritDoc
+     */
     public function findFromUser(string $userId, int $id): Entity
     {
         $builder = $this->db->getQueryBuilder();
@@ -89,9 +93,9 @@ class ItemMapperV2 extends NewsMapperV2
             ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
             ->where('feeds.user_id = :user_id')
             ->andWhere('items.id = :item_id')
-            ->andWhere('deleted_at = 0')
+            ->andWhere('feeds.deleted_at = 0')
             ->setParameter('user_id', $userId, IQueryBuilder::PARAM_STR)
-            ->setParameter('item_id', $id, IQueryBuilder::PARAM_STR);
+            ->setParameter('item_id', $id, IQueryBuilder::PARAM_INT);
 
         return $this->findEntity($builder);
     }
@@ -102,15 +106,15 @@ class ItemMapperV2 extends NewsMapperV2
      * @param int    $feedId   ID of the feed
      * @param string $guidHash hash to find with
      *
-     * @return Item
+     * @return Item|Entity
      *
      * @throws DoesNotExistException
      * @throws MultipleObjectsReturnedException
      */
-    public function findByGuidHash(int $feedId, string $guidHash): Item
+    public function findByGuidHash(int $feedId, string $guidHash): Entity
     {
         $builder = $this->db->getQueryBuilder();
-        $builder->addSelect('*')
+        $builder->select('*')
             ->from($this->tableName)
             ->andWhere('feed_id = :feed_id')
             ->andWhere('guid_hash = :guid_hash')
@@ -120,6 +124,34 @@ class ItemMapperV2 extends NewsMapperV2
         return $this->findEntity($builder);
     }
 
+    /**
+     * Find a user item by a GUID hash.
+     *
+     * @param string $userId
+     * @param int    $feedId   ID of the feed
+     * @param string $guidHash hash to find with
+     *
+     * @return Item|Entity
+     *
+     * @throws DoesNotExistException
+     * @throws MultipleObjectsReturnedException
+     */
+    public function findForUserByGuidHash(string $userId, int $feedId, string $guidHash): Item
+    {
+        $builder = $this->db->getQueryBuilder();
+        $builder->select('items.*')
+            ->from($this->tableName, 'items')
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->andWhere('feeds.user_id = :user_id')
+            ->andWhere('feeds.id = :feed_id')
+            ->andWhere('items.guid_hash = :guid_hash')
+            ->setParameter('user_id', $userId, IQueryBuilder::PARAM_STR)
+            ->setParameter('feed_id', $feedId, IQueryBuilder::PARAM_INT)
+            ->setParameter('guid_hash', $guidHash, IQueryBuilder::PARAM_STR);
+
+        return $this->findEntity($builder);
+    }
+
     /**
      * @param int $feedId
      *
@@ -128,10 +160,10 @@ class ItemMapperV2 extends NewsMapperV2
     public function findAllForFeed(int $feedId): array
     {
         $builder = $this->db->getQueryBuilder();
-        $builder->addSelect('*')
+        $builder->select('*')
             ->from($this->tableName)
-            ->andWhere('feed_id = :feed_id')
-            ->setParameter('feed_id', $feedId, IQueryBuilder::PARAM_INT);
+            ->where('feed_id = :feed_identifier')
+            ->setParameter('feed_identifier', $feedId, IQueryBuilder::PARAM_INT);
 
         return $this->findEntities($builder);
     }
@@ -214,4 +246,312 @@ class ItemMapperV2 extends NewsMapperV2
     {
         //NO-OP
     }
+
+
+    /**
+     * @param string $userId
+     * @param int    $maxItemId
+     *
+     * @TODO: Update this for NC 21
+     */
+    public function readAll(string $userId, int $maxItemId): void
+    {
+        $builder = $this->db->getQueryBuilder();
+
+        $builder->update($this->tableName, 'items')
+                ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+                ->setValue('unread', 0)
+                ->andWhere('items.id =< :maxItemId')
+                ->andWhere('feeds.user_id = :userId')
+                ->setParameter('maxItemId', $maxItemId)
+                ->setParameter('userId', $userId);
+
+        $this->db->executeUpdate($builder->getSQL());
+    }
+
+    /**
+     * @param string $userId
+     *
+     * @return Entity|Item
+     *
+     * @throws DoesNotExistException            The item is not found
+     * @throws MultipleObjectsReturnedException Multiple items found
+     */
+    public function newest(string $userId): Entity
+    {
+        $builder = $this->db->getQueryBuilder();
+
+        $builder->select('items.*')
+                ->from($this->tableName, 'items')
+                ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+                ->where('feeds.user_id = :userId')
+                ->setParameter('userId', $userId)
+                ->orderBy('items.updated_date', 'DESC')
+                ->addOrderBy('items.id', 'DESC')
+                ->setMaxResults(1);
+
+        return $this->findEntity($builder);
+    }
+
+    /**
+     * @param string $userId
+     * @param int    $feedId
+     * @param int    $updatedSince
+     * @param bool   $hideRead
+     *
+     * @return Item[]
+     */
+    public function findAllInFeedAfter(
+        string $userId,
+        int $feedId,
+        int $updatedSince,
+        bool $hideRead
+    ): array {
+        $builder = $this->db->getQueryBuilder();
+
+        $builder->select('items.*')
+            ->from($this->tableName, 'items')
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->andWhere('items.updated_date >= :updatedSince')
+            ->andWhere('feeds.user_id = :userId')
+            ->andWhere('feeds.id = :feedId')
+            ->setParameters([
+                'updatedSince' => $updatedSince,
+                'feedId' => $feedId,
+                'userId'=> $userId,
+            ])
+            ->orderBy('items.updated_date', 'DESC')
+            ->addOrderBy('items.id', 'DESC');
+
+        if ($hideRead === true) {
+            $builder->andWhere('items.unread = 1');
+        }
+
+        return $this->findEntities($builder);
+    }
+
+    /**
+     * @param string   $userId
+     * @param int|null $folderId
+     * @param int      $updatedSince
+     * @param bool     $hideRead
+     *
+     * @return Item[]
+     */
+    public function findAllInFolderAfter(
+        string $userId,
+        ?int $folderId,
+        int $updatedSince,
+        bool $hideRead
+    ): array {
+        $builder = $this->db->getQueryBuilder();
+
+        $builder->select('items.*')
+            ->from($this->tableName, 'items')
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->innerJoin('feeds', FolderMapperV2::TABLE_NAME, 'folders', 'feeds.folder_id = folders.id')
+            ->andWhere('items.updated_date >= :updatedSince')
+            ->andWhere('feeds.user_id = :userId')
+            ->andWhere('folders.id = :folderId')
+            ->setParameters(['updatedSince' => $updatedSince, 'folderId' => $folderId, 'userId' => $userId])
+            ->orderBy('items.updated_date', 'DESC')
+            ->addOrderBy('items.id', 'DESC');
+
+        if ($hideRead === true) {
+            $builder->andWhere('items.unread = 1');
+        }
+
+        return $this->findEntities($builder);
+    }
+
+    /**
+     * @param string $userId
+     * @param int    $updatedSince
+     * @param int    $feedType
+     *
+     * @return Item[]|Entity[]
+     * @throws ServiceValidationException
+     */
+    public function findAllAfter(string $userId, int $feedType, int $updatedSince): array
+    {
+        $builder = $this->db->getQueryBuilder();
+
+        $builder->select('items.*')
+            ->from($this->tableName, 'items')
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->andWhere('items.updated_date >= :updatedSince')
+            ->andWhere('feeds.user_id = :userId')
+            ->setParameters(['updatedSince' => $updatedSince, 'userId' => $userId])
+            ->orderBy('items.updated_date', 'DESC')
+            ->addOrderBy('items.id', 'DESC');
+
+        switch ($feedType) {
+            case FeedType::STARRED:
+                $builder->andWhere('items.starred = 1');
+                break;
+            case FeedType::UNREAD:
+                $builder->andWhere('items.unread = 1');
+                break;
+            default:
+                throw new ServiceValidationException('Unexpected Feed type in call');
+        }
+
+        return $this->findEntities($builder);
+    }
+
+    /**
+     * @param string $userId
+     * @param int    $feedId
+     * @param int    $limit
+     * @param int    $offset
+     * @param bool   $hideRead
+     * @param bool   $oldestFirst
+     * @param array  $search
+     *
+     * @return Item[]
+     */
+    public function findAllFeed(
+        string $userId,
+        int $feedId,
+        int $limit,
+        int $offset,
+        bool $hideRead,
+        bool $oldestFirst,
+        array $search
+    ): array {
+        $builder = $this->db->getQueryBuilder();
+
+        $builder->select('items.*')
+            ->from($this->tableName, 'items')
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->andWhere('feeds.user_id = :userId')
+            ->andWhere('items.feed_id = :feedId')
+            ->setParameter('userId', $userId)
+            ->setParameter('feedId', $feedId)
+            ->setMaxResults($limit)
+            ->setFirstResult($offset)
+            ->orderBy('items.updated_date', ($oldestFirst ? 'ASC' : 'DESC'))
+            ->addOrderBy('items.id', ($oldestFirst ? 'ASC' : 'DESC'));
+
+        if ($search !== []) {
+            foreach ($search as $key => $term) {
+                $term = $this->db->escapeLikeParameter($term);
+                $builder->andWhere("items.search_index LIKE :term${key}")
+                    ->setParameter("term${key}", "%$term%");
+            }
+        }
+
+        if ($hideRead === true) {
+            $builder->andWhere('items.unread = 1');
+        }
+
+        return $this->findEntities($builder);
+    }
+
+    /**
+     * @param string   $userId
+     * @param int|null $folderId
+     * @param int      $limit
+     * @param int      $offset
+     * @param bool     $hideRead
+     * @param bool     $oldestFirst
+     * @param array    $search
+     *
+     * @return Item[]
+     */
+    public function findAllFolder(
+        string $userId,
+        ?int $folderId,
+        int $limit,
+        int $offset,
+        bool $hideRead,
+        bool $oldestFirst,
+        array $search
+    ): array {
+        $builder = $this->db->getQueryBuilder();
+
+        if ($folderId === null) {
+            $folderWhere = $builder->expr()->isNull('feeds.folder_id');
+        } else {
+            $folderWhere = $builder->expr()->eq('feeds.folder_id', $folderId);
+        }
+
+        $builder->select('items.*')
+            ->from($this->tableName, 'items')
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->andWhere('feeds.user_id = :userId')
+            ->andWhere($folderWhere)
+            ->setParameter('userId', $userId)
+            ->setMaxResults($limit)
+            ->setFirstResult($offset)
+            ->orderBy('items.updated_date', ($oldestFirst ? 'ASC' : 'DESC'))
+            ->addOrderBy('items.id', ($oldestFirst ? 'ASC' : 'DESC'));
+
+        if ($search !== []) {
+            foreach ($search as $key => $term) {
+                $term = $this->db->escapeLikeParameter($term);
+                $builder->andWhere("items.search_index LIKE :term${key}")
+                    ->setParameter("term${key}", "%$term%");
+            }
+        }
+
+        if ($hideRead === true) {
+            $builder->andWhere('items.unread = 1');
+        }
+
+        return $this->findEntities($builder);
+    }
+
+    /**
+     * @param string $userId
+     * @param int    $type
+     * @param int    $limit
+     * @param int    $offset
+     * @param bool   $oldestFirst
+     * @param array  $search
+     *
+     * @return Item[]
+     * @throws ServiceValidationException
+     */
+    public function findAllItems(
+        string $userId,
+        int $type,
+        int $limit,
+        int $offset,
+        bool $oldestFirst,
+        array $search
+    ): array {
+        $builder = $this->db->getQueryBuilder();
+
+        $builder->select('items.*')
+            ->from($this->tableName, 'items')
+            ->innerJoin('items', FeedMapperV2::TABLE_NAME, 'feeds', 'items.feed_id = feeds.id')
+            ->andWhere('feeds.user_id = :userId')
+            ->setParameter('userId', $userId)
+            ->setMaxResults($limit)
+            ->setFirstResult($offset)
+            ->orderBy('items.updated_date', ($oldestFirst ? 'ASC' : 'DESC'))
+            ->addOrderBy('items.id', ($oldestFirst ? 'ASC' : 'DESC'));
+
+        if ($search !== []) {
+            foreach ($search as $key => $term) {
+                $term = $this->db->escapeLikeParameter($term);
+                $builder->andWhere("items.search_index LIKE :term${key}")
+                        ->setParameter("term${key}", "%$term%");
+            }
+        }
+
+        switch ($type) {
+            case FeedType::STARRED:
+                $builder->andWhere('items.starred = 1');
+                break;
+            case FeedType::UNREAD:
+                $builder->andWhere('items.unread = 1');
+                break;
+            default:
+                throw new ServiceValidationException('Unexpected Feed type in call');
+        }
+
+        return $this->findEntities($builder);
+    }
 }
diff --git a/lib/Db/MapperFactory.php b/lib/Db/MapperFactory.php
deleted file mode 100644
index 35d587a6b..000000000
--- a/lib/Db/MapperFactory.php
+++ /dev/null
@@ -1,54 +0,0 @@
-<?php
-/**
- * Nextcloud - News
- *
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
- *
- * @author    Alessandro Cosentino <cosenal@gmail.com>
- * @author    Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright 2012 Alessandro Cosentino
- * @copyright 2012-2014 Bernhard Posselt
- */
-
-namespace OCA\News\Db;
-
-use OCA\News\Utility\Time;
-use OCP\IDBConnection;
-
-use OCA\News\Db\Mysql\ItemMapper as MysqlItemMapper;
-use OCA\News\DependencyInjection\IFactory;
-
-/**
- * Class LegacyMapperFactory
- *
- * @package OCA\News\Db
- * @deprecated not needed in modern system
- */
-class MapperFactory implements IFactory
-{
-
-    private $dbType;
-    private $db;
-    /**
-     * @var Time
-     */
-    private $time;
-
-    public function __construct(IDBConnection $db, $databaseType, Time $time)
-    {
-        $this->dbType = $databaseType;
-        $this->db = $db;
-        $this->time = $time;
-    }
-
-    public function build()
-    {
-        switch ($this->dbType) {
-            case 'mysql':
-                return new MysqlItemMapper($this->db, $this->time);
-            default:
-                return new ItemMapper($this->db, $this->time);
-        }
-    }
-}
diff --git a/lib/Db/Mysql/ItemMapper.php b/lib/Db/Mysql/ItemMapper.php
deleted file mode 100644
index 5ad3baf79..000000000
--- a/lib/Db/Mysql/ItemMapper.php
+++ /dev/null
@@ -1,98 +0,0 @@
-<?php
-/**
- * Nextcloud - News
- *
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
- *
- * @author    Alessandro Cosentino <cosenal@gmail.com>
- * @author    Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright 2012 Alessandro Cosentino
- * @copyright 2012-2014 Bernhard Posselt
- */
-
-namespace OCA\News\Db\Mysql;
-
-use OCA\News\Utility\Time;
-use OCP\IDBConnection;
-
-/**
- * Class LegacyItemMapper
- *
- * @package OCA\News\Db\Mysql
- * @deprecated use normal ItemMapper
- */
-class ItemMapper extends \OCA\News\Db\ItemMapper
-{
-
-    public function __construct(IDBConnection $db, Time $time)
-    {
-        parent::__construct($db, $time);
-    }
-
-
-    /**
-     * Delete all items for feeds that have over $threshold unread and not
-     * starred items
-     *
-     * @param int $threshold the number of items that should be deleted
-     *
-     * @return void
-     */
-    public function deleteReadOlderThanThreshold($threshold)
-    {
-        $sql = 'SELECT (COUNT(*) - `feeds`.`articles_per_update`) AS `size`, ' .
-        '`feeds`.`id` AS `feed_id`, `feeds`.`articles_per_update` ' .
-            'FROM `*PREFIX*news_items` `items` ' .
-            'JOIN `*PREFIX*news_feeds` `feeds` ' .
-                'ON `feeds`.`id` = `items`.`feed_id` ' .
-                'AND `items`.`unread` = ? ' .
-                'AND `items`.`starred` = ? ' .
-            'GROUP BY `feeds`.`id`, `feeds`.`articles_per_update` ' .
-            'HAVING COUNT(*) > ?';
-        $params = [false, false, $threshold];
-        $result = $this->execute($sql, $params);
-
-        while ($row = $result->fetch()) {
-            $size = (int) $row['size'];
-            $limit = $size - $threshold;
-
-            if ($limit > 0) {
-                $params = [false, false, $row['feed_id'], $limit];
-
-                $sql = 'DELETE FROM `*PREFIX*news_items` ' .
-                    'WHERE `unread` = ? ' .
-                    'AND `starred` = ? ' .
-                    'AND `feed_id` = ? ' .
-                    'ORDER BY `id` ASC ' .
-                    'LIMIT ?';
-
-                $this->execute($sql, $params);
-            }
-        }
-    }
-
-    /**
-     * @return void
-     */
-    public function readItem($itemId, $isRead, $lastModified, $userId)
-    {
-        $item = $this->find($itemId, $userId);
-
-        if ($isRead) {
-            $sql = 'UPDATE `*PREFIX*news_items` `items`
-                JOIN `*PREFIX*news_feeds` `feeds`
-                    ON `feeds`.`id` = `items`.`feed_id`
-                SET `items`.`unread` = ?,
-                    `items`.`last_modified` = ?
-                WHERE `items`.`fingerprint` = ?
-                    AND `feeds`.`user_id` = ?';
-            $params = [false, $lastModified, $item->getFingerprint(), $userId];
-            $this->execute($sql, $params);
-        } else {
-            $item->setLastModified($lastModified);
-            $item->setUnread(true);
-            $this->update($item);
-        }
-    }
-}
diff --git a/lib/DependencyInjection/IFactory.php b/lib/DependencyInjection/IFactory.php
deleted file mode 100644
index 73c283380..000000000
--- a/lib/DependencyInjection/IFactory.php
+++ /dev/null
@@ -1,25 +0,0 @@
-<?php
-/**
- * Nextcloud - News
- *
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
- *
- * @author    Alessandro Cosentino <cosenal@gmail.com>
- * @author    Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright 2012 Alessandro Cosentino
- * @copyright 2012-2014 Bernhard Posselt
- */
-
-namespace OCA\News\DependencyInjection;
-
-interface IFactory
-{
-
-    /**
-     * Method that constructs the object
-     *
-     * @return mixed the constructed object
-     */
-    public function build();
-}
diff --git a/lib/Explore/RecommendedSiteNotFoundException.php b/lib/Explore/Exceptions/RecommendedSiteNotFoundException.php
similarity index 91%
rename from lib/Explore/RecommendedSiteNotFoundException.php
rename to lib/Explore/Exceptions/RecommendedSiteNotFoundException.php
index 9d3e717c7..444bec23b 100644
--- a/lib/Explore/RecommendedSiteNotFoundException.php
+++ b/lib/Explore/Exceptions/RecommendedSiteNotFoundException.php
@@ -11,7 +11,7 @@
  * @copyright 2012-2014 Bernhard Posselt
  */
 
-namespace OCA\News\Explore;
+namespace OCA\News\Explore\Exceptions;
 
 use Exception;
 
diff --git a/lib/Explore/RecommendedSites.php b/lib/Explore/RecommendedSites.php
index 59ca3f337..f483e64cb 100644
--- a/lib/Explore/RecommendedSites.php
+++ b/lib/Explore/RecommendedSites.php
@@ -13,6 +13,8 @@
 
 namespace OCA\News\Explore;
 
+use OCA\News\Explore\Exceptions\RecommendedSiteNotFoundException;
+
 class RecommendedSites
 {
 
diff --git a/lib/Fetcher/FetcherException.php b/lib/Fetcher/FetcherException.php
deleted file mode 100644
index 263b54239..000000000
--- a/lib/Fetcher/FetcherException.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-/**
- * Nextcloud - News
- *
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
- *
- * @author    Alessandro Cosentino <cosenal@gmail.com>
- * @author    Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright 2012 Alessandro Cosentino
- * @copyright 2012-2014 Bernhard Posselt
- */
-
-namespace OCA\News\Fetcher;
-
-class FetcherException extends \Exception
-{
-
-    /**
-     * Constructor
-     *
-     * @param string $msg the error message
-     */
-    public function __construct(string $msg)
-    {
-        parent::__construct($msg);
-    }
-}
diff --git a/lib/Service/Exceptions/ServiceConflictException.php b/lib/Service/Exceptions/ServiceConflictException.php
index dd6ba03c9..ed3d3a54c 100644
--- a/lib/Service/Exceptions/ServiceConflictException.php
+++ b/lib/Service/Exceptions/ServiceConflictException.php
@@ -13,16 +13,22 @@
 
 namespace OCA\News\Service\Exceptions;
 
+use Exception;
+use OCP\AppFramework\Db\IMapperException;
+
+/**
+ * Class ServiceConflictException
+ *
+ * @package OCA\News\Service\Exceptions
+ */
 class ServiceConflictException extends ServiceException
 {
 
     /**
-     * Constructor
-     *
-     * @param string $msg the error message
+     * @inheritDoc
      */
-    public function __construct(string $msg)
+    public static function from(IMapperException $exception): ServiceException
     {
-        parent::__construct($msg);
+        return new self($exception->getMessage(), $exception->getCode(), $exception);
     }
 }
diff --git a/lib/Service/Exceptions/ServiceException.php b/lib/Service/Exceptions/ServiceException.php
index c5ddddbd9..69b963ab6 100644
--- a/lib/Service/Exceptions/ServiceException.php
+++ b/lib/Service/Exceptions/ServiceException.php
@@ -13,16 +13,35 @@
 
 namespace OCA\News\Service\Exceptions;
 
-class ServiceException extends \Exception
+use Exception;
+use OCP\AppFramework\Db\IMapperException;
+
+/**
+ * Class ServiceException
+ *
+ * @package OCA\News\Service\Exceptions
+ */
+abstract class ServiceException extends Exception
 {
 
     /**
      * Constructor
      *
      * @param string $msg the error message
+     * @param int    $code
+     * @param Exception|null   $previous
      */
-    public function __construct(string $msg)
+    final public function __construct(string $msg, int $code = 0, ?Exception $previous = null)
     {
-        parent::__construct($msg);
+        parent::__construct($msg, $code, $previous);
     }
+
+    /**
+     * Create exception from Mapper exception.
+     *
+     * @param IMapperException|Exception $exception Existing exception
+     *
+     * @return static
+     */
+    abstract public static function from(IMapperException $exception): ServiceException;
 }
diff --git a/lib/Service/Exceptions/ServiceNotFoundException.php b/lib/Service/Exceptions/ServiceNotFoundException.php
index 6c68ea6b2..f70706506 100644
--- a/lib/Service/Exceptions/ServiceNotFoundException.php
+++ b/lib/Service/Exceptions/ServiceNotFoundException.php
@@ -13,16 +13,21 @@
 
 namespace OCA\News\Service\Exceptions;
 
+use Exception;
+use OCP\AppFramework\Db\IMapperException;
+
+/**
+ * Class ServiceNotFoundException
+ *
+ * @package OCA\News\Service\Exceptions
+ */
 class ServiceNotFoundException extends ServiceException
 {
-
     /**
-     * Constructor
-     *
-     * @param string $msg the error message
+     * @inheritDoc
      */
-    public function __construct(string $msg)
+    public static function from(IMapperException $exception): ServiceException
     {
-        parent::__construct($msg);
+        return new self($exception->getMessage(), $exception->getCode(), $exception);
     }
 }
diff --git a/lib/Service/Exceptions/ServiceValidationException.php b/lib/Service/Exceptions/ServiceValidationException.php
index 8e9dc9fee..c41ce4aac 100644
--- a/lib/Service/Exceptions/ServiceValidationException.php
+++ b/lib/Service/Exceptions/ServiceValidationException.php
@@ -13,16 +13,22 @@
 
 namespace OCA\News\Service\Exceptions;
 
+use Exception;
+use OCP\AppFramework\Db\IMapperException;
+
+/**
+ * Class ServiceValidationException
+ *
+ * @package OCA\News\Service\Exceptions
+ */
 class ServiceValidationException extends ServiceException
 {
 
     /**
-     * Constructor
-     *
-     * @param string $msg the error message
+     * @inheritDoc
      */
-    public function __construct(string $msg)
+    public static function from(IMapperException $exception): ServiceException
     {
-        parent::__construct($msg);
+        return new self($exception->getMessage(), $exception->getCode(), $exception);
     }
 }
diff --git a/lib/Service/FeedServiceV2.php b/lib/Service/FeedServiceV2.php
index 078941752..ccb7c047a 100644
--- a/lib/Service/FeedServiceV2.php
+++ b/lib/Service/FeedServiceV2.php
@@ -18,6 +18,7 @@ use FeedIo\Reader\ReadErrorException;
 use HTMLPurifier;
 
 use OCA\News\Db\FeedMapperV2;
+use OCA\News\Db\Folder;
 use OCA\News\Fetcher\FeedFetcher;
 use OCA\News\Service\Exceptions\ServiceConflictException;
 use OCA\News\Service\Exceptions\ServiceNotFoundException;
@@ -119,7 +120,7 @@ class FeedServiceV2 extends Service
         $feeds = $this->mapper->findAllFromUser($userId);
 
         foreach ($feeds as &$feed) {
-            $items = $this->itemService->findAllForFeed($feed->getId());
+            $items = $this->itemService->findAllInFeed($userId, $feed->getId());
             $feed->items = $items;
         }
         return $feeds;
@@ -341,4 +342,21 @@ class FeedServiceV2 extends Service
             $this->fetch($feed);
         }
     }
+
+    /**
+     * Mark a feed as read
+     *
+     * @param string   $userId    Feed owner
+     * @param int      $id        Feed ID
+     * @param int|null $maxItemID Highest item ID to mark as read
+     *
+     * @throws ServiceConflictException
+     * @throws ServiceNotFoundException
+     */
+    public function read(string $userId, int $id, ?int $maxItemID = null): void
+    {
+        $feed = $this->find($userId, $id);
+
+        $this->mapper->read($userId, $feed->getId(), $maxItemID);
+    }
 }
diff --git a/lib/Service/FolderServiceV2.php b/lib/Service/FolderServiceV2.php
index ae8d37816..d13b4afc0 100644
--- a/lib/Service/FolderServiceV2.php
+++ b/lib/Service/FolderServiceV2.php
@@ -178,4 +178,21 @@ class FolderServiceV2 extends Service
         $folder->setOpened($open);
         return $this->mapper->update($folder);
     }
+
+    /**
+     * Mark a folder as read
+     *
+     * @param string   $userId    Folder owner
+     * @param int      $id        Folder ID
+     * @param int|null $maxItemID Highest item ID to mark as read
+     *
+     * @throws ServiceConflictException
+     * @throws ServiceNotFoundException
+     */
+    public function read(string $userId, int $id, ?int $maxItemID = null): void
+    {
+        $folder = $this->find($userId, $id);
+
+        $this->mapper->read($userId, $folder->getId(), $maxItemID);
+    }
 }
diff --git a/lib/Service/ItemService.php b/lib/Service/ItemService.php
deleted file mode 100644
index 8ba0a4b73..000000000
--- a/lib/Service/ItemService.php
+++ /dev/null
@@ -1,352 +0,0 @@
-<?php
-/**
- * Nextcloud - News
- *
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
- *
- * @author    Alessandro Cosentino <cosenal@gmail.com>
- * @author    Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright 2012 Alessandro Cosentino
- * @copyright 2012-2014 Bernhard Posselt
- */
-
-namespace OCA\News\Service;
-
-use OCA\News\AppInfo\Application;
-use OCA\News\Db\ItemMapperV2;
-use OCA\News\Service\Exceptions\ServiceNotFoundException;
-use OCP\IConfig;
-use OCP\AppFramework\Db\DoesNotExistException;
-
-use OCA\News\Db\ItemMapper;
-use OCA\News\Db\FeedType;
-use OCA\News\Utility\Time;
-use Psr\Log\LoggerInterface;
-
-/**
- * Class LegacyItemService
- *
- * @package OCA\News\Service
- * @deprecated use ItemServiceV2
- */
-class ItemService extends Service
-{
-
-    /**
-     * @var IConfig
-     */
-    private $config;
-    /**
-     * @var Time
-     */
-    private $timeFactory;
-    /**
-     * @var ItemMapper
-     */
-    private $oldItemMapper;
-
-    public function __construct(
-        ItemMapperV2 $itemMapper,
-        ItemMapper $oldItemMapper,
-        Time $timeFactory,
-        IConfig $config,
-        LoggerInterface $logger
-    ) {
-        parent::__construct($itemMapper, $logger);
-        $this->config = $config;
-        $this->timeFactory = $timeFactory;
-        $this->oldItemMapper = $oldItemMapper;
-    }
-
-
-    /**
-     * Returns all new items
-     *
-     * @param int|null $id           the id of the feed, 0 for starred or all items
-     * @param int      $type         the type of the feed
-     * @param int      $updatedSince a timestamp with the last modification date
-     *                               returns only items with a >= modified
-     *                               timestamp
-     * @param boolean  $showAll      if unread items should also be returned
-     * @param string   $userId       the name of the user
-     *
-     * @return array of items
-     */
-    public function findAllNew(?int $id, $type, int $updatedSince, bool $showAll, string $userId)
-    {
-        switch ($type) {
-            case FeedType::FEED:
-                return $this->oldItemMapper->findAllNewFeed(
-                    $id,
-                    $updatedSince,
-                    $showAll,
-                    $userId
-                );
-            case FeedType::FOLDER:
-                return $this->oldItemMapper->findAllNewFolder(
-                    $id,
-                    $updatedSince,
-                    $showAll,
-                    $userId
-                );
-            default:
-                return $this->oldItemMapper->findAllNew(
-                    $updatedSince,
-                    $type,
-                    $showAll,
-                    $userId
-                );
-        }
-    }
-
-
-    /**
-     * Returns all items
-     *
-     * @param int|null $id           the id of the feed, 0 for starred or all items
-     * @param int      $type         the type of the feed
-     * @param int      $limit        how many items should be returned
-     * @param int      $offset       the offset
-     * @param boolean  $showAll      if unread items should also be returned
-     * @param boolean  $oldestFirst  if it should be ordered by oldest first
-     * @param string   $userId       the name of the user
-     * @param string[] $search       an array of keywords that the result should
-     *                               contain in either the author, title, link
-     *                               or body
-     *
-     * @return array of items
-     */
-    public function findAllItems(
-        ?int $id,
-        $type,
-        $limit,
-        $offset,
-        $showAll,
-        $oldestFirst,
-        $userId,
-        $search = []
-    ) {
-        switch ($type) {
-            case FeedType::FEED:
-                return $this->oldItemMapper->findAllFeed(
-                    $id,
-                    $limit,
-                    $offset,
-                    $showAll,
-                    $oldestFirst,
-                    $userId,
-                    $search
-                );
-            case FeedType::FOLDER:
-                return $this->oldItemMapper->findAllFolder(
-                    $id,
-                    $limit,
-                    $offset,
-                    $showAll,
-                    $oldestFirst,
-                    $userId,
-                    $search
-                );
-            default:
-                return $this->oldItemMapper->findAllItems(
-                    $limit,
-                    $offset,
-                    $type,
-                    $showAll,
-                    $oldestFirst,
-                    $userId,
-                    $search
-                );
-        }
-    }
-
-    public function findAllForUser(string $userId, array $params = []): array
-    {
-        return $this->mapper->findAllFromUser($userId, $params);
-    }
-
-
-    /**
-     * Star or unstar an item
-     *
-     * @param int     $feedId    the id of the item's feed that should be starred
-     * @param string  $guidHash  the guidHash of the item that should be starred
-     * @param boolean $isStarred if true the item will be marked as starred,
-     *                            if false unstar
-     * @param string  $userId    the name of the user for security reasons
-     *
-     * @throws ServiceNotFoundException if the item does not exist
-     *
-     * @return void
-     */
-    public function star($feedId, $guidHash, $isStarred, $userId): void
-    {
-        try {
-            $item = $this->mapper->findByGuidHash($feedId, $guidHash);
-
-            $item->setStarred($isStarred);
-
-            $this->mapper->update($item);
-        } catch (DoesNotExistException $ex) {
-            throw new ServiceNotFoundException($ex->getMessage());
-        }
-    }
-
-
-    /**
-     * Read or unread an item
-     *
-     * @param int     $itemId the id of the item that should be read
-     * @param boolean $isRead if true the item will be marked as read,
-     *                         if false unread
-     * @param string  $userId the name of the user for security reasons
-     *
-     * @throws ServiceNotFoundException if the item does not exist
-     *
-     * @return void
-     */
-    public function read($itemId, $isRead, $userId): void
-    {
-        try {
-            $lastModified = $this->timeFactory->getMicroTime();
-            $this->oldItemMapper->readItem($itemId, $isRead, $lastModified, $userId);
-        } catch (DoesNotExistException $ex) {
-            throw new ServiceNotFoundException($ex->getMessage());
-        }
-    }
-
-
-    /**
-     * Set all items read
-     *
-     * @param int    $highestItemId all items below that are marked read. This is
-     *                              used to prevent marking items as read that
-     *                              the users hasn't seen yet
-     * @param string $userId        the name of the user
-     *
-     * @return void
-     */
-    public function readAll($highestItemId, $userId): void
-    {
-        $time = $this->timeFactory->getMicroTime();
-        $this->oldItemMapper->readAll($highestItemId, $time, $userId);
-    }
-
-
-    /**
-     * Set a folder read
-     *
-     * @param int|null $folderId      the id of the folder that should be marked read
-     * @param int      $highestItemId all items below that are marked read. This is
-     *                                used to prevent marking items as read that
-     *                                the users hasn't seen yet
-     * @param string   $userId        the name of the user
-     *
-     * @return void
-     */
-    public function readFolder(?int $folderId, $highestItemId, $userId): void
-    {
-        $time = $this->timeFactory->getMicroTime();
-        $this->oldItemMapper->readFolder(
-            $folderId,
-            $highestItemId,
-            $time,
-            $userId
-        );
-    }
-
-
-    /**
-     * Set a feed read
-     *
-     * @param int    $feedId        the id of the feed that should be marked read
-     * @param int    $highestItemId all items below that are marked read. This is
-     *                              used to prevent marking items as read that
-     *                              the users hasn't seen yet
-     * @param string $userId        the name of the user
-     *
-     * @return void
-     */
-    public function readFeed($feedId, $highestItemId, $userId): void
-    {
-        $time = $this->timeFactory->getMicroTime();
-        $this->oldItemMapper->readFeed($feedId, $highestItemId, $time, $userId);
-    }
-
-
-    /**
-     * This method deletes all unread feeds that are not starred and over the
-     * count of $this->autoPurgeCount starting by the oldest. This is to clean
-     * up the database so that old entries don't spam your db. As criteria for
-     * old, the id is taken
-     *
-     * @return void
-     */
-    public function autoPurgeOld(): void
-    {
-        $count = $this->config->getAppValue(
-            Application::NAME,
-            'autoPurgeCount',
-            Application::DEFAULT_SETTINGS['autoPurgeCount']
-        );
-        if ($count >= 0) {
-            $this->oldItemMapper->deleteReadOlderThanThreshold($count);
-        }
-    }
-
-
-    /**
-     * Returns the newest item id, use this for marking feeds read
-     *
-     * @param  string $userId the name of the user
-     * @throws ServiceNotFoundException if there is no newest item
-     * @return int
-     */
-    public function getNewestItemId($userId)
-    {
-        try {
-            return $this->oldItemMapper->getNewestItemId($userId);
-        } catch (DoesNotExistException $ex) {
-            throw new ServiceNotFoundException($ex->getMessage());
-        }
-    }
-
-
-    /**
-     * Returns the starred count
-     *
-     * @param  string $userId the name of the user
-     * @return int the count
-     */
-    public function starredCount($userId)
-    {
-        return $this->oldItemMapper->starredCount($userId);
-    }
-
-
-    /**
-     * @param string $userId from which user the items should be taken
-     * @return array of items which are starred or unread
-     */
-    public function getUnreadOrStarred($userId): array
-    {
-        return $this->oldItemMapper->findAllUnreadOrStarred($userId);
-    }
-
-
-    /**
-     * Regenerates the search index for all items
-     *
-     * @return void
-     */
-    public function generateSearchIndices(): void
-    {
-        $this->oldItemMapper->updateSearchIndices();
-    }
-
-    public function findAll(): array
-    {
-        return $this->mapper->findAll();
-    }
-}
diff --git a/lib/Service/ItemServiceV2.php b/lib/Service/ItemServiceV2.php
index f13b249b8..8a518b5bd 100644
--- a/lib/Service/ItemServiceV2.php
+++ b/lib/Service/ItemServiceV2.php
@@ -13,10 +13,16 @@
 namespace OCA\News\Service;
 
 use OCA\News\AppInfo\Application;
+use OCA\News\Db\Feed;
+use OCA\News\Db\FeedType;
 use OCA\News\Db\Item;
 use OCA\News\Db\ItemMapperV2;
+use OCA\News\Service\Exceptions\ServiceConflictException;
+use OCA\News\Service\Exceptions\ServiceNotFoundException;
+use OCA\News\Service\Exceptions\ServiceValidationException;
 use OCP\AppFramework\Db\DoesNotExistException;
 use OCP\AppFramework\Db\Entity;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
 use OCP\IConfig;
 use Psr\Log\LoggerInterface;
 
@@ -37,7 +43,7 @@ class ItemServiceV2 extends Service
      * ItemService constructor.
      *
      * @param ItemMapperV2    $mapper
-     * @param IConfig          $config
+     * @param IConfig         $config
      * @param LoggerInterface $logger
      */
     public function __construct(
@@ -53,7 +59,7 @@ class ItemServiceV2 extends Service
      * Finds all items of a user
      *
      * @param string $userId The ID/name of the user
-     * @param array $params Filter parameters
+     * @param array  $params Filter parameters
      *
      *
      * @return Item[]
@@ -83,14 +89,15 @@ class ItemServiceV2 extends Service
     public function insertOrUpdate(Item $item): Entity
     {
         try {
-            $db_item = $this->mapper->findByGuidHash($item->getFeedId(), $item->getGuidHash());
+            $db_item = $this->findByGuidHash($item->getFeedId(), $item->getGuidHash());
 
             // Transfer user modifications
             $item->setUnread($db_item->isUnread())
                  ->setStarred($db_item->isStarred())
                  ->setId($db_item->getId());
 
-            $item->generateSearchIndex();
+            $item->generateSearchIndex();//generates fingerprint
+
             // We don't want to update the database record if there is no
             // change in the fetched item
             if ($db_item->getFingerprint() === $item->getFingerprint()) {
@@ -104,13 +111,36 @@ class ItemServiceV2 extends Service
     }
 
     /**
-     * @param int $feedId
+     * Return all starred items
      *
-     * @return array
+     * @param string $userId
+     *
+     * @return Item[]
      */
-    public function findAllForFeed(int $feedId): array
+    public function starred(string $userId): array
     {
-        return $this->mapper->findAllForFeed($feedId);
+        return $this->findAllForUser($userId, ['starred' => 1]);
+    }
+
+    /**
+     * Mark an item as read
+     *
+     * @param string $userId Item owner
+     * @param int    $id     Item ID
+     * @param bool   $read
+     *
+     * @return Item
+     * @throws ServiceNotFoundException
+     * @throws ServiceConflictException
+     */
+    public function read(string $userId, int $id, bool $read): Entity
+    {
+        /** @var Item $item */
+        $item = $this->find($userId, $id);
+
+        $item->setUnread(!$read);
+
+        return $this->mapper->update($item);
     }
 
     /**
@@ -133,13 +163,232 @@ class ItemServiceV2 extends Service
 
         return $this->mapper->deleteOverThreshold($threshold, $removeUnread);
     }
+    /**
+     * Mark an item as starred
+     *
+     * @param string $userId Item owner
+     * @param int    $id     Item ID
+     * @param bool   $starred
+     *
+     * @return Item
+     * @throws ServiceNotFoundException|ServiceConflictException
+     */
+    public function star(string $userId, int $id, bool $starred): Entity
+    {
+        /** @var Item $item */
+        $item = $this->find($userId, $id);
+
+        $item->setStarred($starred);
+
+        return $this->mapper->update($item);
+    }
+
+    /**
+     * Mark an item as starred by GUID hash
+     *
+     * @param string $userId Item owner
+     * @param int    $feedId Item ID
+     * @param string $guidHash
+     * @param bool   $starred
+     *
+     * @return Item
+     * @throws ServiceConflictException
+     * @throws ServiceNotFoundException
+     */
+    public function starByGuid(string $userId, int $feedId, string $guidHash, bool $starred): Entity
+    {
+        try {
+            $item = $this->mapper->findForUserByGuidHash($userId, $feedId, $guidHash);
+        } catch (DoesNotExistException $ex) {
+            throw ServiceNotFoundException::from($ex);
+        } catch (MultipleObjectsReturnedException $ex) {
+            throw ServiceConflictException::from($ex);
+        }
+
+        $item->setStarred($starred);
+
+        return $this->mapper->update($item);
+    }
+
+    /**
+     * Mark all items as read
+     *
+     * @param string $userId Item owner
+     * @param int    $maxItemId
+     *
+     * @return void
+     */
+    public function readAll(string $userId, int $maxItemId): void
+    {
+        $this->mapper->readAll($userId, $maxItemId);
+    }
+
+    /**
+     * @param string $userId
+     *
+     * @return Item
+     */
+    public function newest(string $userId): Entity
+    {
+        try {
+            return $this->mapper->newest($userId);
+        } catch (DoesNotExistException $e) {
+            throw ServiceNotFoundException::from($e);
+        } catch (MultipleObjectsReturnedException $e) {
+            throw ServiceConflictException::from($e);
+        }
+    }
 
     /**
      * @param int    $feedId
      * @param string $guidHash
+     *
+     * @return Item|Entity
+     *
+     * @throws DoesNotExistException
+     * @throws MultipleObjectsReturnedException
      */
-    public function findForGuidHash(int $feedId, string $guidHash)
+    public function findByGuidHash(int $feedId, string $guidHash): Entity
     {
         return $this->mapper->findByGuidHash($feedId, $guidHash);
     }
+
+    /**
+     * Convenience method to find all items in a feed.
+     *
+     * @param string $userId
+     * @param int    $feedId
+     *
+     * @return array
+     */
+    public function findAllInFeed(string $userId, int $feedId): array
+    {
+        return $this->findAllInFeedAfter($userId, $feedId, PHP_INT_MIN, false);
+    }
+
+    /**
+     * Returns all new items in a feed
+     * @param string  $userId       the name of the user
+     * @param int     $feedId       the id of the feed
+     * @param int     $updatedSince a timestamp with the minimal modification date
+     * @param boolean $hideRead     if unread items should also be returned
+     *
+     * @return array of items
+     */
+    public function findAllInFeedAfter(string $userId, int $feedId, int $updatedSince, bool $hideRead): array
+    {
+        return $this->mapper->findAllInFeedAfter($userId, $feedId, $updatedSince, $hideRead);
+    }
+
+    /**
+     * Returns all new items in a folder
+     * @param string   $userId       the name of the user
+     * @param int|null $folderId     the id of the folder
+     * @param int      $updatedSince a timestamp with the minimal modification date
+     * @param boolean  $hideRead     if unread items should also be returned
+     *
+     * @return array of items
+     */
+    public function findAllInFolderAfter(string $userId, ?int $folderId, int $updatedSince, bool $hideRead): array
+    {
+        return $this->mapper->findAllInFolderAfter($userId, $folderId, $updatedSince, $hideRead);
+    }
+
+    /**
+     * Returns all new items of a type
+     *
+     * @param string $userId       the name of the user
+     * @param int    $feedType     the type of feed items to fetch. (starred || unread)
+     * @param int    $updatedSince a timestamp with the minimal modification date
+     *
+     * @return array of items
+     *
+     * @throws ServiceValidationException
+     */
+    public function findAllAfter(string $userId, int $feedType, int $updatedSince): array
+    {
+        if (!in_array($feedType, [FeedType::STARRED, FeedType::UNREAD])) {
+            throw new ServiceValidationException('Trying to find in unknown type');
+        }
+
+        return $this->mapper->findAllAfter($userId, $feedType, $updatedSince);
+    }
+
+
+    /**
+     * Returns all items
+     *
+     * @param int $feedId            the id of the feed
+     * @param int      $limit        how many items should be returned
+     * @param int      $offset       the offset
+     * @param boolean  $hideRead      if unread items should also be returned
+     * @param boolean  $oldestFirst  if it should be ordered by oldest first
+     * @param string   $userId       the name of the user
+     * @param string[] $search       an array of keywords that the result should
+     *                               contain in either the author, title, link
+     *                               or body
+     *
+     * @return array of items
+     */
+    public function findAllInFeedWithFilters(
+        string $userId,
+        int $feedId,
+        int $limit,
+        int $offset,
+        bool $hideRead,
+        bool $oldestFirst,
+        array $search = []
+    ): array {
+        return $this->mapper->findAllFeed($userId, $feedId, $limit, $offset, $hideRead, $oldestFirst, $search);
+    }
+    /**
+     * Returns all items
+     *
+     * @param int|null $folderId     the id of the folder
+     * @param int      $limit        how many items should be returned
+     * @param int      $offset       the offset
+     * @param boolean  $hideRead      if unread items should also be returned
+     * @param boolean  $oldestFirst  if it should be ordered by oldest first
+     * @param string   $userId       the name of the user
+     * @param string[] $search       an array of keywords that the result should
+     *                               contain in either the author, title, link
+     *                               or body
+     *
+     * @return array of items
+     */
+    public function findAllInFolderWithFilters(
+        string $userId,
+        ?int $folderId,
+        int $limit,
+        int $offset,
+        bool $hideRead,
+        bool $oldestFirst,
+        array $search = []
+    ): array {
+        return $this->mapper->findAllFolder($userId, $folderId, $limit, $offset, $hideRead, $oldestFirst, $search);
+    }
+    /**
+     * Returns all items
+     *
+     * @param int      $type         the type of the feed
+     * @param int      $limit        how many items should be returned
+     * @param int      $offset       the offset
+     * @param boolean  $oldestFirst  if it should be ordered by oldest first
+     * @param string   $userId       the name of the user
+     * @param string[] $search       an array of keywords that the result should
+     *                               contain in either the author, title, link
+     *                               or body
+     *
+     * @return array of items
+     */
+    public function findAllWithFilters(
+        string $userId,
+        int $type,
+        int $limit,
+        int $offset,
+        bool $oldestFirst,
+        array $search = []
+    ): array {
+        return $this->mapper->findAllItems($userId, $type, $limit, $offset, $oldestFirst, $search);
+    }
 }
diff --git a/lib/Service/Service.php b/lib/Service/Service.php
index 1d7fdffb6..951d460f6 100644
--- a/lib/Service/Service.php
+++ b/lib/Service/Service.php
@@ -14,6 +14,7 @@
 namespace OCA\News\Service;
 
 use OCA\News\Db\NewsMapperV2;
+use OCA\News\Service\Exceptions\ServiceConflictException;
 use OCA\News\Service\Exceptions\ServiceNotFoundException;
 use OCP\AppFramework\Db\DoesNotExistException;
 use OCP\AppFramework\Db\Entity;
@@ -65,14 +66,13 @@ abstract class Service
      */
     abstract public function findAll(): array;
 
-
     /**
      * Delete an entity
      *
      * @param int    $id     the id of the entity
      * @param string $userId the name of the user for security reasons
      *
-     * @throws ServiceNotFoundException if the entity does not exist, or there
+     * @throws ServiceNotFoundException|ServiceConflictException if the entity does not exist, or there
      * are more than one of it
      */
     public function delete(string $userId, int $id): Entity
@@ -102,7 +102,7 @@ abstract class Service
      * @param string $userId the name of the user for security reasons
      * @param Entity $entity the entity
      *
-     * @throws ServiceNotFoundException if the entity does not exist, or there
+     * @throws ServiceNotFoundException|ServiceConflictException if the entity does not exist, or there
      * are more than one of it
      */
     public function update(string $userId, Entity $entity): Entity
@@ -119,7 +119,7 @@ abstract class Service
      * @param string $userId the name of the user for security reasons
      *
      * @return Entity the entity
-     * @throws ServiceNotFoundException if the entity does not exist, or there
+     * @throws ServiceNotFoundException|ServiceConflictException if the entity does not exist, or there
      * are more than one of it
      */
     public function find(string $userId, int $id): Entity
@@ -127,9 +127,9 @@ abstract class Service
         try {
             return $this->mapper->findFromUser($userId, $id);
         } catch (DoesNotExistException $ex) {
-            throw new ServiceNotFoundException($ex->getMessage());
+            throw ServiceNotFoundException::from($ex);
         } catch (MultipleObjectsReturnedException $ex) {
-            throw new ServiceNotFoundException($ex->getMessage());
+            throw ServiceConflictException::from($ex);
         }
     }
 
diff --git a/phpunit.xml b/phpunit.xml
index 6a2f2d01b..8d21b4051 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -6,6 +6,7 @@
             <directory>./tests/Unit</directory>
         </testsuite>
     </testsuites>
+
     <coverage processUncoveredFiles="true">
         <include>
             <directory suffix=".php">./lib/</directory>
@@ -13,7 +14,10 @@
         <exclude>
             <file>./lib/AppInfo/Application.php</file>
             <file>./lib/Controller/JSONHttpErrorTrait.php</file>
-            <file>./lib/**Exception.php</file>
+            <directory suffix=".php">./lib/*/Exceptions</directory>
+            <directory suffix=".php">./lib/Migration</directory>
+            <file>./lib/Db/FeedType.php</file>
+            <file>./lib/Db/IAPI.php</file>
         </exclude>
         <report>
             <clover outputFile="./build/coverage.xml"/>
diff --git a/tests/Unit/Command/FolderDeleteTest.php b/tests/Unit/Command/FolderDeleteTest.php
index c87b05acd..e9076d9ba 100644
--- a/tests/Unit/Command/FolderDeleteTest.php
+++ b/tests/Unit/Command/FolderDeleteTest.php
@@ -82,7 +82,7 @@ class FolderDeleteTest extends TestCase
      */
     public function testInValid()
     {
-        $this->expectException('OCA\News\Service\Exceptions\ServiceException');
+        $this->expectException('OCA\News\Service\Exceptions\ServiceValidationException');
         $this->expectExceptionMessage('Can not remove root folder!');
 
         $this->consoleInput->expects($this->exactly(2))
diff --git a/tests/Unit/Controller/FeedApiControllerTest.php b/tests/Unit/Controller/FeedApiControllerTest.php
index a6a1db548..4e6f5f1f3 100644
--- a/tests/Unit/Controller/FeedApiControllerTest.php
+++ b/tests/Unit/Controller/FeedApiControllerTest.php
@@ -18,7 +18,7 @@ namespace OCA\News\Tests\Unit\Controller;
 use Exception;
 use OCA\News\Controller\FeedApiController;
 use OCA\News\Service\FeedServiceV2;
-use OCA\News\Service\ItemService;
+use OCA\News\Service\ItemServiceV2;
 use \OCP\AppFramework\Http;
 
 use \OCA\News\Service\Exceptions\ServiceNotFoundException;
@@ -40,7 +40,7 @@ class FeedApiControllerTest extends TestCase
     private $feedService;
 
     /**
-     * @var \PHPUnit\Framework\MockObject\MockObject|ItemService
+     * @var \PHPUnit\Framework\MockObject\MockObject|ItemServiceV2
      */
     private $itemService;
 
@@ -80,7 +80,7 @@ class FeedApiControllerTest extends TestCase
         $this->feedService = $this->getMockBuilder(FeedServiceV2::class)
             ->disableOriginalConstructor()
             ->getMock();
-        $this->itemService = $this->getMockBuilder(ItemService::class)
+        $this->itemService = $this->getMockBuilder(ItemServiceV2::class)
             ->disableOriginalConstructor()
             ->getMock();
         $this->class = new FeedApiController(
@@ -96,18 +96,17 @@ class FeedApiControllerTest extends TestCase
 
     public function testIndex()
     {
-        $feeds = [new Feed()];
-        $starredCount = 3;
-        $newestItemId = 2;
+        $feed = Feed::fromParams(['id' => 5]);
+        $feeds = [$feed];
 
         $this->itemService->expects($this->once())
-            ->method('starredCount')
+            ->method('starred')
             ->with($this->equalTo($this->userID))
-            ->will($this->returnValue($starredCount));
+            ->will($this->returnValue([1, 2, 3]));
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->with($this->equalTo($this->userID))
-            ->will($this->returnValue($newestItemId));
+            ->will($this->returnValue($feeds[0]));
         $this->feedService->expects($this->once())
             ->method('findAllForUser')
             ->with($this->equalTo($this->userID))
@@ -118,8 +117,8 @@ class FeedApiControllerTest extends TestCase
         $this->assertEquals(
             [
                 'feeds' => [$feeds[0]->toAPI()],
-                'starredCount' => $starredCount,
-                'newestItemId' => $newestItemId
+                'starredCount' => 3,
+                'newestItemId' => 5
             ],
             $response
         );
@@ -129,14 +128,13 @@ class FeedApiControllerTest extends TestCase
     public function testIndexNoNewestItemId()
     {
         $feeds = [new Feed()];
-        $starredCount = 3;
 
         $this->itemService->expects($this->once())
-            ->method('starredCount')
+            ->method('starred')
             ->with($this->equalTo($this->userID))
-            ->will($this->returnValue($starredCount));
+            ->will($this->returnValue([1, 2, 3]));
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->with($this->equalTo($this->userID))
             ->will($this->throwException(new ServiceNotFoundException('')));
         $this->feedService->expects($this->once())
@@ -149,7 +147,7 @@ class FeedApiControllerTest extends TestCase
         $this->assertEquals(
             [
                 'feeds' => [$feeds[0]->toAPI()],
-                'starredCount' => $starredCount,
+                'starredCount' => 3,
             ],
             $response
         );
@@ -203,8 +201,8 @@ class FeedApiControllerTest extends TestCase
             ->method('fetch')
             ->with($feeds[0]);
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
-            ->will($this->returnValue(3));
+            ->method('newest')
+            ->will($this->returnValue(Feed::fromParams(['id' => 3])));
 
         $response = $this->class->create('url', 3);
 
@@ -234,7 +232,7 @@ class FeedApiControllerTest extends TestCase
             ->method('fetch')
             ->with($feeds[0]);
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->will($this->throwException(new ServiceNotFoundException('')));
 
         $response = $this->class->create('ho', 3);
@@ -287,12 +285,8 @@ class FeedApiControllerTest extends TestCase
     public function testRead()
     {
         $this->itemService->expects($this->once())
-            ->method('readFeed')
-            ->with(
-                $this->equalTo(3),
-                $this->equalTo(30),
-                $this->equalTo($this->userID)
-            );
+            ->method('read')
+            ->with($this->userID,3,30);
 
         $this->class->read(3, 30);
     }
diff --git a/tests/Unit/Controller/FeedControllerTest.php b/tests/Unit/Controller/FeedControllerTest.php
index 871b781e0..b8929d373 100644
--- a/tests/Unit/Controller/FeedControllerTest.php
+++ b/tests/Unit/Controller/FeedControllerTest.php
@@ -18,7 +18,7 @@ use OCA\News\Db\Folder;
 use OCA\News\Service\FeedServiceV2;
 use OCA\News\Service\FolderServiceV2;
 use OCA\News\Service\ImportService;
-use OCA\News\Service\ItemService;
+use OCA\News\Service\ItemServiceV2;
 use OCP\AppFramework\Http;
 
 use OCA\News\Db\Feed;
@@ -55,8 +55,7 @@ class FeedControllerTest extends TestCase
      */
     private $importService;
     /**
-     * TODO: Remove
-     * @var MockObject|ItemService
+     * @var MockObject|ItemServiceV2
      */
     private $itemService;
 
@@ -87,7 +86,7 @@ class FeedControllerTest extends TestCase
             ->disableOriginalConstructor()
             ->getMock();
         $this->itemService = $this
-            ->getMockBuilder(ItemService::class)
+            ->getMockBuilder(ItemServiceV2::class)
             ->disableOriginalConstructor()
             ->getMock();
         $this->feedService = $this
@@ -138,20 +137,20 @@ class FeedControllerTest extends TestCase
             'feeds' => [
                 ['a feed'],
             ],
-            'starred' => 13
+            'starred' => 4
         ];
         $this->feedService->expects($this->once())
             ->method('findAllForUser')
             ->with($this->uid)
             ->will($this->returnValue($result['feeds']));
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->with($this->uid)
             ->will($this->throwException(new ServiceNotFoundException('')));
         $this->itemService->expects($this->once())
-            ->method('starredCount')
+            ->method('starred')
             ->with($this->uid)
-            ->will($this->returnValue($result['starred']));
+            ->will($this->returnValue([1, 2, 3, 4]));
 
         $response = $this->class->index();
 
@@ -165,7 +164,7 @@ class FeedControllerTest extends TestCase
             'feeds' => [
                 ['a feed'],
             ],
-            'starred' => 13,
+            'starred' => 2,
             'newestItemId' => 5
         ];
         $this->feedService->expects($this->once())
@@ -173,13 +172,13 @@ class FeedControllerTest extends TestCase
             ->with($this->uid)
             ->will($this->returnValue($result['feeds']));
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->with($this->uid)
-            ->will($this->returnValue($result['newestItemId']));
+            ->will($this->returnValue(Feed::fromParams(['id' => 5])));
         $this->itemService->expects($this->once())
-            ->method('starredCount')
+            ->method('starred')
             ->with($this->uid)
-            ->will($this->returnValue($result['starred']));
+            ->will($this->returnValue([1, 2]));
 
         $response = $this->class->index();
 
@@ -224,6 +223,30 @@ class FeedControllerTest extends TestCase
     }
 
 
+    public function testActiveFeed()
+    {
+        $id = 3;
+        $type = FeedType::FEED;
+        $result = [
+            'activeFeed' => [
+                'id' => $id,
+                'type' => $type
+            ]
+        ];
+
+        $this->feedService->expects($this->once())
+            ->method('find')
+            ->with($this->uid, $id)
+            ->will($this->returnValue(new Feed()));
+
+        $this->activeInitMocks($id, $type);
+
+        $response = $this->class->active();
+
+        $this->assertEquals($result, $response);
+    }
+
+
     public function testActiveFeedDoesNotExist()
     {
         $id = 3;
@@ -313,8 +336,8 @@ class FeedControllerTest extends TestCase
         ];
 
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
-            ->will($this->returnValue($result['newestItemId']));
+            ->method('newest')
+            ->will($this->returnValue(Feed::fromParams(['id' => 3])));
         $this->feedService->expects($this->once())
             ->method('purgeDeleted')
             ->with($this->uid, false);
@@ -341,8 +364,8 @@ class FeedControllerTest extends TestCase
         ];
 
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
-            ->will($this->returnValue($result['newestItemId']));
+            ->method('newest')
+            ->will($this->returnValue(Feed::fromParams(['id' => 3])));
         $this->feedService->expects($this->once())
             ->method('purgeDeleted')
             ->with($this->uid, false);
@@ -370,7 +393,7 @@ class FeedControllerTest extends TestCase
             ->with($this->uid, false);
 
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->will($this->throwException(new ServiceNotFoundException('')));
 
         $this->feedService->expects($this->once())
@@ -522,9 +545,9 @@ class FeedControllerTest extends TestCase
             ->will($this->returnValue($feed));
 
         $this->itemService->expects($this->once())
-            ->method('starredCount')
+            ->method('starred')
             ->with($this->uid)
-            ->will($this->returnValue(3));
+            ->will($this->returnValue([1, 2, 3]));
 
         $response = $this->class->import(['json']);
 
@@ -540,9 +563,9 @@ class FeedControllerTest extends TestCase
             ->will($this->returnValue(null));
 
         $this->itemService->expects($this->once())
-            ->method('starredCount')
+            ->method('starred')
             ->with($this->uid)
-            ->will($this->returnValue(3));
+            ->will($this->returnValue([1, 2, 3]));
 
         $response = $this->class->import(['json']);
 
@@ -561,9 +584,9 @@ class FeedControllerTest extends TestCase
             ]
         ];
 
-        $this->itemService->expects($this->once())
-            ->method('readFeed')
-            ->with(4, 5, $this->uid);
+        $this->feedService->expects($this->once())
+            ->method('read')
+            ->with($this->uid, 4, 5);
 
         $response = $this->class->read(4, 5);
         $this->assertEquals($expected, $response);
diff --git a/tests/Unit/Controller/FolderApiControllerTest.php b/tests/Unit/Controller/FolderApiControllerTest.php
index 858e7ff9e..1964f7b47 100644
--- a/tests/Unit/Controller/FolderApiControllerTest.php
+++ b/tests/Unit/Controller/FolderApiControllerTest.php
@@ -37,7 +37,7 @@ class FolderApiControllerTest extends TestCase
 
     private $folderService;
     private $itemService;
-    private $folderAPI;
+    private $class;
     private $user;
     private $msg;
 
@@ -65,7 +65,7 @@ class FolderApiControllerTest extends TestCase
         $this->itemService = $this->getMockBuilder(ItemService::class)
             ->disableOriginalConstructor()
             ->getMock();
-        $this->folderAPI = new FolderApiController(
+        $this->class = new FolderApiController(
             $request,
             $userSession,
             $this->folderService,
@@ -84,7 +84,7 @@ class FolderApiControllerTest extends TestCase
             ->with($this->equalTo($this->user->getUID()))
             ->will($this->returnValue($folders));
 
-        $response = $this->folderAPI->index();
+        $response = $this->class->index();
 
         $this->assertEquals(
             [
@@ -108,7 +108,7 @@ class FolderApiControllerTest extends TestCase
             ->with($this->user->getUID(), $folderName)
             ->will($this->returnValue($folder));
 
-        $response = $this->folderAPI->create($folderName);
+        $response = $this->class->create($folderName);
 
         $this->assertEquals(
             [
@@ -129,7 +129,7 @@ class FolderApiControllerTest extends TestCase
             ->method('create')
             ->will($this->throwException(new ServiceConflictException($msg)));
 
-        $response = $this->folderAPI->create('hi');
+        $response = $this->class->create('hi');
 
         $data = $response->getData();
         $this->assertEquals($msg, $data['message']);
@@ -148,7 +148,7 @@ class FolderApiControllerTest extends TestCase
             ->method('create')
             ->will($this->throwException(new ServiceValidationException($msg)));
 
-        $response = $this->folderAPI->create('hi');
+        $response = $this->class->create('hi');
 
         $data = $response->getData();
         $this->assertEquals($msg, $data['message']);
@@ -164,7 +164,7 @@ class FolderApiControllerTest extends TestCase
             ->method('delete')
             ->with($this->user->getUID(), 23);
 
-        $this->folderAPI->delete(23);
+        $this->class->delete(23);
     }
 
 
@@ -180,7 +180,7 @@ class FolderApiControllerTest extends TestCase
                 )
             );
 
-        $response = $this->folderAPI->delete($folderId);
+        $response = $this->class->delete($folderId);
 
         $data = $response->getData();
         $this->assertEquals($this->msg, $data['message']);
@@ -197,7 +197,7 @@ class FolderApiControllerTest extends TestCase
             ->method('rename')
             ->with($this->user->getUID(), $folderId, $folderName);
 
-        $this->folderAPI->update($folderId, $folderName);
+        $this->class->update($folderId, $folderName);
     }
 
     public function testUpdateDoesNotExist()
@@ -213,7 +213,7 @@ class FolderApiControllerTest extends TestCase
                 )
             );
 
-        $response = $this->folderAPI->update($folderId, $folderName);
+        $response = $this->class->update($folderId, $folderName);
 
         $data = $response->getData();
         $this->assertEquals($this->msg, $data['message']);
@@ -234,7 +234,7 @@ class FolderApiControllerTest extends TestCase
                 )
             );
 
-        $response = $this->folderAPI->update($folderId, $folderName);
+        $response = $this->class->update($folderId, $folderName);
 
         $data = $response->getData();
         $this->assertEquals($this->msg, $data['message']);
@@ -255,7 +255,7 @@ class FolderApiControllerTest extends TestCase
                 )
             );
 
-        $response = $this->folderAPI->update($folderId, $folderName);
+        $response = $this->class->update($folderId, $folderName);
 
         $data = $response->getData();
         $this->assertEquals($this->msg, $data['message']);
@@ -267,15 +267,25 @@ class FolderApiControllerTest extends TestCase
 
     public function testRead()
     {
-        $this->itemService->expects($this->once())
-            ->method('readFolder')
-            ->with(
-                $this->equalTo(3),
-                $this->equalTo(30),
-                $this->equalTo($this->user->getUID())
-            );
+        $this->folderService->expects($this->once())
+            ->method('read')
+            ->with($this->user->getUID(), 3, 30);
+
+        $this->class->read(3, 30);
+    }
+
+    public function testUpdateRoot()
+    {
+        $response = $this->class->update(null, '');
+        $this->assertSame(400, $response->getStatus());
+
+    }
+
+    public function testDeleteRoot()
+    {
+        $response = $this->class->delete(null);
+        $this->assertSame(400, $response->getStatus());
 
-        $this->folderAPI->read(3, 30);
     }
 
 
diff --git a/tests/Unit/Controller/FolderControllerTest.php b/tests/Unit/Controller/FolderControllerTest.php
index 18f4114b6..4f46e1211 100644
--- a/tests/Unit/Controller/FolderControllerTest.php
+++ b/tests/Unit/Controller/FolderControllerTest.php
@@ -84,8 +84,6 @@ class FolderControllerTest extends TestCase
         $this->class = new FolderController(
             $request,
             $this->folderService,
-            $this->feedService,
-            $this->itemService,
             $this->userSession
         );
         $this->msg = 'ron';
@@ -162,6 +160,16 @@ class FolderControllerTest extends TestCase
         $this->class->delete(5);
     }
 
+    public function testDeleteRoot()
+    {
+        $this->folderService->expects($this->never())
+            ->method('markDelete')
+            ->with('jack', 5, true);
+
+        $response = $this->class->delete(null);
+        $this->assertEquals(400, $response->getStatus());
+    }
+
     public function testDeleteDoesNotExist()
     {
         $this->folderService->expects($this->once())
@@ -176,6 +184,20 @@ class FolderControllerTest extends TestCase
         $this->assertEquals($response->getStatus(), Http::STATUS_NOT_FOUND);
     }
 
+    public function testDeleteConflict()
+    {
+        $this->folderService->expects($this->once())
+            ->method('markDelete')
+            ->will($this->throwException(new ServiceConflictException($this->msg)));
+
+        $response = $this->class->delete(5);
+
+        $params = json_decode($response->render(), true);
+
+        $this->assertEquals($this->msg, $params['message']);
+        $this->assertEquals($response->getStatus(), Http::STATUS_CONFLICT);
+    }
+
     public function testRename()
     {
         $folder = new Folder();
@@ -186,11 +208,21 @@ class FolderControllerTest extends TestCase
             ->with('jack', 4, 'tech')
             ->will($this->returnValue($folder));
 
-        $response = $this->class->rename('tech', 4);
+        $response = $this->class->rename(4, 'tech');
 
         $this->assertEquals($result, $response);
     }
 
+    public function testRenameRoot()
+    {
+        $this->folderService->expects($this->never())
+            ->method('rename');
+
+        $response = $this->class->rename(null, 'tech');
+
+        $this->assertEquals(400, $response->getStatus());
+    }
+
     public function testRenameDoesNotExist()
     {
         $msg = 'except';
@@ -199,7 +231,7 @@ class FolderControllerTest extends TestCase
             ->method('rename')
             ->will($this->throwException($ex));
 
-        $response = $this->class->rename('tech', 5);
+        $response = $this->class->rename(5, 'tech');
         $params = json_decode($response->render(), true);
 
         $this->assertEquals($response->getStatus(), Http::STATUS_NOT_FOUND);
@@ -215,7 +247,7 @@ class FolderControllerTest extends TestCase
             ->method('rename')
             ->will($this->throwException($ex));
 
-        $response = $this->class->rename('tech', 1);
+        $response = $this->class->rename(1, 'tech');
         $params = json_decode($response->render(), true);
 
         $this->assertEquals($response->getStatus(), Http::STATUS_CONFLICT);
@@ -226,19 +258,11 @@ class FolderControllerTest extends TestCase
 
     public function testRead()
     {
-        $feed = new Feed();
-        $expected = ['feeds' => [$feed->toAPI()]];
+        $this->folderService->expects($this->once())
+            ->method('read')
+            ->with('jack', 4, 5);
 
-        $this->itemService->expects($this->once())
-            ->method('readFolder')
-            ->with(4, 5, 'jack');
-        $this->feedService->expects($this->once())
-            ->method('findAllForUser')
-            ->with('jack')
-            ->will($this->returnValue([$feed]));
-
-        $response = $this->class->read(4, 5);
-        $this->assertEquals($expected, $response);
+        $this->class->read(4, 5);
     }
 
 
@@ -267,4 +291,20 @@ class FolderControllerTest extends TestCase
         $this->assertEquals($this->msg, $params['message']);
     }
 
+
+    public function testRestoreConflict()
+    {
+        $this->folderService->expects($this->once())
+            ->method('markDelete')
+            ->with('jack', 5, false)
+            ->will($this->throwException(new ServiceConflictException($this->msg)));
+
+        $response = $this->class->restore(5);
+
+        $params = json_decode($response->render(), true);
+
+        $this->assertEquals(Http::STATUS_CONFLICT, $response->getStatus());
+        $this->assertEquals($this->msg, $params['message']);
+    }
+
 }
\ No newline at end of file
diff --git a/tests/Unit/Controller/ItemApiControllerTest.php b/tests/Unit/Controller/ItemApiControllerTest.php
index 91181333a..85e359705 100644
--- a/tests/Unit/Controller/ItemApiControllerTest.php
+++ b/tests/Unit/Controller/ItemApiControllerTest.php
@@ -16,7 +16,6 @@
 namespace OCA\News\Tests\Unit\Controller;
 
 use OCA\News\Controller\ItemApiController;
-use OCA\News\Service\ItemService;
 use OCA\News\Service\ItemServiceV2;
 use \OCP\AppFramework\Http;
 
@@ -31,18 +30,28 @@ use PHPUnit\Framework\TestCase;
 
 class ItemApiControllerTest extends TestCase
 {
-
+    /**
+     * @var ItemServiceV2|\PHPUnit\Framework\MockObject\MockObject
+     */
     private $itemService;
-    private $oldItemService;
-    private $class;
+    /**
+     * @var IUserSession|\PHPUnit\Framework\MockObject\MockObject
+     */
     private $userSession;
+    /**
+     * @var IUser|\PHPUnit\Framework\MockObject\MockObject
+     */
     private $user;
+    /**
+     * @var IRequest|\PHPUnit\Framework\MockObject\MockObject
+     */
     private $request;
     private $msg;
+    private $uid = 'tom';
+    private $class;
 
     protected function setUp(): void
     {
-        $this->user = 'tom';
         $this->appName = 'news';
         $this->request = $this->getMockBuilder(IRequest::class)
             ->disableOriginalConstructor()
@@ -58,24 +67,20 @@ class ItemApiControllerTest extends TestCase
             ->will($this->returnValue($this->user));
         $this->user->expects($this->any())
             ->method('getUID')
-            ->will($this->returnValue('123'));
-        $this->oldItemService = $this->getMockBuilder(ItemService::class)
-            ->disableOriginalConstructor()
-            ->getMock();
+            ->will($this->returnValue($this->uid));
         $this->itemService = $this->getMockBuilder(ItemServiceV2::class)
             ->disableOriginalConstructor()
             ->getMock();
         $this->class = new ItemApiController(
             $this->request,
             $this->userSession,
-            $this->oldItemService,
             $this->itemService
         );
         $this->msg = 'hi';
     }
 
 
-    public function testIndex()
+    public function testIndexForFeed()
     {
         $item = new Item();
         $item->setId(5);
@@ -83,26 +88,52 @@ class ItemApiControllerTest extends TestCase
         $item->setGuidHash('guidhash');
         $item->setFeedId(123);
 
-        $this->oldItemService->expects($this->once())
-            ->method('findAllItems')
-            ->with(
-                $this->equalTo(2),
-                $this->equalTo(1),
-                $this->equalTo(30),
-                $this->equalTo(20),
-                $this->equalTo(true),
-                $this->equalTo(true),
-                $this->equalTo($this->user->getUID())
-            )
+        $this->itemService->expects($this->once())
+            ->method('findAllInFeedWithFilters')
+            ->with($this->uid, 2, 30, 20, false, true)
+            ->will($this->returnValue([$item]));
+
+        $response = $this->class->index(0, 2, true, 30, 20, true);
+
+        $this->assertEquals(['items' => [$item->toApi()]], $response);
+    }
+
+
+    public function testIndexForFolder()
+    {
+        $item = new Item();
+        $item->setId(5);
+        $item->setGuid('guid');
+        $item->setGuidHash('guidhash');
+        $item->setFeedId(123);
+
+        $this->itemService->expects($this->once())
+            ->method('findAllInFolderWithFilters')
+            ->with($this->uid, 2, 30, 20, false, true)
             ->will($this->returnValue([$item]));
 
         $response = $this->class->index(1, 2, true, 30, 20, true);
 
-        $this->assertEquals(
-            [
-            'items' => [$item->toApi()]
-            ], $response
-        );
+        $this->assertEquals(['items' => [$item->toApi()]], $response);
+    }
+
+
+    public function testIndexForItems()
+    {
+        $item = new Item();
+        $item->setId(5);
+        $item->setGuid('guid');
+        $item->setGuidHash('guidhash');
+        $item->setFeedId(123);
+
+        $this->itemService->expects($this->once())
+            ->method('findAllWithFilters')
+            ->with($this->uid, 3, 30, 20, true)
+            ->will($this->returnValue([$item]));
+
+        $response = $this->class->index(3, 2, true, 30, 20, true);
+
+        $this->assertEquals(['items' => [$item->toApi()]], $response);
     }
 
 
@@ -114,30 +145,18 @@ class ItemApiControllerTest extends TestCase
         $item->setGuidHash('guidhash');
         $item->setFeedId(123);
 
-        $this->oldItemService->expects($this->once())
-            ->method('findAllItems')
-            ->with(
-                $this->equalTo(2),
-                $this->equalTo(1),
-                $this->equalTo(-1),
-                $this->equalTo(0),
-                $this->equalTo(false),
-                $this->equalTo(false),
-                $this->equalTo($this->user->getUID())
-            )
+        $this->itemService->expects($this->once())
+            ->method('findAllInFolderWithFilters')
+            ->with($this->uid, 2, -1, 0, true, false)
             ->will($this->returnValue([$item]));
 
         $response = $this->class->index(1, 2, false);
 
-        $this->assertEquals(
-            [
-            'items' => [$item->toApi()]
-            ], $response
-        );
+        $this->assertEquals(['items' => [$item->toApi()]], $response);
     }
 
 
-    public function testUpdated()
+    public function testUpdatedFeed()
     {
         $item = new Item();
         $item->setId(5);
@@ -145,36 +164,78 @@ class ItemApiControllerTest extends TestCase
         $item->setGuidHash('guidhash');
         $item->setFeedId(123);
 
-        $this->oldItemService->expects($this->once())
-            ->method('findAllNew')
-            ->with(
-                $this->equalTo(2),
-                $this->equalTo(1),
-                $this->equalTo(30000000),
-                $this->equalTo(true),
-                $this->equalTo($this->user->getUID())
-            )
+        $this->itemService->expects($this->once())
+            ->method('findAllInFeedAfter')
+            ->with($this->uid, 2, 30000000, false)
+            ->will($this->returnValue([$item]));
+
+        $response = $this->class->updated(0, 2, 30);
+
+        $this->assertEquals(['items' => [$item->toApi()]], $response);
+    }
+
+
+    public function testUpdatedFolder()
+    {
+        $item = new Item();
+        $item->setId(5);
+        $item->setGuid('guid');
+        $item->setGuidHash('guidhash');
+        $item->setFeedId(123);
+
+        $this->itemService->expects($this->once())
+            ->method('findAllInFolderAfter')
+            ->with($this->uid, 2, 30000000, false)
             ->will($this->returnValue([$item]));
 
         $response = $this->class->updated(1, 2, 30);
 
-        $this->assertEquals(
-            [
-            'items' => [$item->toApi()]
-            ], $response
-        );
+        $this->assertEquals(['items' => [$item->toApi()]], $response);
+    }
+
+
+    public function testUpdatedItems()
+    {
+        $item = new Item();
+        $item->setId(5);
+        $item->setGuid('guid');
+        $item->setGuidHash('guidhash');
+        $item->setFeedId(123);
+
+        $this->itemService->expects($this->once())
+            ->method('findAllAfter')
+            ->with($this->uid, 3, 30000000)
+            ->will($this->returnValue([$item]));
+
+        $response = $this->class->updated(3, 2, 30);
+
+        $this->assertEquals(['items' => [$item->toApi()]], $response);
+    }
+
+    public function testUpdatedFeedFullTimestamp()
+    {
+        $item = new Item();
+        $item->setId(5);
+        $item->setGuid('guid');
+        $item->setGuidHash('guidhash');
+        $item->setFeedId(123);
+
+        $this->itemService->expects($this->once())
+            ->method('findAllInFeedAfter')
+            ->with($this->uid, 2, 1609598359000000, false)
+            ->will($this->returnValue([$item]));
+
+        $response = $this->class->updated(0, 2, '1609598359000000');
+
+        $this->assertEquals(['items' => [$item->toApi()]], $response);
     }
 
 
     public function testRead()
     {
-        $this->oldItemService->expects($this->once())
+        $this->itemService->expects($this->once())
             ->method('read')
-            ->with(
-                $this->equalTo(2),
-                $this->equalTo(true),
-                $this->equalTo($this->user->getUID())
-            );
+            ->with($this->user->getUID(), 2, true);
 
         $this->class->read(2);
     }
@@ -182,7 +243,7 @@ class ItemApiControllerTest extends TestCase
 
     public function testReadDoesNotExist()
     {
-        $this->oldItemService->expects($this->once())
+        $this->itemService->expects($this->once())
             ->method('read')
             ->will(
                 $this->throwException(
@@ -200,12 +261,12 @@ class ItemApiControllerTest extends TestCase
 
     public function testUnread()
     {
-        $this->oldItemService->expects($this->once())
+        $this->itemService->expects($this->once())
             ->method('read')
             ->with(
+                $this->equalTo($this->user->getUID()),
                 $this->equalTo(2),
-                $this->equalTo(false),
-                $this->equalTo($this->user->getUID())
+                $this->equalTo(false)
             );
 
         $this->class->unread(2);
@@ -214,7 +275,7 @@ class ItemApiControllerTest extends TestCase
 
     public function testUnreadDoesNotExist()
     {
-        $this->oldItemService->expects($this->once())
+        $this->itemService->expects($this->once())
             ->method('read')
             ->will(
                 $this->throwException(
@@ -232,14 +293,9 @@ class ItemApiControllerTest extends TestCase
 
     public function testStar()
     {
-        $this->oldItemService->expects($this->once())
-            ->method('star')
-            ->with(
-                $this->equalTo(2),
-                $this->equalTo('hash'),
-                $this->equalTo(true),
-                $this->equalTo($this->user->getUID())
-            );
+        $this->itemService->expects($this->once())
+            ->method('starByGuid')
+            ->with('tom', 2, 'hash', true);
 
         $this->class->star(2, 'hash');
     }
@@ -247,13 +303,9 @@ class ItemApiControllerTest extends TestCase
 
     public function testStarDoesNotExist()
     {
-        $this->oldItemService->expects($this->once())
-            ->method('star')
-            ->will(
-                $this->throwException(
-                    new ServiceNotFoundException($this->msg)
-                )
-            );
+        $this->itemService->expects($this->once())
+            ->method('starByGuid')
+            ->will($this->throwException(new ServiceNotFoundException($this->msg)));
 
         $response = $this->class->star(2, 'test');
 
@@ -265,14 +317,9 @@ class ItemApiControllerTest extends TestCase
 
     public function testUnstar()
     {
-        $this->oldItemService->expects($this->once())
-            ->method('star')
-            ->with(
-                $this->equalTo(2),
-                $this->equalTo('hash'),
-                $this->equalTo(false),
-                $this->equalTo($this->user->getUID())
-            );
+        $this->itemService->expects($this->once())
+            ->method('starByGuid')
+            ->with($this->uid, 2, 'hash', false);
 
         $this->class->unstar(2, 'hash');
     }
@@ -280,8 +327,8 @@ class ItemApiControllerTest extends TestCase
 
     public function testUnstarDoesNotExist()
     {
-        $this->oldItemService->expects($this->once())
-            ->method('star')
+        $this->itemService->expects($this->once())
+            ->method('starByGuid')
             ->will(
                 $this->throwException(
                     new ServiceNotFoundException($this->msg)
@@ -298,12 +345,9 @@ class ItemApiControllerTest extends TestCase
 
     public function testReadAll()
     {
-        $this->oldItemService->expects($this->once())
+        $this->itemService->expects($this->once())
             ->method('readAll')
-            ->with(
-                $this->equalTo(30),
-                $this->equalTo($this->user->getUID())
-            );
+            ->with($this->user->getUID(), 30);
 
         $this->class->readAll(30);
     }
@@ -312,11 +356,11 @@ class ItemApiControllerTest extends TestCase
 
     public function testReadMultiple()
     {
-        $this->oldItemService->expects($this->exactly(2))
+        $this->itemService->expects($this->exactly(2))
             ->method('read')
             ->withConsecutive(
-                [2, true, $this->user->getUID()],
-                [4, true, $this->user->getUID()]
+                [$this->user->getUID(), 2, true],
+                [$this->user->getUID(), 4, true]
             );
         $this->class->readMultiple([2, 4]);
     }
@@ -324,24 +368,24 @@ class ItemApiControllerTest extends TestCase
 
     public function testReadMultipleDoesntCareAboutException()
     {
-        $this->oldItemService->expects($this->exactly(2))
+        $this->itemService->expects($this->exactly(2))
             ->method('read')
             ->withConsecutive(
-                [2, true, $this->user->getUID()],
-                [4, true, $this->user->getUID()]
+                [$this->user->getUID(), 2, true],
+                [$this->user->getUID(), 4, true]
             )
-            ->willReturnOnConsecutiveCalls($this->throwException(new ServiceNotFoundException('')), null);
+            ->willReturnOnConsecutiveCalls($this->throwException(new ServiceNotFoundException('')), new Item());
         $this->class->readMultiple([2, 4]);
     }
 
 
     public function testUnreadMultiple()
     {
-        $this->oldItemService->expects($this->exactly(2))
+        $this->itemService->expects($this->exactly(2))
             ->method('read')
             ->withConsecutive(
-                [2, false, $this->user->getUID()],
-                [4, false, $this->user->getUID()]
+                [$this->user->getUID(), 2, false],
+                [$this->user->getUID(), 4, false]
             );
         $this->class->unreadMultiple([2, 4]);
     }
@@ -360,11 +404,11 @@ class ItemApiControllerTest extends TestCase
                     ]
                 ];
 
-        $this->oldItemService->expects($this->exactly(2))
-            ->method('star')
+        $this->itemService->expects($this->exactly(2))
+            ->method('starByGuid')
             ->withConsecutive(
-                [2, 'a', true, $this->user->getUID()],
-                [4, 'b', true, $this->user->getUID()]
+                [$this->user->getUID(), 2, 'a', true],
+                [$this->user->getUID(), 4, 'b', true]
             );
         $this->class->starMultiple($ids);
     }
@@ -383,13 +427,13 @@ class ItemApiControllerTest extends TestCase
                     ]
                 ];
 
-        $this->oldItemService->expects($this->exactly(2))
-            ->method('star')
+        $this->itemService->expects($this->exactly(2))
+            ->method('starByGuid')
             ->withConsecutive(
-                [2, 'a', true, $this->user->getUID()],
-                [4, 'b', true, $this->user->getUID()]
+                [$this->user->getUID(), 2, 'a', true],
+                [$this->user->getUID(), 4, 'b', true]
             )
-            ->willReturnOnConsecutiveCalls($this->throwException(new ServiceNotFoundException('')), null);
+            ->willReturnOnConsecutiveCalls($this->throwException(new ServiceNotFoundException('')), new Item());
 
         $this->class->starMultiple($ids);
     }
@@ -408,11 +452,11 @@ class ItemApiControllerTest extends TestCase
                     ]
                 ];
 
-        $this->oldItemService->expects($this->exactly(2))
-            ->method('star')
+        $this->itemService->expects($this->exactly(2))
+            ->method('starByGuid')
             ->withConsecutive(
-                [2, 'a', false, $this->user->getUID()],
-                [4, 'b', false, $this->user->getUID()]
+                [$this->user->getUID(), 2, 'a', false],
+                [$this->user->getUID(), 4, 'b', false]
             );
 
         $this->class->unstarMultiple($ids);
diff --git a/tests/Unit/Controller/ItemControllerTest.php b/tests/Unit/Controller/ItemControllerTest.php
index 3048f62f9..546b39278 100644
--- a/tests/Unit/Controller/ItemControllerTest.php
+++ b/tests/Unit/Controller/ItemControllerTest.php
@@ -15,7 +15,7 @@ namespace OCA\News\Tests\Unit\Controller;
 
 use OCA\News\Controller\ItemController;
 use OCA\News\Service\FeedServiceV2;
-use OCA\News\Service\ItemService;
+use OCA\News\Service\ItemServiceV2;
 use \OCP\AppFramework\Http;
 
 use \OCA\News\Db\Item;
@@ -39,7 +39,7 @@ class ItemControllerTest extends TestCase
      */
     private $settings;
     /**
-     * @var \PHPUnit\Framework\MockObject\MockObject|ItemService
+     * @var \PHPUnit\Framework\MockObject\MockObject|ItemServiceV2
      */
     private $itemService;
     /**
@@ -72,7 +72,7 @@ class ItemControllerTest extends TestCase
             ->disableOriginalConstructor()
             ->getMock();
         $this->itemService =
-        $this->getMockBuilder(ItemService::class)
+        $this->getMockBuilder(ItemServiceV2::class)
             ->disableOriginalConstructor()
             ->getMock();
         $this->feedService =
@@ -106,7 +106,7 @@ class ItemControllerTest extends TestCase
     {
         $this->itemService->expects($this->once())
             ->method('read')
-            ->with(4, true, 'user');
+            ->with('user', 4, true);
 
         $this->controller->read(4, true);
     }
@@ -133,8 +133,8 @@ class ItemControllerTest extends TestCase
         $this->itemService->expects($this->exactly(2))
             ->method('read')
             ->withConsecutive(
-                [2, true, 'user'],
-                [4, true, 'user']
+                ['user', 2, true],
+                ['user', 4, true]
             );
 
         $this->controller->readMultiple([2, 4]);
@@ -147,10 +147,10 @@ class ItemControllerTest extends TestCase
         $this->itemService->expects($this->exactly(2))
             ->method('read')
             ->withConsecutive(
-                [2, true, 'user'],
-                [4, true, 'user']
+                ['user', 2, true],
+                ['user', 4, true]
             )
-            ->willReturnOnConsecutiveCalls($this->throwException(new ServiceNotFoundException('yo')), null);
+            ->willReturnOnConsecutiveCalls($this->throwException(new ServiceNotFoundException('yo')), new Item());
         $this->controller->readMultiple([2, 4]);
     }
 
@@ -158,8 +158,8 @@ class ItemControllerTest extends TestCase
     public function testStar()
     {
         $this->itemService->expects($this->once())
-            ->method('star')
-            ->with(4, 'test', true, 'user');
+            ->method('starByGuid')
+            ->with('user', 4, 'test', true);
 
         $this->controller->star(4, 'test', true);
     }
@@ -170,7 +170,7 @@ class ItemControllerTest extends TestCase
         $msg = 'ho';
 
         $this->itemService->expects($this->once())
-            ->method('star')
+            ->method('starByGuid')
             ->will($this->throwException(new ServiceNotFoundException($msg)));
 
         $response = $this->controller->star(4, 'test', false);
@@ -189,7 +189,7 @@ class ItemControllerTest extends TestCase
 
         $this->itemService->expects($this->once())
             ->method('readAll')
-            ->with(5, 'user');
+            ->with('user', 5);
         $this->feedService->expects($this->once())
             ->method('findAllForUser')
             ->with('user')
@@ -199,8 +199,14 @@ class ItemControllerTest extends TestCase
         $this->assertEquals($expected, $response);
     }
 
-
-    private function itemsApiExpects($id, $type, $oldestFirst = '1')
+    /**
+     * Setup expectations for settings
+     *
+     * @param        $id
+     * @param        $type
+     * @param string $oldestFirst
+     */
+    private function itemsApiExpects($id, $type, $oldestFirst = '1'): void
     {
         $this->settings->expects($this->exactly(2))
             ->method('getUserValue')
@@ -218,14 +224,14 @@ class ItemControllerTest extends TestCase
     }
 
 
-    public function testIndex()
+    public function testIndexForFeed()
     {
         $feeds = [new Feed()];
         $result = [
             'items' => [new Item()],
             'feeds' => $feeds,
             'newestItemId' => $this->newestItemId,
-            'starred' => 3111
+            'starred' => 3
         ];
 
         $this->itemsApiExpects(2, FeedType::FEED, '0');
@@ -236,18 +242,18 @@ class ItemControllerTest extends TestCase
             ->will($this->returnValue($feeds));
 
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->with('user')
-            ->will($this->returnValue($this->newestItemId));
+            ->will($this->returnValue(Item::fromParams(['id' => $this->newestItemId])));
 
         $this->itemService->expects($this->once())
-            ->method('starredCount')
+            ->method('starred')
             ->with('user')
-            ->will($this->returnValue(3111));
+            ->will($this->returnValue([1, 2, 3]));
 
         $this->itemService->expects($this->once())
-            ->method('findAllItems')
-            ->with(2, FeedType::FEED, 3, 0, true, false, 'user', [])
+            ->method('findAllInFeedWithFilters')
+            ->with('user', 2, 3, 0, false, false, [])
             ->will($this->returnValue($result['items']));
 
         $response = $this->controller->index(FeedType::FEED, 2, 3);
@@ -255,14 +261,88 @@ class ItemControllerTest extends TestCase
     }
 
 
-    public function testIndexSearch()
+    public function testIndexForFolder()
     {
         $feeds = [new Feed()];
         $result = [
             'items' => [new Item()],
             'feeds' => $feeds,
             'newestItemId' => $this->newestItemId,
-            'starred' => 3111
+            'starred' => 3
+        ];
+
+        $this->itemsApiExpects(2, FeedType::FOLDER, '0');
+
+        $this->feedService->expects($this->once())
+            ->method('findAllForUser')
+            ->with('user')
+            ->will($this->returnValue($feeds));
+
+        $this->itemService->expects($this->once())
+            ->method('newest')
+            ->with('user')
+            ->will($this->returnValue(Item::fromParams(['id' => $this->newestItemId])));
+
+        $this->itemService->expects($this->once())
+            ->method('starred')
+            ->with('user')
+            ->will($this->returnValue([1, 2, 3]));
+
+        $this->itemService->expects($this->once())
+            ->method('findAllInFolderWithFilters')
+            ->with('user', 2, 3, 0, false, false, [])
+            ->will($this->returnValue($result['items']));
+
+        $response = $this->controller->index(FeedType::FOLDER, 2, 3);
+        $this->assertEquals($result, $response);
+    }
+
+
+    public function testIndexForOther()
+    {
+        $feeds = [new Feed()];
+        $result = [
+            'items' => [new Item()],
+            'feeds' => $feeds,
+            'newestItemId' => $this->newestItemId,
+            'starred' => 3
+        ];
+
+        $this->itemsApiExpects(2, FeedType::STARRED, '0');
+
+        $this->feedService->expects($this->once())
+            ->method('findAllForUser')
+            ->with('user')
+            ->will($this->returnValue($feeds));
+
+        $this->itemService->expects($this->once())
+            ->method('newest')
+            ->with('user')
+            ->will($this->returnValue(Item::fromParams(['id' => $this->newestItemId])));
+
+        $this->itemService->expects($this->once())
+            ->method('starred')
+            ->with('user')
+            ->will($this->returnValue([1, 2, 3]));
+
+        $this->itemService->expects($this->once())
+            ->method('findAllWithFilters')
+            ->with('user', 2, 3, 0, false, [])
+            ->will($this->returnValue($result['items']));
+
+        $response = $this->controller->index(FeedType::STARRED, 2, 3);
+        $this->assertEquals($result, $response);
+    }
+
+
+    public function testIndexSearchFeed()
+    {
+        $feeds = [new Feed()];
+        $result = [
+            'items' => [new Item()],
+            'feeds' => $feeds,
+            'newestItemId' => $this->newestItemId,
+            'starred' => 3
         ];
 
         $this->itemsApiExpects(2, FeedType::FEED, '0');
@@ -273,18 +353,18 @@ class ItemControllerTest extends TestCase
             ->will($this->returnValue($feeds));
 
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->with('user')
-            ->will($this->returnValue($this->newestItemId));
+            ->will($this->returnValue(Item::fromParams(['id' => $this->newestItemId])));
 
         $this->itemService->expects($this->once())
-            ->method('starredCount')
+            ->method('starred')
             ->with('user')
-            ->will($this->returnValue(3111));
+            ->will($this->returnValue([1, 2, 3]));
 
         $this->itemService->expects($this->once())
-            ->method('findAllItems')
-            ->with(2, FeedType::FEED, 3, 0, true, false, 'user', ['test', 'search'])
+            ->method('findAllInFeedWithFilters')
+            ->with('user', 2, 3, 0, false, false, ['test', 'search'])
             ->will($this->returnValue($result['items']));
 
         $response = $this->controller->index(FeedType::FEED, 2, 3, 0, null, null, 'test%20%20search%20');
@@ -299,8 +379,8 @@ class ItemControllerTest extends TestCase
         $this->itemsApiExpects(2, FeedType::FEED);
 
         $this->itemService->expects($this->once())
-            ->method('findAllItems')
-            ->with(2, FeedType::FEED, 3, 10, true, true, 'user')
+            ->method('findAllInFeedWithFilters')
+            ->with('user', 2, 3, 10, false, true)
             ->will($this->returnValue($result['items']));
 
         $this->feedService->expects($this->never())
@@ -316,7 +396,7 @@ class ItemControllerTest extends TestCase
         $this->itemsApiExpects(2, FeedType::FEED);
 
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->with('user')
             ->will($this->throwException(new ServiceNotFoundException('')));
 
@@ -325,14 +405,14 @@ class ItemControllerTest extends TestCase
     }
 
 
-    public function testNewItems()
+    public function testNewItemsFeed()
     {
         $feeds = [new Feed()];
         $result = [
             'items' => [new Item()],
             'feeds' => $feeds,
             'newestItemId' => $this->newestItemId,
-            'starred' => 3111
+            'starred' => 3
         ];
 
         $this->settings->expects($this->once())
@@ -346,18 +426,18 @@ class ItemControllerTest extends TestCase
             ->will($this->returnValue($feeds));
 
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->with('user')
-            ->will($this->returnValue($this->newestItemId));
+            ->will($this->returnValue(Item::fromParams(['id' => $this->newestItemId])));
 
         $this->itemService->expects($this->once())
-            ->method('starredCount')
+            ->method('starred')
             ->with('user')
-            ->will($this->returnValue(3111));
+            ->will($this->returnValue([1, 2, 3]));
 
         $this->itemService->expects($this->once())
-            ->method('findAllNew')
-            ->with(2, FeedType::FEED, 3, true, 'user')
+            ->method('findAllInFeedAfter')
+            ->with('user', 2, 3, false)
             ->will($this->returnValue($result['items']));
 
         $response = $this->controller->newItems(FeedType::FEED, 2, 3);
@@ -365,6 +445,86 @@ class ItemControllerTest extends TestCase
     }
 
 
+    public function testNewItemsFolder()
+    {
+        $feeds = [new Feed()];
+        $result = [
+            'items' => [new Item()],
+            'feeds' => $feeds,
+            'newestItemId' => $this->newestItemId,
+            'starred' => 3
+        ];
+
+        $this->settings->expects($this->once())
+            ->method('getUserValue')
+            ->with('user', $this->appName, 'showAll')
+            ->will($this->returnValue('1'));
+
+        $this->feedService->expects($this->once())
+            ->method('findAllForUser')
+            ->with('user')
+            ->will($this->returnValue($feeds));
+
+        $this->itemService->expects($this->once())
+            ->method('newest')
+            ->with('user')
+            ->will($this->returnValue(Item::fromParams(['id' => $this->newestItemId])));
+
+        $this->itemService->expects($this->once())
+            ->method('starred')
+            ->with('user')
+            ->will($this->returnValue([1, 2, 3]));
+
+        $this->itemService->expects($this->once())
+            ->method('findAllInFolderAfter')
+            ->with('user', 2, 3, false)
+            ->will($this->returnValue($result['items']));
+
+        $response = $this->controller->newItems(FeedType::FOLDER, 2, 3);
+        $this->assertEquals($result, $response);
+    }
+
+
+    public function testNewItemsOther()
+    {
+        $feeds = [new Feed()];
+        $result = [
+            'items' => [new Item()],
+            'feeds' => $feeds,
+            'newestItemId' => $this->newestItemId,
+            'starred' => 3
+        ];
+
+        $this->settings->expects($this->once())
+            ->method('getUserValue')
+            ->with('user', $this->appName, 'showAll')
+            ->will($this->returnValue('1'));
+
+        $this->feedService->expects($this->once())
+            ->method('findAllForUser')
+            ->with('user')
+            ->will($this->returnValue($feeds));
+
+        $this->itemService->expects($this->once())
+            ->method('newest')
+            ->with('user')
+            ->will($this->returnValue(Item::fromParams(['id' => $this->newestItemId])));
+
+        $this->itemService->expects($this->once())
+            ->method('starred')
+            ->with('user')
+            ->will($this->returnValue([1, 2, 3]));
+
+        $this->itemService->expects($this->once())
+            ->method('findAllAfter')
+            ->with('user', 6, 3)
+            ->will($this->returnValue($result['items']));
+
+        $response = $this->controller->newItems(FeedType::UNREAD, 2, 3);
+        $this->assertEquals($result, $response);
+    }
+
+
     public function testGetNewItemsNoNewestItemsId()
     {
         $this->settings->expects($this->once())
@@ -373,7 +533,7 @@ class ItemControllerTest extends TestCase
             ->will($this->returnValue('1'));
 
         $this->itemService->expects($this->once())
-            ->method('getNewestItemId')
+            ->method('newest')
             ->with('user')
             ->will($this->throwException(new ServiceNotFoundException('')));
 
diff --git a/tests/Unit/Controller/PageControllerTest.php b/tests/Unit/Controller/PageControllerTest.php
index 2f259b316..5f62bfe9f 100644
--- a/tests/Unit/Controller/PageControllerTest.php
+++ b/tests/Unit/Controller/PageControllerTest.php
@@ -16,6 +16,7 @@ namespace OCA\News\Tests\Unit\Controller;
 use OC\L10N\L10N;
 use OCA\News\Controller\PageController;
 use \OCA\News\Db\FeedType;
+use OCA\News\Explore\Exceptions\RecommendedSiteNotFoundException;
 use OCA\News\Explore\RecommendedSites;
 use OCA\News\Service\StatusService;
 use OCP\IConfig;
@@ -26,7 +27,6 @@ use OCP\IUser;
 use OCP\IUserSession;
 use PHPUnit\Framework\TestCase;
 
-
 class PageControllerTest extends TestCase
 {
 
@@ -278,4 +278,24 @@ class PageControllerTest extends TestCase
 
     }
 
+    public function testExploreError()
+    {
+        $this->settings->expects($this->exactly(2))
+            ->method('setUserValue')
+            ->withConsecutive(
+                ['becka', 'news', 'lastViewedFeedId', 0],
+                ['becka', 'news', 'lastViewedFeedType', FeedType::EXPLORE]
+            );
+
+        $this->recommended->expects($this->once())
+            ->method('forLanguage')
+            ->with('nl')
+            ->will($this->throwException(new RecommendedSiteNotFoundException('error')));
+
+        $out = $this->controller->explore('nl');
+
+        $this->assertEquals(404, $out->getStatus());
+
+    }
+
 }
diff --git a/tests/Unit/Db/FeedMapperTest.php b/tests/Unit/Db/FeedMapperTest.php
index c14b8995f..780bfbc38 100644
--- a/tests/Unit/Db/FeedMapperTest.php
+++ b/tests/Unit/Db/FeedMapperTest.php
@@ -69,7 +69,7 @@ class FeedMapperTest extends MapperTestUtility
                             ->getMock();
 
         $func = $this->getMockBuilder(IQueryFunction::class)
-                            ->getMock();
+                     ->getMock();
 
         $funcbuilder->expects($this->once())
                     ->method('count')
@@ -451,4 +451,94 @@ class FeedMapperTest extends MapperTestUtility
         $result = $this->class->findAllFromFolder(null);
         $this->assertEquals($this->feeds, $result);
     }
+
+    /**
+     * @covers \OCA\News\Db\FeedMapperV2::read
+     */
+    public function testRead()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('update')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('setValue')
+            ->with('unread', 0)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(['feeds.user_id = :userId'], ['feeds.id = :feedId'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'admin'], ['feedId', 1])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('getSQL')
+            ->will($this->returnValue('QUERY'));
+
+        $this->db->expects($this->exactly(1))
+            ->method('executeUpdate')
+            ->with('QUERY');
+
+        $this->class->read('admin', 1);
+    }
+
+    /**
+     * @covers \OCA\News\Db\FeedMapperV2::read
+     */
+    public function testReadWithMaxId()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('update')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('setValue')
+            ->with('unread', 0)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(['feeds.user_id = :userId'], ['feeds.id = :feedId'], ['items.id =< :maxItemId'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'admin'], ['feedId', 1], ['maxItemId', 4])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('getSQL')
+            ->will($this->returnValue('QUERY'));
+
+        $this->db->expects($this->exactly(1))
+            ->method('executeUpdate')
+            ->with('QUERY');
+
+        $this->class->read('admin', 1, 4);
+    }
 }
\ No newline at end of file
diff --git a/tests/Unit/Db/FolderMapperTest.php b/tests/Unit/Db/FolderMapperTest.php
index dd87b22b5..026c16bc6 100644
--- a/tests/Unit/Db/FolderMapperTest.php
+++ b/tests/Unit/Db/FolderMapperTest.php
@@ -279,4 +279,94 @@ class FolderMapperTest extends MapperTestUtility
         $result = $this->class->findAll();
         $this->assertEquals($this->folders, $result);
     }
+
+    /**
+     * @covers \OCA\News\Db\FolderMapperV2::read
+     */
+    public function testRead()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('update')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('setValue')
+            ->with('unread', 0)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(['feeds.user_id = :userId'], ['feeds.folder_id = :folderId'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'admin'], ['folderId', 1])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('getSQL')
+            ->will($this->returnValue('QUERY'));
+
+        $this->db->expects($this->exactly(1))
+            ->method('executeUpdate')
+            ->with('QUERY');
+
+        $this->class->read('admin', 1);
+    }
+
+    /**
+     * @covers \OCA\News\Db\FolderMapperV2::read
+     */
+    public function testReadWithMaxId()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('update')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('setValue')
+            ->with('unread', 0)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(['feeds.user_id = :userId'], ['feeds.folder_id = :folderId'], ['items.id =< :maxItemId'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'admin'], ['folderId', 1], ['maxItemId', 4])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('getSQL')
+            ->will($this->returnValue('QUERY'));
+
+        $this->db->expects($this->exactly(1))
+            ->method('executeUpdate')
+            ->with('QUERY');
+
+        $this->class->read('admin', 1, 4);
+    }
 }
\ No newline at end of file
diff --git a/tests/Unit/Db/ItemMapperTest.php b/tests/Unit/Db/ItemMapperTest.php
new file mode 100644
index 000000000..220805061
--- /dev/null
+++ b/tests/Unit/Db/ItemMapperTest.php
@@ -0,0 +1,2360 @@
+<?php
+/**
+ * Nextcloud - News
+ *
+ * This file is licensed under the Affero General Public License version 3 or
+ * later. See the COPYING file.
+ *
+ * @author    Alessandro Cosentino <cosenal@gmail.com>
+ * @author    Bernhard Posselt <dev@bernhard-posselt.com>
+ * @copyright 2012 Alessandro Cosentino
+ * @copyright 2012-2014 Bernhard Posselt
+ */
+
+namespace OCA\News\Tests\Unit\Db;
+
+use OCA\News\Db\Feed;
+use OCA\News\Db\FeedMapperV2;
+use OCA\News\Db\Folder;
+use OCA\News\Db\Item;
+use OCA\News\Db\ItemMapperV2;
+use OCA\News\Db\NewsMapperV2;
+use OCA\News\Service\Exceptions\ServiceValidationException;
+use OCA\News\Utility\Time;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
+use OCP\DB\IResult;
+use OCP\DB\QueryBuilder\IExpressionBuilder;
+use OCP\DB\QueryBuilder\IFunctionBuilder;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\DB\QueryBuilder\IQueryFunction;
+use OCP\IDBConnection;
+use Test\TestCase;
+
+/**
+ * Class ItemMapperTest
+ *
+ * @package OCA\News\Tests\Unit\Db
+ */
+class ItemMapperTest extends MapperTestUtility
+{
+
+    /** @var Time */
+    private $time;
+    /** @var ItemMapperV2 */
+    private $class;
+
+    /**
+     * @covers \OCA\News\Db\ItemMapperV2::__construct
+     */
+    protected function setUp(): void
+    {
+        parent::setUp();
+        $this->time = $this->getMockBuilder(Time::class)
+                           ->getMock();
+
+        $this->class = new ItemMapperV2($this->db, $this->time);
+    }
+
+    /**
+     * @covers \OCA\News\Db\ItemMapperV2::__construct
+     */
+    public function testSetUpSuccess(): void
+    {
+        $this->assertEquals('news_items', $this->class->getTableName());
+    }
+
+    /**
+     * @covers \OCA\News\Db\ItemMapperV2::findAllFromUser
+     */
+    public function testFindAllFromUser()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('where')
+            ->with('feeds.user_id = :user_id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('andWhere')
+            ->with('feeds.deleted_at = 0')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameter')
+            ->withConsecutive(['user_id', 'jack'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(3))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                ['id' => 5],
+                null
+            );
+
+        $result = $this->class->findAllFromUser('jack', []);
+        $this->assertEquals([Item::fromRow(['id' => 4]), Item::fromRow(['id' => 5])], $result);
+    }
+
+    /**
+     * @covers \OCA\News\Db\ItemMapperV2::findAllFromUser
+     */
+    public function testFindAllFromUserWithParams()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('createNamedParameter')
+            ->with('val')
+            ->will($this->returnValue(':val'));
+
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('where')
+            ->with('feeds.user_id = :user_id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(['feeds.deleted_at = 0'], ['key = :val'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameter')
+            ->withConsecutive(['user_id', 'jack'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(3))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                ['id' => 5],
+                null
+            );
+
+        $result = $this->class->findAllFromUser('jack', ['key' => 'val']);
+        $this->assertEquals([Item::fromRow(['id' => 4]), Item::fromRow(['id' => 5])], $result);
+    }
+
+    /**
+     * @covers \OCA\News\Db\ItemMapperV2::findAll
+     */
+    public function testFindAll()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('andWhere')
+            ->with('feeds.deleted_at = 0')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(3))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                ['id' => 5],
+                null
+            );
+
+        $result = $this->class->findAll();
+        $this->assertEquals([Item::fromRow(['id' => 4]), Item::fromRow(['id' => 5])], $result);
+    }
+
+    /**
+     * @covers \OCA\News\Db\ItemMapperV2::findAllForFeed
+     */
+    public function testFindAllForFeed()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('where')
+            ->with('feed_id = :feed_identifier')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('setParameter')
+            ->with('feed_identifier', 4)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(3))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                ['id' => 5],
+                null
+            );
+
+        $result = $this->class->findAllForFeed(4);
+        $this->assertEquals([Item::fromRow(['id' => 4]), Item::fromRow(['id' => 5])], $result);
+    }
+
+    public function testFindFromUser()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('where')
+            ->with('feeds.user_id = :user_id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(['items.id = :item_id'], ['feeds.deleted_at = 0'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('setParameter')
+            ->withConsecutive(['user_id', 'jack'], ['item_id', 4])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findFromUser('jack', 4);
+        $this->assertEquals(Item::fromRow(['id' => 4]), $result);
+    }
+
+    public function testFindByGUIDHash()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(['feed_id = :feed_id'], ['guid_hash = :guid_hash'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('setParameter')
+            ->withConsecutive(['feed_id', 4], ['guid_hash', 'hash'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findByGuidHash(4, 'hash');
+        $this->assertEquals(Item::fromRow(['id' => 4]), $result);
+    }
+
+    public function testFindForUserByGUIDHash()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(['feeds.user_id = :user_id'], ['feeds.id = :feed_id'], ['items.guid_hash = :guid_hash'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('setParameter')
+            ->withConsecutive(['user_id', 'jack'], ['feed_id', 4], ['guid_hash', 'hash'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findForUserByGuidHash('jack', 4, 'hash');
+        $this->assertEquals(Item::fromRow(['id' => 4]), $result);
+    }
+
+    public function testNewest()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('where')
+            ->withConsecutive(['feeds.user_id = :userId'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'jack'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(1)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->newest('jack');
+        $this->assertEquals(Item::fromRow(['id' => 4]), $result);
+    }
+
+    public function testFindAllInFeedAfter()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['items.updated_date >= :updatedSince'],
+                ['feeds.user_id = :userId'],
+                ['feeds.id = :feedId']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameters')
+            ->with([
+                'updatedSince' => 1610903351,
+                'feedId' => 4,
+                'userId' => 'jack',
+            ])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllInFeedAfter('jack', 4, 1610903351, false);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllInFeedAfterHideRead()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(4))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['items.updated_date >= :updatedSince'],
+                ['feeds.user_id = :userId'],
+                ['feeds.id = :feedId'],
+                ['items.unread = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameters')
+            ->with([
+                'updatedSince' => 1610903351,
+                'feedId' => 4,
+                'userId' => 'jack',
+            ])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllInFeedAfter('jack', 4, 1610903351, true);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllInFolderAfter()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('innerJoin')
+            ->withConsecutive(
+                ['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'],
+                ['feeds', 'news_folders', 'folders', 'feeds.folder_id = folders.id']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['items.updated_date >= :updatedSince'],
+                ['feeds.user_id = :userId'],
+                ['folders.id = :folderId']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameters')
+            ->with([
+                'updatedSince' => 1610903351,
+                'folderId' => 4,
+                'userId' => 'jack',
+            ])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllInFolderAfter('jack', 4, 1610903351, false);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllInFolderAfterHideRead()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('innerJoin')
+            ->withConsecutive(
+                ['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'],
+                ['feeds', 'news_folders', 'folders', 'feeds.folder_id = folders.id']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(4))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['items.updated_date >= :updatedSince'],
+                ['feeds.user_id = :userId'],
+                ['folders.id = :folderId'],
+                ['items.unread = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameters')
+            ->with([
+                'updatedSince' => 1610903351,
+                'folderId' => 4,
+                'userId' => 'jack',
+            ])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllInFolderAfter('jack', 4, 1610903351, true);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllAfterUnread()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['items.updated_date >= :updatedSince'],
+                ['feeds.user_id = :userId'],
+                ['items.unread = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameters')
+            ->with([
+                'updatedSince' => 1610903351,
+                'userId' => 'jack',
+            ])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllAfter('jack', 6, 1610903351);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllAfterStarred()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['items.updated_date >= :updatedSince'],
+                ['feeds.user_id = :userId'],
+                ['items.starred = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameters')
+            ->with([
+                'updatedSince' => 1610903351,
+                'userId' => 'jack',
+            ])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->once())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllAfter('jack', 2, 1610903351);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllAfterInvalid()
+    {
+        $this->expectException(ServiceValidationException::class);
+        $this->expectExceptionMessage('Unexpected Feed type in call');
+
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['items.updated_date >= :updatedSince'],
+                ['feeds.user_id = :userId']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameters')
+            ->with([
+                'updatedSince' => 1610903351,
+                'userId' => 'jack',
+            ])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->never())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->never())
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllAfter('jack', 232, 1610903351);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllItemsInvalid()
+    {
+        $this->expectException(ServiceValidationException::class);
+        $this->expectExceptionMessage('Unexpected Feed type in call');
+
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameter')
+            ->with('userId', 'jack')
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->never())
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->never())
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $this->class->findAllItems('jack', 232, 10, 10, false, []);
+    }
+
+    public function testFindAllItemsUnread()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['items.unread = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameter')
+            ->with('userId', 'jack')
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllItems('jack', 6, 10, 10, false, []);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllItemsStarred()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['items.starred = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameter')
+            ->with('userId', 'jack')
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllItems('jack', 2, 10, 10, false, []);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllItemsStarredSearch()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+        $this->db->expects($this->exactly(2))
+            ->method('escapeLikeParameter')
+            ->will($this->returnArgument(0));
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(4))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['items.search_index LIKE :term0'],
+                ['items.search_index LIKE :term1'],
+                ['items.starred = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'jack'], ['term0', '%key%'], ['term1', '%word%'])
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllItems('jack', 2, 10, 10, false, ['key', 'word']);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllFeed()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['items.feed_id = :feedId']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'jack'], ['feedId', 2])
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllFeed('jack', 2, 10, 10, false, false, []);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllFeedHideRead()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['items.feed_id = :feedId'],
+                ['items.unread = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'jack'], ['feedId', 2])
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllFeed('jack', 2, 10, 10, true, false, []);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllFeedSearch()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+        $this->db->expects($this->exactly(2))
+            ->method('escapeLikeParameter')
+            ->will($this->returnArgument(0));
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(4))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['items.feed_id = :feedId'],
+                ['items.search_index LIKE :term0'],
+                ['items.search_index LIKE :term1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(4))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'jack'], ['feedId', 2], ['term0', '%key%'], ['term1', '%word%'])
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllFeed('jack', 2, 10, 10, false, false, ['key', 'word']);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllFolderIdNull()
+    {
+        $expr = $this->getMockBuilder(IExpressionBuilder::class)
+                     ->getMock();
+
+        $expr->expects($this->once())
+             ->method('isNull')
+             ->with('feeds.folder_id')
+             ->will($this->returnValue('x IS NULL'));
+
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->exactly(1))
+            ->method('expr')
+            ->will($this->returnValue($expr));
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['x IS NULL']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'jack'])
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllFolder('jack', null, 10, 10, false, false, []);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllFolderHideRead()
+    {
+        $expr = $this->getMockBuilder(IExpressionBuilder::class)
+                     ->getMock();
+
+        $expr->expects($this->once())
+             ->method('isNull')
+             ->with('feeds.folder_id')
+             ->will($this->returnValue('x IS NULL'));
+
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->exactly(1))
+            ->method('expr')
+            ->will($this->returnValue($expr));
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['x IS NULL'],
+                ['items.unread = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'jack'])
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllFolder('jack', null, 10, 10, true, false, []);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllFolderHideReadInvertOrder()
+    {
+        $expr = $this->getMockBuilder(IExpressionBuilder::class)
+                     ->getMock();
+
+        $expr->expects($this->once())
+             ->method('isNull')
+             ->with('feeds.folder_id')
+             ->will($this->returnValue('x IS NULL'));
+
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->exactly(1))
+            ->method('expr')
+            ->will($this->returnValue($expr));
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['x IS NULL'],
+                ['items.unread = 1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'jack'])
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'ASC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'ASC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllFolder('jack', null, 10, 10, true, true, []);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testFindAllFolderSearchId()
+    {
+        $expr = $this->getMockBuilder(IExpressionBuilder::class)
+                     ->getMock();
+
+        $expr->expects($this->once())
+             ->method('eq')
+             ->with('feeds.folder_id', 2)
+             ->will($this->returnValue('x = y'));
+
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+        $this->db->expects($this->exactly(2))
+            ->method('escapeLikeParameter')
+            ->will($this->returnArgument(0));
+
+        $this->builder->expects($this->exactly(1))
+            ->method('expr')
+            ->will($this->returnValue($expr));
+
+        $this->builder->expects($this->once())
+            ->method('select')
+            ->with('items.*')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('from')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('innerJoin')
+            ->withConsecutive(['items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(4))
+            ->method('andWhere')
+            ->withConsecutive(
+                ['feeds.user_id = :userId'],
+                ['x = y'],
+                ['items.search_index LIKE :term0'],
+                ['items.search_index LIKE :term1']
+            )
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(3))
+            ->method('setParameter')
+            ->withConsecutive(['userId', 'jack'], ['term0', '%key%'], ['term1', '%word%'])
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setMaxResults')
+            ->with(10)
+            ->will($this->returnSelf());
+
+
+        $this->builder->expects($this->exactly(1))
+            ->method('setFirstResult')
+            ->with(10)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('orderBy')
+            ->with('items.updated_date', 'DESC')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('addOrderBy')
+            ->with('items.id', 'DESC')
+            ->willReturnSelf();
+
+        $this->builder->expects($this->exactly(1))
+            ->method('execute')
+            ->will($this->returnValue($this->cursor));
+
+        $this->cursor->expects($this->exactly(2))
+            ->method('fetch')
+            ->willReturnOnConsecutiveCalls(
+                ['id' => 4],
+                false
+            );
+
+        $result = $this->class->findAllFolder('jack', 2, 10, 10, false, false, ['key', 'word']);
+        $this->assertEquals([Item::fromRow(['id' => 4])], $result);
+    }
+
+    public function testReadAll()
+    {
+        $this->db->expects($this->once())
+            ->method('getQueryBuilder')
+            ->willReturn($this->builder);
+
+        $this->builder->expects($this->once())
+            ->method('update')
+            ->with('news_items', 'items')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('innerJoin')
+            ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->once())
+            ->method('setValue')
+            ->with('unread', 0)
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('andWhere')
+            ->withConsecutive(['items.id =< :maxItemId'], ['feeds.user_id = :userId'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(2))
+            ->method('setParameter')
+            ->withConsecutive(['maxItemId', 4], ['userId', 'jack'])
+            ->will($this->returnSelf());
+
+        $this->builder->expects($this->exactly(1))
+            ->method('getSQL')
+            ->will($this->returnValue('QUERY'));
+
+        $this->db->expects($this->once())
+            ->method('executeUpdate')
+            ->with('QUERY');
+
+        $this->class->readAll('jack', 4);
+    }
+
+    public function testPurgeDeletedEmpty()
+    {
+        $this->db->expects($this->never())
+            ->method('getQueryBuilder');
+
+        $this->class->purgeDeleted('jack', 4);
+    }
+
+    public function testDeleteOverThresholdEmptyFeeds()
+    {
+        $builder1 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $func_builder = $this->getMockBuilder(IFunctionBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $func = $this->getMockBuilder(IQueryFunction::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->db->expects($this->exactly(1))
+            ->method('getQueryBuilder')
+            ->willReturnOnConsecutiveCalls($builder1);
+
+        $builder1->expects($this->exactly(2))
+                 ->method('func')
+                 ->willReturn($func_builder);
+
+        $func_builder->expects($this->exactly(1))
+                 ->method('count')
+                 ->with('*', 'itemCount')
+                 ->willReturn($func);
+
+        $func_builder->expects($this->exactly(1))
+                 ->method('max')
+                 ->with('feeds.articles_per_update')
+                 ->willReturn($func);
+
+        $builder1->expects($this->once())
+                 ->method('select')
+                 ->with('feed_id', $func)
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('selectAlias')
+                 ->with($func, 'articlesPerUpdate')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('from')
+                 ->with('news_items', 'items')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('innerJoin')
+                 ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('groupBy')
+                 ->with('feed_id')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('getSQL')
+                 ->willReturn('FEED_SQL');
+
+        $this->class->deleteOverThreshold(1, true);
+    }
+
+    public function testDeleteOverThresholdSuccess()
+    {
+        $builder1 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $builder2 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $builder3 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $result1 = $this->getMockBuilder(IResult::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $result2 = $this->getMockBuilder(IResult::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $result3 = $this->getMockBuilder(IResult::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $func_builder = $this->getMockBuilder(IFunctionBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $func = $this->getMockBuilder(IQueryFunction::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->db->expects($this->exactly(3))
+            ->method('getQueryBuilder')
+            ->willReturnOnConsecutiveCalls($builder1, $builder2, $builder3);
+
+        $builder1->expects($this->exactly(2))
+                 ->method('func')
+                 ->willReturn($func_builder);
+
+        $func_builder->expects($this->exactly(1))
+                 ->method('count')
+                 ->with('*', 'itemCount')
+                 ->willReturn($func);
+
+        $func_builder->expects($this->exactly(1))
+            ->method('max')
+            ->with('feeds.articles_per_update')
+            ->willReturn($func);
+
+        $builder1->expects($this->once())
+            ->method('select')
+            ->with('feed_id', $func)
+            ->willReturnSelf();
+
+        $builder1->expects($this->once())
+            ->method('selectAlias')
+            ->with($func, 'articlesPerUpdate')
+            ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('from')
+                 ->with('news_items', 'items')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('innerJoin')
+                 ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('groupBy')
+                 ->with('feed_id')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('getSQL')
+                 ->willReturn('FEED_SQL');
+
+        $this->db->expects($this->exactly(3))
+                 ->method('executeQuery')
+                 ->withConsecutive(
+                     ['FEED_SQL'],
+                     ['RANGE_SQL', ['feedId' => 5], []],
+                     ['RANGE_SQL', ['feedId' => 1], []]
+                 )
+                 ->willReturnOnConsecutiveCalls($result1, $result2, $result3);
+
+        $result1->expects($this->once())
+                ->method('fetchAll')
+                ->with(2)
+                ->willReturn([
+                    ['itemCount' => 5, 'articlesPerUpdate' => 5, 'feed_id' => 5],
+                    ['itemCount' => 1, 'articlesPerUpdate' => 1, 'feed_id' => 1],
+                ]);
+
+        $builder2->expects($this->once())
+                 ->method('select')
+                 ->with('id')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('from')
+                 ->with('news_items')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('where')
+                 ->with('feed_id = :feedId')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('andWhere')
+                 ->with('starred = false')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('orderBy')
+                 ->with('last_modified', 'DESC')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('addOrderBy')
+                 ->with('id', 'DESC')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->exactly(2))
+                 ->method('getSQL')
+                 ->willReturn('RANGE_SQL');
+
+        $result2->expects($this->once())
+            ->method('fetchAll')
+            ->with(7)
+            ->willReturn([4, 6, 8]);
+
+        $result3->expects($this->once())
+            ->method('fetchAll')
+            ->with(7)
+            ->willReturn([3, 5, 7]);
+
+        $builder3->expects($this->once())
+            ->method('delete')
+            ->with('news_items')
+            ->willReturnSelf();
+
+        $builder3->expects($this->once())
+            ->method('where')
+            ->with('id IN (?)')
+            ->willReturnSelf();
+
+        $builder3->expects($this->exactly(1))
+            ->method('getSQL')
+            ->willReturn('DELETE_SQL');
+
+        $this->db->expects($this->once())
+                 ->method('executeUpdate')
+                 ->with('DELETE_SQL', [[4, 6, 8, 3, 5, 7]], [101])
+                 ->will($this->returnValue(10));
+
+        $res = $this->class->deleteOverThreshold(1, true);
+        $this->assertSame(10, $res);
+    }
+
+    public function testDeleteOverThresholdSuccessUnread()
+    {
+        $builder1 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $builder2 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $builder3 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $result1 = $this->getMockBuilder(IResult::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $result2 = $this->getMockBuilder(IResult::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $result3 = $this->getMockBuilder(IResult::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $func_builder = $this->getMockBuilder(IFunctionBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $func = $this->getMockBuilder(IQueryFunction::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->db->expects($this->exactly(3))
+            ->method('getQueryBuilder')
+            ->willReturnOnConsecutiveCalls($builder1, $builder2, $builder3);
+
+        $builder1->expects($this->exactly(2))
+                 ->method('func')
+                 ->willReturn($func_builder);
+
+        $func_builder->expects($this->exactly(1))
+                 ->method('count')
+                 ->with('*', 'itemCount')
+                 ->willReturn($func);
+
+        $func_builder->expects($this->exactly(1))
+            ->method('max')
+            ->with('feeds.articles_per_update')
+            ->willReturn($func);
+
+        $builder1->expects($this->once())
+            ->method('select')
+            ->with('feed_id', $func)
+            ->willReturnSelf();
+
+        $builder1->expects($this->once())
+            ->method('selectAlias')
+            ->with($func, 'articlesPerUpdate')
+            ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('from')
+                 ->with('news_items', 'items')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('innerJoin')
+                 ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('groupBy')
+                 ->with('feed_id')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('getSQL')
+                 ->willReturn('FEED_SQL');
+
+        $this->db->expects($this->exactly(3))
+                 ->method('executeQuery')
+                 ->withConsecutive(
+                     ['FEED_SQL'],
+                     ['RANGE_SQL', ['feedId' => 5], []],
+                     ['RANGE_SQL', ['feedId' => 1], []]
+                 )
+                 ->willReturnOnConsecutiveCalls($result1, $result2, $result3);
+
+        $result1->expects($this->once())
+                ->method('fetchAll')
+                ->with(2)
+                ->willReturn([
+                    ['itemCount' => 5, 'articlesPerUpdate' => 5, 'feed_id' => 5],
+                    ['itemCount' => 1, 'articlesPerUpdate' => 1, 'feed_id' => 1],
+                ]);
+
+        $builder2->expects($this->once())
+                 ->method('select')
+                 ->with('id')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('from')
+                 ->with('news_items')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('where')
+                 ->with('feed_id = :feedId')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->exactly(2))
+                 ->method('andWhere')
+                 ->withConsecutive(['starred = false'], ['unread = false'])
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('orderBy')
+                 ->with('last_modified', 'DESC')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+            ->method('addOrderBy')
+            ->with('id', 'DESC')
+            ->willReturnSelf();
+
+        $builder2->expects($this->exactly(2))
+                 ->method('getSQL')
+                 ->willReturn('RANGE_SQL');
+
+        $result2->expects($this->once())
+            ->method('fetchAll')
+            ->with(7)
+            ->willReturn([4, 6, 8]);
+
+        $result3->expects($this->once())
+            ->method('fetchAll')
+            ->with(7)
+            ->willReturn([3, 5, 7]);
+
+        $builder3->expects($this->once())
+            ->method('delete')
+            ->with('news_items')
+            ->willReturnSelf();
+
+        $builder3->expects($this->once())
+            ->method('where')
+            ->with('id IN (?)')
+            ->willReturnSelf();
+
+        $builder3->expects($this->exactly(1))
+            ->method('getSQL')
+            ->willReturn('DELETE_SQL');
+
+        $this->db->expects($this->once())
+                 ->method('executeUpdate')
+                 ->with('DELETE_SQL', [[4, 6, 8, 3, 5, 7]], [101])
+                 ->will($this->returnValue(10));
+
+        $res = $this->class->deleteOverThreshold(1, false);
+        $this->assertSame(10, $res);
+    }
+
+    public function testDeleteOverThresholdSuccessUnreadSkipsIfUnderThreshold()
+    {
+        $builder1 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $builder2 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $builder3 = $this->getMockBuilder(IQueryBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $result1 = $this->getMockBuilder(IResult::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $result2 = $this->getMockBuilder(IResult::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $result3 = $this->getMockBuilder(IResult::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $func_builder = $this->getMockBuilder(IFunctionBuilder::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+        $func = $this->getMockBuilder(IQueryFunction::class)
+            ->disableOriginalConstructor()
+            ->getMock();
+
+        $this->db->expects($this->exactly(3))
+            ->method('getQueryBuilder')
+            ->willReturnOnConsecutiveCalls($builder1, $builder2, $builder3);
+
+        $builder1->expects($this->exactly(2))
+                 ->method('func')
+                 ->willReturn($func_builder);
+
+        $func_builder->expects($this->exactly(1))
+                 ->method('count')
+                 ->with('*', 'itemCount')
+                 ->willReturn($func);
+
+        $func_builder->expects($this->exactly(1))
+            ->method('max')
+            ->with('feeds.articles_per_update')
+            ->willReturn($func);
+
+        $builder1->expects($this->once())
+            ->method('select')
+            ->with('feed_id', $func)
+            ->willReturnSelf();
+
+        $builder1->expects($this->once())
+            ->method('selectAlias')
+            ->with($func, 'articlesPerUpdate')
+            ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('from')
+                 ->with('news_items', 'items')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('innerJoin')
+                 ->with('items', 'news_feeds', 'feeds', 'items.feed_id = feeds.id')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('groupBy')
+                 ->with('feed_id')
+                 ->willReturnSelf();
+
+        $builder1->expects($this->once())
+                 ->method('getSQL')
+                 ->willReturn('FEED_SQL');
+
+        $this->db->expects($this->exactly(2))
+                 ->method('executeQuery')
+                 ->withConsecutive(
+                     ['FEED_SQL'],
+                     ['RANGE_SQL', ['feedId' => 5], []]
+                 )
+                 ->willReturnOnConsecutiveCalls($result1, $result2, $result3);
+
+        $result1->expects($this->once())
+                ->method('fetchAll')
+                ->with(2)
+                ->willReturn([
+                    ['itemCount' => 5, 'articlesPerUpdate' => 5, 'feed_id' => 5],
+                    ['itemCount' => 1, 'articlesPerUpdate' => 1, 'feed_id' => 1],
+                ]);
+
+        $builder2->expects($this->once())
+                 ->method('select')
+                 ->with('id')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('from')
+                 ->with('news_items')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('where')
+                 ->with('feed_id = :feedId')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->exactly(2))
+                 ->method('andWhere')
+                 ->withConsecutive(['starred = false'], ['unread = false'])
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+                 ->method('orderBy')
+                 ->with('last_modified', 'DESC')
+                 ->willReturnSelf();
+
+        $builder2->expects($this->once())
+            ->method('addOrderBy')
+            ->with('id', 'DESC')
+            ->willReturnSelf();
+
+        $builder2->expects($this->exactly(1))
+                 ->method('getSQL')
+                 ->willReturn('RANGE_SQL');
+
+        $result2->expects($this->once())
+            ->method('fetchAll')
+            ->with(7)
+            ->willReturn([4, 6, 8]);
+
+        $result3->expects($this->never())
+            ->method('fetchAll')
+            ->with(7)
+            ->willReturn([3, 5, 7]);
+
+        $builder3->expects($this->once())
+            ->method('delete')
+            ->with('news_items')
+            ->willReturnSelf();
+
+        $builder3->expects($this->once())
+            ->method('where')
+            ->with('id IN (?)')
+            ->willReturnSelf();
+
+        $builder3->expects($this->exactly(1))
+            ->method('getSQL')
+            ->willReturn('DELETE_SQL');
+
+        $this->db->expects($this->once())
+                 ->method('executeUpdate')
+                 ->with('DELETE_SQL', [[4, 6, 8]], [101])
+                 ->will($this->returnValue(10));
+
+        $res = $this->class->deleteOverThreshold(3, false);
+        $this->assertSame(10, $res);
+    }
+
+}
\ No newline at end of file
diff --git a/tests/Unit/Db/MapperFactoryTest.php b/tests/Unit/Db/MapperFactoryTest.php
deleted file mode 100644
index 1c4e2f4b6..000000000
--- a/tests/Unit/Db/MapperFactoryTest.php
+++ /dev/null
@@ -1,59 +0,0 @@
-<?php
-/**
- * Nextcloud - News
- *
- * This file is licensed under the Affero General Public License version 3 or
- * later. See the COPYING file.
- *
- * @author    Alessandro Cosentino <cosenal@gmail.com>
- * @author    Bernhard Posselt <dev@bernhard-posselt.com>
- * @copyright 2012 Alessandro Cosentino
- * @copyright 2012-2014 Bernhard Posselt
- */
-
-namespace OCA\News\Tests\Unit\Db;
-
-use OCA\News\Db\ItemMapper;
-use OCA\News\Db\MapperFactory;
-use OCA\News\Utility\Time;
-use PHPUnit\Framework\TestCase;
-
-use OCP\IDBConnection;
-
-use OCA\News\Db\Mysql\ItemMapper as MysqlMapper;
-
-
-class MapperFactoryTest extends TestCase
-{
-
-    /**
-     * @var \PHPUnit\Framework\MockObject\MockObject|IDBConnection
-     */
-    private $db;
-
-    public function setUp(): void
-    {
-        $this->db = $this->getMockBuilder(IDBConnection::class)
-            ->disableOriginalConstructor()
-            ->getMock();
-    }
-
-    public function testGetItemMapperSqlite()
-    {
-        $factory = new MapperFactory($this->db, 'sqlite', new Time());
-        $this->assertTrue($factory->build() instanceof ItemMapper);
-    }
-
-    public function testGetItemMapperPostgres()
-    {
-        $factory = new MapperFactory($this->db, 'pgsql', new Time());
-        $this->assertTrue($factory->build() instanceof ItemMapper);
-    }
-
-    public function testGetItemMapperMysql()
-    {
-        $factory = new MapperFactory($this->db, 'mysql', new Time());
-        $this->assertTrue($factory->build() instanceof MysqlMapper);
-    }
-
-}
diff --git a/tests/Unit/Db/MapperTestUtility.php b/tests/Unit/Db/MapperTestUtility.php
index 3aa1d8aed..4a875fde5 100644
--- a/tests/Unit/Db/MapperTestUtility.php
+++ b/tests/Unit/Db/MapperTestUtility.php
@@ -24,6 +24,7 @@
 namespace OCA\News\Tests\Unit\Db;
 
 use Doctrine\DBAL\Driver\Statement;
+use OC\DB\QueryBuilder\QueryBuilder;
 use OCP\DB\QueryBuilder\IQueryBuilder;
 use OCP\IDBConnection;
 
@@ -74,6 +75,7 @@ abstract class MapperTestUtility extends TestCase
                             ->getMock();
 
         $this->builder = $this->getMockBuilder(IQueryBuilder::class)
+                              ->disableOriginalConstructor()
                               ->getMock();
 
         $this->cursor = $this->getMockBuilder(Statement::class)
diff --git a/tests/Unit/Service/FeedServiceTest.php b/tests/Unit/Service/FeedServiceTest.php
index f869a3ef7..9f4743777 100644
--- a/tests/Unit/Service/FeedServiceTest.php
+++ b/tests/Unit/Service/FeedServiceTest.php
@@ -815,8 +815,8 @@ class FeedServiceTest extends TestCase
             ->will($this->returnValue([$feed1, $feed2]));
 
         $this->itemService->expects($this->exactly(2))
-                          ->method('findAllForFeed')
-                          ->withConsecutive([1], [2])
+                          ->method('findAllInFeed')
+                          ->withConsecutive(['jack', 1], ['jack', 2])
                           ->willReturn(['a']);
 
         $feeds = $this->class->findAllForUserRecursive($this->uid);
@@ -824,4 +824,21 @@ class FeedServiceTest extends TestCase
         $this->assertEquals(['a'], $feeds[1]->items);
     }
 
+    public function testRead()
+    {
+        $feed1 = new Feed();
+        $feed1->setId(1);
+
+        $this->mapper->expects($this->once())
+            ->method('findFromUser')
+            ->with($this->uid, 1)
+            ->will($this->returnValue($feed1));
+
+        $this->mapper->expects($this->exactly(1))
+                     ->method('read')
+                     ->withConsecutive(['jack', 1, null]);
+
+        $this->class->read($this->uid, 1);
+    }
+
 }
diff --git a/tests/Unit/Service/FolderServiceTest.php b/tests/Unit/Service/FolderServiceTest.php
index 2b55ee01a..3e7e98041 100644
--- a/tests/Unit/Service/FolderServiceTest.php
+++ b/tests/Unit/Service/FolderServiceTest.php
@@ -262,4 +262,21 @@ class FolderServiceTest extends TestCase
     }
 
 
+    public function testRead()
+    {
+        $folder = new Folder();
+        $folder->setId(1);
+
+        $this->mapper->expects($this->once())
+            ->method('findFromUser')
+            ->with('jack', 1)
+            ->will($this->returnValue($folder));
+
+        $this->mapper->expects($this->exactly(1))
+            ->method('read')
+            ->withConsecutive(['jack', 1, null]);
+
+        $this->class->read('jack', 1);
+    }
+
 }
diff --git a/tests/Unit/Service/ItemServiceTest.php b/tests/Unit/Service/ItemServiceTest.php
index 96d22235a..e82d5eda7 100644
--- a/tests/Unit/Service/ItemServiceTest.php
+++ b/tests/Unit/Service/ItemServiceTest.php
@@ -13,84 +13,63 @@
 
 namespace OCA\News\Tests\Unit\Service;
 
-use OC\Log;
-use OCA\News\Db\ItemMapper;
 use OCA\News\Db\ItemMapperV2;
-use OCA\News\Service\ItemService;
+use OCA\News\Service\Exceptions\ServiceConflictException;
+use OCA\News\Service\Exceptions\ServiceValidationException;
 use OCA\News\Service\Exceptions\ServiceNotFoundException;
-use OCA\News\Utility\PsrLogger;
-use OCA\News\Utility\Time;
+use OCA\News\Service\ItemServiceV2;
 use \OCP\AppFramework\Db\DoesNotExistException;
 
 use \OCA\News\Db\Item;
 use \OCA\News\Db\FeedType;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
 use OCP\IConfig;
 
+use PHPUnit\Framework\MockObject\MockObject;
 use PHPUnit\Framework\TestCase;
 use Psr\Log\LoggerInterface;
 
-
+/**
+ * Class ItemServiceTest
+ *
+ * @package OCA\News\Tests\Unit\Service
+ */
 class ItemServiceTest extends TestCase
 {
 
     /**
-     * @var \PHPUnit\Framework\MockObject\MockObject|ItemMapper
-     */
-    private $oldItemMapper;
-
-    /**
-     * @var \PHPUnit\Framework\MockObject\MockObject|ItemMapperV2
+     * @var MockObject|ItemMapperV2
      */
     private $mapper;
     /**
-     * @var  ItemService
+     * @var  ItemServiceV2
      */
-    private $itemService;
+    private $class;
 
     /**
-     * @var \PHPUnit\Framework\MockObject\MockObject|IConfig
+     * @var MockObject|IConfig
      */
     private $config;
 
     /**
-     * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface
+     * @var MockObject|LoggerInterface
      */
     private $logger;
-
-    /**
-     * @var \PHPUnit\Framework\MockObject\MockObject|Time
-     */
-    private $timeFactory;
-
     /**
      * @var int
      */
     private $newestItemId;
-
     /**
      * @var string
      */
-    private $time;
+    private $user;
 
 
     protected function setUp(): void
     {
-        $this->time = '222';
-        $this->timeFactory = $this->getMockBuilder(Time::class)
-            ->disableOriginalConstructor()
-            ->getMock();
-        $this->timeFactory->expects($this->any())
-            ->method('getTime')
-            ->will($this->returnValue($this->time));
-        $this->timeFactory->expects($this->any())
-            ->method('getMicroTime')
-            ->will($this->returnValue($this->time));
         $this->mapper = $this->getMockBuilder(ItemMapperV2::class)
             ->disableOriginalConstructor()
             ->getMock();
-        $this->oldItemMapper = $this->getMockBuilder(ItemMapper::class)
-            ->disableOriginalConstructor()
-            ->getMock();
         $this->config = $this->getMockBuilder(IConfig::class)
             ->disableOriginalConstructor()
             ->getMock();
@@ -99,10 +78,8 @@ class ItemServiceTest extends TestCase
             ->disableOriginalConstructor()
             ->getMock();
 
-        $this->itemService = new ItemService(
+        $this->class = new ItemServiceV2(
             $this->mapper,
-            $this->oldItemMapper,
-            $this->timeFactory,
             $this->config,
             $this->logger
         );
@@ -115,164 +92,126 @@ class ItemServiceTest extends TestCase
         $this->newestItemId = 4;
     }
 
-
     public function testFindAllNewFeed()
     {
-        $type = FeedType::FEED;
-        $this->oldItemMapper->expects($this->once())
-            ->method('findAllNewFeed')
-            ->with(
-                $this->equalTo(3),
-                $this->equalTo(20333),
-                $this->equalTo(true),
-                $this->equalTo('jack')
-            )
+        $this->mapper->expects($this->once())
+            ->method('findAllInFeedAfter')
+            ->with('jack', 2, 20333, true)
             ->will($this->returnValue([]));
 
-        $result = $this->itemService->findAllNew(3, $type, 20333, true, 'jack');
+        $result = $this->class->findAllInFeedAfter($this->user, 2, 20333, true);
         $this->assertEquals([], $result);
     }
 
-
     public function testFindAllNewFolder()
     {
-        $type = FeedType::FOLDER;
-        $this->oldItemMapper->expects($this->once())
-            ->method('findAllNewFolder')
-            ->with(
-                $this->equalTo(3),
-                $this->equalTo(20333),
-                $this->equalTo(true),
-                $this->equalTo('jack')
-            )
-            ->will($this->returnValue(['val']));
+        $this->mapper->expects($this->once())
+            ->method('findAllInFolderAfter')
+            ->with('jack', 2, 20333, true)
+            ->will($this->returnValue([]));
 
-        $result = $this->itemService->findAllNew(3, $type, 20333, true, 'jack');
-        $this->assertEquals(['val'], $result);
+        $result = $this->class->findAllInFolderAfter($this->user, 2, 20333, true);
+        $this->assertEquals([], $result);
     }
 
-
-    public function testFindAllNew()
+    public function testFindAllNewItem()
     {
-        $type = FeedType::STARRED;
-        $this->oldItemMapper->expects($this->once())
-            ->method('findAllNew')
-            ->with(
-                $this->equalTo(20333),
-                $this->equalTo($type),
-                $this->equalTo(true),
-                $this->equalTo('jack')
-            )
-            ->will($this->returnValue(['val']));
+        $this->mapper->expects($this->once())
+            ->method('findAllAfter')
+            ->with('jack', 2, 20333)
+            ->will($this->returnValue([]));
 
-        $result = $this->itemService->findAllNew(
-            3, $type, 20333, true,
-            'jack'
-        );
-        $this->assertEquals(['val'], $result);
+        $result = $this->class->findAllAfter($this->user, 2, 20333);
+        $this->assertEquals([], $result);
     }
 
+    public function testFindAllNewItemWrongType()
+    {
+        $this->expectException(ServiceValidationException::class);
+        $this->expectExceptionMessage('Trying to find in unknown type');
+
+        $this->mapper->expects($this->never())
+            ->method('findAllAfter');
+
+        $result = $this->class->findAllAfter($this->user, 3, 20333);
+        $this->assertEquals([], $result);
+    }
 
     public function testFindAllFeed()
     {
-        $type = FeedType::FEED;
-        $this->oldItemMapper->expects($this->once())
+        $this->mapper->expects($this->once())
             ->method('findAllFeed')
-            ->with(
-                $this->equalTo(3),
-                $this->equalTo(20),
-                $this->equalTo(5),
-                $this->equalTo(true),
-                $this->equalTo(false),
-                $this->equalTo('jack'),
-                $this->equalTo([])
-            )
+            ->with('jack', 3, 20, 5, true, false, [])
             ->will($this->returnValue(['val']));
 
-        $result = $this->itemService->findAllItems(
-            3, $type, 20, 5,
-            true, false, 'jack'
+        $result = $this->class->findAllInFeedWithFilters(
+            'jack',
+            3,
+            20,
+            5,
+            true,
+            false
         );
         $this->assertEquals(['val'], $result);
     }
 
-
     public function testFindAllFolder()
     {
-        $type = FeedType::FOLDER;
-        $this->oldItemMapper->expects($this->once())
+        $this->mapper->expects($this->once())
             ->method('findAllFolder')
-            ->with(
-                $this->equalTo(3),
-                $this->equalTo(20),
-                $this->equalTo(5),
-                $this->equalTo(true),
-                $this->equalTo(true),
-                $this->equalTo('jack'),
-                $this->equalTo([])
-            )
+            ->with('jack', 3, 20, 5, true, true, [])
             ->will($this->returnValue(['val']));
 
-        $result = $this->itemService->findAllItems(
-            3, $type, 20, 5,
-            true, true, 'jack'
+        $result = $this->class->findAllInFolderWithFilters(
+            'jack',
+            3,
+            20,
+            5,
+            true,
+            true,
+            []
         );
         $this->assertEquals(['val'], $result);
     }
 
-
-    public function testFindAll()
+    public function testFindAllItems()
     {
         $type = FeedType::STARRED;
-        $this->oldItemMapper->expects($this->once())
+        $this->mapper->expects($this->once())
             ->method('findAllItems')
-            ->with(
-                $this->equalTo(20),
-                $this->equalTo(5),
-                $this->equalTo($type),
-                $this->equalTo(true),
-                $this->equalTo(true),
-                $this->equalTo('jack'),
-                $this->equalTo([])
-            )
+            ->with('jack', $type, 20, 5, true, [])
             ->will($this->returnValue(['val']));
 
-        $result = $this->itemService->findAllItems(
-            3, $type, 20, 5,
-            true, true, 'jack'
-        );
+        $result = $this->class->findAllWithFilters('jack', $type, 20, 5, true);
         $this->assertEquals(['val'], $result);
     }
 
-
     public function testFindAllSearch()
     {
         $type = FeedType::STARRED;
         $search = ['test'];
 
-        $this->oldItemMapper->expects($this->once())
+
+        $this->mapper->expects($this->once())
             ->method('findAllItems')
-            ->with(
-                $this->equalTo(20),
-                $this->equalTo(5),
-                $this->equalTo($type),
-                $this->equalTo(true),
-                $this->equalTo(true),
-                $this->equalTo('jack'),
-                $this->equalTo($search)
-            )
+            ->with('jack', $type, 20, 5, true, $search)
             ->will($this->returnValue(['val']));
 
-        $result = $this->itemService->findAllItems(
-            3, $type, 20, 5,
-            true, true, 'jack', $search
-        );
+        $result = $this->class->findAllWithFilters('jack', $type, 20, 5, true, $search);
         $this->assertEquals(['val'], $result);
     }
 
+    public function testFindAll()
+    {
+        $this->mapper->expects($this->once())
+            ->method('findAll')
+            ->will($this->returnValue(['val']));
 
+        $result = $this->class->findAll();
+        $this->assertEquals(['val'], $result);
+    }
 
-    public function testStar()
+    public function testStarByGuid()
     {
         $itemId = 3;
         $feedId = 5;
@@ -287,20 +226,19 @@ class ItemServiceTest extends TestCase
         $expectedItem->setId($itemId);
 
         $this->mapper->expects($this->once())
-            ->method('findByGuidHash')
-            ->with($feedId, $guidHash)
+            ->method('findForUserByGuidHash')
+            ->with('jack', $feedId, $guidHash)
             ->will($this->returnValue($item));
 
         $this->mapper->expects($this->once())
             ->method('update')
             ->with($this->equalTo($expectedItem));
 
-        $this->itemService->star($feedId, $guidHash, true, 'jack');
+        $this->class->starByGuid('jack', $feedId, $guidHash, true);
 
         $this->assertTrue($item->isStarred());
     }
 
-
     public function testUnstar()
     {
         $itemId = 3;
@@ -317,162 +255,111 @@ class ItemServiceTest extends TestCase
         $expectedItem->setId($itemId);
 
         $this->mapper->expects($this->once())
-            ->method('findByGuidHash')
-            ->with($feedId, $guidHash)
+            ->method('findForUserByGuidHash')
+            ->with('jack', $feedId, $guidHash)
             ->will($this->returnValue($item));
 
         $this->mapper->expects($this->once())
             ->method('update')
             ->with($this->equalTo($expectedItem));
 
-        $this->itemService->star($feedId, $guidHash, false, 'jack');
+        $this->class->starByGuid('jack', $feedId, $guidHash, false);
 
         $this->assertFalse($item->isStarred());
     }
 
     public function testRead()
     {
-        $itemId = 3;
-        $item = new Item();
-        $item->setId($itemId);
-        $item->setUnread(true);
+        $item = $this->getMockBuilder(Item::class)
+                     ->getMock();
 
-        $expectedItem = new Item();
-        $expectedItem->setUnread(false);
-        $expectedItem->setId($itemId);
-        $expectedItem->setLastModified($this->time);
+        $item->expects($this->once())
+             ->method('setUnread')
+             ->with(false);
 
-        $this->oldItemMapper->expects($this->once())
-            ->method('readItem')
-            ->with(
-                $this->equalTo($itemId),
-                $this->equalTo(true),
-                $this->equalTo($this->time),
-                $this->equalTo('jack')
-            )
+        $this->mapper->expects($this->once())
+            ->method('findFromUser')
+            ->with('jack', 3)
             ->will($this->returnValue($item));
 
-        $this->itemService->read($itemId, true, 'jack');
+        $this->mapper->expects($this->once())
+            ->method('update')
+            ->with($item)
+            ->will($this->returnValue($item));
+
+        $this->class->read('jack', 3, true);
     }
 
-
-    public function testReadDoesNotExist()
+    public function testStar()
     {
+        $item = $this->getMockBuilder(Item::class)
+                     ->getMock();
 
-        $this->expectException(ServiceNotFoundException::class);
-        $this->oldItemMapper->expects($this->once())
-            ->method('readItem')
-            ->will($this->throwException(new DoesNotExistException('')));
+        $item->expects($this->once())
+             ->method('setStarred')
+             ->with(true);
 
-        $this->itemService->read(1, true, 'jack');
+        $this->mapper->expects($this->once())
+            ->method('findFromUser')
+            ->with('jack', 3)
+            ->will($this->returnValue($item));
+
+        $this->mapper->expects($this->once())
+            ->method('update')
+            ->with($item)
+            ->will($this->returnValue($item));
+
+        $this->class->star('jack', 3, true);
     }
 
-    public function testStarDoesNotExist()
+    public function testStarByGuidDoesNotExist()
     {
 
         $this->expectException(ServiceNotFoundException::class);
         $this->mapper->expects($this->once())
-            ->method('findByGuidHash')
+            ->method('findForUserByGuidHash')
             ->will($this->throwException(new DoesNotExistException('')));
 
-        $this->itemService->star(1, 'hash', true, 'jack');
+        $this->class->starByGuid('jack', 1, 'hash', true);
     }
 
+    public function testStarByGuidDuplicate()
+    {
+
+        $this->expectException(ServiceConflictException::class);
+        $this->mapper->expects($this->once())
+            ->method('findForUserByGuidHash')
+            ->will($this->throwException(new MultipleObjectsReturnedException('')));
+
+        $this->class->starByGuid('jack', 1, 'hash', true);
+    }
 
     public function testReadAll()
     {
         $highestItemId = 6;
 
-        $this->oldItemMapper->expects($this->once())
+        $this->mapper->expects($this->once())
             ->method('readAll')
-            ->with(
-                $this->equalTo($highestItemId),
-                $this->equalTo($this->time),
-                $this->equalTo('jack')
-            );
+            ->with('jack', $highestItemId);
 
-        $this->itemService->readAll($highestItemId, 'jack');
+        $this->class->readAll('jack', $highestItemId);
     }
 
-
-    public function testReadFolder()
-    {
-        $folderId = 3;
-        $highestItemId = 6;
-
-        $this->oldItemMapper->expects($this->once())
-            ->method('readFolder')
-            ->with(
-                $this->equalTo($folderId),
-                $this->equalTo($highestItemId),
-                $this->equalTo($this->time),
-                $this->equalTo('jack')
-            );
-
-        $this->itemService->readFolder($folderId, $highestItemId, 'jack');
-    }
-
-
-    public function testReadFeed()
-    {
-        $feedId = 3;
-        $highestItemId = 6;
-
-        $this->oldItemMapper->expects($this->once())
-            ->method('readFeed')
-            ->with(
-                $this->equalTo($feedId),
-                $this->equalTo($highestItemId),
-                $this->equalTo($this->time),
-                $this->equalTo('jack')
-            );
-
-        $this->itemService->readFeed($feedId, $highestItemId, 'jack');
-    }
-
-
-    public function testAutoPurgeOldWillPurgeOld()
-    {
-        $this->config->expects($this->once())
-            ->method('getAppValue')
-            ->with('news', 'autoPurgeCount')
-            ->will($this->returnValue(2));
-        $this->oldItemMapper->expects($this->once())
-            ->method('deleteReadOlderThanThreshold')
-            ->with($this->equalTo(2));
-
-        $this->itemService->autoPurgeOld();
-    }
-
-    public function testAutoPurgeOldWontPurgeOld()
-    {
-        $this->config->expects($this->once())
-            ->method('getAppValue')
-            ->with('news', 'autoPurgeCount')
-            ->will($this->returnValue(-1));
-        $this->oldItemMapper->expects($this->never())
-            ->method('deleteReadOlderThanThreshold');
-
-        $this->itemService->autoPurgeOld();
-    }
-
-
     public function testGetNewestItemId()
     {
-        $this->oldItemMapper->expects($this->once())
-            ->method('getNewestItemId')
+        $this->mapper->expects($this->once())
+            ->method('newest')
             ->with($this->equalTo('jack'))
-            ->will($this->returnValue(12));
+            ->will($this->returnValue(Item::fromParams(['id' => 12])));
 
-        $result = $this->itemService->getNewestItemId('jack');
-        $this->assertEquals(12, $result);
+        $result = $this->class->newest('jack');
+        $this->assertEquals(12, $result->getId());
     }
 
-
     public function testGetNewestItemIdDoesNotExist()
     {
-        $this->oldItemMapper->expects($this->once())
-            ->method('getNewestItemId')
+        $this->mapper->expects($this->once())
+            ->method('newest')
             ->with($this->equalTo('jack'))
             ->will(
                 $this->throwException(
@@ -481,37 +368,266 @@ class ItemServiceTest extends TestCase
             );
 
         $this->expectException(ServiceNotFoundException::class);
-        $this->itemService->getNewestItemId('jack');
+        $this->class->newest('jack');
     }
 
+    public function testGetNewestItemDuplicate()
+    {
+        $this->mapper->expects($this->once())
+            ->method('newest')
+            ->with($this->equalTo('jack'))
+            ->will(
+                $this->throwException(
+                    new MultipleObjectsReturnedException('There are no items')
+                )
+            );
+
+        $this->expectException(ServiceConflictException::class);
+        $this->class->newest('jack');
+    }
 
     public function testStarredCount()
     {
-        $star = 18;
+        $this->mapper->expects($this->once())
+            ->method('findAllFromUser')
+            ->with('jack', ['starred' => 1])
+            ->will($this->returnValue([new Item(), new Item()]));
 
-        $this->oldItemMapper->expects($this->once())
-            ->method('starredCount')
-            ->with($this->equalTo('jack'))
-            ->will($this->returnValue($star));
+        $result = $this->class->starred('jack');
 
-        $result = $this->itemService->starredCount('jack');
-
-        $this->assertEquals($star, $result);
+        $this->assertEquals(2, count($result));
     }
 
-
-    public function testGetUnreadOrStarred()
+    public function testInsertOrUpdateInserts()
     {
-        $this->oldItemMapper->expects($this->once())
-            ->method('findAllUnreadOrStarred')
-            ->with($this->equalTo('jack'))
-            ->will($this->returnValue([]));
+        $item = $this->getMockBuilder(Item::class)
+                     ->getMock();
 
-        $result = $this->itemService->getUnreadOrStarred('jack');
+        $item->expects($this->once())
+             ->method('getFeedId')
+             ->will($this->returnValue(1));
 
-        $this->assertEquals([], $result);
+        $item->expects($this->once())
+             ->method('getGuidHash')
+             ->will($this->returnValue('hash'));
+
+        $this->mapper->expects($this->once())
+            ->method('findByGuidHash')
+            ->with(1, 'hash')
+            ->will($this->throwException(new DoesNotExistException('exception')));
+
+        $this->mapper->expects($this->once())
+            ->method('insert')
+            ->with($item)
+            ->will($this->returnValue($item));
+
+        $result = $this->class->insertOrUpdate($item);
+
+        $this->assertEquals($item, $result);
     }
 
+    public function testInsertOrUpdateUpdates()
+    {
+        $item = $this->getMockBuilder(Item::class)
+                     ->getMock();
+        $db_item = $this->getMockBuilder(Item::class)
+                     ->getMock();
 
+        $item->expects($this->once())
+             ->method('getFeedId')
+             ->will($this->returnValue(1));
+
+        $item->expects($this->once())
+             ->method('getGuidHash')
+             ->will($this->returnValue('hash'));
+
+        $item->expects($this->once())
+             ->method('setUnread')
+             ->with(true)
+             ->will($this->returnSelf());
+
+        $db_item->expects($this->once())
+                ->method('isUnread')
+                ->will($this->returnValue(true));
+
+        $item->expects($this->once())
+             ->method('setStarred')
+             ->with(true)
+            ->will($this->returnSelf());
+
+        $db_item->expects($this->once())
+                ->method('isStarred')
+                ->will($this->returnValue(true));
+
+        $item->expects($this->once())
+            ->method('generateSearchIndex')
+            ->will($this->returnSelf());
+
+        $item->expects($this->once())
+            ->method('getFingerprint')
+            ->will($this->returnValue('fingerA'));
+
+        $db_item->expects($this->once())
+            ->method('getFingerprint')
+            ->will($this->returnValue('fingerB'));
+
+        $item->expects($this->never())
+            ->method('resetUpdatedFields');
+
+        $this->mapper->expects($this->once())
+            ->method('findByGuidHash')
+            ->with(1, 'hash')
+            ->will($this->returnValue($db_item));
+
+        $this->mapper->expects($this->once())
+            ->method('update')
+            ->with($item)
+            ->will($this->returnValue($item));
+
+        $result = $this->class->insertOrUpdate($item);
+
+        $this->assertEquals($item, $result);
+    }
+
+    public function testInsertOrUpdateSkipsSame()
+    {
+        $item = $this->getMockBuilder(Item::class)
+                     ->getMock();
+        $db_item = $this->getMockBuilder(Item::class)
+                     ->getMock();
+
+        $item->expects($this->once())
+             ->method('getFeedId')
+             ->will($this->returnValue(1));
+
+        $item->expects($this->once())
+             ->method('getGuidHash')
+             ->will($this->returnValue('hash'));
+
+        $item->expects($this->once())
+             ->method('setUnread')
+             ->with(true)
+             ->will($this->returnSelf());
+
+        $db_item->expects($this->once())
+                ->method('isUnread')
+                ->will($this->returnValue(true));
+
+        $item->expects($this->once())
+             ->method('setStarred')
+             ->with(true)
+            ->will($this->returnSelf());
+
+        $db_item->expects($this->once())
+                ->method('isStarred')
+                ->will($this->returnValue(true));
+
+        $item->expects($this->once())
+            ->method('generateSearchIndex')
+            ->will($this->returnSelf());
+
+        $item->expects($this->once())
+            ->method('getFingerprint')
+            ->will($this->returnValue('fingerA'));
+
+        $db_item->expects($this->once())
+            ->method('getFingerprint')
+            ->will($this->returnValue('fingerA'));
+
+        $item->expects($this->once())
+            ->method('resetUpdatedFields');
+
+        $this->mapper->expects($this->once())
+            ->method('findByGuidHash')
+            ->with(1, 'hash')
+            ->will($this->returnValue($db_item));
+
+        $this->mapper->expects($this->once())
+            ->method('update')
+            ->with($item)
+            ->will($this->returnValue($item));
+
+        $result = $this->class->insertOrUpdate($item);
+
+        $this->assertEquals($item, $result);
+    }
+
+    public function testFindByGuidHash()
+    {
+        $item = $this->getMockBuilder(Item::class)
+                     ->getMock();
+
+        $this->mapper->expects($this->once())
+            ->method('findByGuidHash')
+            ->with(1, 'a')
+            ->will($this->returnValue($item));
+
+        $result = $this->class->findByGuidHash(1, 'a');
+
+        $this->assertEquals($item, $result);
+    }
+
+    public function testFindAllInFeed()
+    {
+        $items = [new Item(), new Item()];
+
+        $this->mapper->expects($this->once())
+            ->method('findAllInFeedAfter')
+            ->with('jack', 1, PHP_INT_MIN, false)
+            ->will($this->returnValue($items));
+
+        $result = $this->class->findAllInFeed('jack', 1);
+
+        $this->assertEquals($items, $result);
+    }
+
+    public function testPurgeOverThreshold()
+    {
+        $this->mapper->expects($this->once())
+            ->method('deleteOverThreshold')
+            ->with(1, true)
+            ->will($this->returnValue(1));
+
+        $result = $this->class->purgeOverThreshold(1, true);
+
+        $this->assertEquals(1, $result);
+    }
+
+    public function testPurgeOverThresholdWithNegative()
+    {
+        $this->mapper->expects($this->never())
+            ->method('deleteOverThreshold');
+
+        $result = $this->class->purgeOverThreshold(-1, true);
+
+        $this->assertEquals(null, $result);
+    }
+
+    public function testPurgeOverThresholdNull()
+    {
+        $this->config->expects($this->once())
+            ->method('getAppValue')
+            ->with('news', 'autoPurgeCount', 200)
+            ->will($this->returnValue(200));
+
+        $this->mapper->expects($this->once())
+            ->method('deleteOverThreshold')
+            ->with(200);
+
+        $this->class->purgeOverThreshold();
+    }
+
+    public function testPurgeOverThresholdSet()
+    {
+        $this->config->expects($this->never())
+            ->method('getAppValue')
+            ->with('news', 'autoPurgeCount', 200);
+
+        $this->mapper->expects($this->once())
+            ->method('deleteOverThreshold')
+            ->with(5);
+
+        $this->class->purgeOverThreshold(5);
+    }
 
 }
diff --git a/tests/Unit/Service/ServiceTest.php b/tests/Unit/Service/ServiceTest.php
index cfaf82c95..cc4e2604b 100644
--- a/tests/Unit/Service/ServiceTest.php
+++ b/tests/Unit/Service/ServiceTest.php
@@ -14,8 +14,8 @@
 namespace OCA\News\Tests\Unit\Service;
 
 use OCA\News\Db\Feed;
-use OCA\News\Db\ItemMapper;
 use OCA\News\Db\ItemMapperV2;
+use OCA\News\Service\Exceptions\ServiceConflictException;
 use OCA\News\Service\Exceptions\ServiceNotFoundException;
 use OCA\News\Service\Service;
 use \OCP\AppFramework\Db\DoesNotExistException;
@@ -112,7 +112,7 @@ class ServiceTest extends TestCase
             ->method('findFromUser')
             ->will($this->throwException($ex));
 
-        $this->expectException(ServiceNotFoundException::class);
+        $this->expectException(ServiceConflictException::class);
         $this->class->find('', 1);
     }
 
diff --git a/tests/psalm-baseline.xml b/tests/psalm-baseline.xml
index 91e13a1db..b22549184 100644
--- a/tests/psalm-baseline.xml
+++ b/tests/psalm-baseline.xml
@@ -1,11 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <files psalm-version="4.4.1@9fd7a7d885b3a216cff8dec9d8c21a132f275224">
   <file src="lib/AppInfo/Application.php">
-    <MissingDependency occurrences="5">
+    <MissingDependency occurrences="4">
       <code>$fs</code>
       <code>$fs</code>
       <code>$fs</code>
-      <code>BeforeUserDeletedEvent</code>
       <code>IRootFolder</code>
     </MissingDependency>
   </file>
@@ -21,18 +20,30 @@
     </InvalidReturnType>
   </file>
   <file src="lib/Hooks/UserDeleteHook.php">
-    <MissingDependency occurrences="2">
-      <code>BeforeUserDeletedEvent</code>
-      <code>UserDeleteHook</code>
-    </MissingDependency>
     <MoreSpecificImplementedParamType occurrences="1">
       <code>$event</code>
     </MoreSpecificImplementedParamType>
   </file>
+  <file src="lib/Service/Exceptions/ServiceConflictException.php">
+    <MoreSpecificImplementedParamType occurrences="1">
+      <code>$exception</code>
+    </MoreSpecificImplementedParamType>
+  </file>
+  <file src="lib/Service/Exceptions/ServiceNotFoundException.php">
+    <MoreSpecificImplementedParamType occurrences="1">
+      <code>$exception</code>
+    </MoreSpecificImplementedParamType>
+  </file>
+  <file src="lib/Service/Exceptions/ServiceValidationException.php">
+    <MoreSpecificImplementedParamType occurrences="1">
+      <code>$exception</code>
+    </MoreSpecificImplementedParamType>
+  </file>
   <file src="lib/Service/FeedServiceV2.php">
-    <UndefinedMethod occurrences="2">
+    <UndefinedMethod occurrences="3">
       <code>findAllFromFolder</code>
       <code>findByURL</code>
+      <code>read</code>
     </UndefinedMethod>
   </file>
   <file src="lib/Service/FolderServiceV2.php">
@@ -43,18 +54,23 @@
       <code>$this-&gt;timeFactory</code>
       <code>TimeFactory</code>
     </UndefinedDocblockClass>
-  </file>
-  <file src="lib/Service/ItemService.php">
     <UndefinedMethod occurrences="1">
-      <code>findByGuidHash</code>
+      <code>read</code>
     </UndefinedMethod>
   </file>
   <file src="lib/Service/ItemServiceV2.php">
-    <UndefinedMethod occurrences="4">
+    <UndefinedMethod occurrences="11">
       <code>deleteOverThreshold</code>
-      <code>findAllForFeed</code>
-      <code>findByGuidHash</code>
+      <code>findAllAfter</code>
+      <code>findAllFeed</code>
+      <code>findAllFolder</code>
+      <code>findAllInFeedAfter</code>
+      <code>findAllInFolderAfter</code>
+      <code>findAllItems</code>
       <code>findByGuidHash</code>
+      <code>findForUserByGuidHash</code>
+      <code>newest</code>
+      <code>readAll</code>
     </UndefinedMethod>
   </file>
 </files>