Phase 01: Foundation - 3 plans in 3 waves (sequential dependency) - Plan 01: Mill build + ZIO HTTP server - Plan 02: PostgreSQL + Quill + Flyway migrations - Plan 03: Repository pattern + Pulumi/Besom deployment Ready for execution
9.9 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation | 03 | execute | 3 |
|
|
true |
|
Purpose: Establish the data access pattern that all future models will follow, and prepare deployment infrastructure. Output: Working UserRepository with compile-time validated queries, Pulumi config for cloud deployment, Dockerfile for containerization.
<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-RESEARCH.md @.planning/phases/01-foundation/01-01-SUMMARY.md @.planning/phases/01-foundation/01-02-SUMMARY.md Task 1: Create Repository pattern with typed errors src/model/User.scala src/repository/RepositoryError.scala src/repository/UserRepository.scala Implement the Repository pattern following ZIO best practices.- src/model/User.scala:
package summercms.model
import java.time.Instant
case class User(
id: Long,
email: String,
passwordHash: String,
createdAt: Instant,
updatedAt: Instant
)
- src/repository/RepositoryError.scala:
package summercms.repository
sealed trait RepositoryError
object RepositoryError:
case class NotFound(entity: String, id: Long) extends RepositoryError
case class NotFoundByField(entity: String, field: String, value: String) extends RepositoryError
case class Conflict(entity: String, message: String) extends RepositoryError
case class ValidationError(errors: List[String]) extends RepositoryError
case class DatabaseError(message: String, cause: Option[Throwable] = None) extends RepositoryError
- src/repository/UserRepository.scala:
-
Trait UserRepository with methods:
- def findById(id: Long): IO[RepositoryError, Option[User]]
- def findByEmail(email: String): IO[RepositoryError, Option[User]]
- def create(email: String, passwordHash: String): IO[RepositoryError, User]
- def update(user: User): IO[RepositoryError, User]
- def delete(id: Long): IO[RepositoryError, Unit]
-
Object UserRepository with:
- val live: ZLayer[Quill.Postgres[SnakeCase], Nothing, UserRepository]
-
Class UserRepositoryLive(quill: Quill.Postgres[SnakeCase]) extends UserRepository:
- import quill.*
- inline def users = quote(querySchemaUser)
- Implement all methods using Quill queries
- Use refineOrDie to convert SQLException to RepositoryError.DatabaseError
- Handle unique constraint violations as Conflict errors
Pattern for error handling:
def create(email: String, passwordHash: String): IO[RepositoryError, User] =
val now = Instant.now()
val user = User(0, email, passwordHash, now, now)
run(users.insertValue(lift(user)).returningGenerated(_.id))
.map(id => user.copy(id = id))
.refineOrDie {
case e: java.sql.SQLException if e.getSQLState == "23505" =>
RepositoryError.Conflict("User", s"Email $email already exists")
case e: java.sql.SQLException =>
RepositoryError.DatabaseError(e.getMessage, Some(e))
}
IMPORTANT: Do not use "io" as a variable name (Quill package conflict).
mill compile succeeds, repository pattern established
User model, RepositoryError ADT, and UserRepository trait+implementation exist and compile
IMPORTANT: Besom does NOT support Mill. Use Scala CLI for the infra/ directory.
- infra/project.scala (Scala CLI project config):
//> using scala 3.3.4
//> using dep org.virtuslab::besom-core:0.4.0
//> using dep org.virtuslab::besom-aws:6.56.0
Note: Use Besom 0.4.0 (latest stable on Maven Central, not 0.5.0 from research).
- infra/Main.scala:
import besom.*
import besom.api.aws
@main def main = Pulumi.run {
// Configuration
val config = besom.Config()
val environment = config.get("environment").getOrElse("dev")
// S3 bucket for assets (optional CDN origin)
val assetsBucket = aws.s3.Bucket(
s"summercms-assets-$environment",
aws.s3.BucketArgs()
)
// RDS PostgreSQL instance
val db = aws.rds.Instance(
s"summercms-db-$environment",
aws.rds.InstanceArgs(
engine = "postgres",
engineVersion = "16.4",
instanceClass = "db.t3.micro",
allocatedStorage = 20,
dbName = "summercms",
username = "summercms",
password = config.requireSecret("dbPassword"),
skipFinalSnapshot = environment == "dev",
publiclyAccessible = environment == "dev"
)
)
// ECS for container deployment (placeholder for now)
// Full ECS setup deferred to when we need actual deployment
Stack.exports(
assetsBucketName = assetsBucket.bucket,
dbEndpoint = db.endpoint,
dbPort = db.port
)
}
This provides:
- Multi-environment support via Pulumi config
- S3 bucket for future asset storage
- RDS PostgreSQL for database
- Secrets management for DB password
- Outputs for connecting the application
Note: Full ECS/container deployment is deferred. This establishes the pattern. Run from infra/ directory:
scala-cli compile .compilespulumi previewshows resources (requires Pulumi CLI and AWS credentials) Besom infrastructure code compiles, defines RDS and S3 resources
Dockerfile:
# Stage 1: Build
FROM eclipse-temurin:21-jdk AS builder
# Install Mill
RUN curl -L https://github.com/com-lihaoyi/mill/releases/download/0.12.3/0.12.3 > /usr/local/bin/mill && \
chmod +x /usr/local/bin/mill
WORKDIR /app
COPY . .
# Build fat JAR
RUN mill assembly
# Stage 2: Runtime
FROM eclipse-temurin:21-jre
WORKDIR /app
# Copy the fat JAR from builder
COPY --from=builder /app/out/assembly.dest/out.jar /app/summercms.jar
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# Install curl for health check
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
EXPOSE 8080
# Run the application
ENTRYPOINT ["java", "-jar", "/app/summercms.jar"]
Features:
- Multi-stage build (builder has JDK+Mill, runtime has only JRE)
- Uses Eclipse Temurin JDK 21 (LTS)
- Health check using /health endpoint
- Minimal runtime image size
- Environment variables configure the app (via application.conf ${?VAR} substitution)
Usage:
docker build -t summercms .
docker run -p 8080:8080 \
-e DB_HOST=host.docker.internal \
-e DB_PASSWORD=secret \
summercms
<success_criteria>
- User model and RepositoryError ADT defined
- UserRepository trait with CRUD methods, typed errors
- UserRepositoryLive implements queries with Quill
- Besom infrastructure defines RDS and S3 for AWS
- Dockerfile builds fat JAR in multi-stage build
- All code compiles without errors </success_criteria>