docs(01): create phase 1 foundation plans
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
This commit is contained in:
325
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
325
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
@@ -0,0 +1,325 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on: ["01-02"]
|
||||
files_modified:
|
||||
- src/repository/RepositoryError.scala
|
||||
- src/repository/UserRepository.scala
|
||||
- src/model/User.scala
|
||||
- infra/project.scala
|
||||
- infra/Main.scala
|
||||
- Dockerfile
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Repository trait defines CRUD operations with typed ZIO errors"
|
||||
- "UserRepositoryLive implements queries using Quill context"
|
||||
- "Besom/Pulumi infrastructure code compiles and can run pulumi preview"
|
||||
- "Dockerfile builds fat JAR into runnable container"
|
||||
artifacts:
|
||||
- path: "src/repository/RepositoryError.scala"
|
||||
provides: "Typed error ADT for repository operations"
|
||||
exports: ["RepositoryError", "NotFound", "Conflict", "DatabaseError"]
|
||||
- path: "src/repository/UserRepository.scala"
|
||||
provides: "User repository trait and implementation"
|
||||
exports: ["UserRepository", "UserRepositoryLive"]
|
||||
- path: "src/model/User.scala"
|
||||
provides: "User domain model"
|
||||
exports: ["User"]
|
||||
- path: "infra/Main.scala"
|
||||
provides: "Pulumi infrastructure definition"
|
||||
contains: "Pulumi.run"
|
||||
- path: "Dockerfile"
|
||||
provides: "Container build configuration"
|
||||
contains: "FROM"
|
||||
key_links:
|
||||
- from: "src/repository/UserRepository.scala"
|
||||
to: "src/db/QuillContext.scala"
|
||||
via: "Quill.Postgres dependency"
|
||||
pattern: "Quill\\.Postgres"
|
||||
- from: "src/repository/UserRepository.scala"
|
||||
to: "src/model/User.scala"
|
||||
via: "CRUD operations on User"
|
||||
pattern: "User"
|
||||
- from: "src/repository/UserRepository.scala"
|
||||
to: "src/repository/RepositoryError.scala"
|
||||
via: "IO[RepositoryError, _] return type"
|
||||
pattern: "RepositoryError"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement the Repository pattern with typed errors and create Pulumi infrastructure configuration using Besom.
|
||||
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create Repository pattern with typed errors</name>
|
||||
<files>
|
||||
src/model/User.scala
|
||||
src/repository/RepositoryError.scala
|
||||
src/repository/UserRepository.scala
|
||||
</files>
|
||||
<action>
|
||||
Implement the Repository pattern following ZIO best practices.
|
||||
|
||||
1. src/model/User.scala:
|
||||
```scala
|
||||
package summercms.model
|
||||
|
||||
import java.time.Instant
|
||||
|
||||
case class User(
|
||||
id: Long,
|
||||
email: String,
|
||||
passwordHash: String,
|
||||
createdAt: Instant,
|
||||
updatedAt: Instant
|
||||
)
|
||||
```
|
||||
|
||||
2. src/repository/RepositoryError.scala:
|
||||
```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
|
||||
```
|
||||
|
||||
3. 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(querySchema[User]("summer_users"))
|
||||
- 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:
|
||||
```scala
|
||||
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).
|
||||
</action>
|
||||
<verify>`mill compile` succeeds, repository pattern established</verify>
|
||||
<done>User model, RepositoryError ADT, and UserRepository trait+implementation exist and compile</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create Pulumi infrastructure with Besom</name>
|
||||
<files>
|
||||
infra/project.scala
|
||||
infra/Main.scala
|
||||
</files>
|
||||
<action>
|
||||
Create Pulumi infrastructure using Besom (Scala Pulumi SDK).
|
||||
|
||||
IMPORTANT: Besom does NOT support Mill. Use Scala CLI for the infra/ directory.
|
||||
|
||||
1. infra/project.scala (Scala CLI project config):
|
||||
```scala
|
||||
//> 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).
|
||||
|
||||
2. infra/Main.scala:
|
||||
```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.
|
||||
</action>
|
||||
<verify>
|
||||
Run from infra/ directory:
|
||||
- `scala-cli compile .` compiles
|
||||
- `pulumi preview` shows resources (requires Pulumi CLI and AWS credentials)
|
||||
</verify>
|
||||
<done>Besom infrastructure code compiles, defines RDS and S3 resources</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Create Dockerfile for containerization</name>
|
||||
<files>Dockerfile</files>
|
||||
<action>
|
||||
Create multi-stage Dockerfile for building and running SummerCMS.
|
||||
|
||||
Dockerfile:
|
||||
```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:
|
||||
```bash
|
||||
docker build -t summercms .
|
||||
docker run -p 8080:8080 \
|
||||
-e DB_HOST=host.docker.internal \
|
||||
-e DB_PASSWORD=secret \
|
||||
summercms
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
`docker build -t summercms .` succeeds
|
||||
`docker run -p 8080:8080 summercms` starts (will fail at DB connect without DB, that's expected)
|
||||
</verify>
|
||||
<done>Dockerfile exists with multi-stage build, health check, and env var configuration</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `mill compile` succeeds (repository code compiles)
|
||||
2. Repository pattern test (manual in mill console or separate test):
|
||||
- UserRepository.findById returns IO[RepositoryError, Option[User]]
|
||||
- Type errors when using wrong types
|
||||
3. Infrastructure:
|
||||
- `cd infra && scala-cli compile .` succeeds
|
||||
- `cd infra && pulumi preview` shows planned resources (requires setup)
|
||||
4. Docker:
|
||||
- `docker build -t summercms .` builds successfully
|
||||
- Image contains /app/summercms.jar
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user