Files
osdb-1-claude-opusplan/docs/planning/specs/02-manage-fruits.md
T
juliaandClaude Sonnet 4.6 1070ba3ac1 docs: add sprint planning — dependency diagram + specs for all 8 stories
Analyzes all Taiga stories, maps hard/soft dependencies, and writes
full specs (Goal, Data Model, Components, Verification, Open Questions)
for every story before any implementation begins.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 11:53:24 +02:00

5.7 KiB

Spec: 02 · Manage Fruits

Taiga Story: #2 · 02-manage-fruits Status: Ready Branch: feature/02-manage-fruits Depends on: 01-Bootstrap


1. Goal

Maintainers can create, read, update, and delete fruits. Each fruit has a name, OSDB Kürzel (unique number), optional comment, synonyms list, a required type, and an images collection.


2. Background / Scope / Context / Constraints

  • "OSDB Kürzel" is the public label for the internal osdb_number field (unique identifier used by legacy system).
  • Fruit type is a fixed enum — no free-form entry.
  • Images have an image type (fruit, flower, tree), a filename, and binary data stored in the DB.
  • Synonyms are a 1:N child table (not a comma-separated string in the parent row).
  • API must support listing, filtering (prepare for story #6), and CRUD.
  • No authentication guard yet (added in story #8); all endpoints are public for now.

Fruit Type Enum

Apfelsorten, Birnensorten, Quittensorten, Aprikosen, Pfirsiche,
Mirabellen, Renekloden, Pflaumen, Zwetschen, Sauerkirschen,
Süßkirschen, Brombeeren, Erdbeeren, Himbeeren, Johannisbeeren,
Stachelbeeren, Wein

3. Necessary Refactorings

None from prior stories (only bootstrap exists). Echo router structure should be established here as GET /api/v1/fruits, POST /api/v1/fruits, etc.


-- Migration: 000002_create_fruits.up.sql

CREATE TYPE fruit_type AS ENUM (
  'Apfelsorten', 'Birnensorten', 'Quittensorten', 'Aprikosen',
  'Pfirsiche', 'Mirabellen', 'Renekloden', 'Pflaumen', 'Zwetschen',
  'Sauerkirschen', 'Süßkirschen', 'Brombeeren', 'Erdbeeren',
  'Himbeeren', 'Johannisbeeren', 'Stachelbeeren', 'Wein'
);

CREATE TABLE fruits (
  id          SERIAL PRIMARY KEY,
  name        VARCHAR(255) NOT NULL,
  osdb_number VARCHAR(50)  NOT NULL UNIQUE,
  comment     TEXT,
  fruit_type  fruit_type   NOT NULL,
  created_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
  updated_at  TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE TABLE fruit_synonyms (
  id       SERIAL PRIMARY KEY,
  fruit_id INTEGER      NOT NULL REFERENCES fruits(id) ON DELETE CASCADE,
  synonym  VARCHAR(255) NOT NULL
);

CREATE TABLE fruit_images (
  id         SERIAL PRIMARY KEY,
  fruit_id   INTEGER      NOT NULL REFERENCES fruits(id) ON DELETE CASCADE,
  filename   VARCHAR(255),
  data       BYTEA        NOT NULL,
  image_type VARCHAR(20)  NOT NULL CHECK (image_type IN ('fruit','flower','tree')),
  title      VARCHAR(255),
  created_at TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);
-- Note: thumbnail_data column added in story #07 migration

Backend

File Purpose
backend/internal/domain/fruit.go Fruit, FruitSynonym, FruitImage structs
backend/internal/repository/fruit_repo.go DB queries (list, get, create, update, delete, images)
backend/internal/handler/fruit_handler.go Echo handlers — request bind/validate, call repo, return JSON
backend/cmd/server/router.go Register /api/v1/fruits route group
migrations/000002_create_fruits.up.sql Schema as above
migrations/000002_create_fruits.down.sql DROP TABLE fruit_images, fruit_synonyms, fruits; DROP TYPE fruit_type

Frontend

File Purpose
frontend/src/api/fruits.ts Axios/fetch wrapper — listFruits, getFruit, createFruit, updateFruit, deleteFruit, addImage, deleteImage
frontend/src/stores/fruitStore.ts Pinia store
frontend/src/views/FruitList.vue Table/card list, search bar (placeholder for #6), link to detail
frontend/src/views/FruitDetail.vue Show + edit form, synonyms editor, image gallery with upload/delete
frontend/src/views/FruitCreate.vue Create form
frontend/src/router/index.ts Add routes: /fruits, /fruits/:id, /fruits/new

API Endpoints

Method Path Description
GET /api/v1/fruits List (query params: name, type)
POST /api/v1/fruits Create
GET /api/v1/fruits/:id Get by ID (includes synonyms)
PUT /api/v1/fruits/:id Full update
DELETE /api/v1/fruits/:id Delete (cascades images+synonyms)
GET /api/v1/fruits/:id/images List images (metadata only, no binary)
POST /api/v1/fruits/:id/images Upload image (multipart)
GET /api/v1/fruits/:id/images/:imageId Serve binary image
DELETE /api/v1/fruits/:id/images/:imageId Delete image

6. Out of Scope

  • Search/filter implementation (story #6)
  • Authentication guards (story #8)
  • Thumbnail generation (story #7)
  • Bulk import (story #3)
  • Publications link (story #4)

7. Verification

Check Expected
POST /api/v1/fruits with missing name 422 Unprocessable Entity
POST /api/v1/fruits with duplicate osdb_number 409 Conflict
GET /api/v1/fruits/:id Returns fruit with synonyms array
POST /api/v1/fruits/:id/images (multipart JPEG) 201 Created; GET image serves correct bytes
DELETE /api/v1/fruits/:id 204; cascades synonyms+images
Frontend: create fruit → appears in list No page reload needed (Pinia reactive)
Frontend: upload image → thumbnail shown Image served from backend
go test ./... All handler + repo tests green
npm run test Vitest tests for store and API wrapper green

8. Open Questions

# Question Impact
1 Image upload max size limit? Multipart config, DB row size
2 Should GET /api/v1/fruits return image URLs or inline base64 thumbnails? Network perf vs simplicity
3 Pagination for fruit list (offset/limit) or full list? UX + performance with large dataset
4 PUT vs PATCH for updates? Client complexity