Add specs + Prisma schema for the documented data model (#3)
* Add project specs, gitignore the source PDF specs/batch-cooking-architecture.md and specs/batch-cooking-modele.md are the clean markdown transcription of "Projet batch cooking.pdf" (a scanned/image-only PDF, no extractable text). The PDF itself is gitignored — source working document, not meant to be committed. * 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). * Add COMMENT ON for every table and column in the init migration Descriptions pulled from specs/batch-cooking-modele.md's per-table field tables. The two tables not in the original spec (join tables recipe_ingredient, user_profile_allergy) get a comment explaining why they exist. Amends the still-unmerged init migration directly rather than adding a follow-up migration, since it hasn't been applied anywhere but this local dev database. Verified: `prisma migrate reset --force` reapplies cleanly, and a query against pg_description confirms every column of every project table has a comment (only Prisma's own internal _prisma_migrations table is uncommented, out of scope).
This commit is contained in:
parent
c53803d708
commit
b90817b8e1
7 changed files with 810 additions and 1 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
10
README.md
10
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
|
||||
|
|
|
|||
271
apps/api/prisma/migrations/20260816100611_init/migration.sql
Normal file
271
apps/api/prisma/migrations/20260816100611_init/migration.sql
Normal file
|
|
@ -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;
|
||||
3
apps/api/prisma/migrations/migration_lock.toml
Normal file
3
apps/api/prisma/migrations/migration_lock.toml
Normal file
|
|
@ -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"
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
72
specs/batch-cooking-architecture.md
Normal file
72
specs/batch-cooking-architecture.md
Normal file
|
|
@ -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<br/><i>(TODO)</i>"]
|
||||
IMPORT["Import d'une recette"]
|
||||
IMP1["Import depuis source"]
|
||||
IMP2["Traduction en étapes"]
|
||||
IMP3["Sauvegarde"]
|
||||
DB[("Database<br/>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.
|
||||
251
specs/batch-cooking-modele.md
Normal file
251
specs/batch-cooking-modele.md
Normal file
|
|
@ -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.
|
||||
Loading…
Reference in a new issue