packages feed

wikimusic-api-1.1.0.1: src/WikiMusic/PostgreSQL/GenreQuery.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE NoFieldSelectors #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module WikiMusic.PostgreSQL.GenreQuery () where

import Control.Concurrent.Async
import Data.Map (elems, keys)
import Data.Map qualified as Map
import Data.Text (pack)
import Hasql.Decoders as D
import Hasql.Encoders as E
import Hasql.Session qualified as Session
import Hasql.Statement (Statement (..))
import WikiMusic.Free.GenreQuery
import WikiMusic.Model.Artwork
import WikiMusic.Model.Comment
import WikiMusic.Model.Genre
import WikiMusic.Model.Opinion
import WikiMusic.Model.Other
import WikiMusic.Model.Thread as CommentThread
import WikiMusic.PostgreSQL.Model.Genre
import WikiMusic.PostgreSQL.ReadAbstraction qualified as ReadAbstraction
import WikiMusic.Protolude

instance Exec GenreQuery where
  execAlgebra (FetchGenres env sortOrder limit offset next) =
    next =<< fetchGenres' env sortOrder limit offset
  execAlgebra (FetchGenresByUUID env sortOrder identifiers next) =
    next =<< fetchGenresByUUID' env sortOrder identifiers
  execAlgebra (EnrichedGenreResponse env genreMap enrichGenreParams next) =
    next =<< enrichedGenreResponse' env genreMap enrichGenreParams
  execAlgebra (FetchGenreComments env identifiers next) =
    next =<< fetchGenreComments' env identifiers
  execAlgebra (FetchGenreOpinions env identifiers next) =
    next =<< fetchGenreOpinions' env identifiers
  execAlgebra (SearchGenres env searchInput sortOrder limit offset next) =
    next =<< searchGenres' env searchInput sortOrder limit offset
  execAlgebra (FetchGenreArtworks env identifiers next) =
    next =<< fetchGenreArtworks' env identifiers

-- | PostgreSQL genre row Hasql decoder for reading from `genres` table
genreRowDecoder :: Result [GenreRow]
genreRowDecoder =
  D.rowList $
    (,,,,,,,,,,,,,)
      <$> D.column (D.nonNullable D.uuid)
      <*> D.column (D.nullable D.uuid)
      <*> D.column (D.nonNullable D.text)
      <*> D.column (D.nonNullable D.uuid)
      <*> D.column (D.nonNullable D.int8)
      <*> D.column (D.nullable D.uuid)
      <*> D.column (D.nonNullable D.timestamptz)
      <*> D.column (D.nullable D.timestamptz)
      <*> D.column (D.nullable D.text)
      <*> D.column (D.nullable D.text)
      <*> D.column (D.nullable D.text)
      <*> D.column (D.nullable D.text)
      <*> D.column (D.nonNullable D.int8)
      <*> D.column (D.nullable D.text)

