packages feed

typson-esqueleto-0.1.0.0: src/Typson/Esqueleto.hs

{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE PolyKinds #-}
--------------------------------------------------------------------------------
-- |
-- Module      : Typson.Esqueleto
-- Description : Provides the Esqueleto integration
-- Copyright   : (c) Aaron Allen, 2020
-- Maintainer  : Aaron Allen <aaronallen8455@gmail.com>
-- License     : BSD-style (see the file LICENSE)
-- Stability   : experimental
-- Portability : non-portable
--
--------------------------------------------------------------------------------
module Typson.Esqueleto
  ( jsonPath
  , NullableJSONB(..)
  , PostgreSqlJSON
  ) where

import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
import           Data.Bifunctor (first)
import           Data.List (foldl')
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import qualified Database.Esqueleto as E
import qualified Database.Esqueleto.PostgreSQL.JSON as E
import qualified Database.Esqueleto.Internal.Internal as E
import           GHC.Generics (Generic)

import           Typson

-- | Use a type-safe JSON path as part of a query.
--
-- @
-- select . from $ \entity ->
--   pure . jsonPath (Proxy @("foo" :-> "bar")) fieldSchemaJ
--     $ entity ^. Field
-- @
jsonPath :: ( TypeAtPath o tree path ~ field
            , ReflectPath path
            , PostgreSqlJSON json
            )
         => proxy (path :: k) -- ^ A path proxy
         -> ObjectTree tree o -- ^ Typson schema
         -> E.SqlExpr (E.Value (json o)) -- ^ Column selector
         -> E.SqlExpr (E.Value (NullableJSONB field))
jsonPath path _ input =
  case reflectPath path of
    p NE.:| ps -> foldl' buildPath (buildPath input p) ps
  where
    buildPath p (Key k) = p `arrOp` E.val k
    buildPath p (Idx i) = p `arrOp` E.val (fromIntegral i :: Int)
    arrOp = E.unsafeSqlBinOp " -> "

-- | Treats SQL @NULL@ as a JSON @null@
newtype NullableJSONB a =
  NullableJSONB
    { unNullableJSONB :: a
    } deriving ( Generic
               , Aeson.FromJSON
               , Aeson.ToJSON
               , Foldable
               , Functor
               , Eq
               , Ord
               , Read
               , Show
               , Traversable
               )
      deriving E.PersistFieldSql via (E.JSONB a)

instance (Aeson.FromJSON a, Aeson.ToJSON a)
      => E.PersistField (NullableJSONB a) where
  toPersistValue = E.toPersistValue . E.JSONB . unNullableJSONB
  fromPersistValue pVal = fmap NullableJSONB $ case pVal of
      E.PersistNull -> first T.pack
                     $ Aeson.parseEither Aeson.parseJSON Aeson.Null
      _ -> E.unJSONB <$> E.fromPersistValue pVal

-- | Members of this class are type constructors used to respresent
-- Postgres JSON columns.
class PostgreSqlJSON (json :: * -> *)
instance PostgreSqlJSON NullableJSONB
instance PostgreSqlJSON E.JSONB