Phase 1-3: Database layer cleanup - Remove 5 backward compatibility VIEWs (ebooks, ebook_ratings, etc.) - Remove all ebook-specific database queries - Add new admin media-items queries (Create, Update, Delete) - Fix sqlc.yaml to point to schema.sql file - Regenerate database code successfully Phase 4: Remove old ebook handlers - Remove all 23 ebook handler functions: * ListEbooks, GetEbook, CreateEbook, UpdateEbook, DeleteEbook * GetEbookRating, CreateOrUpdateEbookRating, DeleteEbookRating, GetEbookRatings * GetEbookNotes, CreateEbookNote, GetEbookNote, UpdateEbookNote, DeleteEbookNote * GetEbookHighlights, CreateEbookHighlight, GetEbookHighlight, UpdateEbookHighlight, DeleteEbookHighlight * GetReadingProgress, UpdateReadingProgress - Remove ebook request types (CreateEbookRequest, UpdateEbookRequest, etc.) Phase 5: Add new admin media-items handlers - CreateMediaItem (admin only, requires library_id) - UpdateMediaItem (admin only) - DeleteMediaItem (admin only) - Add CreateMediaItemRequest, UpdateMediaItemRequest types - All use MustGetAuthenticatedUser for safe context access - Validate admin role before allowing operations - Validate library exists before creating items Phase 6: Update routes - Remove ALL /api/ebooks routes from SetupRoutes() - Remove ebook progress, rating, notes, highlights routes - Add admin.POST/PUT/DELETE /api/media-items routes - Keep all media-items, scanner, and watch mode routes intact Result: Unified API with only /api/media-items endpoints - All features preserved (filtering, sorting, searching) - Better features than old ebook system (more fields, library scoping) - Cleaner codebase with single system - All code compiles successfully Breaking Change: /api/ebooks endpoints removed (use /api/media-items instead) Status: 85% complete (Phases 1-6 done, Phases 7-8 pending: tests + rebuild) Tests: Need update (rename Ebooks → MediaItems, update API paths) Build: Need rebuild with clean cache
513 lines
18 KiB
Markdown
513 lines
18 KiB
Markdown
# Remove Ebooks System - Complete Migration to Media-Items
|
|
|
|
**Date:** January 30, 2026
|
|
**Status:** Ready to Execute
|
|
**Approach:** Complete removal (not deprecation)
|
|
|
|
## Database State Analysis
|
|
|
|
### Good News: Database is Already Clean!
|
|
|
|
The database schema **already uses media_items as the single source of truth**:
|
|
|
|
- ❌ **NO `ebooks` table exists** - it's a VIEW, not a table
|
|
- ✅ `media_items` table has ALL ebook fields PLUS more:
|
|
- All ebook fields: title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors
|
|
- **Additional fields not in old ebook system:** language, edition, page_count, goodreads_id, openlibrary_id, google_books_id, copyright_year, genre, subjects
|
|
- ✅ All user content already uses `media_item_id`:
|
|
- reading_progress (media_item_id)
|
|
- media_ratings (media_item_id)
|
|
- media_notes (media_item_id)
|
|
- media_highlights (media_item_id)
|
|
|
|
### What Exists (to be removed):
|
|
|
|
**Views to Delete:**
|
|
```sql
|
|
-- These are backward compatibility views - no longer needed
|
|
CREATE VIEW ebooks AS ...;
|
|
CREATE VIEW ebook_ratings AS ...;
|
|
CREATE VIEW ebook_reading_progress AS ...;
|
|
CREATE VIEW ebook_notes AS ...;
|
|
CREATE VIEW ebook_highlights AS ...;
|
|
```
|
|
|
|
**Database Queries to Remove:**
|
|
- All `GetEbook*`, `ListEbooks`, `CreateEbook`, `UpdateEbook`, `DeleteEbook` queries
|
|
- These query the VIEW, we should query media_items directly
|
|
|
|
## Code Changes Required
|
|
|
|
### Phase 1: Database Schema Cleanup
|
|
|
|
**File:** `database/schema/schema.sql`
|
|
|
|
**Actions:**
|
|
1. Remove 5 backward compatibility VIEW definitions (lines 124-217)
|
|
2. Keep all media_items tables and indexes
|
|
3. No migration needed - views are just queries, not data
|
|
|
|
**SQL to Remove:**
|
|
```sql
|
|
-- Lines 124-130: ebooks view
|
|
CREATE VIEW ebooks AS ...;
|
|
|
|
-- Lines 143-151: ebook_reading_progress view
|
|
CREATE VIEW ebook_reading_progress AS ...;
|
|
|
|
-- Lines 189-197: ebook_ratings view
|
|
CREATE VIEW ebook_ratings AS ...;
|
|
|
|
-- Lines 199-207: ebook_notes view
|
|
CREATE VIEW ebook_notes AS ...;
|
|
|
|
-- Lines 209-217: ebook_highlights view
|
|
CREATE VIEW ebook_highlights AS ...;
|
|
```
|
|
|
|
### Phase 2: Remove Database Queries
|
|
|
|
**File:** `internal/database/queries/queries.sql`
|
|
|
|
**Queries to Remove:**
|
|
- `GetEbook` - use `GetMediaItem` instead
|
|
- `ListEbooks` - use `ListMediaItems` or `ListMediaItemsFiltered` instead
|
|
- `CreateEbook` - replace with `CreateMediaItem`
|
|
- `UpdateEbook` - replace with `UpdateMediaItem`
|
|
- `DeleteEbook` - replace with `DeleteMediaItem`
|
|
- `GetEbookLibraryID` - no longer needed
|
|
- `GetEbookRating` - use `GetMediaRating` instead
|
|
- `CreateEbookRating` - use `CreateMediaRating` instead
|
|
- `DeleteEbookRating` - use `DeleteMediaRating` instead
|
|
- `GetEbookRatings` - use query on media_ratings instead
|
|
- `GetEbookNotes` - use `GetMediaNotes` instead
|
|
- `CreateEbookNote` - use `CreateMediaNote` instead
|
|
- `GetEbookNote` - use `GetMediaNote` instead
|
|
- `UpdateEbookNote` - use `UpdateMediaNote` instead
|
|
- `DeleteEbookNote` - use `DeleteMediaNote` instead
|
|
- `GetEbookHighlights` - use `GetMediaHighlights` instead
|
|
- `CreateEbookHighlight` - use `CreateMediaHighlight` instead
|
|
- `GetEbookHighlight` - use `GetMediaHighlight` instead
|
|
- `UpdateEbookHighlight` - use `UpdateMediaHighlight` instead
|
|
- `DeleteEbookHighlight` - use `DeleteMediaHighlight` instead
|
|
|
|
**Queries to Add:**
|
|
- `CreateMediaItem` - insert into media_items with library_id
|
|
- `UpdateMediaItem` - update media_items
|
|
- `DeleteMediaItem` - delete from media_items
|
|
|
|
### Phase 3: Remove API Endpoints
|
|
|
|
**File:** `internal/handlers/ebook.go`
|
|
|
|
**Routes to Remove from SetupRoutes():**
|
|
```go
|
|
// Remove ALL of these:
|
|
g.GET("/ebooks", h.ListEbooks)
|
|
g.GET("/ebooks/:id", h.GetEbook)
|
|
g.GET("/ebooks/:id/progress", h.GetReadingProgress)
|
|
g.PUT("/ebooks/:id/progress", h.UpdateReadingProgress)
|
|
g.GET("/ebooks/:id/rating", h.GetEbookRating)
|
|
g.POST("/ebooks/:id/rating", h.CreateOrUpdateEbookRating)
|
|
g.PUT("/ebooks/:id/rating", h.CreateOrUpdateEbookRating)
|
|
g.DELETE("/ebooks/:id/rating", h.DeleteEbookRating)
|
|
g.GET("/ebooks/:id/ratings", h.GetEbookRatings)
|
|
g.GET("/ebooks/:id/notes", h.GetEbookNotes)
|
|
g.POST("/ebooks/:id/notes", h.CreateEbookNote)
|
|
g.GET("/ebooks/:id/notes/:noteId", h.GetEbookNote)
|
|
g.PUT("/ebooks/:id/notes/:noteId", h.UpdateEbookNote)
|
|
g.DELETE("/ebooks/:id/notes/:noteId", h.DeleteEbookNote)
|
|
g.GET("/ebooks/:id/highlights", h.GetEbookHighlights)
|
|
g.POST("/ebooks/:id/highlights", h.CreateEbookHighlight)
|
|
g.GET("/ebooks/:id/highlights/:highlightId", h.GetEbookHighlight)
|
|
g.PUT("/ebooks/:id/highlights/:highlightId", h.UpdateEbookHighlight)
|
|
g.DELETE("/ebooks/:id/highlights/:highlightId", h.DeleteEbookHighlight)
|
|
admin.POST("/ebooks", h.CreateEbook)
|
|
admin.PUT("/ebooks/:id", h.UpdateEbook)
|
|
admin.DELETE("/ebooks", h.DeleteEbook)
|
|
```
|
|
|
|
**Routes to Add:**
|
|
```go
|
|
// Add these admin routes:
|
|
admin.POST("/media-items", h.CreateMediaItem)
|
|
admin.PUT("/media-items/:id", h.UpdateMediaItem)
|
|
admin.DELETE("/media-items/:id", h.DeleteMediaItem)
|
|
```
|
|
|
|
**Handlers to Remove:**
|
|
- `ListEbooks` - use ListMediaItems
|
|
- `GetEbook` - use GetMediaItem
|
|
- `CreateEbook` - replace with CreateMediaItem
|
|
- `UpdateEbook` - replace with UpdateMediaItem
|
|
- `DeleteEbook` - replace with DeleteMediaItem
|
|
- `GetEbookRating` - use GetMediaRating
|
|
- `CreateOrUpdateEbookRating` - use CreateMediaRating
|
|
- `DeleteEbookRating` - use DeleteMediaRating
|
|
- `GetEbookRatings` - use query on media_ratings
|
|
- `GetEbookNotes` - use GetMediaNotes
|
|
- `CreateEbookNote` - use CreateMediaNote
|
|
- `GetEbookNote` - use GetMediaNote
|
|
- `UpdateEbookNote` - use UpdateMediaNote
|
|
- `DeleteEbookNote` - use DeleteMediaNote
|
|
- `GetEbookHighlights` - use GetMediaHighlights
|
|
- `CreateEbookHighlight` - use CreateMediaHighlight
|
|
- `GetEbookHighlight` - use GetMediaHighlight
|
|
- `UpdateEbookHighlight` - use UpdateMediaHighlight
|
|
- `DeleteEbookHighlight` - use DeleteMediaHighlight
|
|
|
|
**Handlers to Add:**
|
|
- `CreateMediaItem(c echo.Context) error` - admin only, requires library_id
|
|
- `UpdateMediaItem(c echo.Context) error` - admin only
|
|
- `DeleteMediaItem(c echo.Context) error` - admin only
|
|
|
|
### Phase 4: Update Integration Tests
|
|
|
|
**File:** `cmd/server/tests/integration_test.go`
|
|
|
|
**Test Cases to Update:**
|
|
- Rename all "Ebooks" test cases to "MediaItems"
|
|
- Update API paths from `/api/ebooks` to `/api/media-items`
|
|
- Update response field names if needed
|
|
- Test the 3 new admin endpoints (Create, Update, Delete)
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Schema Cleanup (5 minutes)
|
|
```bash
|
|
# Edit database/schema/schema.sql
|
|
# Remove lines 124-217 (5 VIEW definitions)
|
|
# Keep all media_items tables
|
|
```
|
|
|
|
### Step 2: Update Database Queries (20 minutes)
|
|
```bash
|
|
# Regenerate database code
|
|
cd internal/database
|
|
sqlc generate
|
|
```
|
|
|
|
Need to create these new queries first in `queries.sql`:
|
|
```sql
|
|
-- Create media item
|
|
CREATE OR REPLACE FUNCTION CreateMediaItem(
|
|
p_library_id UUID,
|
|
p_title VARCHAR(255),
|
|
p_author VARCHAR(255),
|
|
p_isbn VARCHAR(17),
|
|
p_description TEXT,
|
|
p_file_path VARCHAR(500),
|
|
p_file_size BIGINT,
|
|
p_mime_type VARCHAR(100),
|
|
p_cover_image_path VARCHAR(500),
|
|
p_series VARCHAR(255),
|
|
p_series_number INTEGER,
|
|
p_tags TEXT,
|
|
p_asin VARCHAR(20),
|
|
p_date_published DATE,
|
|
p_publisher VARCHAR(255),
|
|
p_contributors TEXT,
|
|
p_language VARCHAR(10),
|
|
p_edition VARCHAR(255),
|
|
p_page_count INTEGER,
|
|
p_goodreads_id VARCHAR(20),
|
|
p_openlibrary_id VARCHAR(100),
|
|
p_google_books_id VARCHAR(100),
|
|
p_copyright_year INTEGER,
|
|
p_genre VARCHAR(100),
|
|
p_subjects TEXT[],
|
|
p_added_by_admin_id UUID
|
|
) RETURNS UUID AS $$
|
|
DECLARE
|
|
v_media_item_id UUID;
|
|
BEGIN
|
|
INSERT INTO media_items (
|
|
library_id, title, author, isbn, description, file_path, file_size,
|
|
mime_type, cover_image_path, series, series_number, tags, asin,
|
|
date_published, publisher, contributors, language, edition, page_count,
|
|
goodreads_id, openlibrary_id, google_books_id, copyright_year,
|
|
genre, subjects, added_by_admin_id
|
|
) VALUES (
|
|
p_library_id, p_title, p_author, p_isbn, p_description, p_file_path, p_file_size,
|
|
p_mime_type, p_cover_image_path, p_series, p_series_number, p_tags, p_asin,
|
|
p_date_published, p_publisher, p_contributors, p_language, p_edition, p_page_count,
|
|
p_goodreads_id, p_openlibrary_id, p_google_books_id, p_copyright_year,
|
|
p_genre, p_subjects, p_added_by_admin_id
|
|
) RETURNING id INTO v_media_item_id;
|
|
|
|
RETURN v_media_item_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Update media item
|
|
CREATE OR REPLACE FUNCTION UpdateMediaItem(
|
|
p_id UUID,
|
|
p_title VARCHAR(255),
|
|
p_author VARCHAR(255),
|
|
p_isbn VARCHAR(17),
|
|
p_description TEXT,
|
|
p_cover_image_path VARCHAR(500),
|
|
p_series VARCHAR(255),
|
|
p_series_number INTEGER,
|
|
p_tags TEXT,
|
|
p_asin VARCHAR(20),
|
|
p_date_published DATE,
|
|
p_publisher VARCHAR(255),
|
|
p_contributors TEXT,
|
|
p_language VARCHAR(10),
|
|
p_edition VARCHAR(255),
|
|
p_page_count INTEGER,
|
|
p_goodreads_id VARCHAR(20),
|
|
p_openlibrary_id VARCHAR(100),
|
|
p_google_books_id VARCHAR(100),
|
|
p_copyright_year INTEGER,
|
|
p_genre VARCHAR(100),
|
|
p_subjects TEXT[]
|
|
) RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
UPDATE media_items SET
|
|
title = p_title,
|
|
author = p_author,
|
|
isbn = p_isbn,
|
|
description = p_description,
|
|
cover_image_path = p_cover_image_path,
|
|
series = p_series,
|
|
series_number = p_series_number,
|
|
tags = p_tags,
|
|
asin = p_asin,
|
|
date_published = p_date_published,
|
|
publisher = p_publisher,
|
|
contributors = p_contributors,
|
|
language = p_language,
|
|
edition = p_edition,
|
|
page_count = p_page_count,
|
|
goodreads_id = p_goodreads_id,
|
|
openlibrary_id = p_openlibrary_id,
|
|
google_books_id = p_google_books_id,
|
|
copyright_year = p_copyright_year,
|
|
genre = p_genre,
|
|
subjects = p_subjects,
|
|
updated_at = NOW()
|
|
WHERE id = p_id;
|
|
|
|
RETURN FOUND;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- Delete media item
|
|
CREATE OR REPLACE FUNCTION DeleteMediaItem(p_id UUID) RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
DELETE FROM media_items WHERE id = p_id;
|
|
RETURN FOUND;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
```
|
|
|
|
### Step 3: Remove Old Handlers (30 minutes)
|
|
```bash
|
|
# Delete these handler functions from internal/handlers/ebook.go:
|
|
# - ListEbooks
|
|
# - GetEbook
|
|
# - CreateEbook
|
|
# - UpdateEbook
|
|
# - DeleteEbook
|
|
# - GetEbookRating, CreateOrUpdateEbookRating, DeleteEbookRating, GetEbookRatings
|
|
# - GetEbookNotes, CreateEbookNote, GetEbookNote, UpdateEbookNote, DeleteEbookNote
|
|
# - GetEbookHighlights, CreateEbookHighlight, GetEbookHighlight, UpdateEbookHighlight, DeleteEbookHighlight
|
|
```
|
|
|
|
### Step 4: Add New Admin Handlers (20 minutes)
|
|
```go
|
|
// CreateMediaItem - admin only, requires library_id
|
|
func (h *Handler) CreateMediaItem(c echo.Context) error {
|
|
user := MustGetAuthenticatedUser(c)
|
|
|
|
// Verify user is admin
|
|
if user.Role != "admin" {
|
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
|
|
}
|
|
|
|
var req CreateMediaItemRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
|
}
|
|
if err := c.Validate(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
// Validate library exists
|
|
_, err := h.db.GetLibrary(c.Request().Context(), req.LibraryID)
|
|
if err != nil {
|
|
if err == pgx.ErrNoRows {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "library not found"})
|
|
}
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
mediaItemID, err := h.db.CreateMediaItem(c.Request().Context(), database.CreateMediaItemParams{
|
|
LibraryID: req.LibraryID,
|
|
Title: req.Title,
|
|
Author: pgtype.Text{String: req.Author, Valid: req.Author != ""},
|
|
Isbn: req.ISBN,
|
|
Description: pgtype.Text{String: req.Description, Valid: req.Description != ""},
|
|
FilePath: req.FilePath,
|
|
FileSize: pgtype.Int8{Int64: req.FileSize, Valid: req.FileSize > 0},
|
|
MimeType: pgtype.Text{String: req.MimeType, Valid: req.MimeType != ""},
|
|
CoverImagePath: pgtype.Text{String: req.CoverImagePath, Valid: req.CoverImagePath != ""},
|
|
Series: pgtype.Text{String: req.Series, Valid: req.Series != ""},
|
|
SeriesNumber: pgtype.Int4{Int32: req.SeriesNumber, Valid: req.SeriesNumber > 0},
|
|
Tags: pgtype.Text{String: req.Tags, Valid: req.Tags != ""},
|
|
Asin: pgtype.Text{String: req.ASIN, Valid: req.ASIN != ""},
|
|
DatePublished: pgtype.Date{Time: parseDate(req.DatePublished), Valid: req.DatePublished != ""},
|
|
Publisher: pgtype.Text{String: req.Publisher, Valid: req.Publisher != ""},
|
|
Contributors: pgtype.Text{String: req.Contributors, Valid: req.Contributors != ""},
|
|
AddedByAdminID: user.ID,
|
|
})
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
// Fetch the created item
|
|
item, err := h.db.GetMediaItem(c.Request().Context(), mediaItemID)
|
|
if err != nil {
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
}
|
|
|
|
return c.JSON(http.StatusCreated, item)
|
|
}
|
|
|
|
type CreateMediaItemRequest struct {
|
|
LibraryID uuid.UUID `json:"library_id" validate:"required"`
|
|
Title string `json:"title" validate:"required,min=1,max=500"`
|
|
Author string `json:"author"`
|
|
ISBN string `json:"isbn"`
|
|
Description string `json:"description"`
|
|
FilePath string `json:"file_path" validate:"required"`
|
|
FileSize int64 `json:"file_size" validate:"required,min=1"`
|
|
MimeType string `json:"mime_type" validate:"required"`
|
|
CoverImagePath string `json:"cover_image_path"`
|
|
Series string `json:"series"`
|
|
SeriesNumber int32 `json:"series_number"`
|
|
Tags string `json:"tags"`
|
|
ASIN string `json:"asin"`
|
|
DatePublished string `json:"date_published"`
|
|
Publisher string `json:"publisher"`
|
|
Contributors string `json:"contributors"`
|
|
}
|
|
|
|
// UpdateMediaItem and DeleteMediaItem similar pattern
|
|
```
|
|
|
|
### Step 5: Update SetupRoutes (5 minutes)
|
|
```go
|
|
func SetupRoutes(g *echo.Group, db *database.Queries) *Handler {
|
|
h := NewHandler(db)
|
|
|
|
// Media item routes (all authenticated users)
|
|
g.GET("/media-items", h.ListMediaItems)
|
|
g.GET("/media-items/filtered", h.ListMediaItemsFiltered)
|
|
g.GET("/media-items/search", h.SearchMediaItems)
|
|
g.GET("/media-items/:id", h.GetMediaItem)
|
|
|
|
// Admin-only routes
|
|
admin := g.Group("", AdminMiddleware)
|
|
admin.POST("/media-items", h.CreateMediaItem)
|
|
admin.PUT("/media-items/:id", h.UpdateMediaItem)
|
|
admin.DELETE("/media-items/:id", h.DeleteMediaItem)
|
|
|
|
// ... rest of routes (ratings, progress, notes, highlights, scanner)
|
|
|
|
return h
|
|
}
|
|
```
|
|
|
|
### Step 6: Update Tests (20 minutes)
|
|
```bash
|
|
# Rename test cases from "Ebooks" to "MediaItems"
|
|
# Update API paths
|
|
# Test new admin endpoints
|
|
```
|
|
|
|
### Step 7: Rebuild and Test (10 minutes)
|
|
```bash
|
|
podman compose down -v
|
|
podman volume prune -f
|
|
podman compose build --no-cache
|
|
podman compose up -d
|
|
go test -v ./cmd/server/tests -run TestIntegrationAPI
|
|
```
|
|
|
|
## Files to Modify
|
|
|
|
1. `database/schema/schema.sql` - Remove 5 VIEW definitions
|
|
2. `internal/database/queries/queries.sql` - Remove ebook queries, add media items queries
|
|
3. `internal/handlers/ebook.go` - Remove ebook handlers, add media items admin handlers
|
|
4. `cmd/server/tests/integration_test.go` - Update test cases
|
|
5. `API_TESTING_SUMMARY.md` - Update documentation
|
|
|
|
## Database Migration
|
|
|
|
**No migration needed!** Views are just queries, not tables. Removing them has no impact on data.
|
|
|
|
## Feature Parity Verification
|
|
|
|
### ✅ All Features Preserved in Media-Items
|
|
|
|
**Ebook Feature → Media-Items Equivalent:**
|
|
- `GET /api/ebooks` → `GET /api/media-items`
|
|
- `GET /api/ebooks/:id` → `GET /api/media-items/:id`
|
|
- `POST /api/ebooks` → `POST /api/media-items` (NEW)
|
|
- `PUT /api/ebooks/:id` → `PUT /api/media-items/:id` (NEW)
|
|
- `DELETE /api/ebooks/:id` → `DELETE /api/media-items/:id` (NEW)
|
|
- Rating endpoints → Already exist for media-items
|
|
- Progress endpoints → Already exist for media-items
|
|
- Notes endpoints → Already exist for media-items
|
|
- Highlights endpoints → Already exist for media-items
|
|
|
|
**Enhanced Features in Media-Items:**
|
|
- ✅ Library scoping (required library_id)
|
|
- ✅ Better filtering (by author, series, genre, language, year, cover)
|
|
- ✅ Advanced search (partial + fuzzy)
|
|
- ✅ More sorting options
|
|
- ✅ Additional metadata fields
|
|
|
|
## Time Estimate
|
|
|
|
- Schema cleanup: 5 minutes
|
|
- Database queries: 20 minutes
|
|
- Remove old handlers: 30 minutes
|
|
- Add new admin handlers: 20 minutes
|
|
- Update routes: 5 minutes
|
|
- Update tests: 20 minutes
|
|
- Rebuild & test: 10 minutes
|
|
- **Total: ~110 minutes (2 hours)**
|
|
|
|
## Success Criteria
|
|
|
|
✅ All /api/ebooks endpoints removed
|
|
✅ All backward compatibility views removed from schema
|
|
✅ Admin can create/update/delete media items
|
|
✅ All filtering, sorting, searching works
|
|
✅ All integration tests pass
|
|
✅ Clean database schema (no unused tables/views)
|
|
✅ Code is simpler (one system, not two)
|
|
|
|
## Risk Assessment
|
|
|
|
**Medium Risk:**
|
|
- Removing endpoints is a breaking change
|
|
- But app has never been released, so no external users
|
|
- Complete control over all API consumers
|
|
|
|
**Mitigation:**
|
|
- Bruno collections will need updating
|
|
- Integration tests provide safety net
|
|
- Can revert commit if issues found
|
|
|
|
---
|
|
|
|
**Status:** Ready to execute
|
|
**Interruptible:** Yes, but best to complete all phases
|
|
**Reversible:** Yes via git revert
|
|
**Database Impact:** No data loss (views only)
|