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:
@@ -32,15 +32,15 @@ Decimal phases appear between their surrounding integers in numeric order.
|
|||||||
**Success Criteria** (what must be TRUE):
|
**Success Criteria** (what must be TRUE):
|
||||||
1. Developer can run `mill run` and see HTTP server responding to requests
|
1. Developer can run `mill run` and see HTTP server responding to requests
|
||||||
2. Database queries execute with compile-time SQL validation via Quill
|
2. Database queries execute with compile-time SQL validation via Quill
|
||||||
3. Database migrations run automatically on application startup
|
3. Database migrations run via Migrator service
|
||||||
4. Models use Repository pattern with ZIO effects for data access
|
4. Models use Repository pattern with ZIO effects for data access
|
||||||
5. Pulumi configuration deploys the application to cloud infrastructure
|
5. Pulumi configuration deploys the application to cloud infrastructure
|
||||||
**Plans**: TBD
|
**Plans**: 3 plans
|
||||||
|
|
||||||
Plans:
|
Plans:
|
||||||
- [ ] 01-01: Mill build setup and ZIO HTTP server
|
- [ ] 01-01-PLAN.md - Mill build setup and ZIO HTTP server with health endpoint
|
||||||
- [ ] 01-02: PostgreSQL integration with Quill and migrations
|
- [ ] 01-02-PLAN.md - PostgreSQL integration with Quill and Flyway migrations
|
||||||
- [ ] 01-03: Repository pattern and Pulumi deployment
|
- [ ] 01-03-PLAN.md - Repository pattern and Pulumi/Besom deployment config
|
||||||
|
|
||||||
### Phase 2: Plugin System
|
### Phase 2: Plugin System
|
||||||
**Goal**: Establish the plugin architecture that all other features build upon
|
**Goal**: Establish the plugin architecture that all other features build upon
|
||||||
@@ -206,7 +206,7 @@ Phases execute in numeric order: 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10
|
|||||||
|
|
||||||
| Phase | Plans Complete | Status | Completed |
|
| Phase | Plans Complete | Status | Completed |
|
||||||
|-------|----------------|--------|-----------|
|
|-------|----------------|--------|-----------|
|
||||||
| 1. Foundation | 0/3 | Not started | - |
|
| 1. Foundation | 0/3 | Planned | - |
|
||||||
| 2. Plugin System | 0/3 | Not started | - |
|
| 2. Plugin System | 0/3 | Not started | - |
|
||||||
| 3. Component System | 0/2 | Not started | - |
|
| 3. Component System | 0/2 | Not started | - |
|
||||||
| 4. Theme Engine | 0/2 | Not started | - |
|
| 4. Theme Engine | 0/2 | Not started | - |
|
||||||
|
|||||||
208
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
208
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- build.mill
|
||||||
|
- src/Main.scala
|
||||||
|
- src/config/AppConfig.scala
|
||||||
|
- src/api/Routes.scala
|
||||||
|
- src/api/HealthRoutes.scala
|
||||||
|
- resources/application.conf
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Developer can run `mill run` and server starts on port 8080"
|
||||||
|
- "GET /health returns 200 with 'ok' body"
|
||||||
|
- "Server reads configuration from application.conf"
|
||||||
|
- "Environment variables override HOCON values"
|
||||||
|
artifacts:
|
||||||
|
- path: "build.mill"
|
||||||
|
provides: "Mill build configuration with ZIO dependencies"
|
||||||
|
contains: "zio-http"
|
||||||
|
- path: "src/Main.scala"
|
||||||
|
provides: "Application entry point extending ZIOAppDefault"
|
||||||
|
exports: ["Main"]
|
||||||
|
- path: "src/config/AppConfig.scala"
|
||||||
|
provides: "Configuration case classes"
|
||||||
|
exports: ["AppConfig", "ServerConfig", "DatabaseConfig"]
|
||||||
|
- path: "src/api/Routes.scala"
|
||||||
|
provides: "Route composition"
|
||||||
|
exports: ["Routes"]
|
||||||
|
- path: "src/api/HealthRoutes.scala"
|
||||||
|
provides: "Health check endpoint"
|
||||||
|
contains: "/health"
|
||||||
|
- path: "resources/application.conf"
|
||||||
|
provides: "HOCON configuration with env overrides"
|
||||||
|
contains: "server.port"
|
||||||
|
key_links:
|
||||||
|
- from: "src/Main.scala"
|
||||||
|
to: "src/api/Routes.scala"
|
||||||
|
via: "Server.serve(routes)"
|
||||||
|
pattern: "Server\\.serve"
|
||||||
|
- from: "src/Main.scala"
|
||||||
|
to: "resources/application.conf"
|
||||||
|
via: "ConfigProvider.fromResourcePath"
|
||||||
|
pattern: "ConfigProvider"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Set up the Mill build system and create a working ZIO HTTP server with health endpoints and HOCON configuration.
|
||||||
|
|
||||||
|
Purpose: Establish the foundational build infrastructure and prove the ZIO HTTP stack works before adding database complexity.
|
||||||
|
Output: Running HTTP server accessible at localhost:8080 with /health endpoint.
|
||||||
|
</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
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create Mill build configuration</name>
|
||||||
|
<files>build.mill</files>
|
||||||
|
<action>
|
||||||
|
Create Mill build file with Scala 3 and ZIO dependencies.
|
||||||
|
|
||||||
|
Use programmatic Mill config (build.mill not YAML) for assembly support:
|
||||||
|
- Scala version: 3.3.4 (LTS, better library compatibility than 3.8.x)
|
||||||
|
- Dependencies:
|
||||||
|
- dev.zio::zio:2.1.14
|
||||||
|
- dev.zio::zio-http:3.0.1 (stable release, not 3.8.x which is unreleased)
|
||||||
|
- dev.zio::zio-config:4.0.2
|
||||||
|
- dev.zio::zio-config-typesafe:4.0.2
|
||||||
|
- dev.zio::zio-config-magnolia:4.0.2
|
||||||
|
- io.getquill::quill-jdbc-zio:4.8.5
|
||||||
|
- org.postgresql:postgresql:42.7.4
|
||||||
|
- org.flywaydb:flyway-core:10.21.0
|
||||||
|
- org.flywaydb:flyway-database-postgresql:10.21.0
|
||||||
|
|
||||||
|
Include assembly configuration for fat JAR with:
|
||||||
|
- Service file merging for META-INF/services
|
||||||
|
- Exclude signature files (*.SF, *.DSA, *.RSA)
|
||||||
|
|
||||||
|
Note: Research suggested newer versions but use stable releases that exist on Maven Central.
|
||||||
|
</action>
|
||||||
|
<verify>Run `mill resolve _` to verify Mill parses the build file correctly</verify>
|
||||||
|
<done>build.mill exists and Mill can parse all dependencies</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create project structure and configuration</name>
|
||||||
|
<files>
|
||||||
|
src/config/AppConfig.scala
|
||||||
|
resources/application.conf
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create configuration infrastructure:
|
||||||
|
|
||||||
|
1. src/config/AppConfig.scala:
|
||||||
|
- Case class AppConfig with server and database nested configs
|
||||||
|
- Case class ServerConfig(host: String, port: Int)
|
||||||
|
- Case class DatabaseConfig(host: String, port: Int, database: String, user: String, password: String)
|
||||||
|
- Use zio-config-magnolia for automatic derivation
|
||||||
|
|
||||||
|
2. resources/application.conf (HOCON):
|
||||||
|
```hocon
|
||||||
|
server {
|
||||||
|
host = "0.0.0.0"
|
||||||
|
host = ${?SERVER_HOST}
|
||||||
|
port = 8080
|
||||||
|
port = ${?SERVER_PORT}
|
||||||
|
}
|
||||||
|
|
||||||
|
database {
|
||||||
|
host = "localhost"
|
||||||
|
host = ${?DB_HOST}
|
||||||
|
port = 5432
|
||||||
|
port = ${?DB_PORT}
|
||||||
|
database = "summercms"
|
||||||
|
database = ${?DB_NAME}
|
||||||
|
user = "summercms"
|
||||||
|
user = ${?DB_USER}
|
||||||
|
password = "summercms"
|
||||||
|
password = ${?DB_PASSWORD}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Pattern: Environment variables override defaults using HOCON substitution syntax.
|
||||||
|
</action>
|
||||||
|
<verify>Files exist at correct paths</verify>
|
||||||
|
<done>AppConfig case classes defined, application.conf has all database and server settings with env overrides</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Create HTTP routes and Main entry point</name>
|
||||||
|
<files>
|
||||||
|
src/api/HealthRoutes.scala
|
||||||
|
src/api/Routes.scala
|
||||||
|
src/Main.scala
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create the HTTP layer:
|
||||||
|
|
||||||
|
1. src/api/HealthRoutes.scala:
|
||||||
|
- Object HealthRoutes with `routes` val returning Routes
|
||||||
|
- GET /health -> Response.text("ok")
|
||||||
|
- Keep simple for now; /ready endpoint added when database exists
|
||||||
|
|
||||||
|
2. src/api/Routes.scala:
|
||||||
|
- Object Routes that composes all route modules
|
||||||
|
- For now, just re-exports HealthRoutes.routes
|
||||||
|
- This is the composition point for future route modules
|
||||||
|
|
||||||
|
3. src/Main.scala:
|
||||||
|
- Object Main extends ZIOAppDefault
|
||||||
|
- Override bootstrap to set ConfigProvider.fromResourcePath()
|
||||||
|
- In run: Server.serve(Routes.routes).provide(Server.defaultWithPort(8080))
|
||||||
|
- Print ASCII art banner on startup (SummerCMS with sun motif)
|
||||||
|
|
||||||
|
ASCII banner example:
|
||||||
|
```
|
||||||
|
\\ | //
|
||||||
|
\\ | //
|
||||||
|
___\\###//___
|
||||||
|
/ SUMMER \\
|
||||||
|
\\ CMS /
|
||||||
|
\\__________/
|
||||||
|
|
||||||
|
Starting on port 8080...
|
||||||
|
```
|
||||||
|
</action>
|
||||||
|
<verify>Run `mill run` and curl localhost:8080/health returns "ok"</verify>
|
||||||
|
<done>Server starts, /health returns 200 with "ok" body, banner displays</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. `mill resolve _` succeeds (build file valid)
|
||||||
|
2. `mill compile` succeeds (code compiles)
|
||||||
|
3. `mill run` starts server (process runs)
|
||||||
|
4. `curl http://localhost:8080/health` returns "ok" (endpoint works)
|
||||||
|
5. Server logs show startup banner
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Mill build configuration exists with all ZIO dependencies
|
||||||
|
- Server starts on port 8080 via `mill run`
|
||||||
|
- GET /health returns 200 OK with "ok" body
|
||||||
|
- Configuration loads from application.conf
|
||||||
|
- ASCII SummerCMS banner displays on startup
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
228
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
228
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
---
|
||||||
|
phase: 01-foundation
|
||||||
|
plan: 02
|
||||||
|
type: execute
|
||||||
|
wave: 2
|
||||||
|
depends_on: ["01-01"]
|
||||||
|
files_modified:
|
||||||
|
- src/db/QuillContext.scala
|
||||||
|
- src/db/Migrator.scala
|
||||||
|
- resources/db/migration/V1__create_summer_users.sql
|
||||||
|
- src/api/HealthRoutes.scala
|
||||||
|
autonomous: true
|
||||||
|
|
||||||
|
must_haves:
|
||||||
|
truths:
|
||||||
|
- "Quill compiles SQL queries at compile time (type errors for bad queries)"
|
||||||
|
- "Flyway migrations run via ZIO effect"
|
||||||
|
- "Migration creates summer_users table in PostgreSQL"
|
||||||
|
- "GET /ready returns 200 when database is connected, 503 when not"
|
||||||
|
artifacts:
|
||||||
|
- path: "src/db/QuillContext.scala"
|
||||||
|
provides: "Quill PostgreSQL context with ZIO integration"
|
||||||
|
exports: ["quillLayer", "dataSourceLayer"]
|
||||||
|
- path: "src/db/Migrator.scala"
|
||||||
|
provides: "Flyway wrapper as ZIO service"
|
||||||
|
exports: ["Migrator", "MigrationStatus"]
|
||||||
|
- path: "resources/db/migration/V1__create_summer_users.sql"
|
||||||
|
provides: "Initial database schema"
|
||||||
|
contains: "CREATE TABLE summer_users"
|
||||||
|
key_links:
|
||||||
|
- from: "src/db/QuillContext.scala"
|
||||||
|
to: "resources/application.conf"
|
||||||
|
via: "Quill.DataSource.fromPrefix"
|
||||||
|
pattern: "fromPrefix.*database"
|
||||||
|
- from: "src/db/Migrator.scala"
|
||||||
|
to: "src/db/QuillContext.scala"
|
||||||
|
via: "ZLayer dependency on DataSource"
|
||||||
|
pattern: "DataSource"
|
||||||
|
- from: "src/api/HealthRoutes.scala"
|
||||||
|
to: "src/db/QuillContext.scala"
|
||||||
|
via: "/ready endpoint checks connection"
|
||||||
|
pattern: "/ready"
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
Integrate PostgreSQL via Quill with compile-time SQL validation and Flyway migrations wrapped in ZIO effects.
|
||||||
|
|
||||||
|
Purpose: Establish database connectivity with type-safe queries and version-controlled schema changes.
|
||||||
|
Output: Working database layer with migrations and a /ready endpoint that verifies connectivity.
|
||||||
|
</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
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Create Quill PostgreSQL context</name>
|
||||||
|
<files>src/db/QuillContext.scala</files>
|
||||||
|
<action>
|
||||||
|
Create Quill context for PostgreSQL with ZIO integration.
|
||||||
|
|
||||||
|
src/db/QuillContext.scala:
|
||||||
|
- Import io.getquill.* and io.getquill.jdbczio.Quill
|
||||||
|
- Create dataSourceLayer: ZLayer[Any, Throwable, javax.sql.DataSource]
|
||||||
|
- Use Quill.DataSource.fromPrefix("database") to read from HOCON config
|
||||||
|
- Create quillLayer: ZLayer[javax.sql.DataSource, Nothing, Quill.Postgres[SnakeCase]]
|
||||||
|
- Use Quill.Postgres.fromNamingStrategy(SnakeCase)
|
||||||
|
- Export combined layer: dataSourceLayer >>> quillLayer
|
||||||
|
|
||||||
|
IMPORTANT: Never use "io" as a variable name in this file (Quill package conflict).
|
||||||
|
|
||||||
|
Configuration note: Quill reads HikariCP config from the "database" prefix in application.conf.
|
||||||
|
The existing database config structure should work, but may need adjustment to match HikariCP's expected format:
|
||||||
|
```hocon
|
||||||
|
database {
|
||||||
|
dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource"
|
||||||
|
dataSource {
|
||||||
|
serverName = "localhost"
|
||||||
|
portNumber = 5432
|
||||||
|
databaseName = "summercms"
|
||||||
|
user = "summercms"
|
||||||
|
password = "summercms"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update resources/application.conf to use HikariCP-compatible format if needed.
|
||||||
|
</action>
|
||||||
|
<verify>`mill compile` succeeds with Quill context</verify>
|
||||||
|
<done>QuillContext.scala exports dataSourceLayer and quillLayer, compiles without errors</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Create Flyway migrator service</name>
|
||||||
|
<files>
|
||||||
|
src/db/Migrator.scala
|
||||||
|
resources/db/migration/V1__create_summer_users.sql
|
||||||
|
</files>
|
||||||
|
<action>
|
||||||
|
Create Flyway wrapper as ZIO service and initial migration.
|
||||||
|
|
||||||
|
1. src/db/Migrator.scala:
|
||||||
|
- Case class MigrationStatus(current: Option[String], pending: Int)
|
||||||
|
- Trait Migrator with methods:
|
||||||
|
- def migrate: Task[Int] (returns count of migrations run)
|
||||||
|
- def status: Task[MigrationStatus]
|
||||||
|
- Object Migrator with:
|
||||||
|
- val live: ZLayer[javax.sql.DataSource, Nothing, Migrator]
|
||||||
|
- Implementation configures Flyway with:
|
||||||
|
- dataSource from layer
|
||||||
|
- locations: "classpath:db/migration"
|
||||||
|
- table: "summer_migrations" (not default flyway_schema_history)
|
||||||
|
- baselineOnMigrate: true (for existing databases)
|
||||||
|
|
||||||
|
2. resources/db/migration/V1__create_summer_users.sql:
|
||||||
|
```sql
|
||||||
|
-- Summer CMS initial schema
|
||||||
|
-- Creates the base users table for future auth
|
||||||
|
|
||||||
|
CREATE TABLE summer_users (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
password_hash VARCHAR(255) NOT NULL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_summer_users_email ON summer_users(email);
|
||||||
|
|
||||||
|
-- Trigger for updated_at
|
||||||
|
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.updated_at = NOW();
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ language 'plpgsql';
|
||||||
|
|
||||||
|
CREATE TRIGGER update_summer_users_updated_at
|
||||||
|
BEFORE UPDATE ON summer_users
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION update_updated_at_column();
|
||||||
|
```
|
||||||
|
|
||||||
|
Naming follows conventions from CONTEXT.md:
|
||||||
|
- snake_case columns
|
||||||
|
- summer_ prefix for core tables
|
||||||
|
- id, created_at, updated_at by default
|
||||||
|
</action>
|
||||||
|
<verify>`mill compile` succeeds, migration file exists at correct path</verify>
|
||||||
|
<done>Migrator service defined with migrate/status methods, V1 migration creates summer_users</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Add /ready endpoint with database check</name>
|
||||||
|
<files>src/api/HealthRoutes.scala</files>
|
||||||
|
<action>
|
||||||
|
Update HealthRoutes to include database connectivity check.
|
||||||
|
|
||||||
|
Modify src/api/HealthRoutes.scala:
|
||||||
|
- Keep existing GET /health -> Response.text("ok") (always 200)
|
||||||
|
- Add GET /ready endpoint:
|
||||||
|
- Requires DataSource from ZLayer
|
||||||
|
- Attempts: ZIO.attempt(ds.getConnection.close())
|
||||||
|
- Success: Response.text("ready") with 200
|
||||||
|
- Failure: Response.status(Status.ServiceUnavailable) with 503
|
||||||
|
|
||||||
|
Pattern:
|
||||||
|
```scala
|
||||||
|
Method.GET / "ready" -> handler {
|
||||||
|
ZIO.serviceWithZIO[javax.sql.DataSource] { ds =>
|
||||||
|
ZIO.attempt {
|
||||||
|
val conn = ds.getConnection
|
||||||
|
conn.close()
|
||||||
|
}.as(Response.text("ready"))
|
||||||
|
.catchAll(_ => ZIO.succeed(Response.status(Status.ServiceUnavailable)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Update Main.scala to:
|
||||||
|
- Provide dataSourceLayer to the server
|
||||||
|
- On startup, run Migrator.migrate and log result
|
||||||
|
- Startup sequence: migrate -> start server
|
||||||
|
|
||||||
|
Note: The /ready endpoint will return 503 if no database is running, which is correct behavior.
|
||||||
|
</action>
|
||||||
|
<verify>
|
||||||
|
With PostgreSQL running: `curl localhost:8080/ready` returns "ready"
|
||||||
|
Without PostgreSQL: `curl localhost:8080/ready` returns 503
|
||||||
|
</verify>
|
||||||
|
<done>/ready endpoint exists, returns 200 when DB connected, 503 when not</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
1. `mill compile` succeeds (Quill context compiles)
|
||||||
|
2. With PostgreSQL running:
|
||||||
|
- `mill run` starts and runs migrations
|
||||||
|
- Logs show "Applied N migrations"
|
||||||
|
- `curl localhost:8080/ready` returns "ready"
|
||||||
|
3. Check database: `psql -d summercms -c "\\dt"` shows summer_users table
|
||||||
|
4. Check migrations table: `psql -d summercms -c "SELECT * FROM summer_migrations"`
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Quill context exists with PostgreSQL configuration
|
||||||
|
- Flyway migrator runs as ZIO effect on startup
|
||||||
|
- V1 migration creates summer_users table with proper schema
|
||||||
|
- /ready endpoint verifies database connectivity
|
||||||
|
- Compile-time SQL validation works (intentional bad query causes compile error)
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
|
||||||
|
</output>
|
||||||
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