Story #02: Manage Fruits — full CRUD with synonyms and images #2

Merged
julia merged 6 commits from feature/02-manage-fruits into main 2026-06-17 12:46:15 +00:00
Owner

Summary

  • Adds full fruit CRUD: list (paginated), create, view, update, delete
  • Synonyms managed as a flat []string replaced atomically on each PUT
  • Images stored as BYTEA in Postgres; served via content-type sniffing; uploaded via multipart (5 MB cap)
  • Consumer-defined FruitRepository interface in handler package; pg impl injected at router startup
  • 25 Go handler unit tests (in-memory fake repo, no DB) + opt-in integration test; 18 frontend Vitest tests
  • Code review findings fixed: io.ReadAll for image upload, whitespace trim in validation, offset reset on create, cleanup error logging

Test plan

  • make test — all Go handler + integration tests and frontend Vitest tests pass
  • make dev-db && make migrate-up && make run-backend && make run-frontend — dev stack starts
  • Navigate to /fruits — empty list shows, "Neue Frucht" button present
  • Create a fruit with synonyms — appears at top of list without reload
  • Open detail — view/edit mode, synonyms editable, image upload works, image visible
  • Upload > 5 MB file — rejected with 413
  • POST blank name — rejected with 422
  • POST duplicate osdb_number — rejected with 409
  • Delete fruit — cascades synonyms and images, redirects to list

🤖 Generated with Claude Code

## Summary - Adds full fruit CRUD: list (paginated), create, view, update, delete - Synonyms managed as a flat `[]string` replaced atomically on each PUT - Images stored as BYTEA in Postgres; served via content-type sniffing; uploaded via multipart (5 MB cap) - Consumer-defined `FruitRepository` interface in handler package; pg impl injected at router startup - 25 Go handler unit tests (in-memory fake repo, no DB) + opt-in integration test; 18 frontend Vitest tests - Code review findings fixed: `io.ReadAll` for image upload, whitespace trim in validation, offset reset on create, cleanup error logging ## Test plan - [ ] `make test` — all Go handler + integration tests and frontend Vitest tests pass - [ ] `make dev-db && make migrate-up && make run-backend && make run-frontend` — dev stack starts - [ ] Navigate to `/fruits` — empty list shows, "Neue Frucht" button present - [ ] Create a fruit with synonyms — appears at top of list without reload - [ ] Open detail — view/edit mode, synonyms editable, image upload works, image visible - [ ] Upload > 5 MB file — rejected with 413 - [ ] POST blank name — rejected with 422 - [ ] POST duplicate osdb_number — rejected with 409 - [ ] Delete fruit — cascades synonyms and images, redirects to list 🤖 Generated with [Claude Code](https://claude.com/claude-code)
julia added 6 commits 2026-06-17 08:29:24 +00:00
- Go backend: Echo v4 + pgxpool + embedded golang-migrate; health endpoints
  at /health and /api/v1/health; TDD health handler (httptest)
- Vue 3 frontend: Vite + TypeScript + Pinia + vue-router + Tailwind CSS v4;
  TDD HelloWorld component (@vue/test-utils + jsdom + vitest)
- Infra: docker-compose postgres:16 with env-interpolated credentials;
  Makefile with dev-db health-wait loop, migrate-up/down, run, test, fmt
- embed.FS migrations at backend/migrations/ (000001 no-op baseline)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Makefile: -include .env (soft) so fresh clone doesn't hard-fail
- main.go: replace log.Fatalf-in-goroutine with error channel; startup
  failures now reach main goroutine so defer pool.Close() always runs
- main.go: context.WithTimeout(30s) on database.Connect to fail fast
  instead of hanging indefinitely on unreachable Postgres
- router.go: store pool on Echo context via middleware so future story
  handlers can retrieve it with c.Get("pool")
- config.go: require DATABASE_URL explicitly (log.Fatal if missing)
  instead of falling back to hardcoded credentials
- HelloWorld.test.ts: URL-aware fetch stub + afterEach restoreAllMocks;
  add flushPromises test verifying backend status renders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
golang-migrate, pgx/v5, and echo/v4 are all imported directly;
go mod tidy correctly marks them as direct dependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: domain structs, FruitRepository interface + pg implementation,
9 Echo v4 handlers (list/get/create/update/delete, image sub-resources),
migration 000002 (fruit_type ENUM, fruits, fruit_synonyms, fruit_images),
route-scoped BodyLimit("5M") for uploads, http.DetectContentType for serving.

Frontend: typed fetch API layer, Pinia setup-style fruitStore, FruitList
(paginated), FruitCreate, and FruitDetail (edit + synonyms editor + image
gallery). 25 backend unit tests + integration test; 18 frontend tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace src.Read(data) with io.ReadAll(src) to prevent silent truncation on large uploads
- Reject zero-byte file uploads with 400 rather than storing empty BYTEA
- Add strings.TrimSpace to validateFruit so whitespace-only name/osdb_number returns 422
- Reset offset to 0 in fruitStore.create() so navigating back to list after create shows page 1
- Log t.Cleanup DELETE error in integration test to surface constraint failures clearly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
julia added 3 commits 2026-06-17 12:46:06 +00:00
Adds a repeatable Python import script that populates fruits, fruit_synonyms,
and fruit_images from 03-data/obstsorten.xml and the three image directories.
Idempotent: deletes synonyms and images per fruit before re-inserting; upserts
fruit row by osdb_number. Reads DATABASE_URL from env.

- map_typ: 23 XML typ codes → 17-value fruit_type enum; aggregate types coerced
  with logged warning; typ='p' (2 entries) mapped to Pfirsiche
- parse_synonyms: comma-split with whitespace/newline strip, empty filter
- parse_date: YYYYMMDD → date, fallback None → DB default NOW()
- find_images: globs {id}_*_s0.* across einzelfruechte/blueten/baume dirs
- 42 unit tests (no DB required) cover all mapping, parsing, and glob logic
- Makefile test target extended to include Python test suite
- 03-data/ and obstsorten.zip added to .gitignore (large binary data)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- find_images: verify exact fruit_id prefix (stem-parse before _s0.) to
  prevent cross-fruit image contamination (e.g. mitschurins steals
  mitschurins_fruchtbare images)
- DATA_DIR uses os.path.abspath to handle symlinks / relative invocation
- counts["images"] derived from IMAGE_DIRS constant, not hardcoded keys
- counts["warnings"] incremented on missing-id/name skip (was missing)
- UPSERT preserves existing comment (comment = fruits.comment, not NULL)
- Print summary and rollback moved inside try/except to avoid NameError
- Remove dead XML_PATH constant
- Tests: add tearDown to clean up tmpdir; fix vacuous _tn exclusion test
  to include positive-control _s0 file; add prefix-collision regression test

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reviewed-on: #3
julia merged commit 2e3f0f366c into main 2026-06-17 12:46:15 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julia/osdb-1-claude-opusplan#2