@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,174 lines (1,012 loc) • 37.5 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.haskellPersistentTemplate = void 0;
exports.haskellPersistentTemplate = {
id: 'haskell-persistent',
name: 'haskell-persistent',
displayName: 'Haskell Persistent ORM',
description: 'Type-safe database access with Persistent ORM, supporting multiple backends and advanced query capabilities',
framework: 'persistent',
language: 'haskell',
version: '2.14',
author: 'Re-Shell Team',
featured: true,
recommended: true,
icon: '🗄️',
type: 'database',
complexity: 'intermediate',
keywords: ['haskell', 'persistent', 'orm', 'database', 'type-safe', 'sql'],
features: [
'Type-safe database queries',
'Multiple backend support (PostgreSQL, MySQL, SQLite, MongoDB)',
'Automatic migrations',
'Schema definition in Haskell',
'Query DSL',
'Raw SQL support',
'Connection pooling',
'Transaction support',
'Joins and relationships',
'JSON field support',
'Database introspection',
'Type-safe updates',
'Streaming queries',
'Database seeding'
],
structure: {
'src/Database/Schema.hs': `{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
module Database.Schema where
import Database.Persist.TH
import Data.Time (UTCTime)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Aeson
import Database.Persist.Sql
import Control.Monad.Reader
-- Define database schema using Template Haskell
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
email Text
username Text Maybe
password Text
firstName Text
lastName Text
avatar Text Maybe
emailVerified Bool default=False
active Bool default=True
role UserRole
createdAt UTCTime default=CURRENT_TIMESTAMP
updatedAt UTCTime default=CURRENT_TIMESTAMP
UniqueEmail email
UniqueUsername username !force
deriving Show Eq
UserProfile
userId UserId
bio Text Maybe
website Text Maybe
location Text Maybe
timezone Text default="UTC"
preferences Value
UniqueUserProfile userId
deriving Show Eq
BlogPost
title Text
slug Text
content Text
summary Text Maybe
authorId UserId
categoryId CategoryId Maybe
published Bool default=False
publishedAt UTCTime Maybe
views Int default=0
createdAt UTCTime default=CURRENT_TIMESTAMP
updatedAt UTCTime default=CURRENT_TIMESTAMP
UniqueSlug slug
deriving Show Eq
Category
name Text
slug Text
description Text Maybe
parentId CategoryId Maybe
sortOrder Int default=0
UniqueCategorySlug slug
deriving Show Eq
Tag
name Text
slug Text
UniqueTagName name
UniqueTagSlug slug
deriving Show Eq
PostTag
postId BlogPostId
tagId TagId
UniquePostTag postId tagId
deriving Show Eq
Comment
postId BlogPostId
userId UserId
parentId CommentId Maybe
content Text
approved Bool default=True
createdAt UTCTime default=CURRENT_TIMESTAMP
updatedAt UTCTime default=CURRENT_TIMESTAMP
deriving Show Eq
Session
userId UserId
token Text
expiresAt UTCTime
createdAt UTCTime default=CURRENT_TIMESTAMP
UniqueToken token
deriving Show Eq
AuditLog
userId UserId Maybe
action Text
entityType Text
entityId Text
oldValue Value Maybe
newValue Value Maybe
ipAddress Text Maybe
userAgent Text Maybe
createdAt UTCTime default=CURRENT_TIMESTAMP
deriving Show Eq
|]
-- Custom types
data UserRole = Admin | Editor | Author | Subscriber
deriving (Show, Read, Eq, Enum, Bounded)
instance PersistField UserRole where
toPersistValue = PersistText . T.pack . show
fromPersistValue (PersistText t) = case reads (T.unpack t) of
[(r, "")] -> Right r
_ -> Left "Invalid UserRole"
fromPersistValue _ = Left "Expected PersistText for UserRole"
instance PersistFieldSql UserRole where
sqlType _ = SqlString
-- JSON instances
instance ToJSON UserRole where
toJSON = toJSON . show
instance FromJSON UserRole where
parseJSON = withText "UserRole" $ \\t ->
case reads (T.unpack t) of
[(r, "")] -> return r
_ -> fail "Invalid UserRole"
-- Entity JSON instances (excluding sensitive fields)
instance ToJSON (Entity User) where
toJSON (Entity uid user) = object
[ "id" .= uid
, "email" .= userEmail user
, "username" .= userUsername user
, "firstName" .= userFirstName user
, "lastName" .= userLastName user
, "avatar" .= userAvatar user
, "emailVerified" .= userEmailVerified user
, "active" .= userActive user
, "role" .= userRole user
, "createdAt" .= userCreatedAt user
]
instance ToJSON (Entity BlogPost) where
toJSON (Entity pid post) = object
[ "id" .= pid
, "title" .= blogPostTitle post
, "slug" .= blogPostSlug post
, "content" .= blogPostContent post
, "summary" .= blogPostSummary post
, "authorId" .= blogPostAuthorId post
, "categoryId" .= blogPostCategoryId post
, "published" .= blogPostPublished post
, "publishedAt" .= blogPostPublishedAt post
, "views" .= blogPostViews post
, "createdAt" .= blogPostCreatedAt post
, "updatedAt" .= blogPostUpdatedAt post
]`,
'src/Database/Models.hs': `{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
module Database.Models where
import Database.Persist
import Database.Persist.Sql
import Database.Schema
import Data.Text (Text)
import Data.Time (UTCTime, getCurrentTime)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (ReaderT)
import qualified Data.Text as T
import Data.Maybe (listToMaybe)
-- User operations
createUser :: MonadIO m => Text -> Text -> Text -> Text -> UserRole -> ReaderT SqlBackend m (Entity User)
createUser email password firstName lastName role = do
now <- liftIO getCurrentTime
insertEntity User
{ userEmail = email
, userUsername = Nothing
, userPassword = password -- Should be hashed!
, userFirstName = firstName
, userLastName = lastName
, userAvatar = Nothing
, userEmailVerified = False
, userActive = True
, userRole = role
, userCreatedAt = now
, userUpdatedAt = now
}
getUserByEmail :: MonadIO m => Text -> ReaderT SqlBackend m (Maybe (Entity User))
getUserByEmail email = selectFirst [UserEmail ==. email] []
getUserWithProfile :: MonadIO m => UserId -> ReaderT SqlBackend m (Maybe (Entity User, Maybe (Entity UserProfile)))
getUserWithProfile userId = do
mUser <- get userId
case mUser of
Nothing -> return Nothing
Just user -> do
mProfile <- selectFirst [UserProfileUserId ==. userId] []
return $ Just (Entity userId user, mProfile)
updateUserProfile :: MonadIO m => UserId -> Maybe Text -> Maybe Text -> Maybe Text -> ReaderT SqlBackend m ()
updateUserProfile userId bio website location = do
now <- liftIO getCurrentTime
mProfile <- selectFirst [UserProfileUserId ==. userId] []
case mProfile of
Nothing -> insert_ UserProfile
{ userProfileUserId = userId
, userProfileBio = bio
, userProfileWebsite = website
, userProfileLocation = location
, userProfileTimezone = "UTC"
, userProfilePreferences = object []
}
Just (Entity profileId _) -> update profileId
[ UserProfileBio =. bio
, UserProfileWebsite =. website
, UserProfileLocation =. location
]
-- Blog post operations
createPost :: MonadIO m => Text -> Text -> Text -> UserId -> ReaderT SqlBackend m (Entity BlogPost)
createPost title slug content authorId = do
now <- liftIO getCurrentTime
insertEntity BlogPost
{ blogPostTitle = title
, blogPostSlug = slug
, blogPostContent = content
, blogPostSummary = Nothing
, blogPostAuthorId = authorId
, blogPostCategoryId = Nothing
, blogPostPublished = False
, blogPostPublishedAt = Nothing
, blogPostViews = 0
, blogPostCreatedAt = now
, blogPostUpdatedAt = now
}
getPostBySlug :: MonadIO m => Text -> ReaderT SqlBackend m (Maybe (Entity BlogPost))
getPostBySlug slug = selectFirst [BlogPostSlug ==. slug] []
getPublishedPosts :: MonadIO m => Int -> Int -> ReaderT SqlBackend m [Entity BlogPost]
getPublishedPosts limit offset = selectList
[BlogPostPublished ==. True]
[Desc BlogPostPublishedAt, LimitTo limit, OffsetBy offset]
getPostsWithAuthor :: MonadIO m => ReaderT SqlBackend m [(Entity BlogPost, Entity User)]
getPostsWithAuthor =
select $ from $ \\(post, user) -> do
where_ (post ^. BlogPostAuthorId ==. user ^. UserId)
orderBy [desc (post ^. BlogPostCreatedAt)]
return (post, user)
publishPost :: MonadIO m => BlogPostId -> ReaderT SqlBackend m ()
publishPost postId = do
now <- liftIO getCurrentTime
update postId
[ BlogPostPublished =. True
, BlogPostPublishedAt =. Just now
, BlogPostUpdatedAt =. now
]
incrementPostViews :: MonadIO m => BlogPostId -> ReaderT SqlBackend m ()
incrementPostViews postId =
update postId [BlogPostViews +=. 1]
-- Category operations
createCategory :: MonadIO m => Text -> Text -> Maybe Text -> Maybe CategoryId -> ReaderT SqlBackend m (Entity Category)
createCategory name slug description parentId = insertEntity Category
{ categoryName = name
, categorySlug = slug
, categoryDescription = description
, categoryParentId = parentId
, categorySortOrder = 0
}
getCategoryTree :: MonadIO m => ReaderT SqlBackend m [Entity Category]
getCategoryTree = do
categories <- selectList [] [Asc CategorySortOrder, Asc CategoryName]
return $ buildTree categories
where
buildTree = id -- Simplified; implement actual tree building logic
-- Tag operations
addTagsToPost :: MonadIO m => BlogPostId -> [Text] -> ReaderT SqlBackend m ()
addTagsToPost postId tagNames = do
-- Delete existing tags
deleteWhere [PostTagPostId ==. postId]
-- Insert new tags
forM_ tagNames $ \\tagName -> do
let slug = T.toLower $ T.replace " " "-" tagName
mTag <- selectFirst [TagName ==. tagName] []
tagId <- case mTag of
Just (Entity tid _) -> return tid
Nothing -> insert Tag
{ tagName = tagName
, tagSlug = slug
}
insert_ PostTag
{ postTagPostId = postId
, postTagTagId = tagId
}
getPostTags :: MonadIO m => BlogPostId -> ReaderT SqlBackend m [Entity Tag]
getPostTags postId =
select $ from $ \\(postTag, tag) -> do
where_ (postTag ^. PostTagPostId ==. val postId
&&. postTag ^. PostTagTagId ==. tag ^. TagId)
return tag
-- Comment operations
createComment :: MonadIO m => BlogPostId -> UserId -> Maybe CommentId -> Text -> ReaderT SqlBackend m (Entity Comment)
createComment postId userId parentId content = do
now <- liftIO getCurrentTime
insertEntity Comment
{ commentPostId = postId
, commentUserId = userId
, commentParentId = parentId
, commentContent = content
, commentApproved = True
, commentCreatedAt = now
, commentUpdatedAt = now
}
getPostComments :: MonadIO m => BlogPostId -> ReaderT SqlBackend m [Entity Comment]
getPostComments postId = selectList
[CommentPostId ==. postId, CommentApproved ==. True]
[Asc CommentCreatedAt]
-- Session management
createSession :: MonadIO m => UserId -> Text -> UTCTime -> ReaderT SqlBackend m (Entity Session)
createSession userId token expiresAt = do
now <- liftIO getCurrentTime
insertEntity Session
{ sessionUserId = userId
, sessionToken = token
, sessionExpiresAt = expiresAt
, sessionCreatedAt = now
}
validateSession :: MonadIO m => Text -> ReaderT SqlBackend m (Maybe (Entity User))
validateSession token = do
now <- liftIO getCurrentTime
mSession <- selectFirst
[SessionToken ==. token, SessionExpiresAt >. now]
[]
case mSession of
Nothing -> return Nothing
Just (Entity _ session) -> get (sessionUserId session) >>= \\case
Nothing -> return Nothing
Just user -> return $ Just $ Entity (sessionUserId session) user
-- Audit logging
logAction :: MonadIO m => Maybe UserId -> Text -> Text -> Text -> Maybe Value -> Maybe Value -> ReaderT SqlBackend m ()
logAction userId action entityType entityId oldValue newValue = do
now <- liftIO getCurrentTime
insert_ AuditLog
{ auditLogUserId = userId
, auditLogAction = action
, auditLogEntityType = entityType
, auditLogEntityId = entityId
, auditLogOldValue = oldValue
, auditLogNewValue = newValue
, auditLogIpAddress = Nothing
, auditLogUserAgent = Nothing
, auditLogCreatedAt = now
}`,
'src/Database/Queries.hs': `{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
module Database.Queries where
import Database.Persist
import Database.Persist.Sql
import Database.Esqueleto.Experimental
import Database.Schema
import Data.Text (Text)
import Data.Time (UTCTime, getCurrentTime, addDays)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Reader (ReaderT)
import qualified Data.Text as T
-- Advanced Esqueleto queries
-- Get posts with author and category information
getPostsWithDetails :: MonadIO m => ReaderT SqlBackend m [(Entity BlogPost, Entity User, Maybe (Entity Category))]
getPostsWithDetails =
select $ do
(post :& author :& category) <-
from $ table
\`innerJoin\` table
\`on\` (\\(post :& author) -> post ^. BlogPostAuthorId ==. author ^. UserId)
\`leftJoin\` table
\`on\` (\\(post :& _ :& category) -> post ^. BlogPostCategoryId ==. category ?. CategoryId)
where_ (post ^. BlogPostPublished ==. val True)
orderBy [desc (post ^. BlogPostPublishedAt)]
limit 10
return (post, author, category)
-- Get popular posts by view count
getPopularPosts :: MonadIO m => Int -> ReaderT SqlBackend m [Entity BlogPost]
getPopularPosts days = do
now <- liftIO getCurrentTime
let since = addDays (fromIntegral $ -days) now
select $ do
post <- from $ table
where_ $ (post ^. BlogPostPublished ==. val True)
&&. (post ^. BlogPostPublishedAt >=. just (val since))
orderBy [desc (post ^. BlogPostViews)]
limit 10
return post
-- Get posts by tag
getPostsByTag :: MonadIO m => Text -> ReaderT SqlBackend m [Entity BlogPost]
getPostsByTag tagSlug =
select $ do
(post :& postTag :& tag) <-
from $ table
\`innerJoin\` table
\`on\` (\\(post :& postTag) -> post ^. BlogPostId ==. postTag ^. PostTagPostId)
\`innerJoin\` table
\`on\` (\\(_ :& postTag :& tag) -> postTag ^. PostTagTagId ==. tag ^. TagId)
where_ $ (tag ^. TagSlug ==. val tagSlug)
&&. (post ^. BlogPostPublished ==. val True)
orderBy [desc (post ^. BlogPostPublishedAt)]
return post
-- Complex aggregation: post count by category
getPostCountByCategory :: MonadIO m => ReaderT SqlBackend m [(Entity Category, Value Int)]
getPostCountByCategory =
select $ do
(category :& post) <-
from $ table
\`leftJoin\` table
\`on\` (\\(category :& post) -> just (category ^. CategoryId) ==. post ?. BlogPostCategoryId)
groupBy (category ^. CategoryId)
let postCount = count (post ?. BlogPostId)
orderBy [desc postCount]
return (category, postCount)
-- Search posts with full-text search (PostgreSQL specific)
searchPosts :: MonadIO m => Text -> ReaderT SqlBackend m [Entity BlogPost]
searchPosts query = rawSql
"SELECT ?? FROM blog_post \\
\\WHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', ?) \\
\\AND published = TRUE \\
\\ORDER BY ts_rank(to_tsvector('english', title || ' ' || content), plainto_tsquery('english', ?)) DESC"
[toPersistValue query, toPersistValue query]
-- User activity statistics
getUserStats :: MonadIO m => UserId -> ReaderT SqlBackend m (Int, Int, Int)
getUserStats userId = do
postCount <- count [BlogPostAuthorId ==. userId]
commentCount <- count [CommentUserId ==. userId]
viewCount <- select $ do
post <- from $ table
where_ (post ^. BlogPostAuthorId ==. val userId)
return $ sum_ (post ^. BlogPostViews)
let totalViews = case viewCount of
[Value (Just v)] -> v
_ -> 0
return (postCount, commentCount, totalViews)
-- Recent comments with post and user info
getRecentComments :: MonadIO m => Int -> ReaderT SqlBackend m [(Entity Comment, Entity BlogPost, Entity User)]
getRecentComments limit =
select $ do
(comment :& post :& user) <-
from $ table
\`innerJoin\` table
\`on\` (\\(comment :& post) -> comment ^. CommentPostId ==. post ^. BlogPostId)
\`innerJoin\` table
\`on\` (\\(comment :& _ :& user) -> comment ^. CommentUserId ==. user ^. UserId)
where_ (comment ^. CommentApproved ==. val True)
orderBy [desc (comment ^. CommentCreatedAt)]
limit (fromIntegral limit)
return (comment, post, user)
-- Subquery example: users who have written posts
getActiveAuthors :: MonadIO m => ReaderT SqlBackend m [Entity User]
getActiveAuthors =
select $ do
author <- from $ table
where_ $ exists $ do
post <- from $ table
where_ (post ^. BlogPostAuthorId ==. author ^. UserId)
return author
-- Window function example (requires PostgreSQL)
getPostsWithRank :: MonadIO m => ReaderT SqlBackend m [(Entity BlogPost, Value Int)]
getPostsWithRank = rawSql
"SELECT ??, RANK() OVER (ORDER BY views DESC) as rank \\
\\FROM blog_post \\
\\WHERE published = TRUE"
[]
-- Transaction example
publishPostWithNotification :: MonadIO m => BlogPostId -> ReaderT SqlBackend m ()
publishPostWithNotification postId = do
-- Start transaction implicitly
now <- liftIO getCurrentTime
-- Update post
update postId
[ BlogPostPublished =. True
, BlogPostPublishedAt =. Just now
]
-- Log the action
mPost <- get postId
case mPost of
Nothing -> error "Post not found"
Just post -> logAction
(Just $ blogPostAuthorId post)
"publish"
"BlogPost"
(T.pack $ show postId)
(Just $ object ["published" .= False])
(Just $ object ["published" .= True])
-- Could add notification logic here
-- Transaction commits automatically if no exception`,
'src/Database/Migration.hs': `{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Database.Migration where
import Database.Persist
import Database.Persist.Sql
import Database.Persist.Postgresql
import Database.Schema
import Control.Monad.Reader
import Control.Monad.Logger
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time
-- Run all migrations
runMigrations :: ConnectionString -> IO ()
runMigrations connStr = runStderrLoggingT $
withPostgresqlPool connStr 10 $ \\pool ->
liftIO $ runSqlPool doMigrations pool
doMigrations :: ReaderT SqlBackend IO ()
doMigrations = do
-- Run auto-generated migrations
runMigration migrateAll
-- Run custom migrations
runCustomMigrations
-- Custom migrations that can't be expressed in the schema
runCustomMigrations :: ReaderT SqlBackend IO ()
runCustomMigrations = do
-- Add indexes
rawExecute "CREATE INDEX IF NOT EXISTS idx_blog_post_published_at ON blog_post(published_at) WHERE published = true" []
rawExecute "CREATE INDEX IF NOT EXISTS idx_blog_post_slug ON blog_post(slug)" []
rawExecute "CREATE INDEX IF NOT EXISTS idx_user_email ON user(email)" []
rawExecute "CREATE INDEX IF NOT EXISTS idx_session_token ON session(token)" []
rawExecute "CREATE INDEX IF NOT EXISTS idx_session_expires ON session(expires_at)" []
-- Add full-text search (PostgreSQL)
rawExecute "CREATE EXTENSION IF NOT EXISTS unaccent" []
rawExecute "CREATE EXTENSION IF NOT EXISTS pg_trgm" []
-- Create search index
rawExecute
"CREATE INDEX IF NOT EXISTS idx_blog_post_search ON blog_post \\
\\USING gin(to_tsvector('english', title || ' ' || content))"
[]
-- Add triggers for updated_at
createUpdatedAtTrigger "user"
createUpdatedAtTrigger "blog_post"
createUpdatedAtTrigger "comment"
-- Add check constraints
rawExecute
"ALTER TABLE blog_post ADD CONSTRAINT check_published_at \\
\\CHECK (published = false OR published_at IS NOT NULL)"
[]
-- Helper to create updated_at trigger
createUpdatedAtTrigger :: Text -> ReaderT SqlBackend IO ()
createUpdatedAtTrigger tableName = do
-- Create trigger function if not exists
rawExecute
"CREATE OR REPLACE FUNCTION update_updated_at_column() \\
\\RETURNS TRIGGER AS $$ \\
\\BEGIN \\
\\ NEW.updated_at = CURRENT_TIMESTAMP; \\
\\ RETURN NEW; \\
\\END; \\
\\$$ language 'plpgsql'"
[]
-- Create trigger
rawExecute
("CREATE TRIGGER update_" <> tableName <> "_updated_at \\
\\BEFORE UPDATE ON " <> tableName <> " \\
\\FOR EACH ROW EXECUTE FUNCTION update_updated_at_column()")
[]
-- Seed initial data
seedDatabase :: ReaderT SqlBackend IO ()
seedDatabase = do
-- Check if already seeded
userCount <- count ([] :: [Filter User])
when (userCount == 0) $ do
now <- liftIO getCurrentTime
-- Create admin user
adminId <- insert User
{ userEmail = "admin@example.com"
, userUsername = Just "admin"
, userPassword = "hashed_password_here" -- Use proper hashing!
, userFirstName = "Admin"
, userLastName = "User"
, userAvatar = Nothing
, userEmailVerified = True
, userActive = True
, userRole = Admin
, userCreatedAt = now
, userUpdatedAt = now
}
-- Create categories
techId <- insert Category
{ categoryName = "Technology"
, categorySlug = "technology"
, categoryDescription = Just "Technology related posts"
, categoryParentId = Nothing
, categorySortOrder = 1
}
lifeId <- insert Category
{ categoryName = "Lifestyle"
, categorySlug = "lifestyle"
, categoryDescription = Just "Lifestyle and personal posts"
, categoryParentId = Nothing
, categorySortOrder = 2
}
-- Create sample post
postId <- insert BlogPost
{ blogPostTitle = "Welcome to Our Blog"
, blogPostSlug = "welcome-to-our-blog"
, blogPostContent = "This is the first post on our new blog!"
, blogPostSummary = Just "Welcome post"
, blogPostAuthorId = adminId
, blogPostCategoryId = Just techId
, blogPostPublished = True
, blogPostPublishedAt = Just now
, blogPostViews = 0
, blogPostCreatedAt = now
, blogPostUpdatedAt = now
}
-- Create tags
welcomeTagId <- insert Tag
{ tagName = "Welcome"
, tagSlug = "welcome"
}
announcementTagId <- insert Tag
{ tagName = "Announcement"
, tagSlug = "announcement"
}
-- Associate tags with post
insert_ PostTag
{ postTagPostId = postId
, postTagTagId = welcomeTagId
}
insert_ PostTag
{ postTagPostId = postId
, postTagTagId = announcementTagId
}
-- Rollback a specific migration
rollbackMigration :: Text -> ReaderT SqlBackend IO ()
rollbackMigration migrationName = case migrationName of
"add_search_index" ->
rawExecute "DROP INDEX IF EXISTS idx_blog_post_search" []
"add_triggers" -> do
rawExecute "DROP TRIGGER IF EXISTS update_user_updated_at ON user" []
rawExecute "DROP TRIGGER IF EXISTS update_blog_post_updated_at ON blog_post" []
rawExecute "DROP TRIGGER IF EXISTS update_comment_updated_at ON comment" []
_ -> error $ "Unknown migration: " <> T.unpack migrationName`,
'src/Database/Config.hs': `{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Database.Config where
import Database.Persist.Postgresql
import Database.Persist.MySQL
import Database.Persist.Sqlite
import Control.Monad.Logger
import Control.Monad.Reader
import Data.Pool
import Data.Text (Text)
import qualified Data.Text as T
import System.Environment
-- Database configuration
data DbConfig = DbConfig
{ dbBackend :: DbBackend
, dbConnStr :: Text
, dbPoolSize :: Int
, dbIdleTimeout :: Int -- seconds
} deriving (Show)
data DbBackend = PostgreSQL | MySQL | SQLite
deriving (Show, Read, Eq)
-- Load config from environment
loadDbConfig :: IO DbConfig
loadDbConfig = do
backend <- read <$> getEnvDefault "DB_BACKEND" "PostgreSQL"
connStr <- T.pack <$> getEnvDefault "DATABASE_URL" defaultConnStr
poolSize <- read <$> getEnvDefault "DB_POOL_SIZE" "10"
idleTimeout <- read <$> getEnvDefault "DB_IDLE_TIMEOUT" "600"
return DbConfig{..}
where
defaultConnStr = case backend of
PostgreSQL -> "host=localhost dbname=myapp user=postgres password=postgres"
MySQL -> "mysql://root:password@localhost:3306/myapp"
SQLite -> "myapp.db"
backend = read $ unsafePerformIO $ getEnvDefault "DB_BACKEND" "PostgreSQL"
getEnvDefault :: String -> String -> IO String
getEnvDefault key def = lookupEnv key >>= \\case
Nothing -> return def
Just val -> return val
-- Create connection pool based on backend
createDbPool :: DbConfig -> IO ConnectionPool
createDbPool DbConfig{..} = case dbBackend of
PostgreSQL -> runStderrLoggingT $
createPostgresqlPool (T.encodeUtf8 dbConnStr) dbPoolSize
MySQL -> runStderrLoggingT $
createMySQLPool defaultConnectInfo
{ connectHost = "localhost"
, connectDatabase = "myapp"
, connectUser = "root"
, connectPassword = "password"
} dbPoolSize
SQLite -> runStderrLoggingT $
createSqlitePool dbConnStr dbPoolSize
-- Run database action with pool
runDb :: ConnectionPool -> ReaderT SqlBackend IO a -> IO a
runDb pool action = runSqlPool action pool
-- Run database action with logging
runDbWithLog :: ConnectionPool -> ReaderT SqlBackend (LoggingT IO) a -> IO a
runDbWithLog pool action = runStderrLoggingT $ runSqlPool action pool
-- Database configuration for different environments
data Environment = Development | Testing | Production
deriving (Show, Read, Eq)
getDbConfigForEnv :: Environment -> DbConfig
getDbConfigForEnv env = case env of
Development -> DbConfig
{ dbBackend = PostgreSQL
, dbConnStr = "host=localhost dbname=myapp_dev user=postgres"
, dbPoolSize = 5
, dbIdleTimeout = 600
}
Testing -> DbConfig
{ dbBackend = SQLite
, dbConnStr = ":memory:"
, dbPoolSize = 1
, dbIdleTimeout = 60
}
Production -> DbConfig
{ dbBackend = PostgreSQL
, dbConnStr = "host=db.example.com dbname=myapp user=myapp password=secret"
, dbPoolSize = 20
, dbIdleTimeout = 1800
}
-- Connection pool with health checks
createHealthCheckPool :: DbConfig -> IO ConnectionPool
createHealthCheckPool config = do
pool <- createDbPool config
-- Add health check
_ <- runDb pool $ rawSql "SELECT 1" []
return pool
-- Read-only replica support
data ReplicatedDbConfig = ReplicatedDbConfig
{ masterConfig :: DbConfig
, replicaConfigs :: [DbConfig]
}
createReplicatedPools :: ReplicatedDbConfig -> IO (ConnectionPool, [ConnectionPool])
createReplicatedPools ReplicatedDbConfig{..} = do
masterPool <- createDbPool masterConfig
replicaPools <- mapM createDbPool replicaConfigs
return (masterPool, replicaPools)`,
'app/Main.hs': `{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Database.Persist
import Database.Persist.Sql
import Database.Schema
import Database.Models
import Database.Queries
import Database.Migration
import Database.Config
import Control.Monad.Logger
import Control.Monad.Reader
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time
main :: IO ()
main = do
-- Load configuration
config <- loadDbConfig
-- Create connection pool
pool <- createDbPool config
-- Run migrations
runDb pool $ do
runMigration migrateAll
runCustomMigrations
-- Seed database if empty
runDb pool seedDatabase
-- Example operations
runStderrLoggingT $ runDb pool $ do
-- Create a user
now <- liftIO getCurrentTime
Entity userId user <- createUser
"john@example.com"
"hashed_password"
"John"
"Doe"
Author
liftIO $ putStrLn $ "Created user: " ++ show userId
-- Create a post
Entity postId post <- createPost
"My First Post"
"my-first-post"
"This is the content of my first post."
userId
-- Add tags
addTagsToPost postId ["haskell", "persistent", "database"]
-- Publish the post
publishPost postId
-- Query posts with details
postsWithDetails <- getPostsWithDetails
liftIO $ putStrLn $ "Posts with details: " ++ show (length postsWithDetails)
-- Search posts
searchResults <- searchPosts "haskell"
liftIO $ putStrLn $ "Search results: " ++ show (length searchResults)
-- Get user statistics
(posts, comments, views) <- getUserStats userId
liftIO $ putStrLn $ "User stats - Posts: " ++ show posts ++
", Comments: " ++ show comments ++
", Views: " ++ show views
-- Example with raw SQL
runDb pool $ do
users <- rawSql
"SELECT ?? FROM user WHERE created_at > ?"
[toPersistValue $ addDays (-7) now]
liftIO $ putStrLn $ "Recent users: " ++ show (length (users :: [Entity User]))
-- Transaction example
result <- runDb pool $ do
-- This entire block runs in a transaction
Entity catId _ <- createCategory "Programming" "programming" Nothing Nothing
Entity postId2 _ <- createPost
"Haskell Tutorial"
"haskell-tutorial"
"Learn Haskell programming"
userId
-- Update post with category
update postId2 [BlogPostCategoryId =. Just catId]
-- If any operation fails, entire transaction is rolled back
return (catId, postId2)
liftIO $ putStrLn $ "Transaction result: " ++ show result
putStrLn "Done!"`,
'README.md': `# Haskell Persistent ORM
Type-safe database access for Haskell applications using the Persistent library.
## Features
- **Type Safety**: Database schema defined in Haskell with compile-time guarantees
- **Multiple Backends**: PostgreSQL, MySQL, SQLite, MongoDB support
- **Automatic Migrations**: Schema changes handled automatically
- **Query DSL**: Type-safe query construction
- **Raw SQL**: Escape hatch for complex queries
- **Relationships**: One-to-many, many-to-many support
- **Transactions**: ACID compliance
- **Connection Pooling**: Efficient resource management
## Quick Start
### Define Schema
\`\`\`haskell
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
email Text
username Text Maybe
UniqueEmail email
deriving Show
|]
\`\`\`
### Basic Operations
\`\`\`haskell
-- Insert
userId <- insert $ User "john@example.com" (Just "john")
-- Select
user <- get userId
users <- selectList [UserEmail ==. "john@example.com"] []
-- Update
update userId [UserUsername =. Just "johndoe"]
-- Delete
delete userId
\`\`\`
### Query DSL with Esqueleto
\`\`\`haskell
-- Complex queries
posts <- select $ do
(post :& author) <-
from $ table
\`innerJoin\` table
\`on\` (\\(post :& author) ->
post ^. BlogPostAuthorId ==. author ^. UserId)
where_ (post ^. BlogPostPublished ==. val True)
orderBy [desc (post ^. BlogPostCreatedAt)]
limit 10
return (post, author)
\`\`\`
## Backend Configuration
### PostgreSQL
\`\`\`haskell
connStr = "host=localhost dbname=myapp user=postgres password=postgres"
pool <- runStderrLoggingT $ createPostgresqlPool connStr 10
\`\`\`
### MySQL
\`\`\`haskell
connInfo = defaultConnectInfo
{ connectHost = "localhost"
, connectDatabase = "myapp"
}
pool <- runStderrLoggingT $ createMySQLPool connInfo 10
\`\`\`
### SQLite
\`\`\`haskell
pool <- runStderrLoggingT $ createSqlitePool "myapp.db" 10
\`\`\`
## Migrations
### Auto-migration
\`\`\`haskell
runSqlPool (runMigration migrateAll) pool
\`\`\`
### Custom migrations
\`\`\`haskell
runSqlPool customMigration pool
where
customMigration = rawExecute
"CREATE INDEX idx_user_email ON user(email)" []
\`\`\`
## Advanced Features
### Transactions
\`\`\`haskell
runSqlPool (do
userId <- insert user
postId <- insert $ BlogPost "Title" userId
-- Automatic rollback on exception
) pool
\`\`\`
### JSON Fields
\`\`\`haskell
Settings
userId UserId
preferences Value -- Stores JSON
deriving Show
\`\`\`
### Custom Types
\`\`\`haskell
data UserRole = Admin | User
deriving (Show, Read, Eq)
instance PersistField UserRole where
toPersistValue = PersistText . T.pack . show
fromPersistValue (PersistText t) =
case readMaybe (T.unpack t) of
Just r -> Right r
Nothing -> Left "Invalid role"
instance PersistFieldSql UserRole where
sqlType _ = SqlString
\`\`\`
### Streaming Queries
\`\`\`haskell
-- Process large result sets efficiently
runSqlPool (selectSource [] [] $$ mapM_C process) pool
\`\`\`
## Performance Tips
1. **Use Indexes**: Add indexes for frequently queried columns
2. **Connection Pooling**: Configure pool size based on load
3. **Batch Operations**: Use \`insertMany\` for bulk inserts
4. **Raw SQL**: For complex queries that Esqueleto can't express
5. **Lazy Loading**: Be aware of N+1 query problems
## Testing
### In-Memory Database
\`\`\`haskell
-- Use SQLite :memory: for tests
testPool <- runNoLoggingT $ createSqlitePool ":memory:" 1
\`\`\`
### Fixtures
\`\`\`haskell
setupFixtures :: ReaderT SqlBackend IO ()
setupFixtures = do
userId <- insert $ User "test@example.com" Nothing
insert_ $ BlogPost "Test Post" userId
\`\`\`
## Common Patterns
### Repository Pattern
\`\`\`haskell
class Monad m => UserRepository m where
createUser :: Text -> m UserId
findUser :: UserId -> m (Maybe User)
findUserByEmail :: Text -> m (Maybe (Entity User))
\`\`\`
### Pagination
\`\`\`haskell
paginate :: Int -> Int -> [SelectOpt record] -> [SelectOpt record]
paginate page size opts = LimitTo size : OffsetBy ((page-1)*size) : opts
\`\`\`
### Soft Deletes
\`\`\`haskell
User
...
deletedAt UTCTime Maybe
deriving Show
-- Query only active records
selectList [UserDeletedAt ==. Nothing] []
\`\`\`
## Troubleshooting
- **Migration Conflicts**: Use \`--force\` flag carefully
- **Connection Leaks**: Always use connection pools
- **Type Errors**: Check schema definition matches database
- **Performance**: Enable query logging in development
## Resources
- [Persistent Documentation](https://www.yesodweb.com/book/persistent)
- [Esqueleto Documentation](https://hackage.haskell.org/package/esqueleto)
- [Yesod Book Database Chapter](https://www.yesodweb.com/book/persistent)`
}
};