Files
basil/packages/api/prisma/schema.prisma
Paul R Kartchner 32322f71dc 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>
2026-01-09 05:04:01 +00:00

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
}