Files
osdb-1-claude-opusplan/docs/planning/specs/04-fruit-publications.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

6.7 KiB

Spec: 04 · Fruit Publications

Taiga Story: #4 · 04-fruit-publications Status: Ready Branch: feature/04-fruit-publications Depends on: 02-manage-fruits (fruits table)


1. Goal

Maintainers can manage publications (books, catalogues) that document fruits. Each publication has a cover image, linked fruits, linked PDFs per fruit, and linked fruit images from the publication. Fruit detail page shows descriptions and publication images.


2. Background / Scope / Context / Constraints

  • osdb_pub_id is the unique external identifier (label: "OSDB Kürzel").
  • Cover image stored as BYTEA in publications.image_data.
  • PDF stored as BYTEA in publication_descriptions.pdf_data (one PDF per fruit per publication).
  • Fruit images from publications stored in publication_fruit_images (BYTEA); image_type hardcoded to 'fruit', cannot be changed.
  • On fruit detail page: show description list only if at least one description exists; link text = publication title; link opens PDF.
  • On fruit detail page: show publication fruit images labeled "Publication title · Author".
  • Mouseclick on cover image → overlay/drawer with full-size image.

3. Necessary Refactorings

  • fruit_images.image_type already defined in story #2 — no changes.
  • thumbnail_data column on publication_fruit_images deferred to story #7 migration.

-- Migration: 000003_create_publications.up.sql

CREATE TABLE publications (
  id           SERIAL PRIMARY KEY,
  title        VARCHAR(255) NOT NULL,
  author       VARCHAR(255),
  osdb_pub_id  VARCHAR(50)  NOT NULL UNIQUE,
  image_data   BYTEA,
  created_at   TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
  updated_at   TIMESTAMPTZ  NOT NULL DEFAULT NOW()
);

CREATE TABLE publication_fruits (
  publication_id INTEGER NOT NULL REFERENCES publications(id) ON DELETE CASCADE,
  fruit_id       INTEGER NOT NULL REFERENCES fruits(id) ON DELETE CASCADE,
  PRIMARY KEY (publication_id, fruit_id)
);

CREATE TABLE publication_descriptions (
  id             SERIAL PRIMARY KEY,
  publication_id INTEGER NOT NULL REFERENCES publications(id) ON DELETE CASCADE,
  fruit_id       INTEGER NOT NULL REFERENCES fruits(id) ON DELETE CASCADE,
  pdf_data       BYTEA   NOT NULL,
  created_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (publication_id, fruit_id)
);

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

Backend

File Purpose
backend/internal/domain/publication.go Publication, PublicationDescription, PublicationFruitImage structs
backend/internal/repository/publication_repo.go DB queries
backend/internal/handler/publication_handler.go Echo handlers
backend/cmd/server/router.go Register /api/v1/publications routes
migrations/000003_create_publications.up.sql Schema above
migrations/000003_create_publications.down.sql DROP TABLEs

API Endpoints

Method Path Description
GET /api/v1/publications List
POST /api/v1/publications Create
GET /api/v1/publications/:id Get (with linked fruits list)
PUT /api/v1/publications/:id Update
DELETE /api/v1/publications/:id Delete
POST /api/v1/publications/:id/image Upload cover image
GET /api/v1/publications/:id/image Serve cover image
DELETE /api/v1/publications/:id/image Delete cover image
GET /api/v1/publications/:id/fruits List linked fruits
POST /api/v1/publications/:id/fruits Link a fruit
DELETE /api/v1/publications/:id/fruits/:fruitId Unlink a fruit
GET /api/v1/publications/:id/descriptions List descriptions (fruit + link metadata)
POST /api/v1/publications/:id/descriptions Upload PDF description (body: fruitId + PDF)
GET /api/v1/publications/:id/descriptions/:descId Serve PDF
DELETE /api/v1/publications/:id/descriptions/:descId Delete
GET /api/v1/publications/:id/fruit-images List pub fruit images
POST /api/v1/publications/:id/fruit-images Upload fruit image (fruitId + image)
GET /api/v1/publications/:id/fruit-images/:imgId Serve image
DELETE /api/v1/publications/:id/fruit-images/:imgId Delete
GET /api/v1/fruits/:id/descriptions All descriptions for a fruit (across publications)
GET /api/v1/fruits/:id/publication-images All pub images for a fruit

Frontend

File Purpose
frontend/src/api/publications.ts API wrapper
frontend/src/stores/publicationStore.ts Pinia store
frontend/src/views/PublicationList.vue List + link to detail
frontend/src/views/PublicationDetail.vue Full management UI
frontend/src/components/ImageOverlayDrawer.vue Full-size image overlay on click
frontend/src/views/FruitDetail.vue Extended: descriptions section + pub images section
frontend/src/router/index.ts Add /publications, /publications/:id

6. Out of Scope

  • Bulk import from XML (story #5)
  • Thumbnail generation (story #7)
  • Authentication guards (story #8)

7. Verification

Check Expected
Create publication with duplicate osdb_pub_id 409 Conflict
Upload cover image → serve back → correct bytes Pass
Link fruit to publication → appears in GET /publications/:id/fruits Pass
Upload PDF description → GET description → Content-Type: application/pdf Pass
Fruit detail page: no descriptions → section hidden Pass
Fruit detail page: 1+ description → links visible Pass
Click cover image thumbnail → overlay opens full-size Pass
Pub fruit images on fruit detail labeled correctly Publication title · Author shown
go test ./... Green
npm run test Green

8. Open Questions

# Question Impact
1 Is one PDF per (publication, fruit) pair enforced (UNIQUE) or allow multiples? Schema + UI
2 Can a fruit image exist in a publication without the fruit being linked via publication_fruits? Data integrity
3 Listing publications: include fruit count or just metadata? API response shape
4 PDF viewer: open in new tab or inline browser PDF viewer? UX