From ff004cdf8d46538f41a77c2f661a0f0ca1980544 Mon Sep 17 00:00:00 2001 From: luciano Date: Wed, 29 Apr 2026 12:28:43 +0200 Subject: [PATCH] feat(db): scaffold Prisma schema with User, Profile, Preferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initial schema covers only the 3 base entities from PRD §8 — additional models (MealPlan, Recipe, WeightEntry, FastEvent, ShoppingList, etc.) will be added alongside their respective features. - Postgres datasource via DATABASE_URL - Enums: Role, Gender, ActivityLevel, Phase, CookingTime, FastingProtocol - Singleton PrismaClient export from @ketopath/db - Root scripts: db:generate / db:migrate / db:studio / db:seed / db:format - Seed stub at prisma/seed.ts - Ignore .claude/settings.local.json (per-user CLI permissions) The first migration must be run by the developer: pnpm db:migrate --name init Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 + package.json | 6 ++ packages/db/.env.example | 1 + packages/db/.gitignore | 2 + packages/db/package.json | 19 +++++- packages/db/prisma/schema.prisma | 101 +++++++++++++++++++++++++++++++ packages/db/prisma/seed.ts | 20 ++++++ packages/db/src/client.ts | 13 ++++ packages/db/src/index.ts | 12 +++- pnpm-lock.yaml | Bin 166996 -> 169379 bytes 10 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 packages/db/.env.example create mode 100644 packages/db/.gitignore create mode 100644 packages/db/prisma/schema.prisma create mode 100644 packages/db/prisma/seed.ts create mode 100644 packages/db/src/client.ts diff --git a/.gitignore b/.gitignore index cb2b88a..169a5a5 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ Thumbs.db # Misc .cache/ + +# Claude Code per-user settings +.claude/settings.local.json diff --git a/package.json b/package.json index 5293a18..c009aa6 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,12 @@ "typecheck": "tsc -b", "test": "pnpm --filter \"./packages/*\" --filter \"./apps/*\" -r --if-present test", "test:e2e": "pnpm --filter @ketopath/web test:e2e", + "db:generate": "pnpm --filter @ketopath/db db:generate", + "db:migrate": "pnpm --filter @ketopath/db db:migrate", + "db:migrate:deploy": "pnpm --filter @ketopath/db db:migrate:deploy", + "db:studio": "pnpm --filter @ketopath/db db:studio", + "db:format": "pnpm --filter @ketopath/db db:format", + "db:seed": "pnpm --filter @ketopath/db db:seed", "prepare": "husky" }, "devDependencies": { diff --git a/packages/db/.env.example b/packages/db/.env.example new file mode 100644 index 0000000..b9e34d3 --- /dev/null +++ b/packages/db/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgresql://luciano:luciano%40@localhost:5432/ketopath?schema=public" diff --git a/packages/db/.gitignore b/packages/db/.gitignore new file mode 100644 index 0000000..f8028b9 --- /dev/null +++ b/packages/db/.gitignore @@ -0,0 +1,2 @@ +.env +*.tsbuildinfo diff --git a/packages/db/package.json b/packages/db/package.json index ebdd467..6a34cf6 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -10,10 +10,25 @@ }, "scripts": { "typecheck": "tsc --noEmit", - "lint": "eslint src --max-warnings=0" + "lint": "eslint src --max-warnings=0", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:migrate:deploy": "prisma migrate deploy", + "db:studio": "prisma studio", + "db:format": "prisma format", + "db:seed": "tsx prisma/seed.ts" + }, + "dependencies": { + "@prisma/client": "^5.18.0" }, "devDependencies": { "@ketopath/eslint-config": "workspace:*", - "@ketopath/tsconfig": "workspace:*" + "@ketopath/tsconfig": "workspace:*", + "prisma": "^5.18.0", + "tsx": "^4.16.2" + }, + "prisma": { + "schema": "prisma/schema.prisma", + "seed": "tsx prisma/seed.ts" } } diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma new file mode 100644 index 0000000..b0e94e9 --- /dev/null +++ b/packages/db/prisma/schema.prisma @@ -0,0 +1,101 @@ +// KetoPath — schema Prisma +// Schema iniziale: solo le 3 entità di base (User, Profile, Preferences). +// Le altre entità del PRD §8 (MealPlan, Recipe, WeightEntry, ecc.) verranno +// aggiunte in step successivi, allineate alle feature corrispondenti. + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum Role { + USER + COACH +} + +enum Gender { + MALE + FEMALE + OTHER +} + +enum ActivityLevel { + SEDENTARY + LIGHT + MODERATE + INTENSE +} + +enum Phase { + INTENSIVE + TRANSITION + MAINTENANCE +} + +enum CookingTime { + LOW + MEDIUM + HIGH +} + +enum FastingProtocol { + FOURTEEN_TEN + SIXTEEN_EIGHT + EIGHTEEN_SIX + TWENTY_FOUR + ESE_24 + FIVE_TWO +} + +model User { + id String @id @default(cuid()) + email String @unique + role Role @default(USER) + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + profile Profile? + preferences Preferences? + + @@map("users") +} + +model Profile { + id String @id @default(cuid()) + userId String @unique @map("user_id") + age Int + gender Gender + heightCm Int @map("height_cm") + weightStartKg Decimal @map("weight_start_kg") @db.Decimal(5, 2) + weightCurrentKg Decimal @map("weight_current_kg") @db.Decimal(5, 2) + weightGoalKg Decimal @map("weight_goal_kg") @db.Decimal(5, 2) + activityLevel ActivityLevel @map("activity_level") + targetDate DateTime? @map("target_date") @db.Date + currentPhase Phase @default(INTENSIVE) @map("current_phase") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("profiles") +} + +model Preferences { + id String @id @default(cuid()) + userId String @unique @map("user_id") + exclusions String[] @default([]) + cuisinePreferences String[] @default([]) @map("cuisine_preferences") + cookingTime CookingTime @default(MEDIUM) @map("cooking_time") + fastingProtocol FastingProtocol? @map("fasting_protocol") + notificationSettings Json @default("{}") @map("notification_settings") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("preferences") +} diff --git a/packages/db/prisma/seed.ts b/packages/db/prisma/seed.ts new file mode 100644 index 0000000..46e0e8d --- /dev/null +++ b/packages/db/prisma/seed.ts @@ -0,0 +1,20 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main(): Promise { + // TODO: dati di seed (utenti demo, ricette base, ecc.) — verranno aggiunti + // quando le entità relative saranno modellate. + // eslint-disable-next-line no-console + console.info('[seed] nothing to seed yet'); +} + +main() + .catch((err: unknown) => { + // eslint-disable-next-line no-console + console.error(err); + process.exitCode = 1; + }) + .finally(() => { + void prisma.$disconnect(); + }); diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts new file mode 100644 index 0000000..655e39f --- /dev/null +++ b/packages/db/src/client.ts @@ -0,0 +1,13 @@ +import { PrismaClient } from '@prisma/client'; + +const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; + +export const prisma: PrismaClient = + globalForPrisma.prisma ?? + new PrismaClient({ + log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'], + }); + +if (process.env.NODE_ENV !== 'production') { + globalForPrisma.prisma = prisma; +} diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index cb0ff5c..e733bd1 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1 +1,11 @@ -export {}; +export { prisma } from './client.js'; +export { + ActivityLevel, + CookingTime, + FastingProtocol, + Gender, + Phase, + Prisma, + Role, +} from '@prisma/client'; +export type { Preferences, Profile, User } from '@prisma/client'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ce307642b3f900d209f399dd8f2a490766d8f67..acb7b3deeb0f7a5405ebdb8fa5c0a8ed4c929601 100644 GIT binary patch delta 1740 zcmc(f&2QUu7{`^cbyZa;+q6!AC_%iW$ZF#FEp}nj9LI_C;>LAi$H^q5_%*f@$8qeu zp{bgkP!a?ZIZsM-K5^|$(Fy*^ZWds=lMQ={;>Ar zFKbVKdTaKM5WTnVnS*XCsdP-u@k4TD^X8XmB87)&IJ$A#L3rZ;l{0A zxV~Yp$L7!CXLvihdE+!H3PItwtJ(0$t<7J*+hD%9v(PM0nCJOk+7?)_DoXlxf35smRU$M#_uWD7zLbZa9^XqV+Wj<~sOM+dPCl$ItG4*;TX1qSUD z7}W#QRWbbcudN%26dVr^ovoE6++pq0)7NLUt=mf+Rg(kbl;(gGd;}T7jiP246{sno z8Cxo(S)?5hnH)5oGLV^rMAV%~wPc5{P3k7yX{HN4AoEO$9<+NbKV}TKVIX1#^C%YR z=22_^!Gp(ZYq41T;qpw)F-%7r>QJdY3!0k3P$94;oF=-A@jBQgWPJDdBC(jK4SM05(t8KO zo}~{u^)~5)m@SrJ%Cduo%aHYI1uG+VmJ&J55HZvD(@r*JSBtHo3J==`Nw+PdQ!W-u zv|3E}3jJ&mm@qchuiyqpUM`rf`AU0dQMQ*r!c{4DAlqIx$tsXu9oqXD5vAQ;PHEz4 za#%64V>wR{2#&W=?>SLwu4Tq*`S2JR!}&%D4LC4pWuOPaxy}6 ziv}p)891dW*XV+(D#)57Os%|_FYuzlWhomu?#0*7OGnR3k@3dri9Juw4G(|b7KXlS z)nO@UP*sN=?1>qx%sb+hBK*{4TT zfyU}XDNyQy<`)e;NS?Unjbqo$o>-ClNBKvQLg$mKroFNX;=42Q_sE9_3+Zlbu_0a> z_;Zsb!1!+Lt83cfx^93@a=!b@r1MVA{&*VsV9A%9vm6I3F&F#mb=Ex79h>voG;@bs zu=`?q`NjC2WxTAqIA7&H;@-vGdlq=~&2N#%;n|$e fz>lNv-@n|LrF!;aBl^+opT8q_k8VDSR#N`~Pjwxl delta 63 zcmV-F0KosFs|wVZ3b3ROlNJYPlXwqnlf4lPvu+SoJcA%nw;)jgaSyjpVF5>Mw`