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>
336 lines
9.5 KiB
Plaintext
336 lines
9.5 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
binaryTargets = ["native", "linux-musl-openssl-3.0.x"]
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model User {
|
|
id String @id @default(cuid())
|
|
email String @unique
|
|
username String? @unique
|
|
passwordHash String? // null for OAuth-only users
|
|
name String?
|
|
avatar String?
|
|
provider String @default("local") // "local", "google", "github", etc.
|
|
providerId String? // OAuth provider's user ID
|
|
role Role @default(USER)
|
|
emailVerified Boolean @default(false)
|
|
emailVerifiedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
recipes Recipe[]
|
|
cookbooks Cookbook[]
|
|
sharedRecipes RecipeShare[]
|
|
refreshTokens RefreshToken[]
|
|
verificationTokens VerificationToken[]
|
|
mealPlans MealPlan[]
|
|
|
|
@@index([email])
|
|
@@index([provider, providerId])
|
|
}
|
|
|
|
model VerificationToken {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
token String @unique
|
|
type TokenType
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([token])
|
|
}
|
|
|
|
enum TokenType {
|
|
EMAIL_VERIFICATION
|
|
PASSWORD_RESET
|
|
}
|
|
|
|
enum Role {
|
|
USER
|
|
ADMIN
|
|
}
|
|
|
|
enum Visibility {
|
|
PRIVATE // Only owner can see
|
|
SHARED // Owner + specific users
|
|
PUBLIC // Everyone can see
|
|
}
|
|
|
|
model RefreshToken {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
token String @unique
|
|
expiresAt DateTime
|
|
createdAt DateTime @default(now())
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId])
|
|
@@index([token])
|
|
}
|
|
|
|
model Recipe {
|
|
id String @id @default(cuid())
|
|
title String
|
|
description String?
|
|
prepTime Int? // minutes
|
|
cookTime Int? // minutes
|
|
totalTime Int? // minutes
|
|
servings Int?
|
|
imageUrl String?
|
|
sourceUrl String? // For imported recipes
|
|
author String?
|
|
cuisine String?
|
|
categories String[] @default([]) // Changed from single category to array
|
|
rating Float?
|
|
userId String? // Recipe owner
|
|
visibility Visibility @default(PRIVATE)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
sections RecipeSection[]
|
|
ingredients Ingredient[]
|
|
instructions Instruction[]
|
|
images RecipeImage[]
|
|
tags RecipeTag[]
|
|
cookbooks CookbookRecipe[]
|
|
sharedWith RecipeShare[]
|
|
meals MealRecipe[]
|
|
|
|
@@index([title])
|
|
@@index([cuisine])
|
|
@@index([userId])
|
|
@@index([visibility])
|
|
}
|
|
|
|
model RecipeSection {
|
|
id String @id @default(cuid())
|
|
recipeId String
|
|
name String // e.g., "Starter", "Dough", "Assembly"
|
|
order Int
|
|
timing String? // e.g., "Day 1 - 8PM", "12 hours before mixing"
|
|
|
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
ingredients Ingredient[]
|
|
instructions Instruction[]
|
|
|
|
@@index([recipeId])
|
|
}
|
|
|
|
model Ingredient {
|
|
id String @id @default(cuid())
|
|
recipeId String? // Optional - can be derived from section
|
|
sectionId String? // Optional - if null, belongs to recipe directly
|
|
name String
|
|
amount String?
|
|
unit String?
|
|
notes String?
|
|
order Int
|
|
|
|
recipe Recipe? @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
section RecipeSection? @relation(fields: [sectionId], references: [id], onDelete: Cascade)
|
|
instructions IngredientInstructionMapping[]
|
|
|
|
@@index([recipeId])
|
|
@@index([sectionId])
|
|
}
|
|
|
|
model Instruction {
|
|
id String @id @default(cuid())
|
|
recipeId String? // Optional - can be derived from section
|
|
sectionId String? // Optional - if null, belongs to recipe directly
|
|
step Int
|
|
text String @db.Text
|
|
imageUrl String?
|
|
timing String? // e.g., "8:00am", "After 30 minutes", "Day 2 - Morning"
|
|
|
|
recipe Recipe? @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
section RecipeSection? @relation(fields: [sectionId], references: [id], onDelete: Cascade)
|
|
ingredients IngredientInstructionMapping[]
|
|
|
|
@@index([recipeId])
|
|
@@index([sectionId])
|
|
}
|
|
|
|
model IngredientInstructionMapping {
|
|
id String @id @default(cuid())
|
|
ingredientId String
|
|
instructionId String
|
|
order Int // Display order within the instruction
|
|
|
|
ingredient Ingredient @relation(fields: [ingredientId], references: [id], onDelete: Cascade)
|
|
instruction Instruction @relation(fields: [instructionId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([ingredientId, instructionId])
|
|
@@index([instructionId])
|
|
@@index([ingredientId])
|
|
}
|
|
|
|
model RecipeImage {
|
|
id String @id @default(cuid())
|
|
recipeId String
|
|
url String
|
|
order Int
|
|
|
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([recipeId])
|
|
}
|
|
|
|
model Tag {
|
|
id String @id @default(cuid())
|
|
name String @unique
|
|
recipes RecipeTag[]
|
|
cookbooks CookbookTag[]
|
|
}
|
|
|
|
model RecipeTag {
|
|
recipeId String
|
|
tagId String
|
|
|
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([recipeId, tagId])
|
|
@@index([recipeId])
|
|
@@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
|
|
userId String
|
|
createdAt DateTime @default(now())
|
|
|
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([recipeId, userId])
|
|
@@index([recipeId])
|
|
@@index([userId])
|
|
}
|
|
|
|
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
|
|
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[]
|
|
tags CookbookTag[]
|
|
includedCookbooks CookbookInclusion[] @relation("ParentCookbook")
|
|
includedIn CookbookInclusion[] @relation("ChildCookbook")
|
|
|
|
@@index([name])
|
|
@@index([userId])
|
|
}
|
|
|
|
model CookbookRecipe {
|
|
id String @id @default(cuid())
|
|
cookbookId String
|
|
recipeId String
|
|
addedAt DateTime @default(now())
|
|
|
|
cookbook Cookbook @relation(fields: [cookbookId], references: [id], onDelete: Cascade)
|
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([cookbookId, recipeId])
|
|
@@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
|
|
}
|