fix(db): add explicit onDelete strategies to all Prisma FK relations

Audit and update all foreign key relations in schema.prisma with
appropriate cascade/restrict/set-null strategies to prevent orphaned
records and FK constraint violations on parent deletion.

Changes (RESTRICT → CASCADE):
- Agent.userId, Listing.propertyId, Transaction.listingId
- Inquiry.listingId, Inquiry.userId, Lead.agentId
- Subscription.userId, UsageRecord.subscriptionId
- Valuation.propertyId, Review.userId

Confirmed correct (no change needed):
- Listing.agentId (SetNull), Listing.sellerId (Restrict)
- Transaction.buyerId (Restrict), Payment.userId (Restrict)
- Payment.transactionId (SetNull), Subscription.planId (Restrict)
- PropertyMedia, SavedSearch, RefreshToken, OAuthAccount (CASCADE)

Migration: 20260411000000_add_cascade_delete_strategies

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-11 00:21:46 +07:00
parent b7f9664709
commit 45e48c063c
2 changed files with 176 additions and 79 deletions

View File

@@ -0,0 +1,59 @@
-- DropForeignKey
ALTER TABLE "Agent" DROP CONSTRAINT "Agent_userId_fkey";
-- DropForeignKey
ALTER TABLE "Listing" DROP CONSTRAINT "Listing_propertyId_fkey";
-- DropForeignKey
ALTER TABLE "Transaction" DROP CONSTRAINT "Transaction_listingId_fkey";
-- DropForeignKey
ALTER TABLE "Inquiry" DROP CONSTRAINT "Inquiry_listingId_fkey";
-- DropForeignKey
ALTER TABLE "Inquiry" DROP CONSTRAINT "Inquiry_userId_fkey";
-- DropForeignKey
ALTER TABLE "Lead" DROP CONSTRAINT "Lead_agentId_fkey";
-- DropForeignKey
ALTER TABLE "Subscription" DROP CONSTRAINT "Subscription_userId_fkey";
-- DropForeignKey
ALTER TABLE "UsageRecord" DROP CONSTRAINT "UsageRecord_subscriptionId_fkey";
-- DropForeignKey
ALTER TABLE "Valuation" DROP CONSTRAINT "Valuation_propertyId_fkey";
-- DropForeignKey
ALTER TABLE "Review" DROP CONSTRAINT "Review_userId_fkey";
-- AddForeignKey: Agent.userId -> User.id (CASCADE)
ALTER TABLE "Agent" ADD CONSTRAINT "Agent_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Listing.propertyId -> Property.id (CASCADE)
ALTER TABLE "Listing" ADD CONSTRAINT "Listing_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Transaction.listingId -> Listing.id (CASCADE)
ALTER TABLE "Transaction" ADD CONSTRAINT "Transaction_listingId_fkey" FOREIGN KEY ("listingId") REFERENCES "Listing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Inquiry.listingId -> Listing.id (CASCADE)
ALTER TABLE "Inquiry" ADD CONSTRAINT "Inquiry_listingId_fkey" FOREIGN KEY ("listingId") REFERENCES "Listing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Inquiry.userId -> User.id (CASCADE)
ALTER TABLE "Inquiry" ADD CONSTRAINT "Inquiry_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Lead.agentId -> Agent.id (CASCADE)
ALTER TABLE "Lead" ADD CONSTRAINT "Lead_agentId_fkey" FOREIGN KEY ("agentId") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Subscription.userId -> User.id (CASCADE)
ALTER TABLE "Subscription" ADD CONSTRAINT "Subscription_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: UsageRecord.subscriptionId -> Subscription.id (CASCADE)
ALTER TABLE "UsageRecord" ADD CONSTRAINT "UsageRecord_subscriptionId_fkey" FOREIGN KEY ("subscriptionId") REFERENCES "Subscription"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Valuation.propertyId -> Property.id (CASCADE)
ALTER TABLE "Valuation" ADD CONSTRAINT "Valuation_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey: Review.userId -> User.id (CASCADE)
ALTER TABLE "Review" ADD CONSTRAINT "Review_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -64,7 +64,6 @@ model User {
@@index([deletedAt])
@@index([deletionScheduledAt])
@@index([createdAt])
// --- Compound indexes (query optimization) ---
@@index([role, isActive, createdAt(sort: Desc)])
@@index([kycStatus, createdAt])
@@ -110,7 +109,7 @@ model OAuthAccount {
model Agent {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
licenseNumber String?
agency String?
qualityScore Float @default(0)
@@ -204,7 +203,6 @@ model Property {
@@index([propertyType])
@@index([district, city])
@@index([location], type: Gist)
// --- Compound indexes (query optimization) ---
@@index([district, propertyType])
@@index([district, city, propertyType])
@@ -227,11 +225,11 @@ model PropertyMedia {
model Listing {
id String @id @default(cuid())
propertyId String
property Property @relation(fields: [propertyId], references: [id])
property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
agentId String?
agent Agent? @relation(fields: [agentId], references: [id])
agent Agent? @relation(fields: [agentId], references: [id], onDelete: SetNull)
sellerId String
seller User @relation(fields: [sellerId], references: [id])
seller User @relation(fields: [sellerId], references: [id], onDelete: Restrict)
transactionType TransactionType
status ListingStatus @default(DRAFT)
priceVND BigInt
@@ -265,7 +263,6 @@ model Listing {
@@index([createdAt])
@@index([featuredUntil])
@@index([expiresAt])
// --- Compound indexes (query optimization) ---
@@index([sellerId, status, publishedAt(sort: Desc)])
@@index([agentId, status])
@@ -309,9 +306,9 @@ enum TransactionStatus {
model Transaction {
id String @id @default(cuid())
listingId String
listing Listing @relation(fields: [listingId], references: [id])
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
buyerId String
buyer User @relation("BuyerTransactions", fields: [buyerId], references: [id])
buyer User @relation("BuyerTransactions", fields: [buyerId], references: [id], onDelete: Restrict)
status TransactionStatus @default(INQUIRY)
agreedPrice BigInt?
depositAmount BigInt?
@@ -330,9 +327,9 @@ model Transaction {
model Inquiry {
id String @id @default(cuid())
listingId String
listing Listing @relation(fields: [listingId], references: [id])
listing Listing @relation(fields: [listingId], references: [id], onDelete: Cascade)
userId String
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
message String @db.Text
phone String?
isRead Boolean @default(false)
@@ -355,7 +352,7 @@ enum LeadStatus {
model Lead {
id String @id @default(cuid())
agentId String
agent Agent @relation(fields: [agentId], references: [id])
agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade)
name String
phone String
email String?
@@ -399,9 +396,9 @@ enum PaymentType {
model Payment {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
transactionId String?
transaction Transaction? @relation(fields: [transactionId], references: [id])
transaction Transaction? @relation(fields: [transactionId], references: [id], onDelete: SetNull)
provider PaymentProvider
type PaymentType
amountVND BigInt
@@ -418,7 +415,6 @@ model Payment {
@@index([status])
@@index([providerTxId])
@@index([createdAt])
// --- Compound indexes (query optimization) ---
@@index([userId, status, createdAt(sort: Desc)])
@@index([userId, type, createdAt(sort: Desc)])
@@ -461,9 +457,9 @@ model Plan {
model Subscription {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
planId String
plan Plan @relation(fields: [planId], references: [id])
plan Plan @relation(fields: [planId], references: [id], onDelete: Restrict)
status SubscriptionStatus @default(ACTIVE)
currentPeriodStart DateTime
currentPeriodEnd DateTime
@@ -480,7 +476,7 @@ model Subscription {
model UsageRecord {
id String @id @default(cuid())
subscriptionId String
subscription Subscription @relation(fields: [subscriptionId], references: [id])
subscription Subscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade)
metric String
count Int
periodStart DateTime
@@ -496,7 +492,7 @@ model UsageRecord {
model Valuation {
id String @id @default(cuid())
propertyId String
property Property @relation(fields: [propertyId], references: [id])
property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
estimatedPrice BigInt
confidence Float
pricePerM2 Float
@@ -581,6 +577,48 @@ model NotificationPreference {
@@index([userId])
}
// =============================================================================
// ADMIN AUDIT LOG
// =============================================================================
enum AdminAction {
LISTING_APPROVED
LISTING_REJECTED
LISTING_BULK_APPROVED
LISTING_BULK_REJECTED
USER_BANNED
USER_UNBANNED
USER_STATUS_UPDATED
KYC_APPROVED
KYC_REJECTED
SUBSCRIPTION_ADJUSTED
}
enum AuditTargetType {
USER
LISTING
SUBSCRIPTION
}
model AdminAuditLog {
id String @id @default(cuid())
action AdminAction
actorId String
targetId String
targetType AuditTargetType
metadata Json?
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
@@index([actorId])
@@index([targetId, targetType])
@@index([action])
@@index([createdAt])
@@index([actorId, createdAt(sort: Desc)])
@@index([action, createdAt(sort: Desc)])
}
// =============================================================================
// REVIEWS
// =============================================================================
@@ -588,7 +626,7 @@ model NotificationPreference {
model Review {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id])
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
targetType String
targetId String
rating Int