@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,177 lines (1,043 loc) • 39.1 kB
JavaScript
"use strict";
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.Http4sGenerator = void 0;
const scala_base_generator_1 = require("./scala-base-generator");
const fs_1 = require("fs");
const path = __importStar(require("path"));
class Http4sGenerator extends scala_base_generator_1.ScalaBackendGenerator {
constructor() {
super('http4s');
}
getFrameworkSettings() {
return `addCompilerPlugin("org.typelevel" % "kind-projector" % "0.13.2" cross CrossVersion.full)
addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1")`;
}
getFrameworkPlugins() {
return `// No additional plugins needed for http4s`;
}
getFrameworkDependencies() {
return `// http4s
"org.http4s" %% "http4s-ember-server" % http4sVersion,
"org.http4s" %% "http4s-ember-client" % http4sVersion,
"org.http4s" %% "http4s-circe" % http4sVersion,
"org.http4s" %% "http4s-dsl" % http4sVersion,
"org.http4s" %% "http4s-prometheus-metrics" % http4sVersion,
// Cats and Cats Effect
"org.typelevel" %% "cats-core" % catsVersion,
"org.typelevel" %% "cats-effect" % "3.5.2",
// JSON
"io.circe" %% "circe-core" % circeVersion,
"io.circe" %% "circe-generic" % circeVersion,
"io.circe" %% "circe-parser" % circeVersion,
"io.circe" %% "circe-refined" % circeVersion,
// Database
"org.tpolecat" %% "doobie-core" % doobieVersion,
"org.tpolecat" %% "doobie-postgres" % doobieVersion,
"org.tpolecat" %% "doobie-hikari" % doobieVersion,
"org.tpolecat" %% "doobie-postgres-circe" % doobieVersion,
"org.tpolecat" %% "doobie-refined" % doobieVersion,
postgresql,
// Redis
"dev.profunktor" %% "redis4cats-effects" % "1.5.1",
// JWT
jwtScala,
"com.auth0" % "java-jwt" % "4.4.0",
// Config
"com.github.pureconfig" %% "pureconfig" % "0.17.4",
"com.github.pureconfig" %% "pureconfig-cats-effect" % "0.17.4",
// Refined types
"eu.timepit" %% "refined" % "0.11.0",
"eu.timepit" %% "refined-cats" % "0.11.0",
// OpenAPI
"com.github.ghostdogpr" %% "tapir-core" % "1.9.0",
"com.github.ghostdogpr" %% "tapir-http4s-server" % "1.9.0",
"com.github.ghostdogpr" %% "tapir-swagger-ui-bundle" % "1.9.0",
"com.github.ghostdogpr" %% "tapir-json-circe" % "1.9.0",
// Logging
logback,
scalaLogging,
"org.typelevel" %% "log4cats-slf4j" % "2.6.0",
// Testing
"org.http4s" %% "http4s-circe" % http4sVersion % Test,
"org.typelevel" %% "cats-effect-testing-scalatest" % "1.5.0" % Test,
"org.typelevel" %% "munit-cats-effect-3" % "1.0.7" % Test,
scalaTest,
// Streaming
"co.fs2" %% "fs2-core" % "3.9.3",
"co.fs2" %% "fs2-io" % "3.9.3"`;
}
async generateFrameworkFiles(projectPath, options) {
const basePackage = options.organization || 'com.example';
const srcDir = path.join(projectPath, 'src/main/scala', ...basePackage.split('.'));
await fs_1.promises.mkdir(srcDir, { recursive: true });
await this.generateMain(srcDir, basePackage, options);
await this.generateServer(srcDir, basePackage, options);
await this.generateConfig(srcDir, basePackage);
await this.generateRoutes(srcDir, basePackage);
await this.generateServices(srcDir, basePackage);
await this.generateRepositories(srcDir, basePackage);
await this.generateModels(srcDir, basePackage);
await this.generateDatabase(srcDir, basePackage);
await this.generateAuth(srcDir, basePackage);
await this.generateMiddleware(srcDir, basePackage);
await this.generateWebSocket(srcDir, basePackage);
await this.generateUtils(srcDir, basePackage);
await this.generateResources(projectPath);
await this.generateTests(projectPath, basePackage);
}
async generateMain(srcDir, basePackage, options) {
const mainContent = `package ${basePackage}
import cats.effect._
import com.comcast.ip4s._
import org.http4s.ember.server.EmberServerBuilder
import org.typelevel.log4cats.LoggerFactory
import org.typelevel.log4cats.slf4j.Slf4jFactory
object Main extends IOApp {
implicit val loggerFactory: LoggerFactory[IO] = Slf4jFactory.create[IO]
def run(args: List[String]): IO[ExitCode] = {
for {
config <- Config.load[IO]
_ <- AppResources.make(config).use { resources =>
for {
repositories <- Repositories.make(resources.postgres)
services <- Services.make(repositories, config)
api = new Api(services)
_ <- EmberServerBuilder
.default[IO]
.withHost(Host.fromString(config.server.host).getOrElse(ipv4"0.0.0.0"))
.withPort(Port.fromInt(config.server.port).getOrElse(port"8080"))
.withHttpApp(api.httpApp)
.build
.useForever
} yield ()
}
} yield ExitCode.Success
}
}`;
await fs_1.promises.writeFile(path.join(srcDir, 'Main.scala'), mainContent);
// AppResources.scala
const appResourcesContent = `package ${basePackage}
import cats.effect._
import cats.syntax.all._
import doobie.hikari.HikariTransactor
import doobie.util.ExecutionContexts
import dev.profunktor.redis4cats.{Redis, RedisCommands}
import org.typelevel.log4cats.LoggerFactory
case class AppResources[F[_]](
postgres: HikariTransactor[F],
redis: RedisCommands[F, String, String]
)
object AppResources {
def make[F[_]: Async: LoggerFactory](config: Config): Resource[F, AppResources[F]] = {
for {
ec <- ExecutionContexts.fixedThreadPool[F](config.database.connections)
postgres <- DatabaseConfig.transactor(config.database, ec)
redis <- Redis[F].utf8(config.redis.uri).widen[RedisCommands[F, String, String]]
} yield AppResources(postgres, redis)
}
}`;
await fs_1.promises.writeFile(path.join(srcDir, 'AppResources.scala'), appResourcesContent);
}
async generateServer(srcDir, basePackage, options) {
const apiContent = `package ${basePackage}
import cats.effect._
import cats.syntax.all._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.server.Router
import org.http4s.server.middleware._
import org.http4s.metrics.prometheus._
import org.http4s.implicits._
import sttp.tapir.swagger.bundle.SwaggerInterpreter
import sttp.tapir.server.http4s.Http4sServerInterpreter
import ${basePackage}.routes._
import ${basePackage}.services._
import ${basePackage}.middleware._
import scala.concurrent.duration._
import org.typelevel.log4cats.LoggerFactory
class Api[F[_]: Async: LoggerFactory](services: Services[F]) extends Http4sDsl[F] {
private val healthRoutes = new HealthRoutes[F]()
private val authRoutes = new AuthRoutes[F](services.authService, services.userService)
private val userRoutes = new UserRoutes[F](services.userService, services.authService)
private val wsRoutes = new WebSocketRoutes[F]()
// Tapir endpoints for Swagger
private val endpoints =
authRoutes.endpoints ++
userRoutes.endpoints ++
healthRoutes.endpoints
private val swaggerRoutes = Http4sServerInterpreter[F]().toRoutes(
SwaggerInterpreter()
.fromEndpoints[F](endpoints, "http4s API", "1.0.0")
)
private val routes: HttpRoutes[F] = Router(
"/api/v1" -> (healthRoutes.routes <+> authRoutes.routes <+> authMiddleware(services.authService)(userRoutes.routes)),
"/ws" -> wsRoutes.routes,
"/docs" -> swaggerRoutes
)
private val middleware: HttpApp[F] => HttpApp[F] = { http =>
RequestLogger.httpApp(true, true)(
ResponseLogger.httpApp(true, true)(
Timeout.httpApp(30.seconds)(
ErrorHandler.handle(
CORS.policy.withAllowOriginAll(
GZip(
Metrics[F](Prometheus.metricsOps[F], "http4s_server")(
http
)
)
)
)
)
)
)
}
val httpApp: HttpApp[F] = middleware(routes.orNotFound)
}`;
await fs_1.promises.writeFile(path.join(srcDir, 'Api.scala'), apiContent);
}
async generateConfig(srcDir, basePackage) {
const configDir = path.join(srcDir, 'config');
await fs_1.promises.mkdir(configDir, { recursive: true });
const configContent = `package ${basePackage}
import cats.effect._
import pureconfig._
import pureconfig.generic.auto._
import pureconfig.module.catseffect.syntax._
import scala.concurrent.duration._
case class Config(
server: ServerConfig,
database: DatabaseConfig,
redis: RedisConfig,
jwt: JwtConfig
)
case class ServerConfig(
host: String,
port: Int
)
case class DatabaseConfig(
driver: String,
url: String,
user: String,
password: String,
connections: Int
)
case class RedisConfig(
uri: String
)
case class JwtConfig(
secret: String,
expiration: FiniteDuration
)
object Config {
def load[F[_]: Async]: F[Config] =
ConfigSource.default.loadF[F, Config]
}`;
await fs_1.promises.writeFile(path.join(srcDir, 'Config.scala'), configContent);
// DatabaseConfig.scala
const dbConfigContent = `package ${basePackage}
import cats.effect._
import doobie._
import doobie.hikari._
import doobie.implicits._
import org.flywaydb.core.Flyway
object DatabaseConfig {
def transactor[F[_]: Async](
config: DatabaseConfig,
ec: ExecutionContext
): Resource[F, HikariTransactor[F]] = {
HikariTransactor.newHikariTransactor[F](
config.driver,
config.url,
config.user,
config.password,
ec
)
}
def migrate[F[_]: Sync](transactor: HikariTransactor[F]): F[Unit] = {
transactor.configure { dataSource =>
Sync[F].delay {
val flyway = Flyway.configure()
.dataSource(dataSource)
.locations("classpath:db/migration")
.load()
flyway.migrate()
()
}
}
}
}`;
await fs_1.promises.writeFile(path.join(srcDir, 'DatabaseConfig.scala'), dbConfigContent);
}
async generateRoutes(srcDir, basePackage) {
const routesDir = path.join(srcDir, 'routes');
await fs_1.promises.mkdir(routesDir, { recursive: true });
// HealthRoutes.scala
const healthRoutesContent = `package ${basePackage}.routes
import cats.effect._
import cats.syntax.all._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.circe._
import io.circe.generic.auto._
import io.circe.syntax._
import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.generic.auto._
class HealthRoutes[F[_]: Sync] extends Http4sDsl[F] {
case class HealthStatus(
status: String,
timestamp: Long,
version: String
)
implicit val healthEncoder: EntityEncoder[F, HealthStatus] = jsonEncoderOf[F, HealthStatus]
val healthEndpoint: PublicEndpoint[Unit, Unit, HealthStatus, Any] =
endpoint
.get
.in("health")
.out(jsonBody[HealthStatus])
.description("Health check endpoint")
val routes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "health" =>
Ok(HealthStatus(
status = "UP",
timestamp = System.currentTimeMillis(),
version = "1.0.0"
))
}
val endpoints = List(healthEndpoint)
}`;
await fs_1.promises.writeFile(path.join(routesDir, 'HealthRoutes.scala'), healthRoutesContent);
// AuthRoutes.scala
const authRoutesContent = `package ${basePackage}.routes
import cats.data._
import cats.effect._
import cats.syntax.all._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.circe._
import ${basePackage}.models._
import ${basePackage}.services._
import io.circe.generic.auto._
import io.circe.syntax._
import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.generic.auto._
class AuthRoutes[F[_]: Concurrent](
authService: AuthService[F],
userService: UserService[F]
) extends Http4sDsl[F] {
implicit val registerDecoder: EntityDecoder[F, RegisterRequest] = jsonOf[F, RegisterRequest]
implicit val loginDecoder: EntityDecoder[F, LoginRequest] = jsonOf[F, LoginRequest]
implicit val authEncoder: EntityEncoder[F, AuthResponse] = jsonEncoderOf[F, AuthResponse]
implicit val errorEncoder: EntityEncoder[F, ErrorResponse] = jsonEncoderOf[F, ErrorResponse]
val registerEndpoint = endpoint
.post
.in("auth" / "register")
.in(jsonBody[RegisterRequest])
.out(statusCode(StatusCode.Created).and(jsonBody[AuthResponse]))
.errorOut(statusCode(StatusCode.BadRequest).and(jsonBody[ErrorResponse]))
.description("Register a new user")
val loginEndpoint = endpoint
.post
.in("auth" / "login")
.in(jsonBody[LoginRequest])
.out(jsonBody[AuthResponse])
.errorOut(statusCode(StatusCode.Unauthorized).and(jsonBody[ErrorResponse]))
.description("Login with credentials")
val routes: HttpRoutes[F] = HttpRoutes.of[F] {
case req @ POST -> Root / "auth" / "register" =>
req.as[RegisterRequest].flatMap { registerReq =>
userService.createUser(registerReq).flatMap { user =>
authService.generateToken(user).flatMap { token =>
Created(AuthResponse(token, user))
}
}
}.handleErrorWith {
case _: IllegalArgumentException =>
BadRequest(ErrorResponse("Email already exists"))
case err =>
InternalServerError(ErrorResponse(err.getMessage))
}
case req @ POST -> Root / "auth" / "login" =>
req.as[LoginRequest].flatMap { loginReq =>
userService.authenticate(loginReq.email, loginReq.password).flatMap {
case Some(user) =>
authService.generateToken(user).flatMap { token =>
Ok(AuthResponse(token, user))
}
case None =>
Unauthorized(ErrorResponse("Invalid credentials"))
}
}
}
val endpoints = List(registerEndpoint, loginEndpoint)
}`;
await fs_1.promises.writeFile(path.join(routesDir, 'AuthRoutes.scala'), authRoutesContent);
// UserRoutes.scala
const userRoutesContent = `package ${basePackage}.routes
import cats.effect._
import cats.syntax.all._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.circe._
import ${basePackage}.models._
import ${basePackage}.services._
import io.circe.generic.auto._
import io.circe.syntax._
import sttp.tapir._
import sttp.tapir.json.circe._
import sttp.tapir.generic.auto._
class UserRoutes[F[_]: Concurrent](
userService: UserService[F],
authService: AuthService[F]
) extends Http4sDsl[F] {
implicit val userEncoder: EntityEncoder[F, User] = jsonEncoderOf[F, User]
implicit val usersEncoder: EntityEncoder[F, List[User]] = jsonEncoderOf[F, List[User]]
implicit val updateDecoder: EntityDecoder[F, UpdateUserRequest] = jsonOf[F, UpdateUserRequest]
implicit val errorEncoder: EntityEncoder[F, ErrorResponse] = jsonEncoderOf[F, ErrorResponse]
val auth = auth.bearer[String]()
val getUsersEndpoint = endpoint
.get
.in("users")
.in(query[Option[Int]]("page"))
.in(query[Option[Int]]("size"))
.in(auth)
.out(jsonBody[List[User]])
.description("Get all users")
val getUserEndpoint = endpoint
.get
.in("users" / path[Long]("userId"))
.in(auth)
.out(jsonBody[User])
.errorOut(statusCode(StatusCode.NotFound).and(jsonBody[ErrorResponse]))
.description("Get user by ID")
val updateUserEndpoint = endpoint
.put
.in("users" / path[Long]("userId"))
.in(auth)
.in(jsonBody[UpdateUserRequest])
.out(jsonBody[User])
.errorOut(statusCode(StatusCode.NotFound).and(jsonBody[ErrorResponse]))
.description("Update user")
def routes(implicit authUser: AuthedRequest[F, User]): HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "users" :? PageQueryParamMatcher(page) +& SizeQueryParamMatcher(size) =>
userService.listUsers(page.getOrElse(1), size.getOrElse(10)).flatMap { users =>
Ok(users)
}
case GET -> Root / "users" / LongVar(userId) =>
userService.findById(userId).flatMap {
case Some(user) => Ok(user)
case None => NotFound(ErrorResponse("User not found"))
}
case req @ PUT -> Root / "users" / LongVar(userId) =>
if (authUser.context.id != userId) {
Forbidden(ErrorResponse("Cannot update other users"))
} else {
req.req.as[UpdateUserRequest].flatMap { updateReq =>
userService.updateUser(userId, updateReq).flatMap { updated =>
Ok(updated)
}
}
}
case DELETE -> Root / "users" / LongVar(userId) =>
if (authUser.context.id != userId) {
Forbidden(ErrorResponse("Cannot delete other users"))
} else {
userService.deleteUser(userId).flatMap { _ =>
NoContent()
}
}
}
val endpoints = List(getUsersEndpoint, getUserEndpoint, updateUserEndpoint)
}
object PageQueryParamMatcher extends OptionalQueryParamDecoderMatcher[Int]("page")
object SizeQueryParamMatcher extends OptionalQueryParamDecoderMatcher[Int]("size")`;
await fs_1.promises.writeFile(path.join(routesDir, 'UserRoutes.scala'), userRoutesContent);
// WebSocketRoutes.scala
const wsRoutesContent = `package ${basePackage}.routes
import cats.effect._
import cats.syntax.all._
import fs2._
import fs2.concurrent.Queue
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.server.websocket.WebSocketBuilder
import org.http4s.websocket.WebSocketFrame
import io.circe.generic.auto._
import io.circe.syntax._
import io.circe.parser._
class WebSocketRoutes[F[_]: Concurrent] extends Http4sDsl[F] {
case class WsMessage(messageType: String, payload: io.circe.Json)
val routes: HttpRoutes[F] = HttpRoutes.of[F] {
case GET -> Root / "echo" =>
val echoRoute = WebSocketBuilder[F].build(
receive = _.evalMap {
case WebSocketFrame.Text(text, _) =>
WebSocketFrame.Text(s"Echo: \$text").pure[F]
case frame =>
frame.pure[F]
},
send = Stream.empty
)
echoRoute
case GET -> Root / "chat" =>
for {
queue <- Queue.unbounded[F, WebSocketFrame]
response <- WebSocketBuilder[F].build(
receive = _.evalMap {
case WebSocketFrame.Text(text, _) =>
decode[WsMessage](text) match {
case Right(msg) if msg.messageType == "broadcast" =>
queue.offer(WebSocketFrame.Text(text))
case _ =>
().pure[F]
}
case _ =>
().pure[F]
},
send = queue.dequeue
)
} yield response
}
}`;
await fs_1.promises.writeFile(path.join(routesDir, 'WebSocketRoutes.scala'), wsRoutesContent);
}
async generateServices(srcDir, basePackage) {
const servicesDir = path.join(srcDir, 'services');
await fs_1.promises.mkdir(servicesDir, { recursive: true });
// Services.scala
const servicesContent = `package ${basePackage}.services
import cats.effect._
import ${basePackage}.repositories._
case class Services[F[_]](
authService: AuthService[F],
userService: UserService[F]
)
object Services {
def make[F[_]: Sync](
repositories: Repositories[F],
config: Config
): F[Services[F]] = {
for {
authService <- AuthService.make[F](config.jwt)
userService <- UserService.make[F](repositories.userRepository)
} yield Services(authService, userService)
}
}`;
await fs_1.promises.writeFile(path.join(servicesDir, 'Services.scala'), servicesContent);
// UserService.scala
const userServiceContent = `package ${basePackage}.services
import cats.effect._
import cats.syntax.all._
import ${basePackage}.models._
import ${basePackage}.repositories.UserRepository
import org.mindrot.jbcrypt.BCrypt
trait UserService[F[_]] {
def createUser(request: RegisterRequest): F[User]
def authenticate(email: String, password: String): F[Option[User]]
def findById(id: Long): F[Option[User]]
def listUsers(page: Int, size: Int): F[List[User]]
def updateUser(id: Long, request: UpdateUserRequest): F[User]
def deleteUser(id: Long): F[Unit]
}
object UserService {
def make[F[_]: Sync](userRepository: UserRepository[F]): F[UserService[F]] = {
Sync[F].pure(new UserService[F] {
override def createUser(request: RegisterRequest): F[User] = {
val hashedPassword = BCrypt.hashpw(request.password, BCrypt.gensalt())
val user = User(
id = 0,
email = request.email,
name = request.name,
passwordHash = hashedPassword,
createdAt = System.currentTimeMillis(),
updatedAt = System.currentTimeMillis()
)
userRepository.create(user)
}
override def authenticate(email: String, password: String): F[Option[User]] = {
userRepository.findByEmail(email).map {
case Some(user) if BCrypt.checkpw(password, user.passwordHash) => Some(user)
case _ => None
}
}
override def findById(id: Long): F[Option[User]] = {
userRepository.findById(id)
}
override def listUsers(page: Int, size: Int): F[List[User]] = {
userRepository.list(offset = (page - 1) * size, limit = size)
}
override def updateUser(id: Long, request: UpdateUserRequest): F[User] = {
userRepository.findById(id).flatMap {
case Some(user) =>
val updated = user.copy(
name = request.name.getOrElse(user.name),
updatedAt = System.currentTimeMillis()
)
userRepository.update(updated)
case None =>
Sync[F].raiseError(new NoSuchElementException(s"User \$id not found"))
}
}
override def deleteUser(id: Long): F[Unit] = {
userRepository.delete(id)
}
})
}
}`;
await fs_1.promises.writeFile(path.join(servicesDir, 'UserService.scala'), userServiceContent);
// AuthService.scala
const authServiceContent = `package ${basePackage}.services
import cats.effect._
import cats.syntax.all._
import ${basePackage}.models.User
import ${basePackage}.JwtConfig
import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm
import java.time.Instant
import scala.util.Try
trait AuthService[F[_]] {
def generateToken(user: User): F[String]
def validateToken(token: String): F[Option[Long]]
}
object AuthService {
def make[F[_]: Sync](config: JwtConfig): F[AuthService[F]] = {
Sync[F].pure(new AuthService[F] {
private val algorithm = Algorithm.HMAC256(config.secret)
override def generateToken(user: User): F[String] = {
Sync[F].delay {
JWT.create()
.withSubject(user.id.toString)
.withClaim("email", user.email)
.withClaim("name", user.name)
.withExpiresAt(Instant.now().plusSeconds(config.expiration.toSeconds))
.withIssuedAt(Instant.now())
.sign(algorithm)
}
}
override def validateToken(token: String): F[Option[Long]] = {
Sync[F].delay {
Try {
val verifier = JWT.require(algorithm).build()
val decoded = verifier.verify(token)
decoded.getSubject.toLong
}.toOption
}
}
})
}
}`;
await fs_1.promises.writeFile(path.join(servicesDir, 'AuthService.scala'), authServiceContent);
}
async generateRepositories(srcDir, basePackage) {
const reposDir = path.join(srcDir, 'repositories');
await fs_1.promises.mkdir(reposDir, { recursive: true });
// Repositories.scala
const repositoriesContent = `package ${basePackage}.repositories
import cats.effect._
import doobie._
import doobie.implicits._
case class Repositories[F[_]](
userRepository: UserRepository[F]
)
object Repositories {
def make[F[_]: Sync](postgres: Transactor[F]): F[Repositories[F]] = {
for {
userRepo <- UserRepository.make(postgres)
} yield Repositories(userRepo)
}
}`;
await fs_1.promises.writeFile(path.join(reposDir, 'Repositories.scala'), repositoriesContent);
// UserRepository.scala
const userRepoContent = `package ${basePackage}.repositories
import cats.effect._
import cats.syntax.all._
import doobie._
import doobie.implicits._
import doobie.postgres.implicits._
import ${basePackage}.models._
trait UserRepository[F[_]] {
def create(user: User): F[User]
def findById(id: Long): F[Option[User]]
def findByEmail(email: String): F[Option[User]]
def list(offset: Int, limit: Int): F[List[User]]
def update(user: User): F[User]
def delete(id: Long): F[Unit]
}
object UserRepository {
def make[F[_]: Sync](postgres: Transactor[F]): F[UserRepository[F]] = {
Sync[F].pure(new UserRepository[F] {
override def create(user: User): F[User] = {
sql"""
INSERT INTO users (email, name, password_hash, created_at, updated_at)
VALUES (\${user.email}, \${user.name}, \${user.passwordHash}, \${user.createdAt}, \${user.updatedAt})
""".update
.withUniqueGeneratedKeys[Long]("id")
.map(id => user.copy(id = id))
.transact(postgres)
}
override def findById(id: Long): F[Option[User]] = {
sql"""
SELECT id, email, name, password_hash, created_at, updated_at
FROM users
WHERE id = \$id
""".query[User].option.transact(postgres)
}
override def findByEmail(email: String): F[Option[User]] = {
sql"""
SELECT id, email, name, password_hash, created_at, updated_at
FROM users
WHERE email = \$email
""".query[User].option.transact(postgres)
}
override def list(offset: Int, limit: Int): F[List[User]] = {
sql"""
SELECT id, email, name, password_hash, created_at, updated_at
FROM users
ORDER BY created_at DESC
LIMIT \$limit OFFSET \$offset
""".query[User].to[List].transact(postgres)
}
override def update(user: User): F[User] = {
sql"""
UPDATE users
SET email = \${user.email},
name = \${user.name},
updated_at = \${user.updatedAt}
WHERE id = \${user.id}
""".update.run
.transact(postgres)
.map(_ => user)
}
override def delete(id: Long): F[Unit] = {
sql"""
DELETE FROM users WHERE id = \$id
""".update.run
.transact(postgres)
.void
}
})
}
}`;
await fs_1.promises.writeFile(path.join(reposDir, 'UserRepository.scala'), userRepoContent);
}
async generateModels(srcDir, basePackage) {
const modelsDir = path.join(srcDir, 'models');
await fs_1.promises.mkdir(modelsDir, { recursive: true });
const modelsContent = `package ${basePackage}.models
import io.circe.generic.semiauto._
import io.circe.{Decoder, Encoder}
// Domain models
case class User(
id: Long,
email: String,
name: String,
passwordHash: String,
createdAt: Long,
updatedAt: Long
)
object User {
implicit val decoder: Decoder[User] = deriveDecoder[User]
implicit val encoder: Encoder[User] = deriveEncoder[User]
}
// Request models
case class RegisterRequest(
email: String,
name: String,
password: String
)
object RegisterRequest {
implicit val decoder: Decoder[RegisterRequest] = deriveDecoder[RegisterRequest]
implicit val encoder: Encoder[RegisterRequest] = deriveEncoder[RegisterRequest]
}
case class LoginRequest(
email: String,
password: String
)
object LoginRequest {
implicit val decoder: Decoder[LoginRequest] = deriveDecoder[LoginRequest]
implicit val encoder: Encoder[LoginRequest] = deriveEncoder[LoginRequest]
}
case class UpdateUserRequest(
name: Option[String] = None
)
object UpdateUserRequest {
implicit val decoder: Decoder[UpdateUserRequest] = deriveDecoder[UpdateUserRequest]
implicit val encoder: Encoder[UpdateUserRequest] = deriveEncoder[UpdateUserRequest]
}
// Response models
case class AuthResponse(
token: String,
user: User
)
object AuthResponse {
implicit val decoder: Decoder[AuthResponse] = deriveDecoder[AuthResponse]
implicit val encoder: Encoder[AuthResponse] = deriveEncoder[AuthResponse]
}
case class ErrorResponse(
error: String,
timestamp: Long = System.currentTimeMillis()
)
object ErrorResponse {
implicit val decoder: Decoder[ErrorResponse] = deriveDecoder[ErrorResponse]
implicit val encoder: Encoder[ErrorResponse] = deriveEncoder[ErrorResponse]
}`;
await fs_1.promises.writeFile(path.join(modelsDir, 'Models.scala'), modelsContent);
}
async generateDatabase(srcDir, basePackage) {
// Database configuration is in DatabaseConfig.scala
}
async generateAuth(srcDir, basePackage) {
// Auth is implemented in AuthService and middleware
}
async generateMiddleware(srcDir, basePackage) {
const middlewareDir = path.join(srcDir, 'middleware');
await fs_1.promises.mkdir(middlewareDir, { recursive: true });
const authMiddlewareContent = `package ${basePackage}.middleware
import cats.data._
import cats.effect._
import cats.syntax.all._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.server.AuthMiddleware
import org.http4s.headers.Authorization
import ${basePackage}.models._
import ${basePackage}.services._
object authMiddleware {
def apply[F[_]: Sync](authService: AuthService[F]): AuthMiddleware[F, User] = {
val authUser: Kleisli[OptionT[F, *], Request[F], User] = Kleisli { request =>
request.headers.get[Authorization] match {
case Some(Authorization(Credentials.Token(AuthScheme.Bearer, token))) =>
OptionT(authService.validateToken(token)).flatMap { userId =>
OptionT(Sync[F].pure(Some(User(
id = userId,
email = "",
name = "",
passwordHash = "",
createdAt = 0,
updatedAt = 0
))))
}
case _ =>
OptionT.none
}
}
val onFailure: AuthedRoutes[String, F] = Kleisli { _ =>
OptionT.liftF(Response[F](Status.Unauthorized).pure[F])
}
AuthMiddleware(authUser, onFailure)
}
}`;
await fs_1.promises.writeFile(path.join(middlewareDir, 'AuthMiddleware.scala'), authMiddlewareContent);
// ErrorHandler.scala
const errorHandlerContent = `package ${basePackage}.middleware
import cats.effect._
import cats.syntax.all._
import org.http4s._
import org.http4s.dsl.Http4sDsl
import org.http4s.circe._
import ${basePackage}.models.ErrorResponse
import io.circe.syntax._
import org.typelevel.log4cats.LoggerFactory
object ErrorHandler {
def handle[F[_]: Sync: LoggerFactory]: HttpApp[F] => HttpApp[F] = { app =>
HttpApp[F] { request =>
app.run(request).handleErrorWith { error =>
LoggerFactory[F].create.flatMap { logger =>
logger.error(error)(s"Error handling request: \${request.method} \${request.uri}") *>
Response[F](
status = Status.InternalServerError,
body = EntityEncoder[F, String].toEntity(
ErrorResponse(error.getMessage).asJson.noSpaces
).body
).pure[F]
}
}
}
}
}`;
await fs_1.promises.writeFile(path.join(middlewareDir, 'ErrorHandler.scala'), errorHandlerContent);
}
async generateWebSocket(srcDir, basePackage) {
// WebSocket implementation is in WebSocketRoutes.scala
}
async generateUtils(srcDir, basePackage) {
const utilsDir = path.join(srcDir, 'utils');
await fs_1.promises.mkdir(utilsDir, { recursive: true });
const validationContent = `package ${basePackage}.utils
import cats.data._
import cats.syntax.all._
import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.string._
import eu.timepit.refined.numeric._
object Validation {
type Email = String Refined MatchesRegex["^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$"]
type Password = String Refined MinSize[8]
type NonEmptyString = String Refined NonEmpty
def validateEmail(email: String): Either[String, Email] = {
refineV[MatchesRegex["^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\\\.[a-zA-Z]{2,}$"]](email)
.leftMap(_ => "Invalid email format")
}
def validatePassword(password: String): Either[String, Password] = {
refineV[MinSize[8]](password)
.leftMap(_ => "Password must be at least 8 characters")
}
def validateNonEmpty(str: String): Either[String, NonEmptyString] = {
refineV[NonEmpty](str)
.leftMap(_ => "Value cannot be empty")
}
}`;
await fs_1.promises.writeFile(path.join(utilsDir, 'Validation.scala'), validationContent);
}
async generateResources(projectPath) {
const resourcesDir = path.join(projectPath, 'src/main/resources');
await fs_1.promises.mkdir(resourcesDir, { recursive: true });
const appConf = `server {
host = "0.0.0.0"
host = \${?HOST}
port = 8080
port = \${?PORT}
}
database {
driver = "org.postgresql.Driver"
url = "jdbc:postgresql://localhost:5432/app_db"
url = \${?DATABASE_URL}
user = "postgres"
user = \${?DB_USER}
password = "postgres"
password = \${?DB_PASSWORD}
connections = 10
connections = \${?DB_CONNECTIONS}
}
redis {
uri = "redis://localhost:6379"
uri = \${?REDIS_URI}
}
jwt {
secret = "your-secret-key-here"
secret = \${?JWT_SECRET}
expiration = 24 hours
expiration = \${?JWT_EXPIRATION}
}`;
await fs_1.promises.writeFile(path.join(resourcesDir, 'application.conf'), appConf);
const logbackXml = `<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.http4s" level="INFO"/>
<logger name="doobie" level="INFO"/>
<logger name="com.example" level="DEBUG"/>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
</configuration>`;
await fs_1.promises.writeFile(path.join(resourcesDir, 'logback.xml'), logbackXml);
// Create db migration directory
const migrationDir = path.join(resourcesDir, 'db/migration');
await fs_1.promises.mkdir(migrationDir, { recursive: true });
const migration1 = `-- V1__Create_users_table.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(255) NOT NULL,
password_hash VARCHAR(255) NOT NULL,
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL
);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_created_at ON users(created_at);`;
await fs_1.promises.writeFile(path.join(migrationDir, 'V1__Create_users_table.sql'), migration1);
}
async generateTests(projectPath, basePackage) {
const testDir = path.join(projectPath, 'src/test/scala', ...basePackage.split('.'));
await fs_1.promises.mkdir(testDir, { recursive: true });
const apiSpecContent = `package ${basePackage}
import cats.effect._
import org.http4s._
import org.http4s.implicits._
import munit.CatsEffectSuite
import ${basePackage}.models._
import ${basePackage}.services._
import io.circe.syntax._
import org.http4s.circe._
class ApiSpec extends CatsEffectSuite {
test("GET /api/v1/health returns 200") {
val api = new Api[IO](createServices())
val request = Request[IO](Method.GET, uri"/api/v1/health")
api.httpApp.run(request).map { response =>
assertEquals(response.status, Status.Ok)
}
}
test("POST /api/v1/auth/register creates new user") {
val api = new Api[IO](createServices())
val registerRequest = RegisterRequest("test@example.com", "Test User", "password123")
val request = Request[IO](Method.POST, uri"/api/v1/auth/register")
.withEntity(registerRequest.asJson)
api.httpApp.run(request).map { response =>
assertEquals(response.status, Status.Created)
}
}
private def createServices(): Services[IO] = {
// Create mock services for testing
Services[IO](
authService = new AuthService[IO] {
def generateToken(user: User): IO[String] = IO.pure("test-token")
def validateToken(token: String): IO[Option[Long]] = IO.pure(Some(1L))
},
userService = new UserService[IO] {
def createUser(request: RegisterRequest): IO[User] = IO.pure(
User(1, request.email, request.name, "hash", 0L, 0L)
)
def authenticate(email: String, password: String): IO[Option[User]] = IO.pure(None)
def findById(id: Long): IO[Option[User]] = IO.pure(None)
def listUsers(page: Int, size: Int): IO[List[User]] = IO.pure(List.empty)
def updateUser(id: Long, request: UpdateUserRequest): IO[User] = IO.pure(
User(id, "test@example.com", "Test", "hash", 0L, 0L)
)
def deleteUser(id: Long): IO[Unit] = IO.unit
}
)
}
}`;
await fs_1.promises.writeFile(path.join(testDir, 'ApiSpec.scala'), apiSpecContent);
}
}
exports.Http4sGenerator = Http4sGenerator;
exports.default = Http4sGenerator;