From 83858c12c3b2111d399ef9ee3a46d6387c1292b3 Mon Sep 17 00:00:00 2001 From: Matilde Lund Date: Thu, 1 Oct 2026 09:10:39 +0200 Subject: [PATCH] Add Prisma schema and initial migration --- docs/schema-graph.graphql | 31 +++ server/.gitignore | 5 + server/package.json | 19 +- .../20260930094150_init/migration.sql | 189 ++++++++++++++++++ server/prisma/migrations/migration_lock.toml | 3 + server/prisma/schema.prisma | 120 +++++++++++ server/prisma/seed.ts | 40 ++++ 7 files changed, 400 insertions(+), 7 deletions(-) create mode 100644 docs/schema-graph.graphql create mode 100644 server/.gitignore create mode 100644 server/prisma/migrations/20260930094150_init/migration.sql create mode 100644 server/prisma/migrations/migration_lock.toml create mode 100644 server/prisma/schema.prisma create mode 100644 server/prisma/seed.ts diff --git a/docs/schema-graph.graphql b/docs/schema-graph.graphql new file mode 100644 index 0000000..cb9d265 --- /dev/null +++ b/docs/schema-graph.graphql @@ -0,0 +1,31 @@ +type Recipe { + id: ID! + title: String! + description: String! + prepTimeMinutes: Int! + servings: Int! + category: String! + imageUrl: String + tags: [Tag!]! + ingredients: [RecipeIngredient!]! + steps: [Step!]! + author: User! + averageRating: Float +} + +type Query { + recipes(search: String, tags: [String!], category: String, sortBy: RecipeSortField, page: Int, pageSize: Int): RecipeConnection! + recipe(id: ID!): Recipe + me: User +} + +type RecipeConnection { + items: [Recipe!]! + totalCount: Int! +} + +enum RecipeSortField { + NEWEST + RATING + PREP_TIME +} \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore new file mode 100644 index 0000000..126419d --- /dev/null +++ b/server/.gitignore @@ -0,0 +1,5 @@ +node_modules +# Keep environment variables out of version control +.env + +/src/generated/prisma diff --git a/server/package.json b/server/package.json index 727c265..4206cb3 100644 --- a/server/package.json +++ b/server/package.json @@ -3,16 +3,17 @@ "version": "1.0.0", "description": "", "main": "index.js", - "scripts": { - "dev": "tsx watch src/index.ts", - "build": "tsc", - "start": "node dist/index.js", - "test": "vitest" -}, + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js", + "test": "vitest" + }, "keywords": [], "author": "", "license": "ISC", "type": "commonjs", + "prisma": { "seed": "tsx prisma/seed.ts" }, "devDependencies": { "@types/cookie-parser": "^1.4.10", "@types/cors": "^2.8.19", @@ -20,10 +21,14 @@ "@types/multer": "^2.2.0", "@types/node": "^24.19.0", "@types/supertest": "^7.2.1", - "prisma": "^8.0.0-rc.17", + "dotenv": "^16.6.1", + "prisma": "^6.19.3", "supertest": "^7.3.0", "tsx": "^4.23.15", "typescript": "^6.0.3", "vitest": "^5.0.2" + }, + "dependencies": { + "@prisma/client": "^6.19.3" } } diff --git a/server/prisma/migrations/20260930094150_init/migration.sql b/server/prisma/migrations/20260930094150_init/migration.sql new file mode 100644 index 0000000..3b06f9a --- /dev/null +++ b/server/prisma/migrations/20260930094150_init/migration.sql @@ -0,0 +1,189 @@ +-- CreateEnum +CREATE TYPE "RecipeStatus" AS ENUM ('DRAFT', 'PUBLISHED'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "googleId" TEXT, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "languagePref" TEXT NOT NULL DEFAULT 'nb', + "unitSystem" TEXT NOT NULL DEFAULT 'metric', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Recipe" ( + "id" TEXT NOT NULL, + "authorId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "prepTimeMinutes" INTEGER NOT NULL, + "servings" INTEGER NOT NULL, + "category" TEXT NOT NULL, + "imageUrl" TEXT, + "status" "RecipeStatus" NOT NULL DEFAULT 'DRAFT', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Recipe_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RecipeIngredient" ( + "id" TEXT NOT NULL, + "recipeId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "amount" DOUBLE PRECISION NOT NULL, + "unit" TEXT NOT NULL, + "order" INTEGER NOT NULL, + + CONSTRAINT "RecipeIngredient_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Step" ( + "id" TEXT NOT NULL, + "recipeId" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "instruction" TEXT NOT NULL, + + CONSTRAINT "Step_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Tag" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Tag_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Allergen" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + + CONSTRAINT "Allergen_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Favorite" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "recipeId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Favorite_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Rating" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "recipeId" TEXT NOT NULL, + "score" INTEGER NOT NULL, + "comment" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Rating_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SearchHistory" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "query" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "SearchHistory_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_RecipeTags" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_RecipeTags_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "_UserAllergens" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_UserAllergens_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_googleId_key" ON "User"("googleId"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE INDEX "Recipe_category_idx" ON "Recipe"("category"); + +-- CreateIndex +CREATE INDEX "Recipe_createdAt_idx" ON "Recipe"("createdAt"); + +-- CreateIndex +CREATE INDEX "RecipeIngredient_recipeId_idx" ON "RecipeIngredient"("recipeId"); + +-- CreateIndex +CREATE INDEX "Step_recipeId_idx" ON "Step"("recipeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Allergen_name_key" ON "Allergen"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Favorite_userId_recipeId_key" ON "Favorite"("userId", "recipeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Rating_userId_recipeId_key" ON "Rating"("userId", "recipeId"); + +-- CreateIndex +CREATE INDEX "_RecipeTags_B_index" ON "_RecipeTags"("B"); + +-- CreateIndex +CREATE INDEX "_UserAllergens_B_index" ON "_UserAllergens"("B"); + +-- AddForeignKey +ALTER TABLE "Recipe" ADD CONSTRAINT "Recipe_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RecipeIngredient" ADD CONSTRAINT "RecipeIngredient_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Step" ADD CONSTRAINT "Step_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Favorite" ADD CONSTRAINT "Favorite_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Favorite" ADD CONSTRAINT "Favorite_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Rating" ADD CONSTRAINT "Rating_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Rating" ADD CONSTRAINT "Rating_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "SearchHistory" ADD CONSTRAINT "SearchHistory_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_RecipeTags" ADD CONSTRAINT "_RecipeTags_A_fkey" FOREIGN KEY ("A") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_RecipeTags" ADD CONSTRAINT "_RecipeTags_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_UserAllergens" ADD CONSTRAINT "_UserAllergens_A_fkey" FOREIGN KEY ("A") REFERENCES "Allergen"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_UserAllergens" ADD CONSTRAINT "_UserAllergens_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/server/prisma/migrations/migration_lock.toml b/server/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/server/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 (e.g., Git) +provider = "postgresql" diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma new file mode 100644 index 0000000..85ef5b9 --- /dev/null +++ b/server/prisma/schema.prisma @@ -0,0 +1,120 @@ +//Oversikt over strukturen på backend, inneholder ikke handleliste enda med ellers alt av grunnleggende struktur +//bruk av @@index er bevist for å kunne filtrere på tidspunkt created, og kategori +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} +//brukeren sin oversikt +model User { + id String @id @default(uuid()) + googleId String? @unique + name String + email String @unique + languagePref String @default("nb") + unitSystem String @default("metric") + createdAt DateTime @default(now()) + + recipes Recipe[] + favorites Favorite[] + ratings Rating[] + searches SearchHistory[] + allergens Allergen[] @relation("UserAllergens") +} +//oppskriftoversikt +model Recipe { + id String @id @default(uuid()) + author User @relation(fields: [authorId], references: [id]) + authorId String + title String + description String + prepTimeMinutes Int + servings Int + category String + imageUrl String? + status RecipeStatus @default(DRAFT) + createdAt DateTime @default(now()) + + ingredients RecipeIngredient[] + steps Step[] + tags Tag[] @relation("RecipeTags") + favorites Favorite[] + ratings Rating[] + + @@index([category]) + @@index([createdAt]) +} + +enum RecipeStatus { + DRAFT + PUBLISHED +} + +model RecipeIngredient { + id String @id @default(uuid()) + recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade) + recipeId String + name String + amount Float + unit String + order Int + + @@index([recipeId]) +} + +model Step { + id String @id @default(uuid()) + recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade) + recipeId String + order Int + instruction String + + @@index([recipeId]) +} + +model Tag { + id String @id @default(uuid()) + name String @unique + recipes Recipe[] @relation("RecipeTags") +} + +model Allergen { + id String @id @default(uuid()) + name String @unique + users User[] @relation("UserAllergens") +} + +model Favorite { + id String @id @default(uuid()) + user User @relation(fields: [userId], references: [id]) + userId String + recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade) + recipeId String + createdAt DateTime @default(now()) + + @@unique([userId, recipeId]) +} + +model Rating { + id String @id @default(uuid()) + user User @relation(fields: [userId], references: [id]) + userId String + recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade) + recipeId String + score Int + comment String? + createdAt DateTime @default(now()) + + @@unique([userId, recipeId]) +} +//tenkte dette var fint for hukommelse senere +model SearchHistory { + id String @id @default(uuid()) + user User @relation(fields: [userId], references: [id]) + userId String + query String + createdAt DateTime @default(now()) +} \ No newline at end of file diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts new file mode 100644 index 0000000..ff856cb --- /dev/null +++ b/server/prisma/seed.ts @@ -0,0 +1,40 @@ +import { PrismaClient } from '@prisma/client'; +const prisma = new PrismaClient(); + +async function main() { + const tagPasta = await prisma.tag.upsert({ where: { name: 'vegetar' }, update: {}, create: { name: 'vegetar'}}); + + const user = await prisma.user.create({ + data: { name: 'Test Testesen', email: 'testesen@eksempel.no' }, + }); + + await prisma.recipe.create({ + data: { + title: "Pesto Pasta", + description: "beste studentmiddagen vi har", + prepTimeMinutes: 15, + servings: 1, + category: 'middag', + status: 'PUBLISHED', + authorId: user.id, + tags: { connect: [{ id: tagPasta.id}]}, + ingredients: { + create: [ + { name: 'Pasta', amount: 200, unit: 'g', order: 1}, + { name: 'Pesto', amount: 0.5, unit: 'stk', order: 2}, + ], + }, + + steps: { + create: [ + { order: 1, instruction: 'Kok opp vann og ha i pasta når det koker. følg koketid på pakka'}, + { order: 2, instruction: 'Ha ferdigkokt pasta i en bolle og ha over et halvt glass pesto, bland sammen'}, + { order: 3, instruction: 'Spis yummy pasta.'} + ], + }, + }, + }); + +} + +main().finally(() => prisma.$disconnect()); \ No newline at end of file