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:
@@ -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;
|
||||
@@ -32,31 +32,31 @@ enum KYCStatus {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String? @unique
|
||||
phone String @unique
|
||||
passwordHash String?
|
||||
fullName String
|
||||
avatarUrl String?
|
||||
role UserRole @default(BUYER)
|
||||
kycStatus KYCStatus @default(NONE)
|
||||
kycData Json?
|
||||
isActive Boolean @default(true)
|
||||
deletedAt DateTime?
|
||||
deletionScheduledAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
email String? @unique
|
||||
phone String @unique
|
||||
passwordHash String?
|
||||
fullName String
|
||||
avatarUrl String?
|
||||
role UserRole @default(BUYER)
|
||||
kycStatus KYCStatus @default(NONE)
|
||||
kycData Json?
|
||||
isActive Boolean @default(true)
|
||||
deletedAt DateTime?
|
||||
deletionScheduledAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
agent Agent?
|
||||
listings Listing[]
|
||||
savedSearches SavedSearch[]
|
||||
subscription Subscription?
|
||||
payments Payment[]
|
||||
reviews Review[]
|
||||
inquiriesSent Inquiry[]
|
||||
refreshTokens RefreshToken[]
|
||||
oauthAccounts OAuthAccount[]
|
||||
buyerTransactions Transaction[] @relation("BuyerTransactions")
|
||||
agent Agent?
|
||||
listings Listing[]
|
||||
savedSearches SavedSearch[]
|
||||
subscription Subscription?
|
||||
payments Payment[]
|
||||
reviews Review[]
|
||||
inquiriesSent Inquiry[]
|
||||
refreshTokens RefreshToken[]
|
||||
oauthAccounts OAuthAccount[]
|
||||
buyerTransactions Transaction[] @relation("BuyerTransactions")
|
||||
|
||||
@@index([role])
|
||||
@@index([kycStatus])
|
||||
@@ -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])
|
||||
@@ -76,14 +75,14 @@ enum OAuthProvider {
|
||||
}
|
||||
|
||||
model RefreshToken {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
token String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
token String @unique
|
||||
family String
|
||||
expiresAt DateTime
|
||||
revokedAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
@@index([family])
|
||||
@@ -110,14 +109,14 @@ 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)
|
||||
totalDeals Int @default(0)
|
||||
responseTimeAvg Int?
|
||||
bio String?
|
||||
serviceAreas Json // ["quan-1", "quan-7", "thu-duc"]
|
||||
serviceAreas Json // ["quan-1", "quan-7", "thu-duc"]
|
||||
isVerified Boolean @default(false)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
@@ -170,10 +169,10 @@ enum Direction {
|
||||
}
|
||||
|
||||
model Property {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
propertyType PropertyType
|
||||
title String
|
||||
description String @db.Text
|
||||
description String @db.Text
|
||||
address String
|
||||
ward String
|
||||
district String
|
||||
@@ -193,8 +192,8 @@ model Property {
|
||||
nearbyPOIs Json?
|
||||
metroDistanceM Float?
|
||||
projectName String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
listings Listing[]
|
||||
valuations Valuation[]
|
||||
@@ -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])
|
||||
@@ -215,7 +213,7 @@ model PropertyMedia {
|
||||
propertyId String
|
||||
property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
|
||||
url String
|
||||
type String // "image" | "video"
|
||||
type String // "image" | "video"
|
||||
order Int @default(0)
|
||||
caption String?
|
||||
aiTags Json?
|
||||
@@ -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?
|
||||
@@ -397,20 +394,20 @@ enum PaymentType {
|
||||
}
|
||||
|
||||
model Payment {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
transactionId String?
|
||||
transaction Transaction? @relation(fields: [transactionId], references: [id])
|
||||
provider PaymentProvider
|
||||
type PaymentType
|
||||
amountVND BigInt
|
||||
status PaymentStatus @default(PENDING)
|
||||
providerTxId String?
|
||||
callbackData Json?
|
||||
idempotencyKey String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
|
||||
transactionId String?
|
||||
transaction Transaction? @relation(fields: [transactionId], references: [id], onDelete: SetNull)
|
||||
provider PaymentProvider
|
||||
type PaymentType
|
||||
amountVND BigInt
|
||||
status PaymentStatus @default(PENDING)
|
||||
providerTxId String?
|
||||
callbackData Json?
|
||||
idempotencyKey String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([userId, provider, idempotencyKey], name: "Payment_idempotency_unique")
|
||||
@@index([userId])
|
||||
@@ -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)])
|
||||
@@ -443,17 +439,17 @@ enum SubscriptionStatus {
|
||||
}
|
||||
|
||||
model Plan {
|
||||
id String @id @default(cuid())
|
||||
tier PlanTier @unique
|
||||
name String
|
||||
priceMonthlyVND BigInt
|
||||
priceYearlyVND BigInt
|
||||
id String @id @default(cuid())
|
||||
tier PlanTier @unique
|
||||
name String
|
||||
priceMonthlyVND BigInt
|
||||
priceYearlyVND BigInt
|
||||
maxListings Int?
|
||||
maxSavedSearches Int?
|
||||
maxSavedSearches Int?
|
||||
maxAnalyticsQueries Int?
|
||||
maxMediaUploads Int?
|
||||
features Json
|
||||
isActive Boolean @default(true)
|
||||
maxMediaUploads Int?
|
||||
features Json
|
||||
isActive Boolean @default(true)
|
||||
|
||||
subscriptions Subscription[]
|
||||
}
|
||||
@@ -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
|
||||
@@ -547,18 +543,18 @@ enum NotificationStatus {
|
||||
}
|
||||
|
||||
model NotificationLog {
|
||||
id String @id @default(cuid())
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
channel NotificationChannel
|
||||
templateKey String
|
||||
subject String?
|
||||
body String @db.Text
|
||||
body String @db.Text
|
||||
metadata Json?
|
||||
status NotificationStatus @default(PENDING)
|
||||
status NotificationStatus @default(PENDING)
|
||||
errorDetail String?
|
||||
sentAt DateTime?
|
||||
readAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([userId])
|
||||
@@index([channel, status])
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user