diff --git a/.gitignore b/.gitignore index 872d5f6..c2e1081 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,6 @@ dist vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ + +# Project docs not meant to be committed +Projet batch cooking.pdf 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..2d429cd --- /dev/null +++ b/apps/api/prisma/migrations/20260816100611_init/migration.sql @@ -0,0 +1,271 @@ +-- CreateTable +CREATE TABLE "house" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "house_pkey" PRIMARY KEY ("id") +); + +COMMENT ON TABLE "house" IS 'Foyer regroupant un ou plusieurs profils utilisateurs et leurs plannings.'; +COMMENT ON COLUMN "house"."id" IS 'Identifiant'; +COMMENT ON COLUMN "house"."name" IS 'Nom du foyer'; + +-- CreateTable +CREATE TABLE "diet" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "diet_pkey" PRIMARY KEY ("id") +); + +COMMENT ON TABLE "diet" IS 'Régime alimentaire pouvant être suivi par un profil utilisateur.'; +COMMENT ON COLUMN "diet"."id" IS 'Identifiant'; +COMMENT ON COLUMN "diet"."name" IS 'Nom du régime alimentaire'; + +-- CreateTable +CREATE TABLE "category" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "category_pkey" PRIMARY KEY ("id") +); + +COMMENT ON TABLE "category" IS 'Table d''énumération, destinée à grandir au fil du projet (portera notamment les nuances liées aux allergies).'; +COMMENT ON COLUMN "category"."id" IS 'Identifiant'; +COMMENT ON COLUMN "category"."name" IS 'Nom de la catégorie'; + +-- CreateTable +CREATE TABLE "allergy" ( + "id" SERIAL NOT NULL, + "cat_id" INTEGER NOT NULL, + + CONSTRAINT "allergy_pkey" PRIMARY KEY ("id") +); + +COMMENT ON TABLE "allergy" IS 'Allergie, rattachée à une catégorie. Associée à user_profiles en many-to-many (table de jointure simple, sans champ additionnel).'; +COMMENT ON COLUMN "allergy"."id" IS 'Identifiant'; +COMMENT ON COLUMN "allergy"."cat_id" IS 'FK → category'; + +-- 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") +); + +COMMENT ON TABLE "user_profiles" IS 'Profil utilisateur, rattaché à un foyer et éventuellement à un régime alimentaire.'; +COMMENT ON COLUMN "user_profiles"."id" IS 'Identifiant'; +COMMENT ON COLUMN "user_profiles"."first_name" IS 'Prénom'; +COMMENT ON COLUMN "user_profiles"."last_name" IS 'Nom'; +COMMENT ON COLUMN "user_profiles"."email" IS 'Email'; +COMMENT ON COLUMN "user_profiles"."house_id" IS 'FK → house'; +COMMENT ON COLUMN "user_profiles"."diet_id" IS 'FK → diet'; + +-- 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") +); + +COMMENT ON TABLE "user_profile_allergy" IS 'Table de jointure simple pour l''association many-to-many user_profiles ↔ allergy (sans champ additionnel, non présente telle quelle dans la spec d''origine).'; +COMMENT ON COLUMN "user_profile_allergy"."user_profile_id" IS 'FK → user_profiles'; +COMMENT ON COLUMN "user_profile_allergy"."allergy_id" IS 'FK → allergy'; + +-- 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") +); + +COMMENT ON TABLE "planning" IS 'Planning de repas d''un foyer sur une période donnée.'; +COMMENT ON COLUMN "planning"."id" IS 'Identifiant'; +COMMENT ON COLUMN "planning"."start_date" IS 'Date de début'; +COMMENT ON COLUMN "planning"."finish_date" IS 'Date de fin'; +COMMENT ON COLUMN "planning"."house_id" IS 'FK → house'; + +-- 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") +); + +COMMENT ON TABLE "planning_item" IS 'Entrée d''un planning : une recette assignée à un jour et un repas donnés.'; +COMMENT ON COLUMN "planning_item"."id" IS 'Identifiant'; +COMMENT ON COLUMN "planning_item"."planning_id" IS 'FK → planning'; +COMMENT ON COLUMN "planning_item"."week_day" IS 'Jour de la semaine'; +COMMENT ON COLUMN "planning_item"."meal" IS 'Repas concerné'; +COMMENT ON COLUMN "planning_item"."recipe_id" IS 'FK → recipe'; + +-- CreateTable +CREATE TABLE "sources" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "url" TEXT, + + CONSTRAINT "sources_pkey" PRIMARY KEY ("id") +); + +COMMENT ON TABLE "sources" IS 'Source d''origine d''une recette (site, livre, etc.), utilisée par le pipeline d''import.'; +COMMENT ON COLUMN "sources"."id" IS 'Identifiant'; +COMMENT ON COLUMN "sources"."name" IS 'Nom de la source'; +COMMENT ON COLUMN "sources"."url" IS 'URL'; + +-- 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") +); + +COMMENT ON TABLE "recipe" IS 'Recette de cuisine. Associée à ingredients en many-to-many (voir recipe_ingredient).'; +COMMENT ON COLUMN "recipe"."id" IS 'Identifiant'; +COMMENT ON COLUMN "recipe"."name" IS 'Nom de la recette'; +COMMENT ON COLUMN "recipe"."source_id" IS 'FK → sources'; +COMMENT ON COLUMN "recipe"."description" IS 'Description'; +COMMENT ON COLUMN "recipe"."picture" IS 'Image'; + +-- CreateTable +CREATE TABLE "ingredients" ( + "id" SERIAL NOT NULL, + "name" TEXT NOT NULL, + "icon" TEXT, + "alternate_recipe" INTEGER, + + CONSTRAINT "ingredients_pkey" PRIMARY KEY ("id") +); + +COMMENT ON TABLE "ingredients" IS 'Ingrédient pouvant entrer dans la composition d''une ou plusieurs recettes.'; +COMMENT ON COLUMN "ingredients"."id" IS 'Identifiant'; +COMMENT ON COLUMN "ingredients"."name" IS 'Nom'; +COMMENT ON COLUMN "ingredients"."icon" IS 'Icône'; +COMMENT ON COLUMN "ingredients"."alternate_recipe" IS 'FK → recipe (recette alternative, ex: faire soi-même plutôt qu''acheter)'; + +-- 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") +); + +COMMENT ON TABLE "recipe_ingredient" IS 'Association recipe ↔ ingredients enrichie d''une quantité et d''une unité (non présent dans la spec d''origine, nécessaire pour les listes de courses / le calcul batch-cooking).'; +COMMENT ON COLUMN "recipe_ingredient"."recipe_id" IS 'FK → recipe'; +COMMENT ON COLUMN "recipe_ingredient"."ingredient_id" IS 'FK → ingredients'; +COMMENT ON COLUMN "recipe_ingredient"."quantity" IS 'Quantité de l''ingrédient nécessaire pour la recette'; +COMMENT ON COLUMN "recipe_ingredient"."unit" IS 'Unité de mesure de la quantité (g, ml, pièce...)'; + +-- CreateTable +CREATE TABLE "tech_step" ( + "id" SERIAL NOT NULL, + + CONSTRAINT "tech_step_pkey" PRIMARY KEY ("id") +); + +COMMENT ON TABLE "tech_step" IS 'Technique culinaire réutilisable (ex: éplucher, mixer, cuire...), référencée par les étapes de recette.'; +COMMENT ON COLUMN "tech_step"."id" IS 'Identifiant'; + +-- 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") +); + +COMMENT ON TABLE "tech_step_mapping" IS 'Utilisé par le pipeline d''import de recette pour détecter automatiquement la technique correspondant à une instruction brute (expression = motif texte, weight = score de correspondance).'; +COMMENT ON COLUMN "tech_step_mapping"."id" IS 'Identifiant'; +COMMENT ON COLUMN "tech_step_mapping"."tech_step_id" IS 'FK → tech_step'; +COMMENT ON COLUMN "tech_step_mapping"."expression" IS 'Expression'; +COMMENT ON COLUMN "tech_step_mapping"."weight" IS 'Poids'; + +-- 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") +); + +COMMENT ON TABLE "step" IS 'Étape d''une recette. Appartient à exactement une recette (one-to-many depuis recipe) : le champ order n''a de sens que dans le contexte d''une recette donnée — voir specs/batch-cooking-modele.md pour la formulation d''origine (many-to-many).'; +COMMENT ON COLUMN "step"."id" IS 'Identifiant'; +COMMENT ON COLUMN "step"."recipe_id" IS 'FK → recipe'; +COMMENT ON COLUMN "step"."description" IS 'Description de l''étape'; +COMMENT ON COLUMN "step"."picture" IS 'Image'; +COMMENT ON COLUMN "step"."order" IS 'Ordre dans la recette'; +COMMENT ON COLUMN "step"."tech_step_id" IS 'FK → tech_step'; + +-- 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") +} diff --git a/specs/batch-cooking-architecture.md b/specs/batch-cooking-architecture.md new file mode 100644 index 0000000..9332a8a --- /dev/null +++ b/specs/batch-cooking-architecture.md @@ -0,0 +1,72 @@ +# Architecture technique — Projet Batch-cooking + +> Documentation de l'architecture serveur/client de l'application. + +--- + +## Vue d'ensemble + +L'application repose sur une architecture **client-serveur** classique : + +- Un **serveur** exposant une **API** (échanges standards) et un canal **websocket** (communication temps réel) +- Plusieurs **clients** (Client 1, Client 2, Client 3...) connectés simultanément au serveur +- Une base de données **PostgreSQL** + +```mermaid +flowchart TB + subgraph SERVER["Server"] + WS["Web socket"] + API["API"] + CALC["Calcul batch-cooking
(TODO)"] + IMPORT["Import d'une recette"] + IMP1["Import depuis source"] + IMP2["Traduction en étapes"] + IMP3["Sauvegarde"] + DB[("Database
PostgreSQL")] + + IMPORT --> IMP1 --> IMP2 --> IMP3 --> DB + CALC --> WS + end + + C1["Client 1"] + C2["Client 2"] + C3["Client 3"] + + API <--> C1 + API <--> C2 + API <--> C3 + WS --> C1 + WS --> C2 + WS --> C3 + + style SERVER fill:none,stroke:#888,stroke-width:1px +``` + +--- + +## Composants + +### API +Point d'entrée principal pour les échanges entre les clients et le serveur (requêtes classiques). + +### Web socket +Canal de communication temps réel entre le serveur et les clients connectés. + +### Module « Calcul batch-cooking » +Logique de calcul du batch-cooking (optimisation du planning/des recettes selon le planning). **Statut : TODO — reste à développer.** + +### Module « Import d'une recette » +Pipeline d'ajout d'une recette, en trois étapes : +1. **Import depuis source** — récupération de la recette (via `sources`) +2. **Traduction en étapes** — découpage en `step` / `tech_step` +3. **Sauvegarde** — persistance en base de données + +### Database (PostgreSQL) +Stockage de l'ensemble des données de l'application (voir le modèle de données pour le détail des tables). + +--- + +## Notes + +- Le module de calcul batch-cooking est le principal chantier restant côté serveur (TODO). +- Le websocket est utilisé pour la communication temps réel, en complément de l'API. diff --git a/specs/batch-cooking-modele.md b/specs/batch-cooking-modele.md new file mode 100644 index 0000000..32804f2 --- /dev/null +++ b/specs/batch-cooking-modele.md @@ -0,0 +1,251 @@ +# Modèle de données — Projet Batch-cooking + +> Documentation du schéma de données de l'application de planification de batch-cooking. + +--- + +## Vue d'ensemble + +Le modèle s'articule autour de trois grands pôles : + +- **Utilisateurs & foyer** — `user_profiles`, `house`, `diet`, `allergy`, `category` +- **Planification** — `planning`, `planning_item` +- **Recettes** — `recipe`, `ingredients`, `step`, `tech_step`, `tech_step_mapping`, `sources` + +--- + +## Schéma entité-relation + +```mermaid +erDiagram + USER_PROFILES }o--|| HOUSE : "vit dans" + USER_PROFILES }o--|| DIET : "suit" + HOUSE ||--o{ PLANNING : "planifie" + PLANNING ||--o{ PLANNING_ITEM : "contient" + PLANNING_ITEM }o--|| RECIPE : "utilise" + RECIPE }o--|| SOURCES : "vient de" + CATEGORY ||--o{ ALLERGY : "classe" + USER_PROFILES }o--o{ ALLERGY : "a" + RECIPE }o--o{ INGREDIENTS : "compose de" + RECIPE }o--o{ STEP : "compose de" + STEP }o--|| TECH_STEP : "utilise" + TECH_STEP ||--o{ TECH_STEP_MAPPING : "mappe" + INGREDIENTS }o--|| RECIPE : "recette alternative" + + USER_PROFILES { + int id PK + string first_name + string last_name + string email + int house_id FK + int diet_id FK + } + HOUSE { + int id PK + string name + } + PLANNING { + int id PK + date start_date + date finish_date + int house_id FK + } + PLANNING_ITEM { + int id PK + int planning_id FK + string week_day + string meal + int recipe_id FK + } + DIET { + int id PK + string name + } + ALLERGY { + int id PK + int cat_id FK + } + CATEGORY { + int id PK + string name + } + INGREDIENTS { + int id PK + string name + string icon + int alternate_recipe FK + } + RECIPE { + int id PK + string name + int source_id FK + string description + string picture + } + STEP { + int id PK + string description + string picture + int order + int tech_step_id FK + } + TECH_STEP { + int id PK + } + TECH_STEP_MAPPING { + int id PK + int tech_step_id FK + string expression + int weight + } + SOURCES { + int id PK + string name + string url + } +``` + +*(Rendu sur les visualiseurs markdown compatibles mermaid — GitHub, VS Code, Obsidian, etc.)* + +--- + +## Tables + +### `user_profiles` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `first_name` | Prénom | +| `last_name` | Nom | +| `email` | Email | +| `house_id` | FK → `house` | +| `diet_id` | FK → `diet` | + +### `house` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `name` | Nom du foyer | + +### `diet` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `name` | Nom du régime alimentaire | + +### `allergy` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `cat_id` | FK → `category` | + +Associée à `user_profiles` en many-to-many (table de jointure simple, sans champ additionnel). + +### `category` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `name` | Nom de la catégorie | + +Table d'énumération, destinée à grandir au fil du projet (portera notamment les nuances liées aux allergies). + +### `planning` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `start_date` | Date de début | +| `finish_date` | Date de fin | +| `house_id` | FK → `house` | + +### `planning_item` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `planning_id` | FK → `planning` | +| `week_day` | Jour de la semaine | +| `meal` | Repas concerné | +| `recipe_id` | FK → `recipe` | + +### `recipe` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `name` | Nom de la recette | +| `source_id` | FK → `sources` | +| `description` | Description | +| `picture` | Image | + +Associée à `ingredients` en many-to-many. + +### `ingredients` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `name` | Nom | +| `icon` | Icône | +| `alternate_recipe` | FK → `recipe` (recette alternative) | + +### `step` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `description` | Description de l'étape | +| `picture` | Image | +| `order` | Ordre dans la recette | +| `tech_step_id` | FK → `tech_step` | + +Associée à `recipe` en many-to-many. + +### `tech_step` +| Champ | Description | +|---|---| +| `id` | Identifiant | + +### `tech_step_mapping` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `tech_step_id` | FK → `tech_step` | +| `expression` | Expression | +| `weight` | Poids | + +### `sources` +| Champ | Description | +|---|---| +| `id` | Identifiant | +| `name` | Nom de la source | +| `url` | URL | + +--- + +## Relations + +### Many-to-one (clés étrangères) + +| Table source | Champ FK | Table cible | +|---|---|---| +| `user_profiles` | `house_id` | `house` | +| `user_profiles` | `diet_id` | `diet` | +| `planning` | `house_id` | `house` | +| `planning_item` | `planning_id` | `planning` | +| `planning_item` | `recipe_id` | `recipe` | +| `allergy` | `cat_id` | `category` | +| `ingredients` | `alternate_recipe` | `recipe` | +| `recipe` | `source_id` | `sources` | +| `step` | `tech_step_id` | `tech_step` | +| `tech_step_mapping` | `tech_step_id` | `tech_step` | + +### Many-to-many (associations) + +| Table A | Table B | Détail | +|---|---|---| +| `user_profiles` | `allergy` | Table de jointure simple | +| `recipe` | `ingredients` | Composition d'une recette | +| `step` | `recipe` | Étapes d'une recette | + +--- + +## Règles de modélisation + +- Toute relation qualifiée d'**« association »** entre deux tables est une relation **many-to-many**. +- `category` est une table d'énumération, amenée à grandir au fur et à mesure du projet.