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).
This commit is contained in:
parent
c66fe09843
commit
c957db27df
4 changed files with 402 additions and 1 deletions
10
README.md
10
README.md
|
|
@ -58,6 +58,9 @@ hors du repo).
|
||||||
# Base de données Postgres locale
|
# Base de données Postgres locale
|
||||||
docker compose up -d
|
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)
|
# Backend (http://localhost:3000)
|
||||||
pnpm dev:api
|
pnpm dev:api
|
||||||
|
|
||||||
|
|
@ -65,6 +68,13 @@ pnpm dev:api
|
||||||
pnpm dev:web
|
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
|
## Qualité / Tests
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
189
apps/api/prisma/migrations/20260816100611_init/migration.sql
Normal file
189
apps/api/prisma/migrations/20260816100611_init/migration.sql
Normal file
|
|
@ -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;
|
||||||
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")
|
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")
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue