docs(17): create phase plan for object storage migration
This commit is contained in:
194
.planning/phases/17-object-storage/17-01-PLAN.md
Normal file
194
.planning/phases/17-object-storage/17-01-PLAN.md
Normal file
@@ -0,0 +1,194 @@
|
||||
---
|
||||
phase: 17-object-storage
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- src/server/services/storage.service.ts
|
||||
- tests/services/storage.service.test.ts
|
||||
- docker-compose.yml
|
||||
- docker-compose.dev.yml
|
||||
- .env.example
|
||||
autonomous: true
|
||||
requirements: [IMG-01, IMG-04]
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Storage service can upload a buffer to S3-compatible storage"
|
||||
- "Storage service can delete an object from S3-compatible storage"
|
||||
- "Storage service can generate a presigned URL for an object"
|
||||
- "Docker Compose starts MinIO with automatic bucket creation"
|
||||
artifacts:
|
||||
- path: "src/server/services/storage.service.ts"
|
||||
provides: "S3 storage abstraction"
|
||||
exports: ["uploadImage", "deleteImage", "getImageUrl"]
|
||||
- path: "tests/services/storage.service.test.ts"
|
||||
provides: "Storage service unit tests with mocked S3Client"
|
||||
- path: "docker-compose.dev.yml"
|
||||
provides: "MinIO service for local development"
|
||||
contains: "minio"
|
||||
- path: "docker-compose.yml"
|
||||
provides: "MinIO service for production"
|
||||
contains: "minio"
|
||||
key_links:
|
||||
- from: "src/server/services/storage.service.ts"
|
||||
to: "@aws-sdk/client-s3"
|
||||
via: "S3Client with forcePathStyle"
|
||||
pattern: "forcePathStyle.*true"
|
||||
- from: "docker-compose.dev.yml"
|
||||
to: "minio-init"
|
||||
via: "mc init container creates bucket"
|
||||
pattern: "mc mb.*gearbox-images"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Create the S3 storage service abstraction and Docker Compose MinIO infrastructure.
|
||||
|
||||
Purpose: Establish the foundation that all subsequent image storage refactoring depends on. The storage service wraps @aws-sdk/client-s3 and the Docker config ensures MinIO is available for development and production.
|
||||
Output: storage.service.ts with uploadImage/deleteImage/getImageUrl, unit tests, MinIO in both Docker Compose files.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/17-object-storage/17-CONTEXT.md
|
||||
@.planning/phases/17-object-storage/17-RESEARCH.md
|
||||
|
||||
@src/server/services/image.service.ts
|
||||
@docker-compose.yml
|
||||
@docker-compose.dev.yml
|
||||
@.env.example
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Install S3 SDK and create storage service</name>
|
||||
<files>src/server/services/storage.service.ts, tests/services/storage.service.test.ts</files>
|
||||
<read_first>
|
||||
- src/server/services/image.service.ts (current local file storage pattern)
|
||||
- .planning/phases/17-object-storage/17-RESEARCH.md (Pattern 1: S3 Client Singleton, Pattern 2: Presigned URL Injection)
|
||||
</read_first>
|
||||
<action>
|
||||
1. Install dependencies: `bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner`
|
||||
|
||||
2. Create `src/server/services/storage.service.ts` per D-03, D-04, D-05:
|
||||
- Create S3Client singleton at module level with `forcePathStyle: true` (REQUIRED for MinIO per research pitfall 4)
|
||||
- Config from env vars: `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`, `S3_BUCKET` (default: "gearbox-images"), `S3_REGION` (default: "us-east-1")
|
||||
- `uploadImage(buffer: Buffer | ArrayBuffer, filename: string, contentType: string): Promise<void>` — uses PutObjectCommand
|
||||
- `deleteImage(filename: string): Promise<void>` — uses DeleteObjectCommand
|
||||
- `getImageUrl(filename: string): Promise<string>` — uses GetObjectCommand + getSignedUrl with configurable expiry (default 1h per D-04, configurable via `S3_PRESIGN_EXPIRY` env var)
|
||||
- Export a helper: `async function withImageUrl<T extends { imageFilename: string | null }>(record: T): Promise<T & { imageUrl: string | null }>` — returns null imageUrl when imageFilename is null, presigned URL otherwise (per D-09)
|
||||
- Export a batch helper: `async function withImageUrls<T extends { imageFilename: string | null }>(records: T[]): Promise<(T & { imageUrl: string | null })[]>` — uses Promise.all for parallelism per research pitfall 5
|
||||
|
||||
3. Create `tests/services/storage.service.test.ts`:
|
||||
- Mock @aws-sdk/client-s3 S3Client.send method using `mock.module` from bun:test
|
||||
- Mock @aws-sdk/s3-request-presigner getSignedUrl
|
||||
- Test uploadImage calls PutObjectCommand with correct Bucket, Key, Body, ContentType
|
||||
- Test deleteImage calls DeleteObjectCommand with correct Bucket, Key
|
||||
- Test getImageUrl calls getSignedUrl and returns the result
|
||||
- Test withImageUrl returns null imageUrl when imageFilename is null
|
||||
- Test withImageUrl returns presigned URL when imageFilename is present
|
||||
- Test withImageUrls processes arrays correctly
|
||||
</action>
|
||||
<verify>
|
||||
<automated>bun test tests/services/storage.service.test.ts</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q "uploadImage" src/server/services/storage.service.ts
|
||||
- grep -q "deleteImage" src/server/services/storage.service.ts
|
||||
- grep -q "getImageUrl" src/server/services/storage.service.ts
|
||||
- grep -q "withImageUrl" src/server/services/storage.service.ts
|
||||
- grep -q "withImageUrls" src/server/services/storage.service.ts
|
||||
- grep -q "forcePathStyle.*true" src/server/services/storage.service.ts
|
||||
- grep -q "S3_ENDPOINT" src/server/services/storage.service.ts
|
||||
- grep -q "PutObjectCommand" src/server/services/storage.service.ts
|
||||
- grep -q "getSignedUrl" src/server/services/storage.service.ts
|
||||
- bun test tests/services/storage.service.test.ts passes
|
||||
</acceptance_criteria>
|
||||
<done>Storage service exports uploadImage, deleteImage, getImageUrl, withImageUrl, withImageUrls. All unit tests pass with mocked S3 client.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add MinIO to Docker Compose and update env config</name>
|
||||
<files>docker-compose.yml, docker-compose.dev.yml, .env.example</files>
|
||||
<read_first>
|
||||
- docker-compose.yml (current production compose)
|
||||
- docker-compose.dev.yml (current dev compose)
|
||||
- .env.example (current env vars)
|
||||
- .planning/phases/17-object-storage/17-RESEARCH.md (Pattern 3: Docker Compose Init Container)
|
||||
</read_first>
|
||||
<action>
|
||||
1. Update `docker-compose.dev.yml` per D-13, D-14:
|
||||
- Add `minio` service using `quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z` (pinned per research — do NOT use latest or Docker Hub)
|
||||
- Command: `server /data --console-address ":9001"`
|
||||
- Environment: `MINIO_ROOT_USER: minioadmin`, `MINIO_ROOT_PASSWORD: minioadmin` (fixed creds for dev per D-14)
|
||||
- Ports: 9000:9000 (API), 9001:9001 (console)
|
||||
- Volume: `minio-data-dev:/data`
|
||||
- Healthcheck: `["CMD", "mc", "ready", "local"]` interval 5s, timeout 3s, retries 5
|
||||
- Add `minio-init` service using `quay.io/minio/mc:latest`
|
||||
- depends_on minio with condition: service_healthy
|
||||
- entrypoint shell script: set alias, create bucket `gearbox-images` with --ignore-existing, exit 0
|
||||
- Add `minio-data-dev` to volumes section
|
||||
- Add S3 env vars to app service (if app service exists in dev compose) — if not, these will be in the shell env
|
||||
- Add comment noting MinIO GitHub repo archived Feb 2026, S3 API abstraction makes provider swappable
|
||||
|
||||
2. Update `docker-compose.yml` (production) per D-13, D-14:
|
||||
- Add `minio` service same image, same healthcheck
|
||||
- Environment: `MINIO_ROOT_USER: ${S3_ACCESS_KEY}`, `MINIO_ROOT_PASSWORD: ${S3_SECRET_KEY}` (env vars for prod per D-14)
|
||||
- Ports: 9000:9000 only (no console in prod)
|
||||
- Volume: `minio-data:/data`
|
||||
- Add `minio-init` same pattern but using `${S3_ACCESS_KEY:-minioadmin}` and `${S3_SECRET_KEY:-minioadmin}` for mc alias
|
||||
- Add S3 env vars to app service: S3_ENDPOINT=http://minio:9000, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET=gearbox-images
|
||||
- Remove `uploads:/app/uploads` volume from app service (per D-08 — no more local file serving)
|
||||
- Remove `uploads` from volumes section
|
||||
- Add `minio-data` to volumes section
|
||||
- app service depends_on should include minio (service_healthy)
|
||||
|
||||
3. Update `.env.example`:
|
||||
- Add S3 section with: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY, S3_BUCKET, S3_REGION
|
||||
- Include defaults as comments (endpoint: http://localhost:9000, bucket: gearbox-images, region: us-east-1)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -q "minio" docker-compose.dev.yml && grep -q "minio" docker-compose.yml && grep -q "S3_ENDPOINT" .env.example && echo "PASS"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- grep -q "quay.io/minio/minio:RELEASE.2025-09-07" docker-compose.dev.yml
|
||||
- grep -q "minio-init" docker-compose.dev.yml
|
||||
- grep -q "gearbox-images" docker-compose.dev.yml
|
||||
- grep -q "quay.io/minio/minio:RELEASE.2025-09-07" docker-compose.yml
|
||||
- grep -q "minio-init" docker-compose.yml
|
||||
- grep -q "S3_ENDPOINT" docker-compose.yml
|
||||
- grep -q "S3_ACCESS_KEY" .env.example
|
||||
- grep -q "S3_SECRET_KEY" .env.example
|
||||
- grep -q "S3_BUCKET" .env.example
|
||||
</acceptance_criteria>
|
||||
<done>Both Docker Compose files include MinIO with automatic bucket creation. Production uses env vars, dev uses fixed credentials. .env.example documents all S3 variables.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
- `bun test tests/services/storage.service.test.ts` passes
|
||||
- `grep -r "forcePathStyle" src/server/services/storage.service.ts` confirms MinIO compatibility
|
||||
- `docker compose -f docker-compose.dev.yml config` validates dev compose syntax
|
||||
- `docker compose config` validates prod compose syntax
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Storage service exists with all 5 exported functions (uploadImage, deleteImage, getImageUrl, withImageUrl, withImageUrls)
|
||||
- Unit tests pass with mocked S3 client
|
||||
- Both Docker Compose files include MinIO + init container
|
||||
- .env.example documents S3 configuration
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/17-object-storage/17-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user