[migration] Normalize schema to MVP target: split recipes into ingredients and steps tables, drop JSON columns, adjust tags, update migration docs. WARNING: Tests fail until API and tests are refactored for new structure.
This commit is contained in:
parent
23aa097458
commit
fa2cceddc3
|
|
@ -0,0 +1,198 @@
|
||||||
|
# Recipe Manager — MVP Execution Plan
|
||||||
|
|
||||||
|
**Date:** 2026-03-25
|
||||||
|
**Owner:** Paul + Cleo
|
||||||
|
**Goal:** Ship a tight, usable MVP with clear scope and a fast implementation path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1) MVP Scope (Step 1 — locked)
|
||||||
|
|
||||||
|
### In Scope (MVP)
|
||||||
|
1. **Recipe CRUD**
|
||||||
|
- Create recipe
|
||||||
|
- Read/list recipes
|
||||||
|
- View single recipe detail
|
||||||
|
- Edit recipe
|
||||||
|
- Delete recipe
|
||||||
|
2. **Tags**
|
||||||
|
- Create/delete tags
|
||||||
|
- Attach/remove tags on recipes
|
||||||
|
- Filter list by tag
|
||||||
|
3. **Ingredients + Steps**
|
||||||
|
- Ingredient list per recipe (qty/unit/item)
|
||||||
|
- Ordered instruction steps
|
||||||
|
4. **Search**
|
||||||
|
- Search by title
|
||||||
|
- Search by ingredient text
|
||||||
|
- Search by tag
|
||||||
|
5. **Backup portability**
|
||||||
|
- JSON export
|
||||||
|
- JSON import (merge strategy: skip duplicates by title+sourceUrl)
|
||||||
|
|
||||||
|
### Out of Scope (Post-MVP)
|
||||||
|
- AI substitutions
|
||||||
|
- Meal planner UX (calendar)
|
||||||
|
- Shopping list automation
|
||||||
|
- Nutrition calculations
|
||||||
|
- OCR/image import
|
||||||
|
- Multi-user auth/roles
|
||||||
|
|
||||||
|
### Definition of Done (MVP)
|
||||||
|
- User can create/edit/delete recipes with ingredients + steps
|
||||||
|
- User can tag and filter recipes
|
||||||
|
- User can search quickly by title/tag/ingredient
|
||||||
|
- User can export and import data JSON without data loss for core fields
|
||||||
|
- API + UI docs updated
|
||||||
|
- Docker compose run path works end-to-end
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2) Architecture (existing stack + MVP alignment)
|
||||||
|
|
||||||
|
- **Backend:** Node/TypeScript REST API (SQLite)
|
||||||
|
- **Frontend:** React/TypeScript (Vite)
|
||||||
|
- **Storage:** SQLite file (`data/recipes.db`)
|
||||||
|
- **Deployment:** Docker + docker-compose
|
||||||
|
|
||||||
|
MVP will reuse existing architecture and avoid stack churn.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3) Data Model (Step 2 target)
|
||||||
|
|
||||||
|
### Tables
|
||||||
|
- `recipes`
|
||||||
|
- `id` (PK)
|
||||||
|
- `title` (TEXT, required)
|
||||||
|
- `description` (TEXT)
|
||||||
|
- `servings` (INTEGER)
|
||||||
|
- `prep_time_minutes` (INTEGER)
|
||||||
|
- `cook_time_minutes` (INTEGER)
|
||||||
|
- `source_url` (TEXT)
|
||||||
|
- `created_at`, `updated_at`
|
||||||
|
|
||||||
|
- `ingredients`
|
||||||
|
- `id` (PK)
|
||||||
|
- `recipe_id` (FK -> recipes.id)
|
||||||
|
- `position` (INTEGER)
|
||||||
|
- `quantity` (TEXT)
|
||||||
|
- `unit` (TEXT)
|
||||||
|
- `item` (TEXT, required)
|
||||||
|
- `notes` (TEXT)
|
||||||
|
|
||||||
|
- `steps`
|
||||||
|
- `id` (PK)
|
||||||
|
- `recipe_id` (FK -> recipes.id)
|
||||||
|
- `position` (INTEGER)
|
||||||
|
- `instruction` (TEXT, required)
|
||||||
|
|
||||||
|
- `tags`
|
||||||
|
- `id` (PK)
|
||||||
|
- `name` (TEXT UNIQUE, required)
|
||||||
|
|
||||||
|
- `recipe_tags`
|
||||||
|
- `recipe_id` (FK -> recipes.id)
|
||||||
|
- `tag_id` (FK -> tags.id)
|
||||||
|
- composite PK (`recipe_id`, `tag_id`)
|
||||||
|
|
||||||
|
### Suggested indexes
|
||||||
|
- `idx_recipes_title` on `recipes(title)`
|
||||||
|
- `idx_ingredients_item` on `ingredients(item)`
|
||||||
|
- `idx_recipe_tags_tag_id` on `recipe_tags(tag_id)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4) API Surface (MVP)
|
||||||
|
|
||||||
|
### Recipes
|
||||||
|
- `GET /api/recipes?query=&tag=`
|
||||||
|
- `GET /api/recipes/:id`
|
||||||
|
- `POST /api/recipes`
|
||||||
|
- `PUT /api/recipes/:id`
|
||||||
|
- `DELETE /api/recipes/:id`
|
||||||
|
|
||||||
|
### Tags
|
||||||
|
- `GET /api/tags`
|
||||||
|
- `POST /api/tags`
|
||||||
|
- `DELETE /api/tags/:id`
|
||||||
|
|
||||||
|
### Backup
|
||||||
|
- `GET /api/export/json`
|
||||||
|
- `POST /api/import/json`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5) Frontend Surface (MVP)
|
||||||
|
|
||||||
|
- `RecipeListPage`
|
||||||
|
- search box
|
||||||
|
- tag filter
|
||||||
|
- recipe cards/list
|
||||||
|
- `RecipeDetailPage`
|
||||||
|
- metadata + ingredients + steps
|
||||||
|
- `RecipeFormPage`
|
||||||
|
- create/edit form with dynamic ingredient + step rows
|
||||||
|
- `TagManager`
|
||||||
|
- add/remove tags
|
||||||
|
- `ImportExportPanel`
|
||||||
|
- export button
|
||||||
|
- import file upload + summary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6) File-by-File Build Board
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
1. DB schema/migrations (recipes, ingredients, steps, tags, recipe_tags)
|
||||||
|
2. Recipe repository/services with nested ingredient/step handling
|
||||||
|
3. Search query enhancements (title/ingredient/tag)
|
||||||
|
4. Tag endpoints + recipe-tag mapping logic
|
||||||
|
5. JSON export endpoint
|
||||||
|
6. JSON import endpoint + dedupe policy
|
||||||
|
7. API tests for CRUD + search + import/export
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
1. Recipe form supports ordered ingredients/steps
|
||||||
|
2. List page unified search + tag filter
|
||||||
|
3. Detail page renders normalized sections
|
||||||
|
4. Tag management wiring
|
||||||
|
5. Import/export UI panel
|
||||||
|
6. UI tests for key flows
|
||||||
|
|
||||||
|
### Docs
|
||||||
|
1. `docs/api.md` update
|
||||||
|
2. `docs/user-guide.md` update
|
||||||
|
3. README MVP usage + backup section
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7) 6-Day Execution Plan
|
||||||
|
|
||||||
|
- **Day 1:** Scope freeze + schema lock + migration prep
|
||||||
|
- **Day 2:** CRUD hardening (nested ingredients/steps correctness)
|
||||||
|
- **Day 3:** Search + tag filter backend/frontend alignment
|
||||||
|
- **Day 4:** Import/export JSON endpoints + UI integration
|
||||||
|
- **Day 5:** Test pass, seed data, bug fixes
|
||||||
|
- **Day 6:** Docs + Docker validation + release tag
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8) Immediate Next Actions (Starting now)
|
||||||
|
|
||||||
|
1. ✅ **Step 1 complete:** MVP scope locked in this document.
|
||||||
|
2. Next: verify existing TODO/roadmap lines up with locked scope.
|
||||||
|
3. Then start Step 2 (data model lock) by checking current schema vs target model and creating the migration delta list.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9) Risks / Decisions to keep an eye on
|
||||||
|
|
||||||
|
- Duplicate logic for import merge rules (title collisions)
|
||||||
|
- Tag naming normalization (case-insensitive unique)
|
||||||
|
- Ingredient parsing consistency for search quality
|
||||||
|
- Backward compatibility with existing recipe records
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If scope changes are requested, update this file first and treat it as the source of truth for implementation sequencing.
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Schema Migration Delta Log — 2026-03-25
|
||||||
|
|
||||||
|
Date: 2026-03-25
|
||||||
|
|
||||||
|
## Delta Summary
|
||||||
|
|
||||||
|
- Normalized `recipes` table: removed JSON columns for ingredients and instructions
|
||||||
|
- Added `ingredients` table (with position/order, FK to recipe)
|
||||||
|
- Added `steps` table (with position/order, FK to recipe)
|
||||||
|
- Removed `notes` and `last_cooked_at` columns from recipes
|
||||||
|
- Changed timestamp columns to DATETIME for better SQLite compatibility
|
||||||
|
- Removed color from tags table
|
||||||
|
- Added index on `ingredients(item)`
|
||||||
|
- Confirmed/completed indexes per spec
|
||||||
|
|
||||||
|
See [../migrations/2026-03-25-schema-migration.md](../migrations/2026-03-25-schema-migration.md) for the detailed migration plan and SQL changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
- [ ] Implement migration logic (split JSON columns)
|
||||||
|
- [ ] Update `src/backend/db/schema.sql` with normalized tables
|
||||||
|
- [ ] Write data migration script for live DBs (extract and insert ingredient/step records)
|
||||||
|
- [ ] Run DB migration
|
||||||
|
- [ ] Update test cases and data fixtures
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Recipe Manager DB Schema Migration — 2026-03-25
|
||||||
|
|
||||||
|
## Schema Diff (current → MVP target)
|
||||||
|
|
||||||
|
The current schema (src/backend/db/schema.sql) differs from the Step 2 MVP spec in docs/recipe-manager-mvp-plan.md as follows:
|
||||||
|
|
||||||
|
### **recipes** table
|
||||||
|
**Current:**
|
||||||
|
- Has columns: id, title, description, ingredients (JSON), instructions (JSON), source_url, notes, servings, prep_time_minutes, cook_time_minutes, created_at (INTEGER), updated_at (INTEGER), last_cooked_at
|
||||||
|
|
||||||
|
**Target:**
|
||||||
|
- Should be normalized: No JSON columns for ingredients/instructions. Should instead link to separate ingredients and steps tables via recipe_id FK.
|
||||||
|
- Remove: ingredients, instructions, notes, last_cooked_at columns.
|
||||||
|
- created_at and updated_at should be DATETIME.
|
||||||
|
|
||||||
|
### **ingredients** table
|
||||||
|
- Does NOT exist in current schema. **Add ingredients table:**
|
||||||
|
- id (PK)
|
||||||
|
- recipe_id (FK)
|
||||||
|
- position (INTEGER)
|
||||||
|
- quantity (TEXT)
|
||||||
|
- unit (TEXT)
|
||||||
|
- item (TEXT, required)
|
||||||
|
- notes (TEXT)
|
||||||
|
|
||||||
|
### **steps** table
|
||||||
|
- Does NOT exist. **Add steps table:**
|
||||||
|
- id (PK)
|
||||||
|
- recipe_id (FK)
|
||||||
|
- position (INTEGER)
|
||||||
|
- instruction (TEXT, required)
|
||||||
|
|
||||||
|
### **tags/recipe_tags**
|
||||||
|
- Already present (mostly matches spec)
|
||||||
|
- Remove color column from tags (not in MVP spec)
|
||||||
|
- "name" should be unique and required (already declared)
|
||||||
|
|
||||||
|
### **Indexes**
|
||||||
|
- Add idx_ingredients_item on ingredients(item)
|
||||||
|
- Add idx_recipe_tags_tag_id on recipe_tags(tag_id) (already present)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Steps
|
||||||
|
1. Create new **ingredients** and **steps** tables
|
||||||
|
2. Populate ingredients & steps tables from JSON columns of recipes
|
||||||
|
3. Migrate tags: drop color column from tags (if exists)
|
||||||
|
4. Drop ingredients, instructions, notes, last_cooked_at columns from recipes
|
||||||
|
5. Change created_at, updated_at columns to DATETIME
|
||||||
|
6. Add new indexes (ingredients(item))
|
||||||
|
7. Update test data and migration documentation
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- This migration normalizes the schema: recipes → ingredients & steps as separate tables
|
||||||
|
- Data in existing recipes.ingredients and instructions must be extracted and split into new tables
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Status: _DRAFT — pending implementation_
|
||||||
|
|
@ -1,28 +1,46 @@
|
||||||
-- Create recipes table
|
-- SCHEMA VERSION: 2026-03-25 — MVP-NORMALIZED
|
||||||
|
|
||||||
|
-- Recipes table (normalized)
|
||||||
CREATE TABLE recipes (
|
CREATE TABLE recipes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
description TEXT,
|
description TEXT,
|
||||||
ingredients TEXT NOT NULL, -- JSON array
|
|
||||||
instructions TEXT NOT NULL, -- JSON array of steps
|
|
||||||
source_url TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
servings INTEGER,
|
servings INTEGER,
|
||||||
prep_time_minutes INTEGER,
|
prep_time_minutes INTEGER,
|
||||||
cook_time_minutes INTEGER,
|
cook_time_minutes INTEGER,
|
||||||
created_at INTEGER NOT NULL,
|
source_url TEXT,
|
||||||
updated_at INTEGER NOT NULL,
|
created_at DATETIME NOT NULL,
|
||||||
last_cooked_at INTEGER
|
updated_at DATETIME NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create tags table
|
-- Ingredients table
|
||||||
|
CREATE TABLE ingredients (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
recipe_id INTEGER NOT NULL,
|
||||||
|
position INTEGER,
|
||||||
|
quantity TEXT,
|
||||||
|
unit TEXT,
|
||||||
|
item TEXT NOT NULL,
|
||||||
|
notes TEXT,
|
||||||
|
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Steps table
|
||||||
|
CREATE TABLE steps (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
recipe_id INTEGER NOT NULL,
|
||||||
|
position INTEGER,
|
||||||
|
instruction TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (recipe_id) REFERENCES recipes(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Tags table (no color)
|
||||||
CREATE TABLE tags (
|
CREATE TABLE tags (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
name TEXT UNIQUE NOT NULL,
|
name TEXT UNIQUE NOT NULL
|
||||||
color TEXT -- Hex color for UI
|
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create recipe_tags join table
|
-- Recipe_tags join table
|
||||||
CREATE TABLE recipe_tags (
|
CREATE TABLE recipe_tags (
|
||||||
recipe_id INTEGER NOT NULL,
|
recipe_id INTEGER NOT NULL,
|
||||||
tag_id INTEGER NOT NULL,
|
tag_id INTEGER NOT NULL,
|
||||||
|
|
@ -31,8 +49,7 @@ CREATE TABLE recipe_tags (
|
||||||
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Indexes for efficiency
|
-- Indexes
|
||||||
CREATE INDEX idx_recipes_title ON recipes(title);
|
CREATE INDEX idx_recipes_title ON recipes(title);
|
||||||
CREATE INDEX idx_recipes_created_at ON recipes(created_at DESC);
|
CREATE INDEX idx_ingredients_item ON ingredients(item);
|
||||||
CREATE INDEX idx_recipe_tags_recipe ON recipe_tags(recipe_id);
|
CREATE INDEX idx_recipe_tags_tag_id ON recipe_tags(tag_id);
|
||||||
CREATE INDEX idx_recipe_tags_tag ON recipe_tags(tag_id);
|
|
||||||
Loading…
Reference in New Issue