-- | Parse a PostgreSQL Hasql row to a domain representation of genre
parseGenreRows :: [GenreRow] -> [(UUID, Genre)]
parseGenreRows = map parser
  where
    parser row = let genre = genreFromRow row in (genre ^. #identifier, genre)

-- | Fetch genres from storage, according to a sort order, limit and offset
fetchGenres' :: (MonadIO m) => Env -> GenreSortOrder -> Limit -> Offset -> m (Map UUID Genre, [UUID])
fetchGenres' env sortOrder (Limit limit) (Offset offset) =
  ReadAbstraction.persistenceReadCall
    (env ^. #pool)
    (Session.statement (fromIntegral limit, fromIntegral offset) stmt)
    (\xs -> (fromList xs, map fst xs))
    parseGenreRows
  where
    stmt = Statement query encoder genreRowDecoder True
    sortOrder' = fromString . WikiMusic.Model.Genre.show $ sortOrder
    query =
      encodeUtf8
        [untrimming|
        SELECT genres.identifier, genres.parent_identifier, genres.display_name, genres.created_by,
        genres.visibility_status, genres.approved_by, genres.created_at, genres.last_edited_at,
        spotify_url, youtube_url, soundcloud_url, wikipedia_url, genres.views, genres.description
        FROM genres
        LEFT JOIN genre_external_sources ON genre_external_sources.genre_identifier = genres.identifier
        ORDER BY $sortOrder'
        LIMIT ($$1) OFFSET $$2
      |]
    encoder =
      contrazip2
        (E.param . E.nonNullable $ E.int8)
        (E.param . E.nonNullable $ E.int8)

-- | Fetch genres by UUID from storage, according to a sort order
fetchGenresByUUID' :: (MonadIO m) => Env -> GenreSortOrder -> [UUID] -> m (Map UUID Genre, [UUID])
fetchGenresByUUID' env sortOrder identifiers =
  ReadAbstraction.persistenceReadCall
    (env ^. #pool)
    (Session.statement identifiers stmt)
    (\xs -> (fromList xs, map fst xs))
    parseGenreRows
  where
    stmt = Statement query encoder genreRowDecoder True
    sortOrder' = fromString . WikiMusic.Model.Genre.show $ sortOrder
    query =
      encodeUtf8
        [untrimming|
        SELECT genres.identifier, genres.parent_identifier, genres.display_name, genres.created_by,
        genres.visibility_status, genres.approved_by, genres.created_at, genres.last_edited_at,
        spotify_url, youtube_url, soundcloud_url, wikipedia_url, genres.views, genres.description 
        FROM genres 
        LEFT JOIN genre_external_sources ON genre_external_sources.genre_identifier = genres.identifier
        WHERE genres.identifier = ANY($$1) ORDER BY $sortOrder'            
      |]
    encoder = E.param . E.nonNullable $ E.foldableArray . E.nonNullable $ E.uuid

-- | Fetch genre artworks from storage
fetchGenreArtworks' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID GenreArtwork)
fetchGenreArtworks' env identifiers =
  ReadAbstraction.fetchArtworks
    (env ^. #pool)
    (parseArtworkRows fromRow)
    "genre_artworks"
    "genre_identifier"
    (vectorFromList identifiers)
  where
    fromRow
      ( identifier,
        genreIdentifier,
        createdBy,
        visibilityStatus,
        approvedBy,
        contentUrl,
        contentCaption,
        createdAt,
        lastEditedAt,
        orderValue
        ) =
        let artwork =
              Artwork
                { identifier = identifier,
                  createdBy = createdBy,
                  visibilityStatus = fromIntegral visibilityStatus,
                  approvedBy = approvedBy,
                  contentUrl = contentUrl,
                  contentCaption = contentCaption,
                  createdAt = createdAt,
                  lastEditedAt = lastEditedAt,
                  orderValue = fromIntegral orderValue
                }
         in GenreArtwork {..}

-- | Enrich genres with related information, according to enrichment parameters
enrichedGenreResponse' :: (MonadIO m) => Env -> Map UUID Genre -> EnrichGenreParams -> m (Map UUID Genre)
enrichedGenreResponse' env genreMap enrichGenreParams = do
  (artworkMap, (opinionMap, commentMap)) <- liftIO $ concurrently getArtwork (concurrently getOpinion getComment)
  let enrichedGenres =
        mapMap
          ( \genre -> do
              let rawCommentMap = Map.filter (matchesGenreIdentifier genre) commentMap
                  allComments = elems rawCommentMap
                  commentThreads = map renderThread $ mkThreads allComments isChildOf' (^. #comment % #parentIdentifier)

              genre
                { comments = commentThreads,
                  artworks = filterMap (matchesGenreIdentifier genre) artworkMap,
                  opinions = filterMap (matchesGenreIdentifier genre) opinionMap
                }
          )
          genreMap

  pure enrichedGenres
  where
    matchesGenreIdentifier genre = (== genre ^. #identifier) . (^. #genreIdentifier)
    isChildOf' p x = Just (p ^. #comment % #identifier) == x ^. #comment % #parentIdentifier
    genreIds = keys genreMap
    getComment =
      if enrichGenreParams ^. #includeComments
        then exec @GenreQuery $ fetchGenreComments env genreIds
        else pure $ fromList []
    getArtwork =
      if enrichGenreParams ^. #includeArtworks
        then exec @GenreQuery $ fetchGenreArtworks env genreIds
        else pure $ fromList []
    getOpinion =
      if enrichGenreParams ^. #includeOpinions
        then exec @GenreQuery $ fetchGenreOpinions env genreIds
        else pure $ fromList []

-- | Fetch genre comments from storage
fetchGenreComments' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID GenreComment)
fetchGenreComments' env identifiers =
  ReadAbstraction.fetchComments
    (env ^. #pool)
    (parseCommentRows fromRow)
    "genre_comments"
    "genre_identifier"
    (vectorFromList identifiers)
  where
    fromRow
      ( identifier,
        genreIdentifier,
        parentIdentifier,
        createdBy,
        visibilityStatus,
        contents,
        approvedBy,
        createdAt,
        lastEditedAt
        ) =
        let comment =
              Comment
                { identifier = identifier,
                  parentIdentifier = parentIdentifier,
                  createdBy = createdBy,
                  visibilityStatus = fromIntegral visibilityStatus,
                  contents = contents,
                  approvedBy = approvedBy,
                  createdAt = createdAt,
                  lastEditedAt = lastEditedAt
                }
         in GenreComment {..}

-- | Search genres by keywords from storage, according to a sort order, limit and offset
searchGenres' :: (MonadIO m) => Env -> SearchInput -> GenreSortOrder -> Limit -> Offset -> m (Map UUID Genre, [UUID])
searchGenres' env searchInput sortOrder (Limit limit) (Offset offset) =
  ReadAbstraction.persistenceReadCall
    (env ^. #pool)
    (Session.statement (fromIntegral limit, fromIntegral offset) stmt)
    (\xs -> (fromList xs, map fst xs))
    parseGenreRows
  where
    sortOrder' = pack . WikiMusic.Model.Genre.show $ sortOrder
    searchConstraints = ReadAbstraction.mkSearchConstraints (searchInput ^. #value)
    query =
      encodeUtf8
        [untrimming|
          SELECT genres.identifier, genres.parent_identifier, genres.display_name, genres.created_by, 
          genres.visibility_status, genres.approved_by, genres.created_at, genres.last_edited_at, 
          spotify_url, youtube_url, soundcloud_url, wikipedia_url, genres.views, genres.description 
          FROM genres 
          LEFT JOIN genre_external_sources ON genre_external_sources.genre_identifier = genres.identifier 
          WHERE $searchConstraints
          ORDER BY $sortOrder'
          LIMIT ($$1) OFFSET $$2
        |]

    encoder =
      contrazip2
        (E.param . E.nonNullable $ E.int8)
        (E.param . E.nonNullable $ E.int8)
    stmt = Statement query encoder genreRowDecoder True

-- | Fetch genre opinions from storage
fetchGenreOpinions' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID GenreOpinion)
fetchGenreOpinions' env identifiers =
  ReadAbstraction.fetchOpinions
    (env ^. #pool)
    (parseOpinionRows fromRow)
    "genre_opinions"
    "genre_identifier"
    (vectorFromList identifiers)
  where
    fromRow
      ( identifier,
        genreIdentifier,
        createdBy,
        isLike,
        isDislike,
        createdAt,
        lastEditedAt
        ) =
        let opinion = Opinion {..}
         in GenreOpinion {..}