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