packages feed

poppy-1.0.0: test/Poppy/IncludeSpec.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeApplications #-}

module Poppy.IncludeSpec
  ( includeSpec,
  )
where

import Data.List (sort)
import Data.Text (Text)
import qualified Data.Text as T
import Data.UUID (UUID, nil)
import GHC.Records (HasField)
import Poppy (asc, desc, load, loadWith, runDb, skip)
import Poppy.Internal.Include (Load (..))
import Poppy.IncludeUpdate (updatedChapters)
import qualified Poppy.Internal.Operations as Ops
import qualified Poppy.ShelfFixtures as ShelfFixtures
import Poppy.Internal.Where (eq, neq)
import Schema.Book (BookRow (..), bookId, bookTitle)
import Schema.Chapter (ChapterRow (..))
import qualified Schema.Client.Book as Book
import qualified Schema.Client.Shelf as Shelf
import Schema.Include.Book (BookInclude (..), BookWith (..))
import Schema.Include.Chapter (ChapterInclude (..), ChapterWith (..))
import Schema.Include.Shelf (ShelfInclude (..), ShelfWith (..))
import Schema.Section (SectionRow (..))
import Schema.Shelf (ShelfRow (..), ShelfTable, shelfName)
import Schema.Tag (TagRow (..))
import Support.TestDb (TestEnv (..))
import Test.Hspec (SpecWith, describe, it, shouldBe)

includeBooks =
  ShelfInclude {books = loadWith (BookInclude {chapters = skip}), tags = skip}

includeBooksChapters =
  ShelfInclude
    { books = loadWith (BookInclude {chapters = loadWith (ChapterInclude {sections = skip})}),
      tags = skip
    }

includeBooksAndTags =
  ShelfInclude {books = loadWith (BookInclude {chapters = skip}), tags = load}

includeBooksChaptersAndTags =
  ShelfInclude
    { books = loadWith (BookInclude {chapters = loadWith (ChapterInclude {sections = skip})}),
      tags = load
    }

includeBooksChaptersSections =
  ShelfInclude
    { books = loadWith (BookInclude {chapters = loadWith (ChapterInclude {sections = load})}),
      tags = skip
    }

shelves include = Shelf.findMany (Shelf.emptyQuery {Shelf.include_ = include})

shelvesWhere include predicate =
  Shelf.findMany (Shelf.emptyQuery {Shelf.include_ = include, Shelf.where_ = Just predicate})

shelfById include pk =
  Shelf.findUnique ((Shelf.uniqueQuery (Shelf.ById pk)) {Shelf.include_ = include})

