batchCooking/apps/api/prisma/schema.prisma
Nicolas c957db27df Add Prisma schema for the documented data model
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).
2026-08-16 12:09:20 +02:00

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")
}