- Modèle Prisma UserPreference (1-1 avec UserProfile, onDelete Cascade), enum ThemePreference (LIGHT/DARK/SYSTEM, défaut SYSTEM) - packages/shared: THEME_PREFERENCES/ThemePreference, PreferencesView, updatePreferencesSchema
271 lines
9.3 KiB
Text
271 lines
9.3 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
|
|
/// The member who administers this household — created it, or inherited
|
|
/// adminship when the previous admin left/deleted their account (see
|
|
/// `house.service.ts`'s `leaveCurrentHouse`). Always set: a house is
|
|
/// deleted outright once it would otherwise have no admin left.
|
|
adminId Int @map("admin_id")
|
|
/// Shareable code another user enters via `POST /house/join` to become a
|
|
/// member — see `house.service.ts`'s generator for the charset/length.
|
|
inviteCode String @unique @map("invite_code")
|
|
|
|
admin UserProfile @relation("HouseAdmin", fields: [adminId], references: [id])
|
|
members UserProfile[] @relation("HouseMember")
|
|
plannings Planning[]
|
|
|
|
@@map("house")
|
|
}
|
|
|
|
/// `name` is `@unique` — not in the original spec doc, added so the seed
|
|
/// script (prisma/seed.ts) can `upsert` by name and stay idempotent/safe to
|
|
/// re-run, and so two reference rows can never silently duplicate the same
|
|
/// regime.
|
|
model Diet {
|
|
id Int @id @default(autoincrement())
|
|
name String @unique
|
|
|
|
users UserProfile[]
|
|
|
|
@@map("diet")
|
|
}
|
|
|
|
/// Not in the original spec doc — a category is either a true (IgE-mediated)
|
|
/// allergy or a non-immune intolerance; the UI groups selectable allergens
|
|
/// into two separate lists (`AllergySelect`, apps/web) instead of one flat
|
|
/// "allergies & intolérances" list.
|
|
enum AllergenKind {
|
|
ALLERGY
|
|
INTOLERANCE
|
|
}
|
|
|
|
/// Enumeration-style table, meant to grow over time (e.g. allergy nuances).
|
|
/// `name` is `@unique` for the same reason as `Diet.name` above. `kind` is
|
|
/// also not in the original spec doc — see {@link AllergenKind}.
|
|
model Category {
|
|
id Int @id @default(autoincrement())
|
|
name String @unique
|
|
kind AllergenKind @default(ALLERGY)
|
|
|
|
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
|
|
/// argon2 hash of the account password. Not in the original spec doc —
|
|
/// added for authentication (login page / profile creation).
|
|
passwordHash String @map("password_hash")
|
|
/// Bumped to invalidate previously-issued JWTs (e.g. on password change).
|
|
/// Not in the original spec doc — required for stateless JWT auth.
|
|
tokenVersion Int @default(0) @map("token_version")
|
|
houseId Int? @map("house_id")
|
|
dietId Int? @map("diet_id")
|
|
|
|
house House? @relation("HouseMember", fields: [houseId], references: [id], onDelete: SetNull)
|
|
diet Diet? @relation(fields: [dietId], references: [id], onDelete: SetNull)
|
|
allergies UserProfileAllergy[]
|
|
/// Households this profile administers. In practice at most one — a
|
|
/// profile can only ever belong to (and thus admin) a single household at
|
|
/// a time — but Prisma models the admin side of a one-to-many FK as a
|
|
/// list regardless of that real-world cardinality.
|
|
administeredHouses House[] @relation("HouseAdmin")
|
|
preferences UserPreference?
|
|
|
|
@@map("user_profiles")
|
|
}
|
|
|
|
/// Not in the original spec doc — personalization settings (theme for now,
|
|
/// meant to grow), one row per profile, created on demand (see
|
|
/// `preferences.service.ts`) rather than at signup — same "absent means the
|
|
/// default" philosophy as `dietId`/allergies.
|
|
enum ThemePreference {
|
|
LIGHT
|
|
DARK
|
|
/// Follow the OS/browser preference — the default. Not "no row yet" (that
|
|
/// case is handled in the service layer) but an explicit choice to track
|
|
/// the system, distinguishable from a user who hasn't decided yet if this
|
|
/// model ever needs that distinction.
|
|
SYSTEM
|
|
}
|
|
|
|
model UserPreference {
|
|
/// Both the primary key and the FK — a strict 1-1 with UserProfile, no
|
|
/// separate auto-incrementing id (a profile has at most one preferences row).
|
|
userProfileId Int @id @map("user_profile_id")
|
|
theme ThemePreference @default(SYSTEM)
|
|
|
|
userProfile UserProfile @relation(fields: [userProfileId], references: [id], onDelete: Cascade)
|
|
|
|
@@map("user_preference")
|
|
}
|
|
|
|
/// 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")
|
|
}
|