diff --git a/.gitea/workflows/main.yml b/.gitea/workflows/main.yml index b7735ff..892ffc9 100644 --- a/.gitea/workflows/main.yml +++ b/.gitea/workflows/main.yml @@ -87,8 +87,17 @@ jobs: - name: Generate Prisma Client run: cd packages/api && npm run prisma:generate - - name: Run database migrations - run: cd packages/api && npm run prisma:migrate + - name: Apply database migrations + run: cd packages/api && npm run prisma:deploy + env: + DATABASE_URL: postgresql://basil:basil@postgres:5432/basil_test?schema=public + + - name: Check for schema drift + run: | + cd packages/api && npx prisma migrate diff \ + --from-url "$DATABASE_URL" \ + --to-schema-datamodel ./prisma/schema.prisma \ + --exit-code && echo "✓ schema.prisma matches applied migrations" env: DATABASE_URL: postgresql://basil:basil@postgres:5432/basil_test?schema=public @@ -276,8 +285,8 @@ jobs: - name: Build application run: npm run build - - name: Run database migrations - run: cd packages/api && npm run prisma:migrate + - name: Apply database migrations + run: cd packages/api && npm run prisma:deploy env: DATABASE_URL: postgresql://basil:basil@postgres:5432/basil?schema=public diff --git a/.gitignore b/.gitignore index a729206..4d515ae 100644 --- a/.gitignore +++ b/.gitignore @@ -62,5 +62,5 @@ backups/ docker-compose.override.yml # Prisma -packages/api/prisma/migrations/ +# Migrations are tracked. Applied automatically by deploy.sh (via `prisma migrate deploy`). # Pipeline Test diff --git a/packages/api/package.json b/packages/api/package.json index 1abf90d..897d3a9 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -13,6 +13,7 @@ "test:coverage": "vitest run --coverage", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev", + "prisma:deploy": "prisma migrate deploy", "prisma:studio": "prisma studio", "create-admin": "tsx src/scripts/create-admin.ts", "lint": "eslint src --ext .ts" diff --git a/packages/api/prisma/migrations/20260416000000_init/migration.sql b/packages/api/prisma/migrations/20260416000000_init/migration.sql new file mode 100644 index 0000000..547db15 --- /dev/null +++ b/packages/api/prisma/migrations/20260416000000_init/migration.sql @@ -0,0 +1,455 @@ +-- CreateEnum +CREATE TYPE "TokenType" AS ENUM ('EMAIL_VERIFICATION', 'PASSWORD_RESET'); + +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); + +-- CreateEnum +CREATE TYPE "Visibility" AS ENUM ('PRIVATE', 'SHARED', 'PUBLIC'); + +-- CreateEnum +CREATE TYPE "MealType" AS ENUM ('BREAKFAST', 'LUNCH', 'DINNER', 'SNACK', 'DESSERT', 'OTHER'); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "username" TEXT, + "passwordHash" TEXT, + "name" TEXT, + "avatar" TEXT, + "provider" TEXT NOT NULL DEFAULT 'local', + "providerId" TEXT, + "role" "Role" NOT NULL DEFAULT 'USER', + "emailVerified" BOOLEAN NOT NULL DEFAULT false, + "emailVerifiedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "token" TEXT NOT NULL, + "type" "TokenType" NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RefreshToken" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Recipe" ( + "id" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT, + "prepTime" INTEGER, + "cookTime" INTEGER, + "totalTime" INTEGER, + "servings" INTEGER, + "imageUrl" TEXT, + "sourceUrl" TEXT, + "author" TEXT, + "cuisine" TEXT, + "categories" TEXT[] DEFAULT ARRAY[]::TEXT[], + "rating" DOUBLE PRECISION, + "userId" TEXT, + "visibility" "Visibility" NOT NULL DEFAULT 'PRIVATE', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Recipe_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RecipeSection" ( + "id" TEXT NOT NULL, + "recipeId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "order" INTEGER NOT NULL, + "timing" TEXT, + + CONSTRAINT "RecipeSection_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Ingredient" ( + "id" TEXT NOT NULL, + "recipeId" TEXT, + "sectionId" TEXT, + "name" TEXT NOT NULL, + "amount" TEXT, + "unit" TEXT, + "notes" TEXT, + "order" INTEGER NOT NULL, + + CONSTRAINT "Ingredient_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Instruction" ( + "id" TEXT NOT NULL, + "recipeId" TEXT, + "sectionId" TEXT, + "step" INTEGER NOT NULL, + "text" TEXT NOT NULL, + "imageUrl" TEXT, + "timing" TEXT, + + CONSTRAINT "Instruction_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "IngredientInstructionMapping" ( + "id" TEXT NOT NULL, + "ingredientId" TEXT NOT NULL, + "instructionId" TEXT NOT NULL, + "order" INTEGER NOT NULL, + + CONSTRAINT "IngredientInstructionMapping_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RecipeImage" ( + "id" TEXT NOT NULL, + "recipeId" TEXT NOT NULL, + "url" TEXT NOT NULL, + "order" INTEGER NOT NULL, + + CONSTRAINT "RecipeImage_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 "RecipeTag" ( + "recipeId" TEXT NOT NULL, + "tagId" TEXT NOT NULL, + + CONSTRAINT "RecipeTag_pkey" PRIMARY KEY ("recipeId","tagId") +); + +-- CreateTable +CREATE TABLE "CookbookTag" ( + "cookbookId" TEXT NOT NULL, + "tagId" TEXT NOT NULL, + + CONSTRAINT "CookbookTag_pkey" PRIMARY KEY ("cookbookId","tagId") +); + +-- CreateTable +CREATE TABLE "RecipeShare" ( + "id" TEXT NOT NULL, + "recipeId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "RecipeShare_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Cookbook" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT, + "coverImageUrl" TEXT, + "userId" TEXT, + "autoFilterCategories" TEXT[] DEFAULT ARRAY[]::TEXT[], + "autoFilterTags" TEXT[] DEFAULT ARRAY[]::TEXT[], + "autoFilterCookbookTags" TEXT[] DEFAULT ARRAY[]::TEXT[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Cookbook_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CookbookRecipe" ( + "id" TEXT NOT NULL, + "cookbookId" TEXT NOT NULL, + "recipeId" TEXT NOT NULL, + "addedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CookbookRecipe_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CookbookInclusion" ( + "id" TEXT NOT NULL, + "parentCookbookId" TEXT NOT NULL, + "childCookbookId" TEXT NOT NULL, + "addedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CookbookInclusion_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MealPlan" ( + "id" TEXT NOT NULL, + "userId" TEXT, + "date" TIMESTAMP(3) NOT NULL, + "notes" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "MealPlan_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Meal" ( + "id" TEXT NOT NULL, + "mealPlanId" TEXT NOT NULL, + "mealType" "MealType" NOT NULL, + "order" INTEGER NOT NULL, + "servings" INTEGER, + "notes" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Meal_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "MealRecipe" ( + "mealId" TEXT NOT NULL, + "recipeId" TEXT NOT NULL, + + CONSTRAINT "MealRecipe_pkey" PRIMARY KEY ("mealId") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); + +-- CreateIndex +CREATE INDEX "User_email_idx" ON "User"("email"); + +-- CreateIndex +CREATE INDEX "User_provider_providerId_idx" ON "User"("provider", "providerId"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE INDEX "VerificationToken_userId_idx" ON "VerificationToken"("userId"); + +-- CreateIndex +CREATE INDEX "VerificationToken_token_idx" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token"); + +-- CreateIndex +CREATE INDEX "RefreshToken_userId_idx" ON "RefreshToken"("userId"); + +-- CreateIndex +CREATE INDEX "RefreshToken_token_idx" ON "RefreshToken"("token"); + +-- CreateIndex +CREATE INDEX "Recipe_title_idx" ON "Recipe"("title"); + +-- CreateIndex +CREATE INDEX "Recipe_cuisine_idx" ON "Recipe"("cuisine"); + +-- CreateIndex +CREATE INDEX "Recipe_userId_idx" ON "Recipe"("userId"); + +-- CreateIndex +CREATE INDEX "Recipe_visibility_idx" ON "Recipe"("visibility"); + +-- CreateIndex +CREATE INDEX "RecipeSection_recipeId_idx" ON "RecipeSection"("recipeId"); + +-- CreateIndex +CREATE INDEX "Ingredient_recipeId_idx" ON "Ingredient"("recipeId"); + +-- CreateIndex +CREATE INDEX "Ingredient_sectionId_idx" ON "Ingredient"("sectionId"); + +-- CreateIndex +CREATE INDEX "Instruction_recipeId_idx" ON "Instruction"("recipeId"); + +-- CreateIndex +CREATE INDEX "Instruction_sectionId_idx" ON "Instruction"("sectionId"); + +-- CreateIndex +CREATE INDEX "IngredientInstructionMapping_instructionId_idx" ON "IngredientInstructionMapping"("instructionId"); + +-- CreateIndex +CREATE INDEX "IngredientInstructionMapping_ingredientId_idx" ON "IngredientInstructionMapping"("ingredientId"); + +-- CreateIndex +CREATE UNIQUE INDEX "IngredientInstructionMapping_ingredientId_instructionId_key" ON "IngredientInstructionMapping"("ingredientId", "instructionId"); + +-- CreateIndex +CREATE INDEX "RecipeImage_recipeId_idx" ON "RecipeImage"("recipeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Tag_name_key" ON "Tag"("name"); + +-- CreateIndex +CREATE INDEX "RecipeTag_recipeId_idx" ON "RecipeTag"("recipeId"); + +-- CreateIndex +CREATE INDEX "RecipeTag_tagId_idx" ON "RecipeTag"("tagId"); + +-- CreateIndex +CREATE INDEX "CookbookTag_cookbookId_idx" ON "CookbookTag"("cookbookId"); + +-- CreateIndex +CREATE INDEX "CookbookTag_tagId_idx" ON "CookbookTag"("tagId"); + +-- CreateIndex +CREATE INDEX "RecipeShare_recipeId_idx" ON "RecipeShare"("recipeId"); + +-- CreateIndex +CREATE INDEX "RecipeShare_userId_idx" ON "RecipeShare"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "RecipeShare_recipeId_userId_key" ON "RecipeShare"("recipeId", "userId"); + +-- CreateIndex +CREATE INDEX "Cookbook_name_idx" ON "Cookbook"("name"); + +-- CreateIndex +CREATE INDEX "Cookbook_userId_idx" ON "Cookbook"("userId"); + +-- CreateIndex +CREATE INDEX "CookbookRecipe_cookbookId_idx" ON "CookbookRecipe"("cookbookId"); + +-- CreateIndex +CREATE INDEX "CookbookRecipe_recipeId_idx" ON "CookbookRecipe"("recipeId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CookbookRecipe_cookbookId_recipeId_key" ON "CookbookRecipe"("cookbookId", "recipeId"); + +-- CreateIndex +CREATE INDEX "CookbookInclusion_parentCookbookId_idx" ON "CookbookInclusion"("parentCookbookId"); + +-- CreateIndex +CREATE INDEX "CookbookInclusion_childCookbookId_idx" ON "CookbookInclusion"("childCookbookId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CookbookInclusion_parentCookbookId_childCookbookId_key" ON "CookbookInclusion"("parentCookbookId", "childCookbookId"); + +-- CreateIndex +CREATE INDEX "MealPlan_userId_idx" ON "MealPlan"("userId"); + +-- CreateIndex +CREATE INDEX "MealPlan_date_idx" ON "MealPlan"("date"); + +-- CreateIndex +CREATE INDEX "MealPlan_userId_date_idx" ON "MealPlan"("userId", "date"); + +-- CreateIndex +CREATE UNIQUE INDEX "MealPlan_userId_date_key" ON "MealPlan"("userId", "date"); + +-- CreateIndex +CREATE INDEX "Meal_mealPlanId_idx" ON "Meal"("mealPlanId"); + +-- CreateIndex +CREATE INDEX "Meal_mealType_idx" ON "Meal"("mealType"); + +-- CreateIndex +CREATE INDEX "MealRecipe_recipeId_idx" ON "MealRecipe"("recipeId"); + +-- AddForeignKey +ALTER TABLE "VerificationToken" ADD CONSTRAINT "VerificationToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Recipe" ADD CONSTRAINT "Recipe_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RecipeSection" ADD CONSTRAINT "RecipeSection_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Ingredient" ADD CONSTRAINT "Ingredient_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Ingredient" ADD CONSTRAINT "Ingredient_sectionId_fkey" FOREIGN KEY ("sectionId") REFERENCES "RecipeSection"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Instruction" ADD CONSTRAINT "Instruction_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Instruction" ADD CONSTRAINT "Instruction_sectionId_fkey" FOREIGN KEY ("sectionId") REFERENCES "RecipeSection"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "IngredientInstructionMapping" ADD CONSTRAINT "IngredientInstructionMapping_ingredientId_fkey" FOREIGN KEY ("ingredientId") REFERENCES "Ingredient"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "IngredientInstructionMapping" ADD CONSTRAINT "IngredientInstructionMapping_instructionId_fkey" FOREIGN KEY ("instructionId") REFERENCES "Instruction"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RecipeImage" ADD CONSTRAINT "RecipeImage_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RecipeTag" ADD CONSTRAINT "RecipeTag_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RecipeTag" ADD CONSTRAINT "RecipeTag_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CookbookTag" ADD CONSTRAINT "CookbookTag_cookbookId_fkey" FOREIGN KEY ("cookbookId") REFERENCES "Cookbook"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CookbookTag" ADD CONSTRAINT "CookbookTag_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RecipeShare" ADD CONSTRAINT "RecipeShare_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RecipeShare" ADD CONSTRAINT "RecipeShare_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Cookbook" ADD CONSTRAINT "Cookbook_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CookbookRecipe" ADD CONSTRAINT "CookbookRecipe_cookbookId_fkey" FOREIGN KEY ("cookbookId") REFERENCES "Cookbook"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CookbookRecipe" ADD CONSTRAINT "CookbookRecipe_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CookbookInclusion" ADD CONSTRAINT "CookbookInclusion_parentCookbookId_fkey" FOREIGN KEY ("parentCookbookId") REFERENCES "Cookbook"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CookbookInclusion" ADD CONSTRAINT "CookbookInclusion_childCookbookId_fkey" FOREIGN KEY ("childCookbookId") REFERENCES "Cookbook"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MealPlan" ADD CONSTRAINT "MealPlan_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Meal" ADD CONSTRAINT "Meal_mealPlanId_fkey" FOREIGN KEY ("mealPlanId") REFERENCES "MealPlan"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MealRecipe" ADD CONSTRAINT "MealRecipe_mealId_fkey" FOREIGN KEY ("mealId") REFERENCES "Meal"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "MealRecipe" ADD CONSTRAINT "MealRecipe_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe"("id") ON DELETE CASCADE ON UPDATE CASCADE; + diff --git a/packages/api/prisma/migrations/20260416010000_add_family_tenant/migration.sql b/packages/api/prisma/migrations/20260416010000_add_family_tenant/migration.sql new file mode 100644 index 0000000..0d1dee9 --- /dev/null +++ b/packages/api/prisma/migrations/20260416010000_add_family_tenant/migration.sql @@ -0,0 +1,59 @@ +-- CreateEnum +CREATE TYPE "FamilyRole" AS ENUM ('OWNER', 'MEMBER'); + +-- AlterTable +ALTER TABLE "Cookbook" ADD COLUMN "familyId" TEXT; + +-- AlterTable +ALTER TABLE "Recipe" ADD COLUMN "familyId" TEXT; + +-- CreateTable +CREATE TABLE "Family" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Family_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "FamilyMember" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "familyId" TEXT NOT NULL, + "role" "FamilyRole" NOT NULL DEFAULT 'MEMBER', + "joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "FamilyMember_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "Family_name_idx" ON "Family"("name"); + +-- CreateIndex +CREATE INDEX "FamilyMember_userId_idx" ON "FamilyMember"("userId"); + +-- CreateIndex +CREATE INDEX "FamilyMember_familyId_idx" ON "FamilyMember"("familyId"); + +-- CreateIndex +CREATE UNIQUE INDEX "FamilyMember_userId_familyId_key" ON "FamilyMember"("userId", "familyId"); + +-- CreateIndex +CREATE INDEX "Cookbook_familyId_idx" ON "Cookbook"("familyId"); + +-- CreateIndex +CREATE INDEX "Recipe_familyId_idx" ON "Recipe"("familyId"); + +-- AddForeignKey +ALTER TABLE "FamilyMember" ADD CONSTRAINT "FamilyMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "FamilyMember" ADD CONSTRAINT "FamilyMember_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "Family"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Recipe" ADD CONSTRAINT "Recipe_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "Family"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Cookbook" ADD CONSTRAINT "Cookbook_familyId_fkey" FOREIGN KEY ("familyId") REFERENCES "Family"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/packages/api/prisma/migrations/migration_lock.toml b/packages/api/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/packages/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 (e.g., Git) +provider = "postgresql" diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 91ad24e..604b5ba 100755 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -131,6 +131,32 @@ EOF log "Docker Compose override file created" } +# Apply database migrations using the newly-pulled API image. +# Runs before restart so a failed migration leaves the old containers running. +run_migrations() { + log "Applying database migrations..." + + if [ -z "$DATABASE_URL" ]; then + error "DATABASE_URL not set in .env — cannot apply migrations" + exit 1 + fi + + local API_IMAGE="${DOCKER_REGISTRY}/${DOCKER_USERNAME}/basil-api:${IMAGE_TAG}" + + # Use --network=host so the container can reach the same DB host the app uses. + # schema.prisma and migrations/ ship inside the API image. + docker run --rm \ + --network host \ + -e DATABASE_URL="$DATABASE_URL" \ + "$API_IMAGE" \ + npx prisma migrate deploy || { + error "Migration failed — aborting deploy. Old containers are still running." + exit 1 + } + + log "Migrations applied successfully" +} + # Restart containers restart_containers() { log "Restarting containers..." @@ -219,6 +245,7 @@ main() { login_to_harbor create_backup pull_images + run_migrations update_docker_compose restart_containers health_check