9.6 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 17-object-storage | 01 | execute | 1 |
|
true |
|
|
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.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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
Task 1: Install S3 SDK and create storage service src/server/services/storage.service.ts, tests/services/storage.service.test.ts - 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) 1. Install dependencies: `bun add @aws-sdk/client-s3 @aws-sdk/s3-request-presigner`-
Create
src/server/services/storage.service.tsper 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 PutObjectCommanddeleteImage(filename: string): Promise<void>— uses DeleteObjectCommandgetImageUrl(filename: string): Promise<string>— uses GetObjectCommand + getSignedUrl with configurable expiry (default 1h per D-04, configurable viaS3_PRESIGN_EXPIRYenv 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
- Create S3Client singleton at module level with
-
Create
tests/services/storage.service.test.ts:- Mock @aws-sdk/client-s3 S3Client.send method using
mock.modulefrom 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 bun test tests/services/storage.service.test.ts <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> Storage service exports uploadImage, deleteImage, getImageUrl, withImageUrl, withImageUrls. All unit tests pass with mocked S3 client.
- Mock @aws-sdk/client-s3 S3Client.send method using
-
Update
docker-compose.yml(production) per D-13, D-14:- Add
minioservice 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-initsame 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/uploadsvolume from app service (per D-08 — no more local file serving) - Remove
uploadsfrom volumes section - Add
minio-datato volumes section - app service depends_on should include minio (service_healthy)
- Add
-
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) grep -q "minio" docker-compose.dev.yml && grep -q "minio" docker-compose.yml && grep -q "S3_ENDPOINT" .env.example && echo "PASS" <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> Both Docker Compose files include MinIO with automatic bucket creation. Production uses env vars, dev uses fixed credentials. .env.example documents all S3 variables.
<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>