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