diff --git a/README.md b/README.md index d75de94..1532820 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,9 @@ hors du repo). # Base de données Postgres locale docker compose up -d +# Applique le schéma (première fois / après un changement de prisma/schema.prisma) +pnpm --filter api exec prisma migrate dev + # Backend (http://localhost:3000) pnpm dev:api @@ -65,6 +68,13 @@ pnpm dev:api pnpm dev:web ``` +> **Conflit de port possible sur `5432`** : si tu as déjà un Postgres natif installé +> sur ta machine (service Windows, Homebrew, etc.), il peut occuper le port 5432 et +> intercepter les connexions à la place du conteneur Docker (symptôme : Prisma +> renvoie `P1000: Authentication failed` alors que les identifiants sont corrects). +> Dans ce cas, mets `POSTGRES_PORT=5433` (ou autre) dans ton `.env` **et** adapte le +> port dans la `DATABASE_URL` de `apps/api/.env`. + ## Qualité / Tests ```bash diff --git a/apps/api/prisma/migrations/20260816100611_init/migration.sql b/apps/api/prisma/migrations/20260816100611_init/migration.sql new file mode 100644 index 0000000..41febff --- /dev/null +++ b/apps/api/prisma/migrations/20260816100611_init/migration.sql @@ -0,0 +1,189 @@ +-- CreateTable +CREATE TABLE "house" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "house_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "diet" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "diet_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "category" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "category_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "allergy" ( + "id" SERIAL NOT NULL, + "cat_id" INTEGER NOT NULL, + + CONSTRAINT "allergy_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_profiles" ( + "id" SERIAL NOT NULL, + "first_name" TEXT NOT NULL, + "last_name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "house_id" INTEGER, + "diet_id" INTEGER, + + CONSTRAINT "user_profiles_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "user_profile_allergy" ( + "user_profile_id" INTEGER NOT NULL, + "allergy_id" INTEGER NOT NULL, + + CONSTRAINT "user_profile_allergy_pkey" PRIMARY KEY ("user_profile_id","allergy_id") +); + +-- CreateTable +CREATE TABLE "planning" ( + "id" SERIAL NOT NULL, + "start_date" DATE NOT NULL, + "finish_date" DATE NOT NULL, + "house_id" INTEGER NOT NULL, + + CONSTRAINT "planning_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "planning_item" ( + "id" SERIAL NOT NULL, + "planning_id" INTEGER NOT NULL, + "week_day" TEXT NOT NULL, + "meal" TEXT NOT NULL, + "recipe_id" INTEGER NOT NULL, + + CONSTRAINT "planning_item_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "sources" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "url" TEXT, + + CONSTRAINT "sources_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "recipe" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "source_id" INTEGER, + "description" TEXT, + "picture" TEXT, + + CONSTRAINT "recipe_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ingredients" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "icon" TEXT, + "alternate_recipe" INTEGER, + + CONSTRAINT "ingredients_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "recipe_ingredient" ( + "recipe_id" INTEGER NOT NULL, + "ingredient_id" INTEGER NOT NULL, + "quantity" DECIMAL(10,2) NOT NULL, + "unit" TEXT NOT NULL, + + CONSTRAINT "recipe_ingredient_pkey" PRIMARY KEY ("recipe_id","ingredient_id") +); + +-- CreateTable +CREATE TABLE "tech_step" ( + "id" SERIAL NOT NULL, + + CONSTRAINT "tech_step_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "tech_step_mapping" ( + "id" SERIAL NOT NULL, + "tech_step_id" INTEGER NOT NULL, + "expression" TEXT NOT NULL, + "weight" INTEGER NOT NULL, + + CONSTRAINT "tech_step_mapping_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "step" ( + "id" SERIAL NOT NULL, + "recipe_id" INTEGER NOT NULL, + "description" TEXT NOT NULL, + "picture" TEXT, + "order" INTEGER NOT NULL, + "tech_step_id" INTEGER, + + CONSTRAINT "step_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "user_profiles_email_key" ON "user_profiles"("email"); + +-- AddForeignKey +ALTER TABLE "allergy" ADD CONSTRAINT "allergy_cat_id_fkey" FOREIGN KEY ("cat_id") REFERENCES "category"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_house_id_fkey" FOREIGN KEY ("house_id") REFERENCES "house"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_profiles" ADD CONSTRAINT "user_profiles_diet_id_fkey" FOREIGN KEY ("diet_id") REFERENCES "diet"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_profile_allergy" ADD CONSTRAINT "user_profile_allergy_user_profile_id_fkey" FOREIGN KEY ("user_profile_id") REFERENCES "user_profiles"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "user_profile_allergy" ADD CONSTRAINT "user_profile_allergy_allergy_id_fkey" FOREIGN KEY ("allergy_id") REFERENCES "allergy"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "planning" ADD CONSTRAINT "planning_house_id_fkey" FOREIGN KEY ("house_id") REFERENCES "house"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "planning_item" ADD CONSTRAINT "planning_item_planning_id_fkey" FOREIGN KEY ("planning_id") REFERENCES "planning"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "planning_item" ADD CONSTRAINT "planning_item_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "recipe"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "recipe" ADD CONSTRAINT "recipe_source_id_fkey" FOREIGN KEY ("source_id") REFERENCES "sources"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ingredients" ADD CONSTRAINT "ingredients_alternate_recipe_fkey" FOREIGN KEY ("alternate_recipe") REFERENCES "recipe"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "recipe_ingredient" ADD CONSTRAINT "recipe_ingredient_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "recipe_ingredient" ADD CONSTRAINT "recipe_ingredient_ingredient_id_fkey" FOREIGN KEY ("ingredient_id") REFERENCES "ingredients"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "tech_step_mapping" ADD CONSTRAINT "tech_step_mapping_tech_step_id_fkey" FOREIGN KEY ("tech_step_id") REFERENCES "tech_step"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "step" ADD CONSTRAINT "step_recipe_id_fkey" FOREIGN KEY ("recipe_id") REFERENCES "recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "step" ADD CONSTRAINT "step_tech_step_id_fkey" FOREIGN KEY ("tech_step_id") REFERENCES "tech_step"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/migration_lock.toml b/apps/api/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..fbffa92 --- /dev/null +++ b/apps/api/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 4856958..ba92c96 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -7,4 +7,203 @@ datasource db { url = env("DATABASE_URL") } -// Models will be added once the data model is specified. +// ----------------------------------------------------------------------------- +// 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") +}