packages feed

serversession-backend-persistent-2.0.2: src/Web/ServerSession/Backend/Persistent/Internal/Impl.hs

{-# LANGUAGE CPP #-}
-- | Internal module exposing the guts of the package.  Use at
-- your own risk.  No API stability guarantees apply.
module Web.ServerSession.Backend.Persistent.Internal.Impl
  ( PersistentSession(..)
  , PersistentSessionId
  , EntityField(..)
  , serverSessionDefsBySessionMap
  , PersistentSessionBySessionMap
  , mkServerSessionDefs
  , psKey
  , toPersistentSession
  , fromPersistentSession
  , SqlStorage(..)
  , throwSS
  ) where

import           Control.Applicative                                 as A
import           Control.Monad                                       (void)
import           Control.Monad.IO.Class                              (liftIO)
import           Data.Proxy                                          (Proxy (..))
import           Data.Time                                           (UTCTime)
import           Data.Typeable                                       (Typeable)
import           Database.Persist                                    (PersistEntity (..))
import           Web.PathPieces                                      (PathPiece)
import           Web.ServerSession.Core

import qualified Control.Exception                                   as E
import qualified Data.Aeson                                          as A
import qualified Data.Text                                           as T
import qualified Database.Persist                                    as P
import qualified Database.Persist.EntityDef.Internal                 as P (EntityDef (..))
import qualified Database.Persist.Quasi.Internal                     as P (UnboundEntityDef,
                                                                           unbindEntityDef)
import qualified Database.Persist.Sql                                as P

import           Web.ServerSession.Backend.Persistent.Internal.Types

-- We can't use the Template Haskell since we want to generalize
-- some fields.
--
-- This is going to be a pain to upgrade when the next major
-- persistent version comes :(.

-- | Entity corresponding to a 'Session'.
--
-- We're bending @persistent@ in ways it wasn't expected to.  In
-- particular, this entity is parametrized over the session type.
data PersistentSession sess =
  PersistentSession
    { persistentSessionKey        :: !(SessionId sess)    -- ^ Session ID, primary key.
    , persistentSessionAuthId     :: !(Maybe ByteStringJ) -- ^ Value of "_ID" session key.
    , persistentSessionSession    :: !(Decomposed sess)   -- ^ Rest of the session data.
    , persistentSessionCreatedAt  :: !UTCTime             -- ^ When this session was created.
    , persistentSessionAccessedAt :: !UTCTime             -- ^ When this session was last accessed.
    } deriving (Typeable)

deriving instance Eq   (Decomposed sess) => Eq   (PersistentSession sess)
deriving instance Ord  (Decomposed sess) => Ord  (PersistentSession sess)
deriving instance Show (Decomposed sess) => Show (PersistentSession sess)

#if MIN_VERSION_persistent(2,14,0)
instance P.SafeToInsert (PersistentSession sess)
#endif

type PersistentSessionId sess = Key (PersistentSession sess)

instance forall sess. P.PersistFieldSql (Decomposed sess) => P.PersistEntity (PersistentSession sess) where
  type PersistEntityBackend (PersistentSession sess) = P.SqlBackend

  data Unique (PersistentSession sess)

  newtype Key (PersistentSession sess) =
    PersistentSessionKey' {unPersistentSessionKey :: SessionId sess}
    deriving ( Eq, Ord, Show, Read, PathPiece
             , P.PersistField, P.PersistFieldSql, A.ToJSON, A.FromJSON )

  data EntityField (PersistentSession sess) typ
    = typ ~ PersistentSessionId sess => PersistentSessionId
    | typ ~ SessionId sess           => PersistentSessionKey
    | typ ~ Maybe ByteStringJ        => PersistentSessionAuthId
    | typ ~ Decomposed sess          => PersistentSessionSession
    | typ ~ UTCTime                  => PersistentSessionCreatedAt
    | typ ~ UTCTime                  => PersistentSessionAccessedAt

  keyToValues = (:[]) . P.toPersistValue . unPersistentSessionKey
  keyFromValues [x] | Right v <- P.fromPersistValue x = Right $ PersistentSessionKey' v
  keyFromValues xs  = Left $ T.pack $ "PersistentSession/keyFromValues: " ++ show xs

  entityDef _
    = P.EntityDef
    { entityHaskell = P.EntityNameHS "PersistentSession" -- it's dummy.
    , entityDB = P.EntityNameDB "persistent_session"
      -- Since backend is not only persistent, we use the natural key here.
    , entityId = P.EntityIdNaturalKey $ P.CompositeDef (pure $ pfd PersistentSessionKey) []
    , entityAttrs = ["json"]
    , entityFields =
      [ pfd PersistentSessionKey
      , pfd PersistentSessionAuthId
      , pfd PersistentSessionSession
      , pfd PersistentSessionCreatedAt
      , pfd PersistentSessionAccessedAt ]
    , entityUniques = []
    , entityForeigns = []
    , entityDerives = ["Eq", "Ord", "Show", "Typeable"]
    , entityExtra = mempty
    , entitySum = False
    , entityComments = Nothing
    }
    where pfd :: P.EntityField (PersistentSession sess) typ -> P.FieldDef
          pfd = P.persistFieldDef

  toPersistFields (PersistentSession a b c d e) =
#if MIN_VERSION_persistent(2,14,0)
    [ P.toPersistValue a
    , P.toPersistValue b
    , P.toPersistValue c
    , P.toPersistValue d
    , P.toPersistValue e ]
#else
    [ P.SomePersistField a
    , P.SomePersistField b
    , P.SomePersistField c
    , P.SomePersistField d
    , P.SomePersistField e ]
#endif

  fromPersistValues [a, b, c, d, e] =
    PersistentSession
      A.<$> err "key"      (P.fromPersistValue a)
      <*> err "authId"     (P.fromPersistValue b)
      <*> err "session"    (P.fromPersistValue c)
      <*> err "createdAt"  (P.fromPersistValue d)
      <*> err "accessedAt" (P.fromPersistValue e)
    where
      err :: T.Text -> Either T.Text a -> Either T.Text a
      err s (Left r)  = Left $ T.concat ["PersistentSession/fromPersistValues/", s, ": ", r]
      err _ (Right v) = Right v
  fromPersistValues x = Left $ T.pack $ "PersistentSession/fromPersistValues: " ++ show x

  persistUniqueToFieldNames _ = error "Degenerate case, should never happen"
  persistUniqueToValues _     = error "Degenerate case, should never happen"
  persistUniqueKeys _         = []

  persistFieldDef PersistentSessionId
    = persistFieldDefPersistentSessionKey
  persistFieldDef PersistentSessionKey
    = persistFieldDefPersistentSessionKey
  persistFieldDef PersistentSessionAuthId
    = P.FieldDef
        (P.FieldNameHS "authId")
        (P.FieldNameDB "auth_id")
        (P.FTTypeCon Nothing "ByteStringJ")
        (P.sqlType (Proxy :: Proxy ByteStringJ))
        [P.FieldAttrMaybe, P.FieldAttrDefault "NULL"]
        True
        P.NoReference
        (P.FieldCascade {fcOnUpdate = Nothing, fcOnDelete = Nothing})
        Nothing
        Nothing
        False
  persistFieldDef PersistentSessionSession
    = P.FieldDef
        (P.FieldNameHS "session")
        (P.FieldNameDB "session")
        (P.FTTypeCon Nothing "Decomposed sess")
        (P.sqlType (Proxy :: Proxy (Decomposed sess))) -- Important!
        []
        True
        P.NoReference
        (P.FieldCascade {fcOnUpdate = Nothing, fcOnDelete = Nothing})
        Nothing
        Nothing
        False
  persistFieldDef PersistentSessionCreatedAt
    = P.FieldDef
        (P.FieldNameHS "createdAt")
        (P.FieldNameDB "created_at")
        (P.FTTypeCon Nothing "UTCTime")
        (P.sqlType (Proxy :: Proxy UTCTime))
        []
        True
        P.NoReference
        (P.FieldCascade {fcOnUpdate = Nothing, fcOnDelete = Nothing})
        Nothing
        Nothing
        False
  persistFieldDef PersistentSessionAccessedAt
    = P.FieldDef
        (P.FieldNameHS "accessedAt")
        (P.FieldNameDB "accessed_at")
        (P.FTTypeCon Nothing "UTCTime")
        (P.sqlType (Proxy :: Proxy UTCTime))
        []
        True
        P.NoReference
        (P.FieldCascade {fcOnUpdate = Nothing, fcOnDelete = Nothing})
        Nothing
        Nothing
        False

  persistIdField = PersistentSessionId

  fieldLens PersistentSessionId = lensPTH
    P.entityKey
    (\(P.Entity _ v) k -> P.Entity k v)
  fieldLens PersistentSessionKey = lensPTH
    (persistentSessionKey . P.entityVal)
    (\(P.Entity k v) x -> P.Entity k (v {persistentSessionKey = x}))
  fieldLens PersistentSessionAuthId = lensPTH
    (persistentSessionAuthId . P.entityVal)
    (\(P.Entity k v) x -> P.Entity k (v {persistentSessionAuthId = x}))
  fieldLens PersistentSessionSession = lensPTH
    (persistentSessionSession . P.entityVal)
    (\(P.Entity k v) x -> P.Entity k (v {persistentSessionSession = x}))
  fieldLens PersistentSessionCreatedAt = lensPTH
    (persistentSessionCreatedAt . P.entityVal)
    (\(P.Entity k v) x -> P.Entity k (v {persistentSessionCreatedAt = x}))
  fieldLens PersistentSessionAccessedAt = lensPTH
    (persistentSessionAccessedAt . P.entityVal)
    (\(P.Entity k v) x -> P.Entity k (v {persistentSessionAccessedAt = x}))

-- | To avoid type argument mismatch, the definition is spit out and finalized.
persistFieldDefPersistentSessionKey :: P.FieldDef
persistFieldDefPersistentSessionKey =
  P.FieldDef
  (P.FieldNameHS "key")
  (P.FieldNameDB "key")
  (P.FTTypeCon Nothing "SessionId sess")
  (P.sqlType (Proxy :: Proxy (SessionId sess)))
  [P.FieldAttrMaxlen 30]
  True
  P.NoReference
  (P.FieldCascade {fcOnUpdate = Nothing, fcOnDelete = Nothing})
  Nothing
  Nothing
  False

-- | Copy-paste from @Database.Persist.TH@.  Who needs lens anyway...
lensPTH :: Functor f => (s -> a) -> (s -> b -> t) -> (a -> f b) -> s -> f t
lensPTH sa sbt afb s = fmap (sbt s) (afb $ sa s)


instance A.ToJSON (Decomposed sess) => A.ToJSON (PersistentSession sess) where
  toJSON (PersistentSession key authId session createdAt accessedAt) =
    A.object
      [ "key"        A..= key
      , "authId"     A..= authId
      , "session"    A..= session
      , "createdAt"  A..= createdAt
      , "accessedAt" A..= accessedAt ]

instance A.FromJSON (Decomposed sess) => A.FromJSON (PersistentSession sess) where
  parseJSON (A.Object obj) =
    PersistentSession
      <$> obj A..: "key"
      <*> obj A..: "authId"
      <*> obj A..: "session"
      <*> obj A..: "createdAt"
      <*> obj A..: "accessedAt"
  parseJSON _ = mempty

instance ( A.ToJSON (Decomposed sess)
         , P.PersistFieldSql (Decomposed sess)
         ) => A.ToJSON (P.Entity (PersistentSession sess)) where
  toJSON = P.entityIdToJSON

instance ( A.FromJSON (Decomposed sess)
         , P.PersistFieldSql (Decomposed sess)
         ) => A.FromJSON (P.Entity (PersistentSession sess)) where
  parseJSON = P.entityIdFromJSON

type PersistentSessionBySessionMap = PersistentSession SessionMap

-- | Simple version.
-- Entity definitions needed to generate the SQL schema for 'SqlStorage'.
-- Example:
--
-- @
-- mkMigrate \"migrateAll\" serverSessionDefsBySessionMap
-- @
--
-- Note: Also import `PersistentSessionBySessionMap` in the same module.
serverSessionDefsBySessionMap :: [P.UnboundEntityDef]
serverSessionDefsBySessionMap = mkServerSessionDefs (Proxy :: Proxy PersistentSessionBySessionMap) "PersistentSessionBySessionMap"

-- | Entity definitions needed to generate the SQL schema for 'SqlStorage'.
-- Generate schema by specifying Haskell name in Text.
--
-- Example using 'SessionMap':
--
-- @
-- type PersistentSessionBySessionMap = PersistentSession SessionMap
-- mkMigrate \"migrateAll\" (mkServerSessionDefs (Proxy :: Proxy PersistentSessionBySessionMap) \"PersistentSessionBySessionMap\")
-- @
mkServerSessionDefs :: forall sess. PersistEntity sess => Proxy sess -> T.Text -> [P.UnboundEntityDef]
mkServerSessionDefs _ name =
  -- The name of a variable of type sess is no longer taken into account, so it is now necessary to pass it by Text.
  [P.unbindEntityDef $ (entityDef (Proxy :: Proxy sess)) { P.entityHaskell = P.EntityNameHS name }]

-- | Generate a key to the entity from the session ID.
psKey :: SessionId sess -> Key (PersistentSession sess)
psKey = PersistentSessionKey'


-- | Convert from 'Session' to 'PersistentSession'.
toPersistentSession :: Session sess -> PersistentSession sess
toPersistentSession Session {..} =
  PersistentSession
    { persistentSessionKey        = sessionKey
    , persistentSessionAuthId     = fmap B sessionAuthId
    , persistentSessionSession    = sessionData
    , persistentSessionCreatedAt  = sessionCreatedAt
    , persistentSessionAccessedAt = sessionAccessedAt
    }


-- | Convert from 'PersistentSession' to 'Session'.
fromPersistentSession :: PersistentSession sess -> Session sess
fromPersistentSession PersistentSession {..} =
  Session
    { sessionKey        = persistentSessionKey
    , sessionAuthId     = fmap unB persistentSessionAuthId
    , sessionData       = persistentSessionSession
    , sessionCreatedAt  = persistentSessionCreatedAt
    , sessionAccessedAt = persistentSessionAccessedAt
    }


-- | SQL session storage backend using @persistent@.
newtype SqlStorage sess =
  SqlStorage
    { connPool :: P.ConnectionPool
      -- ^ Pool of DB connections.  You may use the same pool as
      -- your application.
    } deriving (Typeable)


instance forall sess.
         ( IsSessionData sess
         , P.PersistFieldSql (Decomposed sess)
         ) => Storage (SqlStorage sess) where
  type SessionData  (SqlStorage sess) = sess
  type TransactionM (SqlStorage sess) = P.SqlPersistT IO
  runTransactionM  = flip P.runSqlPool . connPool
  getSession     _ = fmap (fmap fromPersistentSession) . P.get . psKey
  deleteSession  _ = P.delete . psKey
  deleteAllSessionsOfAuthId _ authId =
    P.deleteWhere [field P.==. Just (B authId)]
    where
      field :: EntityField (PersistentSession sess) (Maybe ByteStringJ)
      field = PersistentSessionAuthId
  insertSession s session = do
    mold <- getSession s (sessionKey session)
    maybe
      (void $ P.insert $ toPersistentSession session)
      (\old -> throwSS $ SessionAlreadyExists old session)
      mold
  replaceSession _ session = do
    let key = psKey $ sessionKey session
    mold <- P.get key
    maybe
      (throwSS $ SessionDoesNotExist session)
      (\_old -> void $ P.replace key $ toPersistentSession session)
      mold


-- | Specialization of 'E.throwIO' for 'SqlStorage'.
throwSS
  :: Storage (SqlStorage sess)
  => StorageException (SqlStorage sess)
  -> TransactionM (SqlStorage sess) a
throwSS = liftIO . E.throwIO