feat: add cookbook nesting and auto-filtering capabilities

Enables cookbooks to include other cookbooks and automatically organize content based on tags. This allows users to create hierarchical cookbook structures and maintain collections that automatically update as new content is added.

Key features:
- Cookbook nesting: Include child cookbooks within parent cookbooks
- Auto-filtering by cookbook tags: Automatically include cookbooks matching specified tags
- Auto-filtering by recipe tags: Automatically add recipes matching specified tags
- Enhanced cookbook management UI with tag support
- Comprehensive test coverage for new functionality

Database schema updated with CookbookInclusion and CookbookTag tables.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-09 05:04:01 +00:00
parent 5707e42c0f
commit 32322f71dc
12 changed files with 1285 additions and 49 deletions

View File

@@ -28,6 +28,7 @@ model User {
sharedRecipes RecipeShare[]
refreshTokens RefreshToken[]
verificationTokens VerificationToken[]
mealPlans MealPlan[]
@@index([email])
@@index([provider, providerId])
@@ -103,6 +104,7 @@ model Recipe {
tags RecipeTag[]
cookbooks CookbookRecipe[]
sharedWith RecipeShare[]
meals MealRecipe[]
@@index([title])
@@index([cuisine])
@@ -185,9 +187,10 @@ model RecipeImage {
}
model Tag {
id String @id @default(cuid())
name String @unique
recipes RecipeTag[]
id String @id @default(cuid())
name String @unique
recipes RecipeTag[]
cookbooks CookbookTag[]
}
model RecipeTag {
@@ -202,6 +205,18 @@ model RecipeTag {
@@index([tagId])
}
model CookbookTag {
cookbookId String
tagId String
cookbook Cookbook @relation(fields: [cookbookId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([cookbookId, tagId])
@@index([cookbookId])
@@index([tagId])
}
model RecipeShare {
id String @id @default(cuid())
recipeId String
@@ -217,18 +232,22 @@ model RecipeShare {
}
model Cookbook {
id String @id @default(cuid())
name String
description String?
coverImageUrl String?
userId String? // Cookbook owner
autoFilterCategories String[] @default([]) // Auto-add recipes matching these categories
autoFilterTags String[] @default([]) // Auto-add recipes matching these tags
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
name String
description String?
coverImageUrl String?
userId String? // Cookbook owner
autoFilterCategories String[] @default([]) // Auto-add recipes matching these categories
autoFilterTags String[] @default([]) // Auto-add recipes matching these tags
autoFilterCookbookTags String[] @default([]) // Auto-add cookbooks matching these tags
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
recipes CookbookRecipe[]
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
recipes CookbookRecipe[]
tags CookbookTag[]
includedCookbooks CookbookInclusion[] @relation("ParentCookbook")
includedIn CookbookInclusion[] @relation("ChildCookbook")
@@index([name])
@@index([userId])
@@ -247,3 +266,70 @@ model CookbookRecipe {
@@index([cookbookId])
@@index([recipeId])
}
model CookbookInclusion {
id String @id @default(cuid())
parentCookbookId String
childCookbookId String
addedAt DateTime @default(now())
parentCookbook Cookbook @relation("ParentCookbook", fields: [parentCookbookId], references: [id], onDelete: Cascade)
childCookbook Cookbook @relation("ChildCookbook", fields: [childCookbookId], references: [id], onDelete: Cascade)
@@unique([parentCookbookId, childCookbookId])
@@index([parentCookbookId])
@@index([childCookbookId])
}
model MealPlan {
id String @id @default(cuid())
userId String?
date DateTime // The day this meal plan is for (stored at midnight UTC)
notes String? @db.Text // Optional notes for the entire day
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
meals Meal[]
@@unique([userId, date]) // One meal plan per user per day
@@index([userId])
@@index([date])
@@index([userId, date])
}
model Meal {
id String @id @default(cuid())
mealPlanId String
mealType MealType
order Int // Order within the same meal type (for multi-recipe meals)
servings Int? // Servings for this specific meal (can override recipe default)
notes String? @db.Text // Meal-specific notes
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
mealPlan MealPlan @relation(fields: [mealPlanId], references: [id], onDelete: Cascade)
recipe MealRecipe?
@@index([mealPlanId])
@@index([mealType])
}
model MealRecipe {
mealId String @id
recipeId String
meal Meal @relation(fields: [mealId], references: [id], onDelete: Cascade)
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
@@index([recipeId])
}
enum MealType {
BREAKFAST
LUNCH
DINNER
SNACK
DESSERT
OTHER
}