UNPKG

@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,619 lines (1,374 loc) 54.2 kB
"use strict"; /** * Spock Framework Template Generator * A lightweight Haskell web framework for rapid development */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.SpockGenerator = void 0; const haskell_base_generator_1 = require("./haskell-base-generator"); const fs_1 = require("fs"); const path = __importStar(require("path")); class SpockGenerator extends haskell_base_generator_1.HaskellBackendGenerator { constructor() { super('Spock'); } getFrameworkDependencies() { return [ 'Spock: ^0.14', 'Spock-core: ^0.14', 'reroute: ^0.6', 'hvect: ^0.4', 'wai: ^3.2', 'warp: ^3.3', 'http-types: ^0.12', 'aeson: ^2.1', 'text: ^2.0', 'bytestring: ^0.11', 'mtl: ^2.3', 'transformers: ^0.6', 'stm: ^2.5', 'containers: ^0.6', 'unordered-containers: ^0.2', 'hashable: ^1.4', 'time: ^1.12', 'uuid: ^1.3', 'random: ^1.2', 'hasql: ^1.6', 'hasql-pool: ^0.9', 'hasql-migration: ^0.3', 'hasql-transaction: ^1.0', 'contravariant: ^1.5', 'profunctors: ^5.6', 'vector: ^0.13', 'jose: ^0.10', 'cryptonite: ^0.30', 'memory: ^0.18', 'base64-bytestring: ^1.2', 'wai-cors: ^0.2', 'wai-extra: ^3.1', 'case-insensitive: ^1.2', 'cookie: ^0.4', 'vault: ^0.3', 'lifted-base: ^0.2', 'monad-control: ^1.0', 'resourcet: ^1.3', 'unliftio: ^0.2', 'async: ^2.2', 'retry: ^0.9', 'network: ^3.1', 'http-client: ^0.7', 'http-client-tls: ^0.3' ]; } getExtraDeps() { return []; } async generateFrameworkFiles(projectPath, options) { // Generate main application await this.generateMainApp(projectPath, options); // Generate app structure await this.generateAppStructure(projectPath); // Generate routes await this.generateRoutes(projectPath); // Generate handlers await this.generateHandlers(projectPath); // Generate middleware await this.generateMiddleware(projectPath); // Generate models await this.generateModels(projectPath); // Generate database await this.generateDatabase(projectPath); // Generate services await this.generateServices(projectPath); // Generate config await this.generateConfig(projectPath, options); // Generate utilities await this.generateUtilities(projectPath); // Generate types await this.generateTypes(projectPath); } async generateMainApp(projectPath, options) { const mainContent = `{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE DataKinds #-} module Main where import Control.Monad (void) import System.Environment (lookupEnv) import Network.Wai.Handler.Warp (run) import Web.Spock import Web.Spock.Config import App import Config import Database import Routes import Middleware main :: IO () main = do -- Load configuration config <- loadConfig -- Initialize database dbPool <- initDatabase config -- Run migrations runMigrations dbPool -- Create session configuration sessionCfg <- defaultSessionCfg "${options.name}_session" (configSessionTimeout config) -- Create Spock configuration spockCfg <- defaultSpockCfg sessionCfg (PCPool dbPool) (AppState config) -- Get port from environment or config port <- maybe (configPort config) read <$> lookupEnv "PORT" putStrLn $ "Starting Spock server on port " ++ show port -- Run application runSpock port $ spock spockCfg app app :: SpockM Connection Session AppState () app = do -- Apply middleware middleware corsMiddleware middleware loggingMiddleware middleware errorHandlerMiddleware -- Define routes routes `; await fs_1.promises.writeFile(path.join(projectPath, 'app', 'Main.hs'), mainContent); } async generateAppStructure(projectPath) { const appContent = `{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeOperators #-} module App where import Control.Monad.Reader import Data.Text (Text) import Data.Time.Clock import Data.UUID import Hasql.Pool (Pool) import Hasql.Connection (Connection) import Web.Spock import qualified Data.Vault.Lazy as Vault import Config -- Application State data AppState = AppState { appConfig :: Config } -- Session type data Session = Session { sessionUserId :: Maybe UUID , sessionCreated :: UTCTime , sessionData :: [(Text, Text)] } deriving (Show, Eq) -- Create empty session emptySession :: IO Session emptySession = do now <- getCurrentTime return Session { sessionUserId = Nothing , sessionCreated = now , sessionData = [] } -- Application monad type alias type Api = SpockM Connection Session AppState type ApiAction a = SpockAction Connection Session AppState a -- Context keys for request-scoped data userIdKey :: Vault.Key UUID userIdKey = unsafePerformIO Vault.newKey {-# NOINLINE userIdKey #-} requestIdKey :: Vault.Key Text requestIdKey = unsafePerformIO Vault.newKey {-# NOINLINE requestIdKey #-} -- Get current user ID from context getCurrentUserId :: ApiAction (Maybe UUID) getCurrentUserId = do vault <- getContext return $ Vault.lookup userIdKey vault -- Require authenticated user requireAuth :: ApiAction a -> ApiAction a requireAuth action = do maybeUserId <- getCurrentUserId case maybeUserId of Nothing -> do setStatus status401 json $ object [ "error" .= ("Unauthorized" :: Text) , "message" .= ("Authentication required" :: Text) ] Just _ -> action -- Get app config getConfig :: ApiAction Config getConfig = appConfig <$> getState -- Error type data ApiError = ApiError { errorCode :: Int , errorMessage :: Text , errorDetails :: Maybe Value } deriving (Show) instance ToJSON ApiError where toJSON err = object $ [ "error" .= object [ "code" .= errorCode err , "message" .= errorMessage err ] ] ++ maybe [] (\\d -> ["details" .= d]) (errorDetails err) -- Error helpers throwError :: Int -> Text -> ApiAction a throwError code msg = do setStatus $ mkStatus code (encodeUtf8 msg) json $ ApiError code msg Nothing badRequest :: Text -> ApiAction a badRequest = throwError 400 unauthorized :: Text -> ApiAction a unauthorized = throwError 401 forbidden :: Text -> ApiAction a forbidden = throwError 403 notFound :: Text -> ApiAction a notFound = throwError 404 internalError :: Text -> ApiAction a internalError = throwError 500 `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'App.hs'), appContent); } async generateRoutes(projectPath) { const routesContent = `{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeOperators #-} module Routes where import Web.Spock import Web.Spock.Action import App import Handlers.Auth import Handlers.User import Handlers.Health import Handlers.Admin routes :: Api () routes = do -- Health check routes get root $ redirect "/health" get "health" healthCheckHandler get ("api" <//> "v1" <//> "health") healthCheckHandler -- Public API routes subcomponent "api/v1" $ do -- Authentication routes post "auth/register" registerHandler post "auth/login" loginHandler post "auth/refresh" refreshTokenHandler post "auth/logout" $ requireAuth logoutHandler get "auth/me" $ requireAuth getMeHandler -- User routes (protected) get "users" $ requireAuth listUsersHandler get ("users" <//> var) $ requireAuth getUserHandler put ("users" <//> var) $ requireAuth updateUserHandler delete ("users" <//> var) $ requireAuth deleteUserHandler -- Admin routes subcomponent "admin" $ requireAuth $ do post "users" createUserHandler get "stats" getStatsHandler -- Static file serving (optional) -- wildcard $ \\path -> do -- file <- liftIO $ serveStatic ("static/" ++ T.unpack path) -- case file of -- Nothing -> notFound "File not found" -- Just content -> bytes content -- Catch-all 404 handler hookAny GET $ \\_ -> notFound "Endpoint not found" hookAny POST $ \\_ -> notFound "Endpoint not found" hookAny PUT $ \\_ -> notFound "Endpoint not found" hookAny DELETE $ \\_ -> notFound "Endpoint not found" `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Routes.hs'), routesContent); } async generateHandlers(projectPath) { await fs_1.promises.mkdir(path.join(projectPath, 'src', 'Handlers'), { recursive: true }); // Health handler const healthContent = `{-# LANGUAGE OverloadedStrings #-} module Handlers.Health where import Control.Exception (try, SomeException) import Data.Time.Clock import Web.Spock import App import Database healthCheckHandler :: ApiAction () healthCheckHandler = do currentTime <- liftIO getCurrentTime -- Check database connection pool <- getContext >>= \\ctx -> return $ pcPool ctx dbStatus <- liftIO $ checkDatabaseHealth pool -- Get version from config config <- getConfig json $ object [ "status" .= ("healthy" :: Text) , "timestamp" .= currentTime , "version" .= configVersion config , "environment" .= show (configEnv config) , "services" .= object [ "database" .= object [ "status" .= if dbStatus then "up" else "down" :: Text , "type" .= ("postgresql" :: Text) ] ] ] checkDatabaseHealth :: Pool Connection -> IO Bool checkDatabaseHealth pool = do result <- try $ use pool $ statement () checkHealthQuery case result of Left (_ :: SomeException) -> return False Right (Right True) -> return True _ -> return False `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Handlers', 'Health.hs'), healthContent); // Auth handler const authContent = `{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} module Handlers.Auth where import Control.Monad (when, unless) import Crypto.KDF.BCrypt (validatePassword, hashPassword) import Data.Time.Clock import Data.UUID.V4 (nextRandom) import Web.Spock import qualified Data.Text.Encoding as T import App import Types.Auth import Types.User import Database.User import Services.JWT import Utils.Validation registerHandler :: ApiAction () registerHandler = do RegisterRequest{..} <- jsonBody' -- Validate input unless (isValidEmail registerEmail) $ badRequest "Invalid email format" unless (isValidPassword registerPassword) $ badRequest "Password must be at least 8 characters with mixed case and numbers" -- Check if user exists pool <- getContext >>= \\ctx -> return $ pcPool ctx existingUser <- liftIO $ getUserByEmail pool registerEmail case existingUser of Right (Just _) -> badRequest "Email already registered" Left err -> internalError $ "Database error: " <> pack (show err) Right Nothing -> do -- Hash password hashedPw <- liftIO $ hashPassword 12 (T.encodeUtf8 registerPassword) -- Create user userId <- liftIO nextRandom now <- liftIO getCurrentTime let newUser = User { userId = userId , userEmail = registerEmail , userPasswordHash = hashedPw , userName = registerName , userRole = "user" , userEmailVerified = False , userCreatedAt = now , userUpdatedAt = now } -- Save user result <- liftIO $ createUser pool newUser case result of Left err -> internalError $ "Failed to create user: " <> pack (show err) Right _ -> do -- Generate tokens config <- getConfig accessToken <- liftIO $ generateAccessToken config userId refreshToken <- liftIO $ generateRefreshToken config userId -- Update session modifySession $ \\s -> s { sessionUserId = Just userId } -- Return response json $ object [ "user" .= toPublicUser newUser , "tokens" .= object [ "access" .= accessToken , "refresh" .= refreshToken ] ] loginHandler :: ApiAction () loginHandler = do LoginRequest{..} <- jsonBody' -- Find user pool <- getContext >>= \\ctx -> return $ pcPool ctx userResult <- liftIO $ getUserByEmail pool loginEmail case userResult of Left err -> internalError $ "Database error: " <> pack (show err) Right Nothing -> unauthorized "Invalid credentials" Right (Just user) -> do -- Verify password let valid = validatePassword (T.encodeUtf8 loginPassword) (userPasswordHash user) unless valid $ unauthorized "Invalid credentials" -- Generate tokens config <- getConfig accessToken <- liftIO $ generateAccessToken config (userId user) refreshToken <- liftIO $ generateRefreshToken config (userId user) -- Update session modifySession $ \\s -> s { sessionUserId = Just (userId user) } -- Return response json $ object [ "user" .= toPublicUser user , "tokens" .= object [ "access" .= accessToken , "refresh" .= refreshToken ] ] refreshTokenHandler :: ApiAction () refreshTokenHandler = do RefreshRequest{..} <- jsonBody' config <- getConfig case verifyRefreshToken config refreshToken of Left err -> unauthorized $ "Invalid refresh token: " <> err Right claims -> do -- Generate new access token newAccessToken <- liftIO $ generateAccessToken config (jwtUserId claims) json $ object [ "tokens" .= object [ "access" .= newAccessToken , "refresh" .= refreshToken -- Keep same refresh token ] ] logoutHandler :: ApiAction () logoutHandler = do -- Clear session modifySession $ \\s -> s { sessionUserId = Nothing } -- In production, you might want to blacklist the token json $ object ["message" .= ("Logged out successfully" :: Text)] getMeHandler :: ApiAction () getMeHandler = do maybeUserId <- getSession >>= \\s -> return (sessionUserId s) case maybeUserId of Nothing -> unauthorized "Not logged in" Just uid -> do pool <- getContext >>= \\ctx -> return $ pcPool ctx userResult <- liftIO $ getUserById pool uid case userResult of Left err -> internalError $ "Database error: " <> pack (show err) Right Nothing -> notFound "User not found" Right (Just user) -> json $ object ["user" .= toPublicUser user] `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Handlers', 'Auth.hs'), authContent); // User handler const userContent = `{-# LANGUAGE OverloadedStrings #-} module Handlers.User where import Control.Monad (when, unless) import Data.Maybe (fromMaybe) import Data.UUID (UUID, fromText) import Web.Spock import Web.Spock.Action import App import Types.User import Database.User import Utils.Pagination listUsersHandler :: ApiAction () listUsersHandler = do -- Get pagination params page <- fromMaybe 1 <$> param "page" limit <- fromMaybe 10 <$> param "limit" search <- param "search" let offset = (page - 1) * limit pool <- getContext >>= \\ctx -> return $ pcPool ctx -- Get users and count usersResult <- liftIO $ getUsers pool limit offset search countResult <- liftIO $ getUserCount pool search case (usersResult, countResult) of (Right users, Right total) -> do let publicUsers = map toPublicUser users paginatedResponse publicUsers page limit total _ -> internalError "Failed to fetch users" getUserHandler :: UUID -> ApiAction () getUserHandler uid = do pool <- getContext >>= \\ctx -> return $ pcPool ctx userResult <- liftIO $ getUserById pool uid case userResult of Left err -> internalError $ "Database error: " <> pack (show err) Right Nothing -> notFound "User not found" Right (Just user) -> json $ object ["user" .= toPublicUser user] updateUserHandler :: UUID -> ApiAction () updateUserHandler uid = do -- Check permission currentUser <- requireCurrentUser unless (userId currentUser == uid || userRole currentUser == "admin") $ forbidden "You can only update your own profile" UpdateUserRequest{..} <- jsonBody' -- Validate updates case updateEmail of Just email -> unless (isValidEmail email) $ badRequest "Invalid email format" Nothing -> return () pool <- getContext >>= \\ctx -> return $ pcPool ctx -- Check if email is taken case updateEmail of Just newEmail -> do existingResult <- liftIO $ getUserByEmail pool newEmail case existingResult of Right (Just existing) -> when (userId existing /= uid) $ badRequest "Email already taken" _ -> return () Nothing -> return () -- Update user now <- liftIO getCurrentTime updateResult <- liftIO $ updateUser pool uid UpdateUserData { updateUserName = updateName , updateUserEmail = updateEmail , updateUserUpdatedAt = now } case updateResult of Left err -> internalError $ "Update failed: " <> pack (show err) Right Nothing -> notFound "User not found" Right (Just user) -> json $ object ["user" .= toPublicUser user] deleteUserHandler :: UUID -> ApiAction () deleteUserHandler uid = do -- Only admins can delete users currentUser <- requireCurrentUser unless (userRole currentUser == "admin") $ forbidden "Admin access required" -- Prevent self-deletion when (userId currentUser == uid) $ badRequest "Cannot delete your own account" pool <- getContext >>= \\ctx -> return $ pcPool ctx deleteResult <- liftIO $ deleteUser pool uid case deleteResult of Left err -> internalError $ "Delete failed: " <> pack (show err) Right False -> notFound "User not found" Right True -> json $ object ["message" .= ("User deleted successfully" :: Text)] -- Helper to get current user requireCurrentUser :: ApiAction User requireCurrentUser = do maybeUserId <- getSession >>= \\s -> return (sessionUserId s) case maybeUserId of Nothing -> unauthorized "Authentication required" Just uid -> do pool <- getContext >>= \\ctx -> return $ pcPool ctx userResult <- liftIO $ getUserById pool uid case userResult of Right (Just user) -> return user _ -> unauthorized "User not found" `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Handlers', 'User.hs'), userContent); // Admin handler const adminContent = `{-# LANGUAGE OverloadedStrings #-} module Handlers.Admin where import Web.Spock import App import Types.User import Database.Stats createUserHandler :: ApiAction () createUserHandler = do -- Verify admin currentUser <- requireCurrentUser unless (userRole currentUser == "admin") $ forbidden "Admin access required" -- Implementation similar to register but with admin controls json $ object ["message" .= ("Admin user creation not implemented" :: Text)] getStatsHandler :: ApiAction () getStatsHandler = do -- Verify admin currentUser <- requireCurrentUser unless (userRole currentUser == "admin") $ forbidden "Admin access required" pool <- getContext >>= \\ctx -> return $ pcPool ctx -- Get various stats userCountResult <- liftIO $ getTotalUserCount pool activeUsersResult <- liftIO $ getActiveUserCount pool case (userCountResult, activeUsersResult) of (Right userCount, Right activeUsers) -> json $ object [ "stats" .= object [ "totalUsers" .= userCount , "activeUsers" .= activeUsers , "newUsersToday" .= (0 :: Int) -- TODO: Implement , "totalSessions" .= (0 :: Int) -- TODO: Implement ] ] _ -> internalError "Failed to fetch statistics" `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Handlers', 'Admin.hs'), adminContent); } async generateMiddleware(projectPath) { const middlewareContent = `{-# LANGUAGE OverloadedStrings #-} module Middleware where import Control.Monad.IO.Class (liftIO) import Data.Text.Lazy (toStrict) import Data.UUID.V4 (nextRandom) import Network.HTTP.Types import Network.Wai import Network.Wai.Middleware.Cors import Network.Wai.Middleware.RequestLogger import Web.Spock import qualified Data.ByteString.Char8 as BS import qualified Data.CaseInsensitive as CI import qualified Data.Text as T import qualified Data.Vault.Lazy as Vault import App import Services.JWT -- CORS middleware corsMiddleware :: Middleware corsMiddleware = cors $ const $ Just CorsResourcePolicy { corsOrigins = Nothing -- Allow all origins , corsMethods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"] , corsRequestHeaders = ["Authorization", "Content-Type"] , corsExposedHeaders = Nothing , corsMaxAge = Just 86400 , corsVaryOrigin = False , corsRequireOrigin = False , corsIgnoreFailures = False } -- Logging middleware loggingMiddleware :: Middleware loggingMiddleware = logStdoutDev -- Error handler middleware errorHandlerMiddleware :: Middleware errorHandlerMiddleware app req respond = do app req respond \`catch\` handleException where handleException :: SomeException -> IO ResponseReceived handleException e = respond $ responseLBS status500 [("Content-Type", "application/json")] $ encode $ object [ "error" .= object [ "code" .= (500 :: Int) , "message" .= ("Internal server error" :: Text) , "details" .= show e ] ] -- JWT authentication middleware (for specific routes) jwtMiddleware :: Text -> Middleware jwtMiddleware secret app req respond = do let headers = requestHeaders req authHeader = lookup "Authorization" headers case authHeader >>= extractBearer of Nothing -> app req respond Just token -> case verifyAccessToken secret token of Left _ -> respond $ responseLBS status401 [("Content-Type", "application/json")] $ encode $ object [ "error" .= object [ "code" .= (401 :: Int) , "message" .= ("Invalid or expired token" :: Text) ] ] Right claims -> do -- Add user ID to vault let vault' = Vault.insert userIdKey (jwtUserId claims) (vault req) req' = req { vault = vault' } app req' respond where extractBearer :: ByteString -> Maybe Text extractBearer auth = case BS.words auth of ["Bearer", token] -> Just $ T.pack $ BS.unpack token _ -> Nothing -- Request ID middleware requestIdMiddleware :: Middleware requestIdMiddleware app req respond = do requestId <- liftIO $ toText <$> nextRandom let vault' = Vault.insert requestIdKey requestId (vault req) req' = req { vault = vault' } app req' respond -- Rate limiting middleware (simple in-memory implementation) -- In production, use Redis or similar rateLimitMiddleware :: Int -> Middleware rateLimitMiddleware _limit app req respond = do -- TODO: Implement proper rate limiting app req respond `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Middleware.hs'), middlewareContent); } async generateModels(projectPath) { await fs_1.promises.mkdir(path.join(projectPath, 'src', 'Types'), { recursive: true }); // User types const userTypesContent = `{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} module Types.User where import Data.Aeson import Data.ByteString (ByteString) import Data.Text (Text) import Data.Time.Clock import Data.UUID import GHC.Generics -- User model data User = User { userId :: UUID , userEmail :: Text , userPasswordHash :: ByteString , userName :: Text , userRole :: Text , userEmailVerified :: Bool , userCreatedAt :: UTCTime , userUpdatedAt :: UTCTime } deriving (Show, Eq, Generic) -- Public user (without sensitive data) data PublicUser = PublicUser { publicUserId :: UUID , publicUserEmail :: Text , publicUserName :: Text , publicUserRole :: Text , publicUserEmailVerified :: Bool , publicUserCreatedAt :: UTCTime } deriving (Show, Eq, Generic) instance ToJSON PublicUser where toJSON u = object [ "id" .= publicUserId u , "email" .= publicUserEmail u , "name" .= publicUserName u , "role" .= publicUserRole u , "emailVerified" .= publicUserEmailVerified u , "createdAt" .= publicUserCreatedAt u ] -- Convert User to PublicUser toPublicUser :: User -> PublicUser toPublicUser u = PublicUser { publicUserId = userId u , publicUserEmail = userEmail u , publicUserName = userName u , publicUserRole = userRole u , publicUserEmailVerified = userEmailVerified u , publicUserCreatedAt = userCreatedAt u } -- Update user request data UpdateUserRequest = UpdateUserRequest { updateName :: Maybe Text , updateEmail :: Maybe Text } deriving (Show, Generic) instance FromJSON UpdateUserRequest where parseJSON = withObject "UpdateUserRequest" $ \\v -> UpdateUserRequest <$> v .:? "name" <*> v .:? "email" -- Update user data (for database) data UpdateUserData = UpdateUserData { updateUserName :: Maybe Text , updateUserEmail :: Maybe Text , updateUserUpdatedAt :: UTCTime } deriving (Show) `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Types', 'User.hs'), userTypesContent); // Auth types const authTypesContent = `{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} module Types.Auth where import Data.Aeson import Data.Text (Text) import Data.UUID import GHC.Generics -- Register request data RegisterRequest = RegisterRequest { registerEmail :: Text , registerPassword :: Text , registerName :: Text } deriving (Show, Generic) instance FromJSON RegisterRequest where parseJSON = withObject "RegisterRequest" $ \\v -> RegisterRequest <$> v .: "email" <*> v .: "password" <*> v .: "name" -- Login request data LoginRequest = LoginRequest { loginEmail :: Text , loginPassword :: Text } deriving (Show, Generic) instance FromJSON LoginRequest where parseJSON = withObject "LoginRequest" $ \\v -> LoginRequest <$> v .: "email" <*> v .: "password" -- Refresh token request data RefreshRequest = RefreshRequest { refreshToken :: Text } deriving (Show, Generic) instance FromJSON RefreshRequest where parseJSON = withObject "RefreshRequest" $ \\v -> RefreshRequest <$> v .: "refreshToken" -- JWT Claims data JWTClaims = JWTClaims { jwtUserId :: UUID , jwtEmail :: Text , jwtRole :: Text , jwtExp :: Integer } deriving (Show, Eq) `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Types', 'Auth.hs'), authTypesContent); } async generateDatabase(projectPath) { const databaseContent = `{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} module Database where import Control.Exception (bracket) import Data.Text (Text) import Hasql.Connection (Connection, Settings, settings) import Hasql.Pool (Pool, acquire, release, use) import Hasql.Session (Session, statement) import Hasql.Statement (Statement) import qualified Hasql.Decoders as D import qualified Hasql.Encoders as E import qualified Hasql.Migration as M import Config -- Initialize database connection pool initDatabase :: Config -> IO (Pool Connection) initDatabase config = do let dbSettings = settings (encodeUtf8 $ configDbHost config) (fromIntegral $ configDbPort config) (encodeUtf8 $ configDbUser config) (encodeUtf8 $ configDbPassword config) (encodeUtf8 $ configDbName config) acquire (configDbPoolSize config) 30 dbSettings -- Run migrations runMigrations :: Pool Connection -> IO () runMigrations pool = do putStrLn "Running database migrations..." let migrations = M.MigrationInitialization : map M.MigrationScript [ createUsersTable , createSessionsTable , createIndexes ] result <- use pool $ M.runMigration M.defaultOptions migrations case result of Left err -> error $ "Migration failed: " ++ show err Right _ -> putStrLn "Migrations completed successfully" -- Migration scripts createUsersTable :: ByteString createUsersTable = [q| CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY, email VARCHAR(255) NOT NULL UNIQUE, password_hash BYTEA NOT NULL, name VARCHAR(255) NOT NULL, role VARCHAR(50) NOT NULL DEFAULT 'user', email_verified BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); |] createSessionsTable :: ByteString createSessionsTable = [q| CREATE TABLE IF NOT EXISTS sessions ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token TEXT NOT NULL UNIQUE, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); |] createIndexes :: ByteString createIndexes = [q| CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON sessions(user_id); CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token); CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at); |] -- Health check query checkHealthQuery :: Statement () Bool checkHealthQuery = Statement "SELECT true" E.noParams (D.singleRow D.bool) True `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Database.hs'), databaseContent); // User database module await fs_1.promises.mkdir(path.join(projectPath, 'src', 'Database'), { recursive: true }); const userDbContent = `{-# LANGUAGE OverloadedStrings #-} module Database.User where import Data.ByteString (ByteString) import Data.Text (Text) import Data.Time.Clock import Data.UUID import Hasql.Pool (Pool, use) import Hasql.Session (Session, statement) import Hasql.Statement (Statement) import qualified Hasql.Decoders as D import qualified Hasql.Encoders as E import Hasql.Connection (Connection) import Types.User -- Get user by ID getUserById :: Pool Connection -> UUID -> IO (Either String (Maybe User)) getUserById pool uid = do result <- use pool $ statement uid getUserByIdQuery return $ case result of Left err -> Left $ show err Right user -> Right user getUserByIdQuery :: Statement UUID (Maybe User) getUserByIdQuery = Statement "SELECT id, email, password_hash, name, role, email_verified, created_at, updated_at FROM users WHERE id = $1" (E.param (E.nonNullable E.uuid)) (D.rowMaybe userDecoder) True -- Get user by email getUserByEmail :: Pool Connection -> Text -> IO (Either String (Maybe User)) getUserByEmail pool email = do result <- use pool $ statement email getUserByEmailQuery return $ case result of Left err -> Left $ show err Right user -> Right user getUserByEmailQuery :: Statement Text (Maybe User) getUserByEmailQuery = Statement "SELECT id, email, password_hash, name, role, email_verified, created_at, updated_at FROM users WHERE email = $1" (E.param (E.nonNullable E.text)) (D.rowMaybe userDecoder) True -- Create user createUser :: Pool Connection -> User -> IO (Either String ()) createUser pool user = do result <- use pool $ statement user createUserQuery return $ case result of Left err -> Left $ show err Right _ -> Right () createUserQuery :: Statement User () createUserQuery = Statement "INSERT INTO users (id, email, password_hash, name, role, email_verified, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)" userEncoder D.noResult True -- Get users with pagination getUsers :: Pool Connection -> Int -> Int -> Maybe Text -> IO (Either String [User]) getUsers pool limit offset search = do result <- use pool $ statement (limit, offset, search) getUsersQuery return $ case result of Left err -> Left $ show err Right users -> Right users getUsersQuery :: Statement (Int, Int, Maybe Text) [User] getUsersQuery = Statement "SELECT id, email, password_hash, name, role, email_verified, created_at, updated_at FROM users \\ WHERE ($3 IS NULL OR name ILIKE '%' || $3 || '%' OR email ILIKE '%' || $3 || '%') \\ ORDER BY created_at DESC LIMIT $1 OFFSET $2" ((,,) <$> E.param (E.nonNullable E.int4) <*> E.param (E.nonNullable E.int4) <*> E.param (E.nullable E.text)) (D.rowList userDecoder) True -- Get user count getUserCount :: Pool Connection -> Maybe Text -> IO (Either String Int) getUserCount pool search = do result <- use pool $ statement search getUserCountQuery return $ case result of Left err -> Left $ show err Right count -> Right $ fromIntegral count getUserCountQuery :: Statement (Maybe Text) Int64 getUserCountQuery = Statement "SELECT COUNT(*) FROM users WHERE ($1 IS NULL OR name ILIKE '%' || $1 || '%' OR email ILIKE '%' || $1 || '%')" (E.param (E.nullable E.text)) (D.singleRow (D.column (D.nonNullable D.int8))) True -- Update user updateUser :: Pool Connection -> UUID -> UpdateUserData -> IO (Either String (Maybe User)) updateUser pool uid updates = do result <- use pool $ do statement (uid, updates) updateUserQuery statement uid getUserByIdQuery return $ case result of Left err -> Left $ show err Right user -> Right user updateUserQuery :: Statement (UUID, UpdateUserData) () updateUserQuery = Statement "UPDATE users SET \\ name = COALESCE($2, name), \\ email = COALESCE($3, email), \\ updated_at = $4 \\ WHERE id = $1" ((,) <$> E.param (E.nonNullable E.uuid) <*> updateEncoder) D.noResult True where updateEncoder = UpdateUserData <$> E.param (E.nullable E.text) <*> E.param (E.nullable E.text) <*> E.param (E.nonNullable E.timestamptz) -- Delete user deleteUser :: Pool Connection -> UUID -> IO (Either String Bool) deleteUser pool uid = do result <- use pool $ statement uid deleteUserQuery return $ case result of Left err -> Left $ show err Right count -> Right (count > 0) deleteUserQuery :: Statement UUID Int64 deleteUserQuery = Statement "DELETE FROM users WHERE id = $1" (E.param (E.nonNullable E.uuid)) D.rowsAffected True -- Decoders and encoders userDecoder :: D.Row User userDecoder = User <$> D.column (D.nonNullable D.uuid) <*> D.column (D.nonNullable D.text) <*> D.column (D.nonNullable D.bytea) <*> D.column (D.nonNullable D.text) <*> D.column (D.nonNullable D.text) <*> D.column (D.nonNullable D.bool) <*> D.column (D.nonNullable D.timestamptz) <*> D.column (D.nonNullable D.timestamptz) userEncoder :: E.Params User userEncoder = contramap userId (E.param (E.nonNullable E.uuid)) <> contramap userEmail (E.param (E.nonNullable E.text)) <> contramap userPasswordHash (E.param (E.nonNullable E.bytea)) <> contramap userName (E.param (E.nonNullable E.text)) <> contramap userRole (E.param (E.nonNullable E.text)) <> contramap userEmailVerified (E.param (E.nonNullable E.bool)) <> contramap userCreatedAt (E.param (E.nonNullable E.timestamptz)) <> contramap userUpdatedAt (E.param (E.nonNullable E.timestamptz)) `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Database', 'User.hs'), userDbContent); // Stats database module const statsDbContent = `{-# LANGUAGE OverloadedStrings #-} module Database.Stats where import Hasql.Pool (Pool, use) import Hasql.Session (statement) import Hasql.Statement (Statement) import qualified Hasql.Decoders as D import qualified Hasql.Encoders as E import Hasql.Connection (Connection) -- Get total user count getTotalUserCount :: Pool Connection -> IO (Either String Int) getTotalUserCount pool = do result <- use pool $ statement () getTotalUserCountQuery return $ case result of Left err -> Left $ show err Right count -> Right $ fromIntegral count getTotalUserCountQuery :: Statement () Int64 getTotalUserCountQuery = Statement "SELECT COUNT(*) FROM users" E.noParams (D.singleRow (D.column (D.nonNullable D.int8))) True -- Get active user count (logged in within last 30 days) getActiveUserCount :: Pool Connection -> IO (Either String Int) getActiveUserCount pool = do result <- use pool $ statement () getActiveUserCountQuery return $ case result of Left err -> Left $ show err Right count -> Right $ fromIntegral count getActiveUserCountQuery :: Statement () Int64 getActiveUserCountQuery = Statement "SELECT COUNT(DISTINCT user_id) FROM sessions WHERE expires_at > CURRENT_TIMESTAMP" E.noParams (D.singleRow (D.column (D.nonNullable D.int8))) True `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Database', 'Stats.hs'), statsDbContent); } async generateServices(projectPath) { await fs_1.promises.mkdir(path.join(projectPath, 'src', 'Services'), { recursive: true }); // JWT service const jwtServiceContent = `{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} module Services.JWT where import Control.Monad (when) import Crypto.JWT import Data.Aeson import Data.Text (Text) import Data.Text.Encoding (encodeUtf8, decodeUtf8) import Data.Time.Clock import Data.Time.Clock.POSIX import Data.UUID import qualified Data.Map.Strict as Map import Config import Types.Auth -- Generate access token generateAccessToken :: Config -> UUID -> IO Text generateAccessToken config userId = do now <- getCurrentTime let expTime = addUTCTime (15 * 60) now -- 15 minutes createToken config userId "access" expTime -- Generate refresh token generateRefreshToken :: Config -> UUID -> IO Text generateRefreshToken config userId = do now <- getCurrentTime let expTime = addUTCTime (7 * 24 * 60 * 60) now -- 7 days createToken config userId "refresh" expTime -- Create JWT token createToken :: Config -> UUID -> Text -> UTCTime -> IO Text createToken config userId tokenType expTime = do now <- getCurrentTime let claims = emptyClaimsSet & claimIss ?~ fromString (configJwtIssuer config) & claimSub ?~ fromString (toText userId) & claimAud ?~ Audience [fromString "spock-api"] & claimExp ?~ NumericDate (utcTimeToPOSIXSeconds expTime) & claimIat ?~ NumericDate (utcTimeToPOSIXSeconds now) & addClaim "type" (toJSON tokenType) & addClaim "userId" (toJSON $ toText userId) let key = fromOctets $ encodeUtf8 $ configJwtSecret config result <- runJOSE $ do alg <- bestJWSAlg key signClaims key (newJWSHeader ((), alg)) claims case result of Left err -> error $ "JWT generation failed: " ++ show err Right jwt -> return $ decodeUtf8 $ encodeCompact jwt -- Verify access token verifyAccessToken :: Text -> Text -> Either Text JWTClaims verifyAccessToken secret token = verifyToken secret token "access" -- Verify refresh token verifyRefreshToken :: Text -> Text -> Either Text JWTClaims verifyRefreshToken secret token = verifyToken secret token "refresh" -- Generic token verification verifyToken :: Text -> Text -> Text -> Either Text JWTClaims verifyToken secret token expectedType = do let key = fromOctets $ encodeUtf8 secret result <- runJOSE $ do jwt <- decodeCompact $ encodeUtf8 token verifyClaims (defaultJWTValidationSettings (== "spock-api")) key jwt case result of Left err -> Left $ "JWT verification failed: " <> pack (show err) Right claimsSet -> do -- Check token type case Map.lookup "type" (unregisteredClaims claimsSet) of Just (String t) | t == expectedType -> return () _ -> Left "Invalid token type" -- Extract user ID userId <- case Map.lookup "userId" (unregisteredClaims claimsSet) of Just (String uid) -> case fromText uid of Nothing -> Left "Invalid user ID in token" Just u -> Right u _ -> Left "User ID missing from token" -- Get expiration expTime <- case claimExp claimsSet of Nothing -> Left "Token missing expiration" Just (NumericDate exp) -> Right $ floor exp Right $ JWTClaims userId "" "" expTime -- Helper to add custom claims addClaim :: Text -> Value -> ClaimsSet -> ClaimsSet addClaim key value claims = claims { unregisteredClaims = Map.insert key value (unregisteredClaims claims) } `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Services', 'JWT.hs'), jwtServiceContent); } async generateConfig(projectPath, options) { const configContent = `{-# LANGUAGE OverloadedStrings #-} module Config where import Data.Text (Text) import System.Environment (getEnv, lookupEnv) data Environment = Development | Production deriving (Show, Eq) data Config = Config { configEnv :: Environment , configPort :: Int , configVersion :: Text , configSessionTimeout :: Int -- seconds , configDbHost :: Text , configDbPort :: Int , configDbUser :: Text , configDbPassword :: Text , configDbName :: Text , configDbPoolSize :: Int , configJwtSecret :: Text , configJwtIssuer :: Text , configCorsOrigin :: Text } deriving (Show) loadConfig :: IO Config loadConfig = do env <- maybe Development parseEnv <$> lookupEnv "ENV" port <- maybe 3000 read <$> lookupEnv "PORT" dbHost <- maybe "localhost" pack <$> lookupEnv "DB_HOST" dbPort <- maybe 5432 read <$> lookupEnv "DB_PORT" dbUser <- maybe "postgres" pack <$> lookupEnv "DB_USER" dbPassword <- maybe "postgres" pack <$> lookupEnv "DB_PASSWORD" dbName <- maybe "${options.name}" pack <$> lookupEnv "DB_NAME" dbPoolSize <- maybe 10 read <$> lookupEnv "DB_POOL_SIZE" jwtSecret <- maybe "your-256-bit-secret-change-in-production" pack <$> lookupEnv "JWT_SECRET" jwtIssuer <- maybe "${options.name}-api" pack <$> lookupEnv "JWT_ISSUER" corsOrigin <- maybe "*" pack <$> lookupEnv "CORS_ORIGIN" return Config { configEnv = env , configPort = port , configVersion = "1.0.0" , configSessionTimeout = 3600 -- 1 hour , configDbHost = dbHost , configDbPort = dbPort , configDbUser = dbUser , configDbPassword = dbPassword , configDbName = dbName , configDbPoolSize = dbPoolSize , configJwtSecret = jwtSecret , configJwtIssuer = jwtIssuer , configCorsOrigin = corsOrigin } where parseEnv "production" = Production parseEnv "prod" = Production parseEnv _ = Development `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Config.hs'), configContent); } async generateUtilities(projectPath) { await fs_1.promises.mkdir(path.join(projectPath, 'src', 'Utils'), { recursive: true }); // Validation utilities const validationContent = `{-# LANGUAGE OverloadedStrings #-} module Utils.Validation where import Data.Char (isAlphaNum, isDigit, isLower, isUpper) import Data.Text (Text) import qualified Data.Text as T import Text.Regex.TDFA ((=~)) -- Email validation isValidEmail :: Text -> Bool isValidEmail email = T.unpack email =~ ("^[a-zA-Z0-9+._-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$" :: String) -- Password validation (min 8 chars, mixed case, number) isValidPassword :: Text -> Bool isValidPassword password = T.length password >= 8 && any isUpper (T.unpack password) && any isLower (T.unpack password) && any isDigit (T.unpack password) -- Username validation isValidUsername :: Text -> Bool isValidUsername username = T.length username >= 3 && T.length username <= 30 && T.all (\\c -> isAlphaNum c || c == '_' || c == '-') username -- UUID validation isValidUUID :: Text -> Bool isValidUUID uuid = T.unpack uuid =~ ("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" :: String) -- Phone number validation (basic) isValidPhone :: Text -> Bool isValidPhone phone = T.length phone >= 10 && T.all (\\c -> isDigit c || c \`elem\` "+-() ") phone -- URL validation isValidURL :: Text -> Bool isValidURL url = T.unpack url =~ ("^https?://[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}" :: String) -- Sanitize text input sanitizeText :: Text -> Text sanitizeText = T.strip . T.filter (\\c -> c /= '<' && c /= '>' && c /= '&' && c /= '\"') -- Validate required field requireField :: Text -> Text -> Either Text Text requireField fieldName value | T.null (T.strip value) = Left $ fieldName <> " is required" | otherwise = Right value -- Validate field length validateLength :: Text -> Int -> Int -> Text -> Either Text Text validateLength fieldName minLen maxLen value | len < minLen = Left $ fieldName <> " must be at least " <> T.pack (show minLen) <> " characters" | len > maxLen = Left $ fieldName <> " must be at most " <> T.pack (show maxLen) <> " characters" | otherwise = Right value where len = T.length value `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Utils', 'Validation.hs'), validationContent); // Pagination utilities const paginationContent = `{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DeriveGeneric #-} module Utils.Pagination where import Data.Aeson import GHC.Generics import Web.Spock import App -- Pagination response data PaginatedResponse a = PaginatedResponse { items :: [a] , pagination :: PaginationInfo } deriving (Show, Generic) data PaginationInfo = PaginationInfo { currentPage :: Int , pageSize :: Int , totalItems :: Int , totalPages :: Int , hasNext :: Bool , hasPrev :: Bool } deriving (Show, Generic) instance ToJSON a => ToJSON (PaginatedResponse a) instance ToJSON PaginationInfo -- Create paginated response paginatedResponse :: ToJSON a => [a] -> Int -> Int -> Int -> ApiAction () paginatedResponse items page limit total = do let totalPages = ceiling (fromIntegral total / fromIntegral limit :: Double) hasNext = page < totalPages hasPrev = page > 1 paginationInfo = PaginationInfo { currentPage = page , pageSize = limit , totalItems = total , totalPages = totalPages , hasNext = hasNext , hasPrev = hasPrev } response = PaginatedResponse { items = items , pagination = paginationInfo } json response -- Parse pagination parameters data PaginationParams = PaginationParams { pageParam :: Int , limitParam :: Int , offsetParam :: Int } deriving (Show) parsePaginationParams :: ApiAction PaginationParams parsePaginationParams = do page <- max 1 . fromMaybe 1 <$> param "page" limit <- min 100 . max 1 . fromMaybe 10 <$> param "limit" let offset = (page - 1) * limit return PaginationParams { pageParam = page , limitParam = limit , offsetParam = offset } -- Validate pagination parameters validatePagination :: Int -> Int -> (Int, Int) validatePagination page limit = let validPage = max 1 page validLimit = min 100 $ max 1 limit offset = (validPage - 1) * validLimit in (validLimit, offset) `; await fs_1.promises.writeFile(path.join(projectPath, 'src', 'Utils', 'Pagination.hs'), paginationContent); } async generateTypes(projectPath) { // Types are already generated in generateModels } async generateHealthCheck(projectPath) { // Health check is implemented in Handlers.Health } async generateAPIDocs(projectPath) { const apiDocsContent = `# ${this.config.framework} API Documentation ## Overview This is a RESTful API built with Spock framework in Haskell. ## Authentication The API uses JWT (JSON Web Tokens) for authentication. Include the token in the Authorization header: \`\`\` Authorization: Bearer <your-jwt-token> \`\`\` ## Base URL \`\`\` http://localhost:3000/api/v1 \`\`\` ## Endpoints ### Health Check \`\`\`http GET /health GET /api/v1/health \`\`\` Returns the healt