{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, FlexibleContexts #-} {- | Type classes for PostgreSQL-backed data models. -} module Database.PostgreSQL.Models ( FromParams(..), Param , PostgreSQLModel(..) , HasMany(..) , TableName(..), fromTableName , fromString, IsString ) where import Data.List (intersperse) import Data.String import Database.PostgreSQL.Simple import Database.PostgreSQL.Simple.ToField import Database.PostgreSQL.Simple.ToRow import Database.PostgreSQL.Simple.FromField import Network.Wai.Parse class FromParams p where -- | Converts an HTML form into a type fromParams :: [Param] -> Maybe p instance (FromParams a, FromParams b) => FromParams (a, b) where fromParams params = do a <- fromParams params b <- fromParams params return (a, b) -- | Wrapper type representing PostgreSQL table names newtype TableName p = TableName String deriving (Show, Eq) -- | Unwraps a 'TableName' fromTableName :: TableName p -> String fromTableName (TableName s) = s -- | Basis type for PostgreSQL-backed data-models. Instances must, at minimum, -- implement 'primaryKey', 'tableName' and 'columns'. /Note: the column ordering -- must match that used in the type's implementation of 'FromRow' and 'ToRow'/ class (FromRow p, ToRow p, ToField (PrimaryKey p), FromField (PrimaryKey p)) => PostgreSQLModel p where type PrimaryKey p :: * -- | Given a model, returns the value of it's primary key. In many cases, this -- will simply be an alias to a record accessor. primaryKey :: p -> Maybe (PrimaryKey p) -- | Returns the 'TableName' of the model. Instances should have a top-level value -- for the 'TableName' that is always returned. For example: -- -- > employees :: TableName Employee -- > employees = TableName "employees" -- > -- > instance PostgreSQLModel Employee where -- > ... -- > tableName _ = employess -- tableName :: p -> TableName p -- | Column names excluding primary key. Column order /must/ match the ordering -- used in 'ToRow' and 'FromRow'. columns :: TableName p -> [String] -- | Name of primary key column (default: \"id\"). primaryKeyName :: TableName p -> String primaryKeyName _ = "id" -- | Column names with primary-key name prepended. columns_ :: TableName p -> [String] columns_ tName = (primaryKeyName tName):(columns tName) orderBy :: TableName p -> Maybe String orderBy _ = Nothing -- | Inserts the model into the database. It relies on the primary key -- being autogenerated by the database. insert :: p -> Connection -> IO (PrimaryKey p) insert model conn = do query conn template fields >>= return . head . head where template = fromString $ concat [ "insert into " , fromTableName tName , " (" ++ cols ++ ")" , " VALUES (" ++ qs ++ ") RETURNING " , primaryKeyName tName] tName = tableName model qs = concat $ intersperse ", " $ map (const "?") $ colNames cols = concat $ intersperse ", " $ colNames colNameFields = case primaryKey model of Nothing -> (columns tName, toRow model) Just pkey -> ( columns_ tName , (toField pkey):(toRow model)) (_, fields) = colNameFields (colNames, _) = colNameFields -- | Create or update the model (uses the primary key to determine if -- the model already exists in the database) upsert :: p -> Connection -> IO (PrimaryKey p) upsert model conn = do case primaryKey model of Nothing -> insert model conn Just pkey -> do execute conn template $ toRow model ++ [toField pkey] return pkey where template = fromString $ concat ["update " , fromTableName tName , " SET " , cols, " where ", primaryKeyName tName , " = ?"] tName = tableName model cols = concat $ intersperse ", " $ map (++ " =?") (columns tName) -- | Retrieves the single model corresponding to the given primary key find :: TableName p -> PrimaryKey p -> Connection -> IO (Maybe p) find tn = findFirst tn (primaryKeyName tn) -- | Finds the first model in the database based on the column-value contstraint. findFirst :: ToField f => TableName p -> String -- ^ Search column name -> f -- ^ Search value -> Connection -> IO (Maybe p) findFirst tName col val conn = do models <- query conn template (Only val) case models of (model:_) -> return $ Just model [] -> return Nothing where template = fromString $ concat ["select ", cols, " from " , fromTableName tName , " where " , col , " = ?" , maybe "" (" order by " ++) $ orderBy tName , " limit 1"] cols = concat $ intersperse ", " $ columns_ tName -- | Retrieves all models in the table findAll :: TableName p -> Connection -> IO [p] findAll tName conn = query_ conn template where template = fromString $ concat [ "select ", cols, " from " , fromTableName tName , maybe "" (" order by " ++) $ orderBy tName] cols = concat $ intersperse ", " $ columns_ tName -- | Retrieves all models in the table subject to the column-value constraint. findAllBy :: ToField f => TableName p -> String -- ^ Search column name -> f -- ^ Search value -> Connection -> IO [p] findAllBy tName col val conn = query conn template (Only val) where template = fromString $ concat [ "select ", cols, " from " , fromTableName tName , " where ", col, " = ?" , maybe "" (" order by " ++) $ orderBy tName] cols = concat $ intersperse ", " $ columns_ tName -- | Defines a \"has-many\" relationship between two models, where the 'parent' -- model may be associated with zero or more of the 'child' model. Specifically, -- the 'child' table has a foreign key column pointing to the parent model. class (PostgreSQLModel parent, PostgreSQLModel child) => HasMany parent child where foreignKey :: TableName parent -> TableName child -> String foreignKey tName _ = fromTableName tName ++ "_" ++ (primaryKeyName tName) childrenOf :: parent -> TableName child -> Connection -> IO [child] childrenOf parent ctName conn = query conn template (Only $ primaryKey parent) where template = fromString $ concat $ [ "select ", childColumns, " from " , fromTableName ctName , " where " , foreignKey ptName ctName , " = ?" , maybe "" (" order by " ++) $ orderBy ctName] ptName = tableName parent childColumns = concat $ intersperse ", " $ columns_ ctName childOf :: parent -> TableName child -> PrimaryKey child -> Connection -> IO (Maybe child) childOf parent ctName v = childOfBy parent ctName (primaryKeyName ctName) v childOfBy :: ToField v => parent -> TableName child -> String -> v -> Connection -> IO (Maybe child) childOfBy parent ctName col pkeyc conn = do mchildren <- query conn template (primaryKey parent, pkeyc) case mchildren of [] -> return Nothing (c:_) -> return $ Just c where template = fromString $ concat $ [ "select ", childColumns, " from " , fromTableName ctName , " where " , foreignKey ptName ctName, " = ?" , " and ", col, " = ?" , maybe "" (" order by " ++) $ orderBy ctName , " limit 1"] ptName = tableName parent childColumns = concat $ intersperse ", " $ columns_ ctName childrenOfBy :: ToField f => parent -> TableName child -> String -> f -> Connection -> IO [child] childrenOfBy parent ctName col val conn = query conn template (primaryKey parent, val) where template = fromString $ concat $ [ "select ", childColumns, " from " , fromTableName ctName , " where " , foreignKey ptName ctName , " = ? and ", col, " = ?" , maybe "" (" order by " ++) $ orderBy ctName] ptName = tableName parent childColumns = concat $ intersperse ", " $ columns_ ctName insertFor :: parent -> child -> Connection -> IO (PrimaryKey child) insertFor parent chld conn = do query conn template (fields ++ [toField $ primaryKey parent]) >>= return . head . head where template = fromString $ concat [ "insert into " , fromTableName ctName , " (", cols, ", ", foreignKey ptName ctName, ")" , " VALUES (", qs, ", ?) " , " RETURNING " , primaryKeyName ctName] ctName = tableName chld ptName = tableName parent qs = concat $ intersperse ", " $ map (const "?") $ colNames cols = concat $ intersperse ", " $ colNames colNameFields = case primaryKey chld of Nothing -> (columns ctName, toRow chld) Just pkey -> ( columns_ ctName , (toField pkey):(toRow chld)) (_, fields) = colNameFields (colNames, _) = colNameFields