includeSpec :: SpecWith TestEnv
includeSpec =
  describe "Poppy.Internal.Include" $ do
    it "findMany nests books under their shelf" $ \TestEnv {envPool = pool} -> do
      fiction <- ShelfFixtures.insertShelf pool "fiction"
      _ <- ShelfFixtures.insertShelf pool "nonfiction"
      _ <- ShelfFixtures.insertBook pool fiction.id "Dune"
      _ <- ShelfFixtures.insertBook pool fiction.id "Neuromancer"
      results <- runDb pool (shelves includeBooks)
      length results `shouldBe` 2
      fictionResult <- lookupShelf fiction.id results
      let bookIds = map ((.id) . (.book)) fictionResult.books
      bookIds `shouldBe` sort bookIds
      sort (map ((.title) . (.book)) fictionResult.books) `shouldBe` ["Dune", "Neuromancer"]

    it "findMany includes a shelf with no books as empty list" $ \TestEnv {envPool = pool} -> do
      empty <- ShelfFixtures.insertShelf pool "empty"
      stocked <- ShelfFixtures.insertShelf pool "stocked"
      _ <- ShelfFixtures.insertBook pool stocked.id "Book"
      results <- runDb pool (shelves includeBooks)
      emptyResult <- lookupShelf empty.id results
      length emptyResult.books `shouldBe` 0

    it "findMany without include uses single-table read" $ \TestEnv {envPool = pool} -> do
      _ <- ShelfFixtures.insertShelf pool "alpha"
      _ <- ShelfFixtures.insertShelf pool "beta"
      results <- runDb pool (Ops.findMany @ShelfTable @ShelfRow id)
      length results `shouldBe` 2
      sort (map (.name) results) `shouldBe` ["alpha", "beta"]

    it "findMany returns included roots in primary-key order" $ \TestEnv {envPool = pool} -> do
      firstShelf <- ShelfFixtures.insertShelf pool "first"
      secondShelf <- ShelfFixtures.insertShelf pool "second"
      results <- runDb pool (shelves includeBooks)
      map ((.id) . (.shelf)) results `shouldBe` sort [firstShelf.id, secondShelf.id]

    it "findMany applies root filter before nesting" $ \TestEnv {envPool = pool} -> do
      fiction <- ShelfFixtures.insertShelf pool "fiction"
      _ <- ShelfFixtures.insertShelf pool "nonfiction"
      _ <- ShelfFixtures.insertBook pool fiction.id "Dune"
      results <-
        runDb pool (shelvesWhere includeBooks (eq shelfName "fiction"))
      length results `shouldBe` 1
      (head results).shelf.name `shouldBe` "fiction"
      map ((.title) . (.book)) (head results).books `shouldBe` ["Dune"]

    it "findMany nests chapters under books" $ \TestEnv {envPool = pool} -> do
      shelf <- ShelfFixtures.insertShelf pool "fiction"
      book <- ShelfFixtures.insertBook pool shelf.id "Dune"
      _ <- ShelfFixtures.insertChapter pool book.id "Arrakis"
      _ <- ShelfFixtures.insertChapter pool book.id "Caladan"
      results <- runDb pool (shelves includeBooksChapters)
      [result] <- pure results
      result.shelf.name `shouldBe` "fiction"
      [bookWithChapters] <- pure result.books
      (.title) bookWithChapters.book `shouldBe` "Dune"
      let chapterIds = map ((.id) . (.chapter)) bookWithChapters.chapters
      chapterIds `shouldBe` sort chapterIds
      sort (map ((.heading) . (.chapter)) bookWithChapters.chapters) `shouldBe` ["Arrakis", "Caladan"]

    it "findMany includes a book with no chapters as empty list" $ \TestEnv {envPool = pool} -> do
      shelf <- ShelfFixtures.insertShelf pool "fiction"
      book <- ShelfFixtures.insertBook pool shelf.id "Dune"
      _ <- ShelfFixtures.insertBook pool shelf.id "Neuromancer"
      _ <- ShelfFixtures.insertChapter pool book.id "Chiba"
      results <- runDb pool (shelves includeBooksChapters)
      [result] <- pure results
      dune <- lookupBook "Dune" result
      neuromancer <- lookupBook "Neuromancer" result
      map ((.heading) . (.chapter)) dune.chapters `shouldBe` ["Chiba"]
      length neuromancer.chapters `shouldBe` 0

    it "findMany returns all nested results" $ \TestEnv {envPool = pool} -> do
      _ <- ShelfFixtures.insertShelf pool "one"
      _ <- ShelfFixtures.insertShelf pool "two"
      results <- runDb pool (shelves includeBooks)
      length results `shouldBe` 2

    it "findUnique returns Just for an existing primary key" $ \TestEnv {envPool = pool} -> do
      shelf <- ShelfFixtures.insertShelf pool "fiction"
      _ <- ShelfFixtures.insertBook pool shelf.id "Dune"
      Right result <- runDb pool (shelfById includeBooks shelf.id)
      fmap ((.name) . (.shelf)) result `shouldBe` Just "fiction"

    it "findUnique returns all children for a parent with several has-many rows" $ \TestEnv {envPool = pool} -> do
      shelf <- ShelfFixtures.insertShelf pool "fiction"
      _ <- ShelfFixtures.insertBook pool shelf.id "Dune"
      _ <- ShelfFixtures.insertBook pool shelf.id "Neuromancer"
      Right result <- runDb pool (shelfById includeBooks shelf.id)
      let bookIds = maybe [] (map ((.id) . (.book)) . (.books)) result
      bookIds `shouldBe` sort bookIds
      fmap (sort . map ((.title) . (.book)) . (.books)) result
        `shouldBe` Just ["Dune", "Neuromancer"]

    it "findUnique includes a shelf with no books as Just with an empty list" $ \TestEnv {envPool = pool} -> do
      empty <- ShelfFixtures.insertShelf pool "empty"
      Right result <- runDb pool (shelfById includeBooks empty.id)
      fmap ((.name) . (.shelf)) result `shouldBe` Just "empty"
      fmap (length . (.books)) result `shouldBe` Just 0

    it "findUnique returns Nothing when missing" $ \TestEnv {envPool = pool} -> do
      Right result <- runDb pool (shelfById includeBooks (nil :: UUID))
      case result of
        Nothing -> pure ()
        Just _ -> fail "expected Nothing"

    it "findMany with include paginates parent rows" $ \TestEnv {envPool = pool} -> do
      mapM_ (\n -> ShelfFixtures.insertShelf pool ("shelf-" `T.append` T.pack (show n))) ([1 .. 12] :: [Int])
      results <-
        runDb
          pool
          (Shelf.findMany (Shelf.emptyQuery {Shelf.include_ = includeBooks, Shelf.limit_ = Just 10}))
      length results `shouldBe` 10

    it "findMany with include applies offset to parent rows" $ \TestEnv {envPool = pool} -> do
      _ <- ShelfFixtures.insertShelf pool "alpha"
      _ <- ShelfFixtures.insertShelf pool "bravo"
      _ <- ShelfFixtures.insertShelf pool "charlie"
      results <-
        runDb
          pool
          ( Shelf.findMany
              Shelf.emptyQuery
                { Shelf.include_ = includeBooks,
                  Shelf.orderBy_ = [asc shelfName],
                  Shelf.limit_ = Just 1,
                  Shelf.offset_ = Just 1
                }
          )
      length results `shouldBe` 1
      (head results).shelf.name `shouldBe` "bravo"

    it "findMany with include orders parent rows" $ \TestEnv {envPool = pool} -> do
      _ <- ShelfFixtures.insertShelf pool "zeta"
      _ <- ShelfFixtures.insertShelf pool "alpha"
      results <-
        runDb
          pool
          (Shelf.findMany (Shelf.emptyQuery {Shelf.include_ = includeBooks, Shelf.orderBy_ = [asc shelfName]}))
      map ((.name) . (.shelf)) results `shouldBe` ["alpha", "zeta"]

    it "findMany loads sibling hasMany collections independently" $ \TestEnv {envPool = pool} -> do
      shelf <- ShelfFixtures.insertShelf pool "fiction"
      _ <- ShelfFixtures.insertBook pool shelf.id "Dune"
      _ <- ShelfFixtures.insertBook pool shelf.id "Neuromancer"
      _ <- ShelfFixtures.insertTag pool shelf.id "scifi"
      _ <- ShelfFixtures.insertTag pool shelf.id "classic"
      results <- runDb pool (shelves includeBooksAndTags)
      [result] <- pure results
      sort (map ((.title) . (.book)) result.books) `shouldBe` ["Dune", "Neuromancer"]
      sort (map (.label) result.tags) `shouldBe` ["classic", "scifi"]

    it "findMany combines nested books with sibling tags" $ \TestEnv {envPool = pool} -> do
      shelf <- ShelfFixtures.insertShelf pool "fiction"
      book <- ShelfFixtures.insertBook pool shelf.id "Dune"
      _ <- ShelfFixtures.insertChapter pool book.id "Arrakis"
      _ <- ShelfFixtures.insertTag pool shelf.id "scifi"
      results <- runDb pool (shelves includeBooksChaptersAndTags)
      [result] <- pure results
      [bookWithChapters] <- pure result.books
      (.title) bookWithChapters.book `shouldBe` "Dune"
      map ((.heading) . (.chapter)) bookWithChapters.chapters `shouldBe` ["Arrakis"]
      map (.label) result.tags `shouldBe` ["scifi"]

    it "findMany nests sections under chapters at four levels deep" $ \TestEnv {envPool = pool} -> do
      shelf <- ShelfFixtures.insertShelf pool "fiction"
      book <- ShelfFixtures.insertBook pool shelf.id "Dune"
      chapter <- ShelfFixtures.insertChapter pool book.id "Arrakis"
      _ <- ShelfFixtures.insertSection pool chapter.id "Desert"
      _ <- ShelfFixtures.insertSection pool chapter.id "Sietch"
      results <- runDb pool (shelves includeBooksChaptersSections)
      [result] <- pure results
      [bookWithChapters] <- pure result.books
      [chapterWithSections] <- pure bookWithChapters.chapters
      let sectionIds = map (.id) chapterWithSections.sections
      sectionIds `shouldBe` sort sectionIds
      sort (map (.label) chapterWithSections.sections) `shouldBe` ["Desert", "Sietch"]

    it "one renderer accepts a book loaded on its own and under a shelf" $ \TestEnv {envPool = pool} -> do
      shelf <- ShelfFixtures.insertShelf pool "fiction"
      book <- ShelfFixtures.insertBook pool shelf.id "Dune"
      _ <- ShelfFixtures.insertChapter pool book.id "Arrakis"
      let bookInclude = BookInclude {chapters = load}
      [fromBook] <-
        runDb
          pool
          (Book.findMany (Book.emptyQuery {Book.include_ = bookInclude, Book.where_ = Just (eq bookId book.id)}))
      [fromShelf] <-
        runDb
          pool
          (shelves (ShelfInclude {books = loadWith bookInclude, tags = skip}))
      renderBook fromBook `shouldBe` ["Arrakis"]
      renderBook (head fromShelf.books) `shouldBe` ["Arrakis"]

    it "updates a book include from the caller when the field name is unique" $ \_ ->
      updatedChapters `shouldBe` load

    it "findMany includes only books matching where_" $ \TestEnv {envPool = pool} -> do
      fiction <- ShelfFixtures.insertShelf pool "fiction"
      mystery <- ShelfFixtures.insertShelf pool "mystery"
      _ <- ShelfFixtures.insertBook pool fiction.id "Dune"
      _ <- ShelfFixtures.insertBook pool fiction.id "Neuromancer"
      _ <- ShelfFixtures.insertBook pool mystery.id "Rebecca"
      let include =
            ShelfInclude
              { books =
                  (loadWith (BookInclude {chapters = skip}))
                    { where_ = Just (eq bookTitle "Dune")
                    },
                tags = skip
              }
      results <- runDb pool (shelves include)
      fictionResult <- lookupShelf fiction.id results
      mysteryResult <- lookupShelf mystery.id results
      map ((.title) . (.book)) fictionResult.books `shouldBe` ["Dune"]
      length mysteryResult.books `shouldBe` 0

    it "findMany orders included books" $ \TestEnv {envPool = pool} -> do
      shelf <- ShelfFixtures.insertShelf pool "fiction"
      _ <- ShelfFixtures.insertBook pool shelf.id "Neuromancer"
      _ <- ShelfFixtures.insertBook pool shelf.id "Dune"
      _ <- ShelfFixtures.insertBook pool shelf.id "Contact"
      let include =
            ShelfInclude
              { books =
                  (loadWith (BookInclude {chapters = skip}))
                    { orderBy_ = [desc bookTitle]
                    },
                tags = skip
              }
      results <- runDb pool (shelves include)
      [result] <- pure results
      map ((.title) . (.book)) result.books `shouldBe` ["Neuromancer", "Dune", "Contact"]

    it "findMany takes two ordered books per shelf" $ \TestEnv {envPool = pool} -> do
      empty <- ShelfFixtures.insertShelf pool "empty"
      fiction <- ShelfFixtures.insertShelf pool "fiction"
      history <- ShelfFixtures.insertShelf pool "history"
      mapM_ (ShelfFixtures.insertBook pool fiction.id) ["alpha", "bravo", "charlie", "zzz"]
      mapM_ (ShelfFixtures.insertBook pool history.id) ["m", "n", "zzz"]
      let include =
            ShelfInclude
              { books =
                  (loadWith (BookInclude {chapters = skip}))
                    { where_ = Just (neq bookTitle "zzz"),
                      orderBy_ = [asc bookTitle],
                      take_ = Just 2
                    },
                tags = skip
              }
      results <- runDb pool (shelves include)
      emptyResult <- lookupShelf empty.id results
      fictionResult <- lookupShelf fiction.id results
      historyResult <- lookupShelf history.id results
      length emptyResult.books `shouldBe` 0
      map ((.title) . (.book)) fictionResult.books `shouldBe` ["alpha", "bravo"]
      map ((.title) . (.book)) historyResult.books `shouldBe` ["m", "n"]

renderBook :: (HasField "chapters" row [ChapterRow]) => row -> [Text]
renderBook row = map (.heading) row.chapters

lookupShelf wanted results =
  case filter ((== wanted) . (.id) . (.shelf)) results of
    [shelf] -> pure shelf
    _ -> fail "expected exactly one shelf in results"

lookupBook title result =
  case filter ((== title) . (.title) . (.book)) result.books of
    [book] -> pure book
    _ -> fail "expected exactly one book in results"