Models every table from specs/batch-cooking-modele.md: users/household (user_profiles, house, diet, allergy, category), planning (planning, planning_item), and recipes (recipe, ingredients, step, tech_step, tech_step_mapping, sources). Two deliberate deviations from the literal spec doc, per project discussion: - recipe_ingredient (recipe <-> ingredients) carries quantity + unit. The spec describes a plain many-to-many with no extra fields, but a shopping list / batch-cooking calculation needs quantities. - step is modeled one-to-many from recipe (not many-to-many as labeled in the doc): the documented `order` column only makes sense scoped to a single recipe, which isn't reconcilable with steps being shared across recipes. Everything else follows the doc as-is, including field nullability choices made where the doc doesn't specify (e.g. user_profiles.house_id optional, recipe.source_id optional) and onDelete behavior (Cascade for owned child records, SetNull for optional references) — first draft, not meant as final production hardening. Verified: `prisma validate`, `prisma generate`, and a real `prisma migrate dev` against a local Postgres (via docker-compose) — the migration applies cleanly and produces the expected schema. README: documents the migrate command and a Postgres port-conflict gotcha hit during validation (a native Postgres service on this machine was already bound to 5432, intercepting the Docker container's connections).
209 lines
6.1 KiB
Text
209 lines
6.1 KiB
Text
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Users & household
|
|
// See specs/batch-cooking-modele.md for the source data model documentation.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
model House {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
|
|
members UserProfile[]
|
|
plannings Planning[]
|
|
|
|
@@map("house")
|
|
}
|
|
|
|
model Diet {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
|
|
users UserProfile[]
|
|
|
|
@@map("diet")
|
|
}
|
|
|
|
/// Enumeration-style table, meant to grow over time (e.g. allergy nuances).
|
|
model Category {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
|
|
allergies Allergy[]
|
|
|
|
@@map("category")
|
|
}
|
|
|
|
model Allergy {
|
|
id Int @id @default(autoincrement())
|
|
categoryId Int @map("cat_id")
|
|
|
|
category Category @relation(fields: [categoryId], references: [id])
|
|
users UserProfileAllergy[]
|
|
|
|
@@map("allergy")
|
|
}
|
|
|
|
model UserProfile {
|
|
id Int @id @default(autoincrement())
|
|
firstName String @map("first_name")
|
|
lastName String @map("last_name")
|
|
email String @unique
|
|
houseId Int? @map("house_id")
|
|
dietId Int? @map("diet_id")
|
|
|
|
house House? @relation(fields: [houseId], references: [id], onDelete: SetNull)
|
|
diet Diet? @relation(fields: [dietId], references: [id], onDelete: SetNull)
|
|
allergies UserProfileAllergy[]
|
|
|
|
@@map("user_profiles")
|
|
}
|
|
|
|
/// Explicit join table for the user_profiles <-> allergy association
|
|
/// (documented in the spec as a plain many-to-many, no extra fields).
|
|
model UserProfileAllergy {
|
|
userProfileId Int @map("user_profile_id")
|
|
allergyId Int @map("allergy_id")
|
|
|
|
userProfile UserProfile @relation(fields: [userProfileId], references: [id], onDelete: Cascade)
|
|
allergy Allergy @relation(fields: [allergyId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([userProfileId, allergyId])
|
|
@@map("user_profile_allergy")
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Planning
|
|
// -----------------------------------------------------------------------------
|
|
|
|
model Planning {
|
|
id Int @id @default(autoincrement())
|
|
startDate DateTime @map("start_date") @db.Date
|
|
finishDate DateTime @map("finish_date") @db.Date
|
|
houseId Int @map("house_id")
|
|
|
|
house House @relation(fields: [houseId], references: [id], onDelete: Cascade)
|
|
items PlanningItem[]
|
|
|
|
@@map("planning")
|
|
}
|
|
|
|
model PlanningItem {
|
|
id Int @id @default(autoincrement())
|
|
planningId Int @map("planning_id")
|
|
weekDay String @map("week_day")
|
|
meal String
|
|
recipeId Int @map("recipe_id")
|
|
|
|
planning Planning @relation(fields: [planningId], references: [id], onDelete: Cascade)
|
|
recipe Recipe @relation(fields: [recipeId], references: [id])
|
|
|
|
@@map("planning_item")
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Recipes
|
|
// -----------------------------------------------------------------------------
|
|
|
|
model Source {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
url String?
|
|
|
|
recipes Recipe[]
|
|
|
|
@@map("sources")
|
|
}
|
|
|
|
model Recipe {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
sourceId Int? @map("source_id")
|
|
description String?
|
|
picture String?
|
|
|
|
source Source? @relation(fields: [sourceId], references: [id], onDelete: SetNull)
|
|
ingredients RecipeIngredient[]
|
|
steps Step[]
|
|
planningItems PlanningItem[]
|
|
/// Ingredients for which this recipe is offered as a make-it-yourself alternative.
|
|
alternateFor Ingredient[] @relation("IngredientAlternateRecipe")
|
|
|
|
@@map("recipe")
|
|
}
|
|
|
|
model Ingredient {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
icon String?
|
|
alternateRecipeId Int? @map("alternate_recipe")
|
|
|
|
alternateRecipe Recipe? @relation("IngredientAlternateRecipe", fields: [alternateRecipeId], references: [id], onDelete: SetNull)
|
|
recipes RecipeIngredient[]
|
|
|
|
@@map("ingredients")
|
|
}
|
|
|
|
/// recipe <-> ingredients association. The spec documents this as a plain
|
|
/// many-to-many, but a shopping list / batch-cooking calculation needs a
|
|
/// quantity per recipe, so this join table carries quantity + unit
|
|
/// (project decision, not in the original spec doc).
|
|
model RecipeIngredient {
|
|
recipeId Int @map("recipe_id")
|
|
ingredientId Int @map("ingredient_id")
|
|
quantity Decimal @db.Decimal(10, 2)
|
|
unit String
|
|
|
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
ingredient Ingredient @relation(fields: [ingredientId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([recipeId, ingredientId])
|
|
@@map("recipe_ingredient")
|
|
}
|
|
|
|
model TechStep {
|
|
id Int @id @default(autoincrement())
|
|
|
|
steps Step[]
|
|
mappings TechStepMapping[]
|
|
|
|
@@map("tech_step")
|
|
}
|
|
|
|
/// Used by the recipe-import pipeline to auto-detect which technique a raw
|
|
/// instruction step corresponds to (expression = text pattern, weight = match score).
|
|
model TechStepMapping {
|
|
id Int @id @default(autoincrement())
|
|
techStepId Int @map("tech_step_id")
|
|
expression String
|
|
weight Int
|
|
|
|
techStep TechStep @relation(fields: [techStepId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("tech_step_mapping")
|
|
}
|
|
|
|
/// Modeled as one-to-many (a step belongs to exactly one recipe), not the
|
|
/// many-to-many noted in the spec doc: `order` only makes sense scoped to a
|
|
/// single recipe, which isn't reconcilable with steps being shared across
|
|
/// recipes. See specs/batch-cooking-modele.md for the original wording.
|
|
model Step {
|
|
id Int @id @default(autoincrement())
|
|
recipeId Int @map("recipe_id")
|
|
description String
|
|
picture String?
|
|
order Int
|
|
techStepId Int? @map("tech_step_id")
|
|
|
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
techStep TechStep? @relation(fields: [techStepId], references: [id], onDelete: SetNull)
|
|
|
|
@@map("step")
|
|
}
|