packages feed

wikimusic-api-1.1.0.1: src/WikiMusic/PostgreSQL/SongQuery.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.SongQuery () where

import Control.Concurrent.Async
import Data.ByteString.Lazy qualified as BL
import Data.Map (elems, keys)
import Data.Map qualified as Map
import Data.Text (intercalate, pack, strip)
import Data.Vector qualified as V
import Hasql.Decoders as D
import Hasql.Encoders as E
import Hasql.Session qualified as Session
import Hasql.Statement (Statement (..))
import WikiMusic.Free.SongQuery
import WikiMusic.Model.Artwork
import WikiMusic.Model.Comment
import WikiMusic.Model.Opinion
import WikiMusic.Model.Other
import WikiMusic.Model.Song
import WikiMusic.Model.Thread as CommentThread
import WikiMusic.PostgreSQL.Model.Song
import WikiMusic.PostgreSQL.ReadAbstraction qualified as ReadAbstraction
import WikiMusic.Protolude

fetchSongContents' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID SongContent)
fetchSongContents' env identifiers = do
  let fromRow
        ( identifier,
          songIdentifier,
          versionName,
          createdBy,
          visibilityStatus,
          approvedBy,
          instrumentType,
          asciiLegend,
          asciiContents,
          pdfContents,
          guitarProContents,
          createdAt,
          lastEditedAt
          ) =
          SongContent
            { identifier = identifier,
              songIdentifier = songIdentifier,
              versionName = versionName,
              createdBy = createdBy,
              visibilityStatus = fromIntegral visibilityStatus,
              approvedBy = approvedBy,
              instrumentType = instrumentType,
              asciiLegend = asciiLegend,
              asciiContents = asciiContents,
              pdfContents = pdfContents,
              guitarProContents = guitarProContents,
              createdAt = createdAt,
              lastEditedAt = lastEditedAt
            }
      stmt = Statement query encoder decoder True
      query =
        encodeUtf8
          [trimming|
          SELECT identifier, song_identifier, version_name, created_by, visibility_status,
          approved_by, instrument_type, ascii_legend,
          ascii_contents, pdf_contents, guitarpro_contents, created_at, last_edited_at
          FROM song_contents
          WHERE song_identifier = ANY($$1)
         |]
      encoder =
        E.param . E.nonNullable $ (E.foldableArray . E.nonNullable $ E.uuid)
      decoder = D.rowVector vector
      vector =
        (,,,,,,,,,,,,)
          <$> D.column (D.nonNullable D.uuid)
          <*> D.column (D.nonNullable 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.text)
          <*> 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.timestamptz)
          <*> D.column (D.nullable D.timestamptz)
      parseSongContentRows =
        map
          ( \row ->
              let songContent :: SongContent = fromRow row
               in (songContent ^. #identifier, songContent)
          )

  ReadAbstraction.persistenceReadCall
    (env ^. #pool)
    (Session.statement identifiers stmt)
    fromList
    (parseSongContentRows . V.toList)

-- | PostgreSQL song row Hasql decoder for reading from `songs` table
songRowDecoder :: Result [SongRow]
songRowDecoder =
  D.rowList $
    (,,,,,,,,,,,,,,,,,)
      <$> D.column (D.nonNullable D.uuid)
      <*> D.column (D.nonNullable D.text)
      <*> 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.nullable 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 song
parseSongRows :: [SongRow] -> [(UUID, Song)]
parseSongRows = map parser
  where
    parser row = let song = songFromRow row in (song ^. #identifier, song)

-- | Fetch song artworks from storage
fetchSongArtworks' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID SongArtwork)
fetchSongArtworks' env identifiers =
  ReadAbstraction.fetchArtworks
    (env ^. #pool)
    (parseArtworkRows fromRow)
    "song_artworks"
    "song_identifier"
    (vectorFromList identifiers)
  where
    fromRow
      ( identifier,
        songIdentifier,
        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 SongArtwork {..}

-- | Fetch song comments from storage
fetchSongComments' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID SongComment)
fetchSongComments' env identifiers =
  ReadAbstraction.fetchComments
    (env ^. #pool)
    (parseCommentRows fromRow)
    "song_comments"
    "song_identifier"
    (vectorFromList identifiers)
  where
    fromRow
      ( identifier,
        songIdentifier,
        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 SongComment {..}

-- | Fetch song opinions from storage
fetchSongOpinions' :: (MonadIO m) => Env -> [UUID] -> m (Map UUID SongOpinion)
fetchSongOpinions' env identifiers =
  ReadAbstraction.fetchOpinions
    (env ^. #pool)
    (parseOpinionRows fromRow)
    "song_opinions"
    "song_identifier"
    (vectorFromList identifiers)
  where
    fromRow
      ( identifier,
        songIdentifier,
        createdBy,
        isLike,
        isDislike,
        createdAt,
        lastEditedAt
        ) =
        let opinion = Opinion {..}
         in SongOpinion {..}

fetchSongs' :: (MonadIO m) => Env -> SongSortOrder -> Limit -> Offset -> m (Map UUID Song, [UUID])
fetchSongs' env sortOrder (Limit limit) (Offset offset) = do
  ReadAbstraction.persistenceReadCall
    (env ^. #pool)
    (Session.statement (fromIntegral limit, fromIntegral offset) stmt)
    (\queryResults -> (fromList queryResults, map fst queryResults))
    parseSongRows
  where
    sortOrder' = pack . WikiMusic.Model.Song.show $ sortOrder
    query =
      encodeUtf8
        [trimming|
                  SELECT songs.identifier, songs.display_name, songs.music_key,
                  songs.music_tuning, songs.music_creation_date, songs.album_name, songs.album_info_link,
                  songs.created_by, songs.visibility_status, songs.approved_by, songs.created_at, songs.last_edited_at,
                  spotify_url, youtube_url, soundcloud_url, wikipedia_url, songs.views, songs.description
                  FROM songs
                  LEFT JOIN song_external_sources ON song_external_sources.song_identifier = songs.identifier
                  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 songRowDecoder True

fetchSongsByUUID' :: (MonadIO m) => Env -> SongSortOrder -> [UUID] -> m (Map UUID Song, [UUID])
fetchSongsByUUID' env sortOrder identifiers = do
  ReadAbstraction.persistenceReadCall
    (env ^. #pool)
    (Session.statement identifiers stmt)
    (\queryResults -> (fromList queryResults, map fst queryResults))
    parseSongRows
  where
    stmt = Statement (BL.toStrict query) encoder songRowDecoder True
    sortOrder' = fromString . WikiMusic.Model.Song.show $ sortOrder
    query =
      encodeUtf8
        [trimming|
          SELECT songs.identifier, songs.display_name, songs.music_key,
          songs.music_tuning, songs.music_creation_date, songs.album_name, songs.album_info_link,
          songs.created_by, songs.visibility_status, songs.approved_by, songs.created_at, songs.last_edited_at,
          spotify_url, youtube_url, soundcloud_url, wikipedia_url, songs.views, songs.description
          FROM songs
          LEFT JOIN song_external_sources ON song_external_sources.song_identifier = songs.identifier
          WHERE songs.identifier = ANY($$1) ORDER BY $sortOrder'
          |]
    encoder = E.param . E.nonNullable $ (E.foldableArray . E.nonNullable $ E.uuid)

instance Exec SongQuery where
  execAlgebra (FetchSongs env sortOrder limit offset next) = next =<< fetchSongs' env sortOrder limit offset
  execAlgebra (FetchSongsByUUID env sortOrder identifiers next) = next =<< fetchSongsByUUID' env sortOrder identifiers
  execAlgebra (EnrichedSongResponse env songMap enrichSongParams next) = do
    let isChildOf' p x = Just (p ^. #comment % #identifier) == (x ^. #comment % #parentIdentifier)
        songIds = keys songMap
        getComment =
          if enrichSongParams ^. #includeComments
            then exec @SongQuery $ fetchSongComments env songIds
            else pure $ fromList []
        getArtwork =
          if enrichSongParams ^. #includeArtworks
            then exec @SongQuery $ fetchSongArtworks env songIds
            else pure $ fromList []
        getOpinion =
          if enrichSongParams ^. #includeOpinions
            then exec @SongQuery $ fetchSongOpinions env songIds
            else pure $ fromList []
        getArtist =
          if enrichSongParams ^. #includeArtists
            then exec @SongQuery $ fetchSongArtists env songIds
            else pure $ fromList []
        getContent =
          if enrichSongParams ^. #includeContents
            then exec @SongQuery $ fetchSongContents env songIds
            else pure $ fromList []

    (commentMap, artworkMap) <- concurrently getComment getArtwork
    (contentMap, opinionMap) <- concurrently getContent getOpinion
    artistPerSongList <- getArtist
    let matchesSongIdentifier song = (== song ^. #identifier) . (^. #songIdentifier)
    let enrichedSongs =
          mapMap
            ( \song -> do
                let rawCommentMap = Map.filter (matchesSongIdentifier song) commentMap
                    allComments = elems rawCommentMap
                    commentThreads = map renderThread $ mkThreads allComments isChildOf' (^. #comment % #parentIdentifier)
                    relevantArtists = filter (\(songId, _, _) -> songId == song ^. #identifier) artistPerSongList

                song
                  { comments = commentThreads,
                    contents = filterMap (matchesSongIdentifier song) contentMap,
                    artworks = filterMap (matchesSongIdentifier song) artworkMap,
                    opinions = filterMap (matchesSongIdentifier song) opinionMap,
                    artists = Map.fromList $ map (\(_, artistId, artistName) -> (artistId, artistName)) relevantArtists
                  }
            )
            songMap
    next enrichedSongs
  execAlgebra (FetchSongComments env identifiers next) =
    next =<< fetchSongComments' env identifiers
  execAlgebra (FetchSongOpinions env identifiers next) =
    next =<< fetchSongOpinions' env identifiers
  execAlgebra (FetchSongArtworks env identifiers next) =
    next =<< fetchSongArtworks' env identifiers
  execAlgebra (FetchSongArtists env identifiers next) = do
    let stmt = Statement query encoder decoder True
        query =
          "SELECT song_artists.song_identifier, artists.identifier, artists.display_name FROM song_artists \
          \ INNER JOIN artists ON song_artists.artist_identifier = artists.identifier \
          \ WHERE song_identifier = ANY($1)"
        encoder =
          E.param . E.nonNullable $ (E.foldableArray . E.nonNullable $ E.uuid)
        decoder = D.rowVector vector
        vector =
          (,,)
            <$> D.column (D.nonNullable D.uuid)
            <*> D.column (D.nonNullable D.uuid)
            <*> D.column (D.nonNullable D.text)
    r <-
      ReadAbstraction.persistenceReadCall
        (env ^. #pool)
        (Session.statement identifiers stmt)
        fromList
        V.toList
    next r
  execAlgebra (SearchSongs env searchInput sortOrder (Limit limit) (Offset offset) next) = do
    let sortOrder' = pack . WikiMusic.Model.Song.show $ sortOrder
        cleanWords = map (filterText (\x -> x /= '\'' && x /= '%' && x /= ';' && x /= '"') . strip) (words $ searchInput ^. #value)
        inputs = map (\x -> [untrimming|songs.display_name ILIKE '%$x%' OR songs.album_name ILIKE '%$x%'|]) cleanWords
        search' = Data.Text.intercalate " AND " inputs
        query =
          [untrimming|
           SELECT songs.identifier, songs.display_name, songs.music_key,
           songs.music_tuning, songs.music_creation_date, songs.album_name, songs.album_info_link,
           songs.created_by, songs.visibility_status, songs.approved_by, songs.created_at, songs.last_edited_at,
           spotify_url, youtube_url, soundcloud_url, wikipedia_url, songs.views, songs.description
           FROM songs
           LEFT JOIN song_external_sources ON song_external_sources.song_identifier = songs.identifier
           WHERE ${search'}
           ORDER BY ${sortOrder'}
           LIMIT ($$1) OFFSET $$2
          |]

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

    r <-
      ReadAbstraction.persistenceReadCall
        (env ^. #pool)
        (Session.statement (fromIntegral limit, fromIntegral offset) stmt)
        (\queryResults -> (fromList queryResults, map fst queryResults))
        parseSongRows
    next r
  execAlgebra (FetchSongContents env identifiers next) = next =<< fetchSongContents' env identifiers