Release 2025-05-19

This commit is contained in:
pluja
2025-05-19 10:23:36 +00:00
parent 2657f936bc
commit 565e9a0ad1
267 changed files with 49417 additions and 0 deletions

View File

@@ -0,0 +1,798 @@
-- CreateEnum
CREATE TYPE "CommentStatus" AS ENUM ('PENDING', 'HUMAN_PENDING', 'APPROVED', 'VERIFIED', 'REJECTED');
-- CreateEnum
CREATE TYPE "OrderIdStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED');
-- CreateEnum
CREATE TYPE "VerificationStatus" AS ENUM ('COMMUNITY_CONTRIBUTED', 'APPROVED', 'VERIFICATION_SUCCESS', 'VERIFICATION_FAILED');
-- CreateEnum
CREATE TYPE "ServiceInfoBanner" AS ENUM ('NONE', 'NO_LONGER_OPERATIONAL');
-- CreateEnum
CREATE TYPE "ServiceVisibility" AS ENUM ('PUBLIC', 'UNLISTED', 'HIDDEN');
-- CreateEnum
CREATE TYPE "Currency" AS ENUM ('MONERO', 'BITCOIN', 'LIGHTNING', 'FIAT', 'CASH');
-- CreateEnum
CREATE TYPE "EventType" AS ENUM ('WARNING', 'WARNING_SOLVED', 'ALERT', 'ALERT_SOLVED', 'INFO', 'NORMAL', 'UPDATE');
-- CreateEnum
CREATE TYPE "ServiceUserRole" AS ENUM ('OWNER', 'ADMIN', 'MODERATOR', 'SUPPORT', 'TEAM_MEMBER');
-- CreateEnum
CREATE TYPE "AccountStatusChange" AS ENUM ('ADMIN_TRUE', 'ADMIN_FALSE', 'VERIFIED_TRUE', 'VERIFIED_FALSE', 'VERIFIER_TRUE', 'VERIFIER_FALSE', 'SPAMMER_TRUE', 'SPAMMER_FALSE');
-- CreateEnum
CREATE TYPE "NotificationType" AS ENUM ('COMMENT_STATUS_CHANGE', 'REPLY_COMMENT_CREATED', 'COMMUNITY_NOTE_ADDED', 'ROOT_COMMENT_CREATED', 'SUGGESTION_MESSAGE', 'SUGGESTION_STATUS_CHANGE', 'ACCOUNT_STATUS_CHANGE', 'EVENT_CREATED', 'SERVICE_VERIFICATION_STATUS_CHANGE');
-- CreateEnum
CREATE TYPE "CommentStatusChange" AS ENUM ('MARKED_AS_SPAM', 'UNMARKED_AS_SPAM', 'MARKED_FOR_ADMIN_REVIEW', 'UNMARKED_FOR_ADMIN_REVIEW', 'STATUS_CHANGED_TO_APPROVED', 'STATUS_CHANGED_TO_VERIFIED', 'STATUS_CHANGED_TO_REJECTED', 'STATUS_CHANGED_TO_PENDING');
-- CreateEnum
CREATE TYPE "ServiceVerificationStatusChange" AS ENUM ('STATUS_CHANGED_TO_COMMUNITY_CONTRIBUTED', 'STATUS_CHANGED_TO_APPROVED', 'STATUS_CHANGED_TO_VERIFICATION_SUCCESS', 'STATUS_CHANGED_TO_VERIFICATION_FAILED');
-- CreateEnum
CREATE TYPE "ServiceSuggestionStatusChange" AS ENUM ('STATUS_CHANGED_TO_PENDING', 'STATUS_CHANGED_TO_APPROVED', 'STATUS_CHANGED_TO_REJECTED', 'STATUS_CHANGED_TO_WITHDRAWN');
-- CreateEnum
CREATE TYPE "ServiceSuggestionStatus" AS ENUM ('PENDING', 'APPROVED', 'REJECTED', 'WITHDRAWN');
-- CreateEnum
CREATE TYPE "ServiceSuggestionType" AS ENUM ('CREATE_SERVICE', 'EDIT_SERVICE');
-- CreateEnum
CREATE TYPE "AttributeCategory" AS ENUM ('PRIVACY', 'TRUST');
-- CreateEnum
CREATE TYPE "AttributeType" AS ENUM ('GOOD', 'BAD', 'WARNING', 'INFO');
-- CreateEnum
CREATE TYPE "VerificationStepStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'PASSED', 'FAILED');
-- CreateTable
CREATE TABLE "Comment" (
"id" SERIAL NOT NULL,
"upvotes" INTEGER NOT NULL DEFAULT 0,
"status" "CommentStatus" NOT NULL DEFAULT 'PENDING',
"suspicious" BOOLEAN NOT NULL DEFAULT false,
"requiresAdminReview" BOOLEAN NOT NULL DEFAULT false,
"communityNote" TEXT,
"verificationNote" TEXT,
"internalNote" TEXT,
"privateContext" TEXT,
"orderId" VARCHAR(100),
"orderIdStatus" "OrderIdStatus" DEFAULT 'PENDING',
"kycRequested" BOOLEAN NOT NULL DEFAULT false,
"fundsBlocked" BOOLEAN NOT NULL DEFAULT false,
"content" TEXT NOT NULL,
"rating" SMALLINT,
"ratingActive" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"authorId" INTEGER NOT NULL,
"serviceId" INTEGER NOT NULL,
"parentId" INTEGER,
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Notification" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"type" "NotificationType" NOT NULL,
"read" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"aboutCommentId" INTEGER,
"aboutEventId" INTEGER,
"aboutServiceId" INTEGER,
"aboutServiceSuggestionId" INTEGER,
"aboutServiceSuggestionMessageId" INTEGER,
"aboutAccountStatusChange" "AccountStatusChange",
"aboutCommentStatusChange" "CommentStatusChange",
"aboutServiceVerificationStatusChange" "ServiceVerificationStatusChange",
"aboutSuggestionStatusChange" "ServiceSuggestionStatusChange",
CONSTRAINT "Notification_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NotificationPreferences" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
"enableOnMyCommentStatusChange" BOOLEAN NOT NULL DEFAULT true,
"enableAutowatchMyComments" BOOLEAN NOT NULL DEFAULT true,
"enableNotifyPendingRepliesOnWatch" BOOLEAN NOT NULL DEFAULT false,
CONSTRAINT "NotificationPreferences_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "NotificationPreferenceOnServiceVerificationChangeFilterFilter" (
"id" SERIAL NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"verificationStatus" "VerificationStepStatus" NOT NULL,
"notificationPreferencesId" INTEGER NOT NULL,
"currencies" "Currency"[],
"scores" INTEGER[],
CONSTRAINT "NotificationPreferenceOnServiceVerificationChangeFilterFil_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Event" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"content" TEXT NOT NULL,
"source" TEXT,
"type" "EventType" NOT NULL,
"visible" BOOLEAN NOT NULL DEFAULT true,
"startedAt" TIMESTAMP(3) NOT NULL,
"endedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"serviceId" INTEGER NOT NULL,
CONSTRAINT "Event_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ServiceSuggestion" (
"id" SERIAL NOT NULL,
"type" "ServiceSuggestionType" NOT NULL,
"status" "ServiceSuggestionStatus" NOT NULL DEFAULT 'PENDING',
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
"serviceId" INTEGER NOT NULL,
CONSTRAINT "ServiceSuggestion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ServiceSuggestionMessage" (
"id" SERIAL NOT NULL,
"content" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
"suggestionId" INTEGER NOT NULL,
CONSTRAINT "ServiceSuggestionMessage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Service" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT NOT NULL,
"kycLevel" INTEGER NOT NULL DEFAULT 4,
"overallScore" INTEGER NOT NULL DEFAULT 0,
"privacyScore" INTEGER NOT NULL DEFAULT 0,
"trustScore" INTEGER NOT NULL DEFAULT 0,
"isRecentlyListed" BOOLEAN NOT NULL DEFAULT false,
"averageUserRating" DOUBLE PRECISION,
"serviceVisibility" "ServiceVisibility" NOT NULL DEFAULT 'PUBLIC',
"serviceInfoBanner" "ServiceInfoBanner" NOT NULL DEFAULT 'NONE',
"serviceInfoBannerNotes" TEXT,
"verificationStatus" "VerificationStatus" NOT NULL DEFAULT 'COMMUNITY_CONTRIBUTED',
"verificationSummary" TEXT,
"verificationProofMd" TEXT,
"verifiedAt" TIMESTAMP(3),
"userSentiment" JSONB,
"userSentimentAt" TIMESTAMP(3),
"referral" TEXT,
"acceptedCurrencies" "Currency"[] DEFAULT ARRAY[]::"Currency"[],
"serviceUrls" TEXT[],
"tosUrls" TEXT[] DEFAULT ARRAY[]::TEXT[],
"onionUrls" TEXT[] DEFAULT ARRAY[]::TEXT[],
"i2pUrls" TEXT[] DEFAULT ARRAY[]::TEXT[],
"imageUrl" TEXT,
"tosReview" JSONB,
"tosReviewAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"listedAt" TIMESTAMP(3),
CONSTRAINT "Service_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ServiceContactMethod" (
"id" SERIAL NOT NULL,
"label" TEXT NOT NULL,
"value" TEXT NOT NULL,
"iconId" TEXT NOT NULL,
"info" TEXT NOT NULL,
"serviceId" INTEGER NOT NULL,
CONSTRAINT "ServiceContactMethod_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Attribute" (
"id" SERIAL NOT NULL,
"slug" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"privacyPoints" INTEGER NOT NULL DEFAULT 0,
"trustPoints" INTEGER NOT NULL DEFAULT 0,
"category" "AttributeCategory" NOT NULL,
"type" "AttributeType" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Attribute_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "InternalUserNote" (
"id" SERIAL NOT NULL,
"content" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"userId" INTEGER NOT NULL,
"addedByUserId" INTEGER,
CONSTRAINT "InternalUserNote_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"displayName" TEXT,
"link" TEXT,
"picture" TEXT,
"spammer" BOOLEAN NOT NULL DEFAULT false,
"verified" BOOLEAN NOT NULL DEFAULT false,
"admin" BOOLEAN NOT NULL DEFAULT false,
"verifier" BOOLEAN NOT NULL DEFAULT false,
"verifiedLink" TEXT,
"secretTokenHash" TEXT NOT NULL,
"totalKarma" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "CommentVote" (
"id" SERIAL NOT NULL,
"downvote" BOOLEAN NOT NULL DEFAULT false,
"commentId" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CommentVote_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ServiceAttribute" (
"serviceId" INTEGER NOT NULL,
"attributeId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ServiceAttribute_pkey" PRIMARY KEY ("serviceId","attributeId")
);
-- CreateTable
CREATE TABLE "KarmaTransaction" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"action" TEXT NOT NULL,
"points" INTEGER NOT NULL DEFAULT 0,
"commentId" INTEGER,
"suggestionId" INTEGER,
"description" TEXT NOT NULL,
"processed" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "KarmaTransaction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "VerificationStep" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL,
"status" "VerificationStepStatus" NOT NULL DEFAULT 'PENDING',
"evidenceMd" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"serviceId" INTEGER NOT NULL,
CONSTRAINT "VerificationStep_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Category" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"icon" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ServiceVerificationRequest" (
"id" SERIAL NOT NULL,
"serviceId" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "ServiceVerificationRequest_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ServiceScoreRecalculationJob" (
"id" SERIAL NOT NULL,
"serviceId" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"processedAt" TIMESTAMP(3),
CONSTRAINT "ServiceScoreRecalculationJob_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ServiceUser" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"serviceId" INTEGER NOT NULL,
"role" "ServiceUserRole" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ServiceUser_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_watchedComments" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_watchedComments_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_onEventCreatedForServices" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_onEventCreatedForServices_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_onRootCommentCreatedForServices" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_onRootCommentCreatedForServices_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_onVerificationChangeForServices" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_onVerificationChangeForServices_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_AttributeToNotificationPreferenceOnServiceVerificationChangeFi" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_AttributeToNotificationPreferenceOnServiceVerification_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_ServiceToCategory" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_ServiceToCategory_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateTable
CREATE TABLE "_CategoryToNotificationPreferenceOnServiceVerificationChangeFil" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL,
CONSTRAINT "_CategoryToNotificationPreferenceOnServiceVerificationC_AB_pkey" PRIMARY KEY ("A","B")
);
-- CreateIndex
CREATE INDEX "Comment_status_idx" ON "Comment"("status");
-- CreateIndex
CREATE INDEX "Comment_createdAt_idx" ON "Comment"("createdAt");
-- CreateIndex
CREATE INDEX "Comment_serviceId_idx" ON "Comment"("serviceId");
-- CreateIndex
CREATE INDEX "Comment_authorId_idx" ON "Comment"("authorId");
-- CreateIndex
CREATE INDEX "Comment_upvotes_idx" ON "Comment"("upvotes");
-- CreateIndex
CREATE INDEX "Comment_rating_idx" ON "Comment"("rating");
-- CreateIndex
CREATE INDEX "Comment_ratingActive_idx" ON "Comment"("ratingActive");
-- CreateIndex
CREATE UNIQUE INDEX "Comment_serviceId_orderId_key" ON "Comment"("serviceId", "orderId");
-- CreateIndex
CREATE INDEX "Notification_userId_idx" ON "Notification"("userId");
-- CreateIndex
CREATE INDEX "Notification_read_idx" ON "Notification"("read");
-- CreateIndex
CREATE INDEX "Notification_createdAt_idx" ON "Notification"("createdAt");
-- CreateIndex
CREATE INDEX "Notification_userId_read_createdAt_idx" ON "Notification"("userId", "read", "createdAt");
-- CreateIndex
CREATE INDEX "Notification_userId_type_aboutCommentId_idx" ON "Notification"("userId", "type", "aboutCommentId");
-- CreateIndex
CREATE INDEX "idx_notification_suggestion_message" ON "Notification"("userId", "type", "aboutServiceSuggestionMessageId");
-- CreateIndex
CREATE INDEX "idx_notification_suggestion_status" ON "Notification"("userId", "type", "aboutServiceSuggestionId");
-- CreateIndex
CREATE INDEX "idx_notification_account_status" ON "Notification"("userId", "type", "aboutAccountStatusChange");
-- CreateIndex
CREATE UNIQUE INDEX "NotificationPreferences_userId_key" ON "NotificationPreferences"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "NotificationPreferenceOnServiceVerificationChangeFilterFilt_key" ON "NotificationPreferenceOnServiceVerificationChangeFilterFilter"("verificationStatus", "notificationPreferencesId");
-- CreateIndex
CREATE INDEX "Event_visible_idx" ON "Event"("visible");
-- CreateIndex
CREATE INDEX "Event_startedAt_idx" ON "Event"("startedAt");
-- CreateIndex
CREATE INDEX "Event_createdAt_idx" ON "Event"("createdAt");
-- CreateIndex
CREATE INDEX "Event_endedAt_idx" ON "Event"("endedAt");
-- CreateIndex
CREATE INDEX "Event_type_idx" ON "Event"("type");
-- CreateIndex
CREATE INDEX "Event_serviceId_idx" ON "Event"("serviceId");
-- CreateIndex
CREATE INDEX "ServiceSuggestion_userId_idx" ON "ServiceSuggestion"("userId");
-- CreateIndex
CREATE INDEX "ServiceSuggestion_serviceId_idx" ON "ServiceSuggestion"("serviceId");
-- CreateIndex
CREATE INDEX "ServiceSuggestionMessage_userId_idx" ON "ServiceSuggestionMessage"("userId");
-- CreateIndex
CREATE INDEX "ServiceSuggestionMessage_suggestionId_idx" ON "ServiceSuggestionMessage"("suggestionId");
-- CreateIndex
CREATE INDEX "ServiceSuggestionMessage_createdAt_idx" ON "ServiceSuggestionMessage"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "Service_slug_key" ON "Service"("slug");
-- CreateIndex
CREATE INDEX "Service_listedAt_idx" ON "Service"("listedAt");
-- CreateIndex
CREATE INDEX "Service_overallScore_idx" ON "Service"("overallScore");
-- CreateIndex
CREATE INDEX "Service_privacyScore_idx" ON "Service"("privacyScore");
-- CreateIndex
CREATE INDEX "Service_trustScore_idx" ON "Service"("trustScore");
-- CreateIndex
CREATE INDEX "Service_averageUserRating_idx" ON "Service"("averageUserRating");
-- CreateIndex
CREATE INDEX "Service_name_idx" ON "Service"("name");
-- CreateIndex
CREATE INDEX "Service_verificationStatus_idx" ON "Service"("verificationStatus");
-- CreateIndex
CREATE INDEX "Service_kycLevel_idx" ON "Service"("kycLevel");
-- CreateIndex
CREATE INDEX "Service_createdAt_idx" ON "Service"("createdAt");
-- CreateIndex
CREATE INDEX "Service_updatedAt_idx" ON "Service"("updatedAt");
-- CreateIndex
CREATE INDEX "Service_slug_idx" ON "Service"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "Attribute_slug_key" ON "Attribute"("slug");
-- CreateIndex
CREATE INDEX "InternalUserNote_userId_idx" ON "InternalUserNote"("userId");
-- CreateIndex
CREATE INDEX "InternalUserNote_addedByUserId_idx" ON "InternalUserNote"("addedByUserId");
-- CreateIndex
CREATE INDEX "InternalUserNote_createdAt_idx" ON "InternalUserNote"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "User_name_key" ON "User"("name");
-- CreateIndex
CREATE UNIQUE INDEX "User_secretTokenHash_key" ON "User"("secretTokenHash");
-- CreateIndex
CREATE INDEX "User_createdAt_idx" ON "User"("createdAt");
-- CreateIndex
CREATE INDEX "User_totalKarma_idx" ON "User"("totalKarma");
-- CreateIndex
CREATE INDEX "CommentVote_commentId_idx" ON "CommentVote"("commentId");
-- CreateIndex
CREATE INDEX "CommentVote_userId_idx" ON "CommentVote"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "CommentVote_commentId_userId_key" ON "CommentVote"("commentId", "userId");
-- CreateIndex
CREATE INDEX "KarmaTransaction_createdAt_idx" ON "KarmaTransaction"("createdAt");
-- CreateIndex
CREATE INDEX "KarmaTransaction_userId_idx" ON "KarmaTransaction"("userId");
-- CreateIndex
CREATE INDEX "KarmaTransaction_processed_idx" ON "KarmaTransaction"("processed");
-- CreateIndex
CREATE INDEX "KarmaTransaction_suggestionId_idx" ON "KarmaTransaction"("suggestionId");
-- CreateIndex
CREATE INDEX "KarmaTransaction_commentId_idx" ON "KarmaTransaction"("commentId");
-- CreateIndex
CREATE INDEX "VerificationStep_serviceId_idx" ON "VerificationStep"("serviceId");
-- CreateIndex
CREATE INDEX "VerificationStep_status_idx" ON "VerificationStep"("status");
-- CreateIndex
CREATE INDEX "VerificationStep_createdAt_idx" ON "VerificationStep"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "Category_name_key" ON "Category"("name");
-- CreateIndex
CREATE UNIQUE INDEX "Category_slug_key" ON "Category"("slug");
-- CreateIndex
CREATE INDEX "Category_name_idx" ON "Category"("name");
-- CreateIndex
CREATE INDEX "Category_slug_idx" ON "Category"("slug");
-- CreateIndex
CREATE INDEX "ServiceVerificationRequest_serviceId_idx" ON "ServiceVerificationRequest"("serviceId");
-- CreateIndex
CREATE INDEX "ServiceVerificationRequest_userId_idx" ON "ServiceVerificationRequest"("userId");
-- CreateIndex
CREATE INDEX "ServiceVerificationRequest_createdAt_idx" ON "ServiceVerificationRequest"("createdAt");
-- CreateIndex
CREATE UNIQUE INDEX "ServiceVerificationRequest_serviceId_userId_key" ON "ServiceVerificationRequest"("serviceId", "userId");
-- CreateIndex
CREATE UNIQUE INDEX "ServiceScoreRecalculationJob_serviceId_key" ON "ServiceScoreRecalculationJob"("serviceId");
-- CreateIndex
CREATE INDEX "ServiceScoreRecalculationJob_processedAt_idx" ON "ServiceScoreRecalculationJob"("processedAt");
-- CreateIndex
CREATE INDEX "ServiceScoreRecalculationJob_createdAt_idx" ON "ServiceScoreRecalculationJob"("createdAt");
-- CreateIndex
CREATE INDEX "ServiceUser_userId_idx" ON "ServiceUser"("userId");
-- CreateIndex
CREATE INDEX "ServiceUser_serviceId_idx" ON "ServiceUser"("serviceId");
-- CreateIndex
CREATE INDEX "ServiceUser_role_idx" ON "ServiceUser"("role");
-- CreateIndex
CREATE UNIQUE INDEX "ServiceUser_userId_serviceId_key" ON "ServiceUser"("userId", "serviceId");
-- CreateIndex
CREATE INDEX "_watchedComments_B_index" ON "_watchedComments"("B");
-- CreateIndex
CREATE INDEX "_onEventCreatedForServices_B_index" ON "_onEventCreatedForServices"("B");
-- CreateIndex
CREATE INDEX "_onRootCommentCreatedForServices_B_index" ON "_onRootCommentCreatedForServices"("B");
-- CreateIndex
CREATE INDEX "_onVerificationChangeForServices_B_index" ON "_onVerificationChangeForServices"("B");
-- CreateIndex
CREATE INDEX "_AttributeToNotificationPreferenceOnServiceVerification_B_index" ON "_AttributeToNotificationPreferenceOnServiceVerificationChangeFi"("B");
-- CreateIndex
CREATE INDEX "_ServiceToCategory_B_index" ON "_ServiceToCategory"("B");
-- CreateIndex
CREATE INDEX "_CategoryToNotificationPreferenceOnServiceVerificationC_B_index" ON "_CategoryToNotificationPreferenceOnServiceVerificationChangeFil"("B");
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Comment" ADD CONSTRAINT "Comment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_aboutCommentId_fkey" FOREIGN KEY ("aboutCommentId") REFERENCES "Comment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_aboutEventId_fkey" FOREIGN KEY ("aboutEventId") REFERENCES "Event"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_aboutServiceId_fkey" FOREIGN KEY ("aboutServiceId") REFERENCES "Service"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_aboutServiceSuggestionId_fkey" FOREIGN KEY ("aboutServiceSuggestionId") REFERENCES "ServiceSuggestion"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Notification" ADD CONSTRAINT "Notification_aboutServiceSuggestionMessageId_fkey" FOREIGN KEY ("aboutServiceSuggestionMessageId") REFERENCES "ServiceSuggestionMessage"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "NotificationPreferences" ADD CONSTRAINT "NotificationPreferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "NotificationPreferenceOnServiceVerificationChangeFilterFilter" ADD CONSTRAINT "NotificationPreferenceOnServiceVerificationChangeFilterFil_fkey" FOREIGN KEY ("notificationPreferencesId") REFERENCES "NotificationPreferences"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Event" ADD CONSTRAINT "Event_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceSuggestion" ADD CONSTRAINT "ServiceSuggestion_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceSuggestion" ADD CONSTRAINT "ServiceSuggestion_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceSuggestionMessage" ADD CONSTRAINT "ServiceSuggestionMessage_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceSuggestionMessage" ADD CONSTRAINT "ServiceSuggestionMessage_suggestionId_fkey" FOREIGN KEY ("suggestionId") REFERENCES "ServiceSuggestion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceContactMethod" ADD CONSTRAINT "ServiceContactMethod_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InternalUserNote" ADD CONSTRAINT "InternalUserNote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "InternalUserNote" ADD CONSTRAINT "InternalUserNote_addedByUserId_fkey" FOREIGN KEY ("addedByUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CommentVote" ADD CONSTRAINT "CommentVote_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "CommentVote" ADD CONSTRAINT "CommentVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceAttribute" ADD CONSTRAINT "ServiceAttribute_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceAttribute" ADD CONSTRAINT "ServiceAttribute_attributeId_fkey" FOREIGN KEY ("attributeId") REFERENCES "Attribute"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KarmaTransaction" ADD CONSTRAINT "KarmaTransaction_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KarmaTransaction" ADD CONSTRAINT "KarmaTransaction_commentId_fkey" FOREIGN KEY ("commentId") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "KarmaTransaction" ADD CONSTRAINT "KarmaTransaction_suggestionId_fkey" FOREIGN KEY ("suggestionId") REFERENCES "ServiceSuggestion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "VerificationStep" ADD CONSTRAINT "VerificationStep_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceVerificationRequest" ADD CONSTRAINT "ServiceVerificationRequest_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceVerificationRequest" ADD CONSTRAINT "ServiceVerificationRequest_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceUser" ADD CONSTRAINT "ServiceUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "ServiceUser" ADD CONSTRAINT "ServiceUser_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_watchedComments" ADD CONSTRAINT "_watchedComments_A_fkey" FOREIGN KEY ("A") REFERENCES "Comment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_watchedComments" ADD CONSTRAINT "_watchedComments_B_fkey" FOREIGN KEY ("B") REFERENCES "NotificationPreferences"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_onEventCreatedForServices" ADD CONSTRAINT "_onEventCreatedForServices_A_fkey" FOREIGN KEY ("A") REFERENCES "NotificationPreferences"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_onEventCreatedForServices" ADD CONSTRAINT "_onEventCreatedForServices_B_fkey" FOREIGN KEY ("B") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_onRootCommentCreatedForServices" ADD CONSTRAINT "_onRootCommentCreatedForServices_A_fkey" FOREIGN KEY ("A") REFERENCES "NotificationPreferences"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_onRootCommentCreatedForServices" ADD CONSTRAINT "_onRootCommentCreatedForServices_B_fkey" FOREIGN KEY ("B") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_onVerificationChangeForServices" ADD CONSTRAINT "_onVerificationChangeForServices_A_fkey" FOREIGN KEY ("A") REFERENCES "NotificationPreferences"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_onVerificationChangeForServices" ADD CONSTRAINT "_onVerificationChangeForServices_B_fkey" FOREIGN KEY ("B") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_AttributeToNotificationPreferenceOnServiceVerificationChangeFi" ADD CONSTRAINT "_AttributeToNotificationPreferenceOnServiceVerificationC_A_fkey" FOREIGN KEY ("A") REFERENCES "Attribute"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_AttributeToNotificationPreferenceOnServiceVerificationChangeFi" ADD CONSTRAINT "_AttributeToNotificationPreferenceOnServiceVerificationC_B_fkey" FOREIGN KEY ("B") REFERENCES "NotificationPreferenceOnServiceVerificationChangeFilterFilter"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ServiceToCategory" ADD CONSTRAINT "_ServiceToCategory_A_fkey" FOREIGN KEY ("A") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ServiceToCategory" ADD CONSTRAINT "_ServiceToCategory_B_fkey" FOREIGN KEY ("B") REFERENCES "Service"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_CategoryToNotificationPreferenceOnServiceVerificationChangeFil" ADD CONSTRAINT "_CategoryToNotificationPreferenceOnServiceVerificationCh_A_fkey" FOREIGN KEY ("A") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_CategoryToNotificationPreferenceOnServiceVerificationChangeFil" ADD CONSTRAINT "_CategoryToNotificationPreferenceOnServiceVerificationCh_B_fkey" FOREIGN KEY ("B") REFERENCES "NotificationPreferenceOnServiceVerificationChangeFilterFilter"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "lastLoginAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;

View File

@@ -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"

590
web/prisma/schema.prisma Normal file
View File

@@ -0,0 +1,590 @@
// This is your Prisma schema file
datasource db {
provider = "postgres"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
generator json {
provider = "prisma-json-types-generator"
}
enum CommentStatus {
PENDING
HUMAN_PENDING
APPROVED
VERIFIED
REJECTED
}
enum OrderIdStatus {
PENDING
APPROVED
REJECTED
}
model Comment {
id Int @id @default(autoincrement())
/// Computed via trigger. Do not update through prisma.
upvotes Int @default(0)
status CommentStatus @default(PENDING)
suspicious Boolean @default(false)
requiresAdminReview Boolean @default(false)
communityNote String?
verificationNote String?
internalNote String?
privateContext String?
orderId String? @db.VarChar(100)
orderIdStatus OrderIdStatus? @default(PENDING)
kycRequested Boolean @default(false)
fundsBlocked Boolean @default(false)
content String
rating Int? @db.SmallInt
ratingActive Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
authorId Int
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId Int
parentId Int?
parent Comment? @relation("CommentReplies", fields: [parentId], references: [id], onDelete: Cascade)
replies Comment[] @relation("CommentReplies")
karmaTransactions KarmaTransaction[]
votes CommentVote[]
notificationPreferenceswatchedComments NotificationPreferences[] @relation("watchedComments")
Notification Notification[]
@@unique([serviceId, orderId], name: "unique_orderId_per_service")
@@index([status])
@@index([createdAt])
@@index([serviceId])
@@index([authorId])
@@index([upvotes])
@@index([rating])
@@index([ratingActive])
}
enum VerificationStatus {
COMMUNITY_CONTRIBUTED
// COMMUNITY_VERIFIED
APPROVED
VERIFICATION_SUCCESS
VERIFICATION_FAILED
}
enum ServiceInfoBanner {
NONE
NO_LONGER_OPERATIONAL
}
enum ServiceVisibility {
PUBLIC
UNLISTED
HIDDEN
}
enum Currency {
MONERO
BITCOIN
LIGHTNING
FIAT
CASH
}
enum EventType {
WARNING
WARNING_SOLVED
ALERT
ALERT_SOLVED
INFO
NORMAL
UPDATE
}
enum ServiceUserRole {
OWNER
ADMIN
MODERATOR
SUPPORT
TEAM_MEMBER
}
enum AccountStatusChange {
ADMIN_TRUE
ADMIN_FALSE
VERIFIED_TRUE
VERIFIED_FALSE
VERIFIER_TRUE
VERIFIER_FALSE
SPAMMER_TRUE
SPAMMER_FALSE
}
enum NotificationType {
COMMENT_STATUS_CHANGE
REPLY_COMMENT_CREATED
COMMUNITY_NOTE_ADDED
/// Comment that is not a reply. May include a rating.
ROOT_COMMENT_CREATED
SUGGESTION_MESSAGE
SUGGESTION_STATUS_CHANGE
// KARMA_UNLOCK // TODO: [KARMA_UNLOCK] Will be added later, when karma unloks are in the database, not in the code.
/// Marked as spammer, promoted to admin, etc.
ACCOUNT_STATUS_CHANGE
EVENT_CREATED
SERVICE_VERIFICATION_STATUS_CHANGE
}
enum CommentStatusChange {
MARKED_AS_SPAM
UNMARKED_AS_SPAM
MARKED_FOR_ADMIN_REVIEW
UNMARKED_FOR_ADMIN_REVIEW
STATUS_CHANGED_TO_APPROVED
STATUS_CHANGED_TO_VERIFIED
STATUS_CHANGED_TO_REJECTED
STATUS_CHANGED_TO_PENDING
}
enum ServiceVerificationStatusChange {
STATUS_CHANGED_TO_COMMUNITY_CONTRIBUTED
STATUS_CHANGED_TO_APPROVED
STATUS_CHANGED_TO_VERIFICATION_SUCCESS
STATUS_CHANGED_TO_VERIFICATION_FAILED
}
enum ServiceSuggestionStatusChange {
STATUS_CHANGED_TO_PENDING
STATUS_CHANGED_TO_APPROVED
STATUS_CHANGED_TO_REJECTED
STATUS_CHANGED_TO_WITHDRAWN
}
model Notification {
id Int @id @default(autoincrement())
userId Int
user User @relation("NotificationOwner", fields: [userId], references: [id], onDelete: Cascade)
type NotificationType
read Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
aboutComment Comment? @relation(fields: [aboutCommentId], references: [id])
aboutCommentId Int?
aboutEvent Event? @relation(fields: [aboutEventId], references: [id])
aboutEventId Int?
aboutService Service? @relation(fields: [aboutServiceId], references: [id])
aboutServiceId Int?
aboutServiceSuggestion ServiceSuggestion? @relation(fields: [aboutServiceSuggestionId], references: [id])
aboutServiceSuggestionId Int?
aboutServiceSuggestionMessage ServiceSuggestionMessage? @relation(fields: [aboutServiceSuggestionMessageId], references: [id])
aboutServiceSuggestionMessageId Int?
aboutAccountStatusChange AccountStatusChange?
aboutCommentStatusChange CommentStatusChange?
aboutServiceVerificationStatusChange ServiceVerificationStatusChange?
aboutSuggestionStatusChange ServiceSuggestionStatusChange?
@@index([userId])
@@index([read])
@@index([createdAt])
@@index([userId, read, createdAt])
@@index([userId, type, aboutCommentId])
@@index([userId, type, aboutServiceSuggestionMessageId], map: "idx_notification_suggestion_message")
@@index([userId, type, aboutServiceSuggestionId], map: "idx_notification_suggestion_status")
@@index([userId, type, aboutAccountStatusChange], map: "idx_notification_account_status")
}
model NotificationPreferences {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId Int @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
enableOnMyCommentStatusChange Boolean @default(true)
enableAutowatchMyComments Boolean @default(true)
enableNotifyPendingRepliesOnWatch Boolean @default(false)
onEventCreatedForServices Service[] @relation("onEventCreatedForServices")
onRootCommentCreatedForServices Service[] @relation("onRootCommentCreatedForServices")
onVerificationChangeForServices Service[] @relation("onVerificationChangeForServices")
watchedComments Comment[] @relation("watchedComments")
onServiceVerificationChangeFilter NotificationPreferenceOnServiceVerificationChangeFilterFilter[]
}
model NotificationPreferenceOnServiceVerificationChangeFilterFilter {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
verificationStatus VerificationStepStatus
notificationPreferences NotificationPreferences @relation(fields: [notificationPreferencesId], references: [id], onDelete: Cascade)
notificationPreferencesId Int
categories Category[]
attributes Attribute[]
currencies Currency[]
/// 0-10
scores Int[]
@@unique([verificationStatus, notificationPreferencesId])
}
model Event {
id Int @id @default(autoincrement())
title String
content String
source String?
type EventType
visible Boolean @default(true)
startedAt DateTime
/// If null, the event is ongoing. If same as startedAt, the event is a one-time event. If startedAt is in the future, the event is upcoming.
endedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId Int
Notification Notification[]
@@index([visible])
@@index([startedAt])
@@index([createdAt])
@@index([endedAt])
@@index([type])
@@index([serviceId])
}
enum ServiceSuggestionStatus {
PENDING
APPROVED
REJECTED
WITHDRAWN
}
enum ServiceSuggestionType {
CREATE_SERVICE
EDIT_SERVICE
}
model ServiceSuggestion {
id Int @id @default(autoincrement())
type ServiceSuggestionType
status ServiceSuggestionStatus @default(PENDING)
notes String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId Int
serviceId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
messages ServiceSuggestionMessage[]
Notification Notification[]
KarmaTransaction KarmaTransaction[]
@@index([userId])
@@index([serviceId])
}
model ServiceSuggestionMessage {
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId Int
suggestionId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
suggestion ServiceSuggestion @relation(fields: [suggestionId], references: [id], onDelete: Cascade)
notifications Notification[]
@@index([userId])
@@index([suggestionId])
@@index([createdAt])
}
model Service {
id Int @id @default(autoincrement())
name String
slug String @unique
description String
categories Category[] @relation("ServiceToCategory")
kycLevel Int @default(4)
overallScore Int @default(0)
privacyScore Int @default(0)
trustScore Int @default(0)
/// Computed via trigger. Do not update through prisma.
isRecentlyListed Boolean @default(false)
/// Computed via trigger. Do not update through prisma.
averageUserRating Float?
serviceVisibility ServiceVisibility @default(PUBLIC)
serviceInfoBanner ServiceInfoBanner @default(NONE)
serviceInfoBannerNotes String?
verificationStatus VerificationStatus @default(COMMUNITY_CONTRIBUTED)
verificationSummary String?
verificationRequests ServiceVerificationRequest[]
verificationProofMd String?
/// Computed via trigger when the service status is VERIFICATION_SUCCESS. Do not update through prisma.
verifiedAt DateTime?
/// [UserSentiment]
userSentiment Json?
userSentimentAt DateTime?
referral String?
acceptedCurrencies Currency[] @default([])
serviceUrls String[]
tosUrls String[] @default([])
onionUrls String[] @default([])
i2pUrls String[] @default([])
imageUrl String?
/// [TosReview]
tosReview Json?
tosReviewAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
listedAt DateTime?
comments Comment[]
events Event[]
contactMethods ServiceContactMethod[] @relation("ServiceToContactMethod")
attributes ServiceAttribute[]
verificationSteps VerificationStep[]
suggestions ServiceSuggestion[]
onEventCreatedForServices NotificationPreferences[] @relation("onEventCreatedForServices")
onRootCommentCreatedForServices NotificationPreferences[] @relation("onRootCommentCreatedForServices")
onVerificationChangeForServices NotificationPreferences[] @relation("onVerificationChangeForServices")
Notification Notification[]
affiliatedUsers ServiceUser[] @relation("ServiceUsers")
@@index([listedAt])
@@index([overallScore])
@@index([privacyScore])
@@index([trustScore])
@@index([averageUserRating])
@@index([name])
@@index([verificationStatus])
@@index([kycLevel])
@@index([createdAt])
@@index([updatedAt])
@@index([slug])
}
model ServiceContactMethod {
id Int @id @default(autoincrement())
label String
/// Including the protocol (e.g. "mailto:", "tel:", "https://")
value String
iconId String
info String
services Service @relation("ServiceToContactMethod", fields: [serviceId], references: [id], onDelete: Cascade)
serviceId Int
}
enum AttributeCategory {
PRIVACY
TRUST
}
enum AttributeType {
GOOD
BAD
WARNING
INFO
}
model Attribute {
id Int @id @default(autoincrement())
slug String @unique
title String
/// Markdown
description String
privacyPoints Int @default(0)
trustPoints Int @default(0)
category AttributeCategory
type AttributeType
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
services ServiceAttribute[]
notificationPreferencesOnServiceVerificationChange NotificationPreferenceOnServiceVerificationChangeFilterFilter[]
}
model InternalUserNote {
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
user User @relation("UserRecievedNotes", fields: [userId], references: [id], onDelete: Cascade)
userId Int
addedByUser User? @relation("UserAddedNotes", fields: [addedByUserId], references: [id], onDelete: SetNull)
addedByUserId Int?
@@index([userId])
@@index([addedByUserId])
@@index([createdAt])
}
model User {
id Int @id @default(autoincrement())
name String @unique
displayName String?
link String?
picture String?
spammer Boolean @default(false)
verified Boolean @default(false)
admin Boolean @default(false)
verifier Boolean @default(false)
verifiedLink String?
secretTokenHash String @unique
/// Computed via trigger. Do not update through prisma.
totalKarma Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
lastLoginAt DateTime @default(now())
comments Comment[]
karmaTransactions KarmaTransaction[]
commentVotes CommentVote[]
suggestions ServiceSuggestion[]
suggestionMessages ServiceSuggestionMessage[]
internalNotes InternalUserNote[] @relation("UserRecievedNotes")
addedInternalNotes InternalUserNote[] @relation("UserAddedNotes")
verificationRequests ServiceVerificationRequest[]
notifications Notification[] @relation("NotificationOwner")
notificationPreferences NotificationPreferences?
serviceAffiliations ServiceUser[] @relation("UserServices")
@@index([createdAt])
@@index([totalKarma])
}
model CommentVote {
id Int @id @default(autoincrement())
downvote Boolean @default(false) // false = upvote, true = downvote
comment Comment @relation(fields: [commentId], references: [id], onDelete: Cascade)
commentId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
@@unique([commentId, userId]) // Ensure one vote per user per comment
@@index([commentId])
@@index([userId])
}
model ServiceAttribute {
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId Int
attribute Attribute @relation(fields: [attributeId], references: [id], onDelete: Cascade)
attributeId Int
createdAt DateTime @default(now())
@@id([serviceId, attributeId])
}
model KarmaTransaction {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
action String
points Int @default(0)
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade)
commentId Int?
suggestion ServiceSuggestion? @relation(fields: [suggestionId], references: [id], onDelete: Cascade)
suggestionId Int?
description String
processed Boolean @default(false)
createdAt DateTime @default(now())
@@index([createdAt])
@@index([userId])
@@index([processed])
@@index([suggestionId])
@@index([commentId])
}
enum VerificationStepStatus {
PENDING
IN_PROGRESS
PASSED
FAILED
}
model VerificationStep {
id Int @id @default(autoincrement())
title String
description String
status VerificationStepStatus @default(PENDING)
evidenceMd String?
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId Int
@@index([serviceId])
@@index([status])
@@index([createdAt])
}
model Category {
id Int @id @default(autoincrement())
name String @unique
icon String
slug String @unique
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
services Service[] @relation("ServiceToCategory")
notificationPreferencesOnServiceVerificationChange NotificationPreferenceOnServiceVerificationChangeFilterFilter[]
@@index([name])
@@index([slug])
}
model ServiceVerificationRequest {
id Int @id @default(autoincrement())
service Service @relation(fields: [serviceId], references: [id], onDelete: Cascade)
serviceId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
@@unique([serviceId, userId])
@@index([serviceId])
@@index([userId])
@@index([createdAt])
}
model ServiceScoreRecalculationJob {
id Int @id @default(autoincrement())
serviceId Int @unique
createdAt DateTime @default(now())
processedAt DateTime? @updatedAt
@@index([processedAt])
@@index([createdAt])
}
model ServiceUser {
id Int @id @default(autoincrement())
userId Int
user User @relation("UserServices", fields: [userId], references: [id], onDelete: Cascade)
serviceId Int
service Service @relation("ServiceUsers", fields: [serviceId], references: [id], onDelete: Cascade)
role ServiceUserRole
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([userId, serviceId])
@@index([userId])
@@index([serviceId])
@@index([role])
}

View File

@@ -0,0 +1,265 @@
-- This script manages user karma based on comment interactions. It handles karma points
-- for comment approvals, verifications, spam status changes, and votes (upvotes/downvotes).
-- Karma transactions are recorded, and user karma totals are updated accordingly.
-- Drop existing triggers first
DROP TRIGGER IF EXISTS comment_status_change_trigger ON "Comment";
DROP TRIGGER IF EXISTS comment_suspicious_change_trigger ON "Comment";
DROP TRIGGER IF EXISTS comment_upvote_change_trigger ON "Comment";
DROP TRIGGER IF EXISTS comment_vote_change_trigger ON "CommentVote";
DROP TRIGGER IF EXISTS suggestion_status_change_trigger ON "ServiceSuggestion";
-- Drop existing functions
DROP FUNCTION IF EXISTS handle_comment_upvote_change();
DROP FUNCTION IF EXISTS handle_comment_status_change();
DROP FUNCTION IF EXISTS handle_comment_approval();
DROP FUNCTION IF EXISTS handle_comment_verification();
DROP FUNCTION IF EXISTS handle_comment_spam_status();
DROP FUNCTION IF EXISTS handle_comment_vote_change();
DROP FUNCTION IF EXISTS insert_karma_transaction();
DROP FUNCTION IF EXISTS update_user_karma();
DROP FUNCTION IF EXISTS handle_suggestion_status_change();
-- Helper function to insert karma transaction
CREATE OR REPLACE FUNCTION insert_karma_transaction(
p_user_id INT,
p_points INT,
p_action TEXT,
p_comment_id INT,
p_description TEXT,
p_suggestion_id INT DEFAULT NULL
) RETURNS VOID AS $$
BEGIN
INSERT INTO "KarmaTransaction" (
"userId", "points", "action", "commentId",
"suggestionId",
"description", "processed", "createdAt"
)
VALUES (
p_user_id, p_points, p_action, p_comment_id,
p_suggestion_id,
p_description, true, NOW()
);
END;
$$ LANGUAGE plpgsql;
-- Helper function to update user karma
CREATE OR REPLACE FUNCTION update_user_karma(
p_user_id INT,
p_karma_change INT
) RETURNS VOID AS $$
BEGIN
UPDATE "User"
SET "totalKarma" = "totalKarma" + p_karma_change
WHERE id = p_user_id;
END;
$$ LANGUAGE plpgsql;
-- Handle comment approval
CREATE OR REPLACE FUNCTION handle_comment_approval(
NEW RECORD,
OLD RECORD
) RETURNS VOID AS $$
BEGIN
IF OLD.status = 'PENDING' AND NEW.status = 'APPROVED' THEN
PERFORM insert_karma_transaction(
NEW."authorId",
1,
'comment_approved',
NEW.id,
format('Your comment #comment-%s in %s has been approved!',
NEW.id,
(SELECT name FROM "Service" WHERE id = NEW."serviceId"))
);
PERFORM update_user_karma(NEW."authorId", 1);
END IF;
END;
$$ LANGUAGE plpgsql;
-- Handle comment verification
CREATE OR REPLACE FUNCTION handle_comment_verification(
NEW RECORD,
OLD RECORD
) RETURNS VOID AS $$
BEGIN
IF NEW.status = 'VERIFIED' AND OLD.status != 'VERIFIED' THEN
PERFORM insert_karma_transaction(
NEW."authorId",
5,
'comment_verified',
NEW.id,
format('Your comment #comment-%s in %s has been verified!',
NEW.id,
(SELECT name FROM "Service" WHERE id = NEW."serviceId"))
);
PERFORM update_user_karma(NEW."authorId", 5);
END IF;
END;
$$ LANGUAGE plpgsql;
-- Handle spam status changes
CREATE OR REPLACE FUNCTION handle_comment_spam_status(
NEW RECORD,
OLD RECORD
) RETURNS VOID AS $$
BEGIN
-- Handle marking as spam
IF NEW.suspicious = true AND OLD.suspicious = false THEN
PERFORM insert_karma_transaction(
NEW."authorId",
-10,
'comment_spam',
NEW.id,
format('Your comment #comment-%s in %s has been marked as spam.',
NEW.id,
(SELECT name FROM "Service" WHERE id = NEW."serviceId"))
);
PERFORM update_user_karma(NEW."authorId", -10);
-- Handle unmarking as spam
ELSIF NEW.suspicious = false AND OLD.suspicious = true THEN
PERFORM insert_karma_transaction(
NEW."authorId",
10,
'comment_spam_reverted',
NEW.id,
format('Your comment #comment-%s in %s is no longer marked as spam.',
NEW.id,
(SELECT name FROM "Service" WHERE id = NEW."serviceId"))
);
PERFORM update_user_karma(NEW."authorId", 10);
END IF;
END;
$$ LANGUAGE plpgsql;
-- Function for handling vote changes
CREATE OR REPLACE FUNCTION handle_comment_vote_change()
RETURNS TRIGGER AS $$
DECLARE
karma_points INT;
vote_action TEXT;
vote_description TEXT;
comment_author_id INT;
service_name TEXT;
upvote_change INT := 0; -- Variable to track change in upvotes
BEGIN
-- Get comment author and service info
SELECT c."authorId", s.name INTO comment_author_id, service_name
FROM "Comment" c
JOIN "Service" s ON c.id = COALESCE(NEW."commentId", OLD."commentId") AND c."serviceId" = s.id;
-- Calculate karma impact based on vote type
IF TG_OP = 'INSERT' THEN
-- New vote
karma_points := CASE WHEN NEW.downvote THEN -1 ELSE 1 END;
vote_action := CASE WHEN NEW.downvote THEN 'comment_downvote' ELSE 'comment_upvote' END;
vote_description := format('Your comment #comment-%s in %s received %s',
NEW."commentId",
service_name,
CASE WHEN NEW.downvote THEN 'a downvote' ELSE 'an upvote' END);
upvote_change := CASE WHEN NEW.downvote THEN -1 ELSE 1 END; -- -1 for downvote, +1 for upvote
ELSIF TG_OP = 'DELETE' THEN
-- Removed vote
karma_points := CASE WHEN OLD.downvote THEN 1 ELSE -1 END;
vote_action := 'comment_vote_removed';
vote_description := format('A vote was removed from your comment #comment-%s in %s',
OLD."commentId",
service_name);
upvote_change := CASE WHEN OLD.downvote THEN 1 ELSE -1 END; -- +1 if downvote removed, -1 if upvote removed
ELSIF TG_OP = 'UPDATE' THEN
-- Changed vote (from upvote to downvote or vice versa)
karma_points := CASE WHEN NEW.downvote THEN -2 ELSE 2 END;
vote_action := CASE WHEN NEW.downvote THEN 'comment_downvote' ELSE 'comment_upvote' END;
vote_description := format('Your comment #comment-%s in %s vote changed to %s',
NEW."commentId",
service_name,
CASE WHEN NEW.downvote THEN 'downvote' ELSE 'upvote' END);
upvote_change := CASE WHEN NEW.downvote THEN -2 ELSE 2 END; -- -2 if upvote->downvote, +2 if downvote->upvote
END IF;
-- Record karma transaction and update user karma
PERFORM insert_karma_transaction(
comment_author_id,
karma_points,
vote_action,
COALESCE(NEW."commentId", OLD."commentId"),
vote_description
);
PERFORM update_user_karma(comment_author_id, karma_points);
-- Update comment's upvotes count incrementally
UPDATE "Comment"
SET upvotes = upvotes + upvote_change
WHERE id = COALESCE(NEW."commentId", OLD."commentId");
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Main function for handling status changes
CREATE OR REPLACE FUNCTION handle_comment_status_change()
RETURNS TRIGGER AS $$
BEGIN
PERFORM handle_comment_approval(NEW, OLD);
PERFORM handle_comment_verification(NEW, OLD);
PERFORM handle_comment_spam_status(NEW, OLD);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create triggers
CREATE TRIGGER comment_status_change_trigger
AFTER UPDATE OF status
ON "Comment"
FOR EACH ROW
EXECUTE FUNCTION handle_comment_status_change();
CREATE TRIGGER comment_suspicious_change_trigger
AFTER UPDATE OF suspicious
ON "Comment"
FOR EACH ROW
EXECUTE FUNCTION handle_comment_status_change();
CREATE TRIGGER comment_vote_change_trigger
AFTER INSERT OR UPDATE OR DELETE
ON "CommentVote"
FOR EACH ROW
EXECUTE FUNCTION handle_comment_vote_change();
-- Function to handle suggestion status changes and award karma
CREATE OR REPLACE FUNCTION handle_suggestion_status_change()
RETURNS TRIGGER AS $$
DECLARE
service_name TEXT;
BEGIN
-- Award karma for first approval
-- Check that OLD.status is not NULL to handle the initial creation case if needed,
-- and ensure it wasn't already APPROVED.
IF OLD.status IS DISTINCT FROM 'APPROVED' AND NEW.status = 'APPROVED' THEN
-- Fetch service name for the description
SELECT name INTO service_name FROM "Service" WHERE id = NEW."serviceId";
-- Insert karma transaction, linking it to the suggestion
PERFORM insert_karma_transaction(
NEW."userId",
10,
'suggestion_approved',
NULL, -- p_comment_id (not applicable)
format('Your suggestion for service ''%s'' has been approved!', service_name),
NEW.id -- p_suggestion_id
);
-- Update user's total karma
PERFORM update_user_karma(NEW."userId", 10);
END IF;
RETURN NEW; -- Result is ignored since this is an AFTER trigger
END;
$$ LANGUAGE plpgsql;
-- Create triggers
CREATE TRIGGER suggestion_status_change_trigger
AFTER UPDATE OF status
ON "ServiceSuggestion"
FOR EACH ROW
EXECUTE FUNCTION handle_suggestion_status_change();

View File

@@ -0,0 +1,264 @@
-- This script defines PostgreSQL functions and triggers for managing service scores:
-- 1. Automatically calculates and updates privacy, trust, and overall scores
-- for services when services or their attributes change.
-- 2. Updates the isRecentlyListed flag for services listed within the last 15 days.
-- 3. Queues asynchronous score recalculation in "ServiceScoreRecalculationJob"
-- when an "Attribute" definition (e.g., points) is updated, ensuring
-- efficient handling of widespread score updates.
-- Drop existing triggers first
DROP TRIGGER IF EXISTS service_score_update_trigger ON "Service";
DROP TRIGGER IF EXISTS service_attribute_change_trigger ON "ServiceAttribute";
DROP TRIGGER IF EXISTS attribute_change_trigger ON "Attribute";
-- Drop existing functions
DROP FUNCTION IF EXISTS calculate_service_scores();
DROP FUNCTION IF EXISTS calculate_privacy_score();
DROP FUNCTION IF EXISTS calculate_trust_score();
DROP FUNCTION IF EXISTS calculate_overall_score();
DROP FUNCTION IF EXISTS recalculate_scores_for_attribute();
-- Calculate privacy score based on service attributes and properties
CREATE OR REPLACE FUNCTION calculate_privacy_score(service_id INT)
RETURNS INT AS $$
DECLARE
privacy_score INT := 50; -- Start from middle value (50)
kyc_factor INT;
onion_factor INT := 0;
i2p_factor INT := 0;
monero_factor INT := 0;
open_source_factor INT := 0;
p2p_factor INT := 0;
decentralized_factor INT := 0;
attributes_score INT := 0;
BEGIN
-- Get service data
SELECT
CASE
WHEN "kycLevel" = 0 THEN 25 -- No KYC is best for privacy
WHEN "kycLevel" = 1 THEN 10 -- Minimal KYC
WHEN "kycLevel" = 2 THEN -5 -- Moderate KYC
WHEN "kycLevel" = 3 THEN -15 -- More KYC
WHEN "kycLevel" = 4 THEN -25 -- Full mandatory KYC
ELSE 0 -- Default to no change
END
INTO kyc_factor
FROM "Service"
WHERE "id" = service_id;
-- Check for onion URLs
IF EXISTS (
SELECT 1 FROM "Service"
WHERE "id" = service_id AND array_length("onionUrls", 1) > 0
) THEN
onion_factor := 5;
END IF;
-- Check for i2p URLs
IF EXISTS (
SELECT 1 FROM "Service"
WHERE "id" = service_id AND array_length("i2pUrls", 1) > 0
) THEN
i2p_factor := 5;
END IF;
-- Check for Monero acceptance
IF EXISTS (
SELECT 1 FROM "Service"
WHERE "id" = service_id AND 'MONERO' = ANY("acceptedCurrencies")
) THEN
monero_factor := 5;
END IF;
-- Calculate score from privacy attributes - directly use the points
SELECT COALESCE(SUM(a."privacyPoints"), 0)
INTO attributes_score
FROM "ServiceAttribute" sa
JOIN "Attribute" a ON sa."attributeId" = a."id"
WHERE sa."serviceId" = service_id AND a."category" = 'PRIVACY';
-- Calculate final privacy score (base 100)
privacy_score := privacy_score + kyc_factor + onion_factor + i2p_factor + monero_factor + open_source_factor + p2p_factor + decentralized_factor + attributes_score;
-- Ensure the score is in reasonable bounds (0-100)
privacy_score := GREATEST(0, LEAST(100, privacy_score));
RETURN privacy_score;
END;
$$ LANGUAGE plpgsql;
-- Calculate trust score based on service attributes and verification status
CREATE OR REPLACE FUNCTION calculate_trust_score(service_id INT)
RETURNS INT AS $$
DECLARE
trust_score INT := 50; -- Start from middle value (50)
verification_factor INT;
attributes_score INT := 0;
BEGIN
-- Get verification status factor
SELECT
CASE
WHEN "verificationStatus" = 'VERIFICATION_SUCCESS' THEN 10
WHEN "verificationStatus" = 'APPROVED' THEN 5
WHEN "verificationStatus" = 'COMMUNITY_CONTRIBUTED' THEN 0
WHEN "verificationStatus" = 'VERIFICATION_FAILED' THEN -50
ELSE 0
END
INTO verification_factor
FROM "Service"
WHERE id = service_id;
-- Calculate score from trust attributes - directly use the points
SELECT COALESCE(SUM(a."trustPoints"), 0)
INTO attributes_score
FROM "ServiceAttribute" sa
JOIN "Attribute" a ON sa."attributeId" = a.id
WHERE sa."serviceId" = service_id AND a.category = 'TRUST';
-- Apply penalty if service was listed within the last 15 days
IF EXISTS (
SELECT 1
FROM "Service"
WHERE id = service_id
AND "listedAt" IS NOT NULL
AND "verificationStatus" = 'APPROVED'
AND (NOW() - "listedAt") <= INTERVAL '15 days'
) THEN
trust_score := trust_score - 10;
-- Update the isRecentlyListed flag to true
UPDATE "Service"
SET "isRecentlyListed" = TRUE
WHERE id = service_id;
ELSE
-- Update the isRecentlyListed flag to false
UPDATE "Service"
SET "isRecentlyListed" = FALSE
WHERE id = service_id;
END IF;
-- Calculate final trust score (base 100)
trust_score := trust_score + verification_factor + attributes_score;
-- Ensure the score is in reasonable bounds (0-100)
trust_score := GREATEST(0, LEAST(100, trust_score));
RETURN trust_score;
END;
$$ LANGUAGE plpgsql;
-- Calculate overall score based on weighted average of privacy and trust scores
CREATE OR REPLACE FUNCTION calculate_overall_score(service_id INT, privacy_score INT, trust_score INT)
RETURNS INT AS $$
DECLARE
overall_score INT;
BEGIN
overall_score := CAST(ROUND(((privacy_score * 0.6) + (trust_score * 0.4)) / 10.0) AS INT);
RETURN GREATEST(0, LEAST(10, overall_score));
END;
$$ LANGUAGE plpgsql;
-- Main function to calculate all scores for a service
CREATE OR REPLACE FUNCTION calculate_service_scores()
RETURNS TRIGGER AS $$
DECLARE
privacy_score INT;
trust_score INT;
overall_score INT;
service_id INT;
BEGIN
-- Determine which service ID to use based on the trigger context and table
IF TG_TABLE_NAME = 'Service' THEN
IF TG_OP = 'INSERT' OR TG_OP = 'UPDATE' THEN
service_id := NEW."id";
END IF;
ELSIF TG_TABLE_NAME = 'ServiceAttribute' THEN
IF TG_OP = 'DELETE' THEN
service_id := OLD."serviceId";
ELSE -- INSERT or UPDATE
service_id := NEW."serviceId";
END IF;
END IF;
-- Calculate each score
privacy_score := calculate_privacy_score(service_id);
trust_score := calculate_trust_score(service_id);
overall_score := calculate_overall_score(service_id, privacy_score, trust_score);
-- Cap score if service is flagged as scam (verificationStatus = 'VERIFICATION_FAILED')
IF (SELECT "verificationStatus" FROM "Service" WHERE "id" = service_id) = 'VERIFICATION_FAILED' THEN
IF overall_score > 3 THEN
overall_score := 3;
ELSIF overall_score < 0 THEN
overall_score := 0;
END IF;
END IF;
-- Update the service with the new scores
UPDATE "Service"
SET
"privacyScore" = privacy_score,
"trustScore" = trust_score,
"overallScore" = overall_score
WHERE "id" = service_id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger to recalculate scores when service is created or updated
CREATE TRIGGER service_score_update_trigger
AFTER INSERT OR UPDATE
ON "Service"
FOR EACH ROW
WHEN (pg_trigger_depth() < 2) -- Prevent recursive triggering
EXECUTE FUNCTION calculate_service_scores();
-- Create trigger to recalculate scores when service attributes change
CREATE TRIGGER service_attribute_change_trigger
AFTER INSERT OR UPDATE OR DELETE
ON "ServiceAttribute"
FOR EACH ROW
WHEN (pg_trigger_depth() < 2) -- Prevent recursive triggering
EXECUTE FUNCTION calculate_service_scores();
-- Function to queue score recalculation for all services with a specific attribute
CREATE OR REPLACE FUNCTION queue_service_score_recalculation_for_attribute()
RETURNS TRIGGER AS $$
DECLARE
service_rec RECORD;
BEGIN
-- Only trigger recalculation if relevant fields changed
IF (TG_OP = 'UPDATE' AND (
OLD."privacyPoints" != NEW."privacyPoints" OR
OLD."trustPoints" != NEW."trustPoints" OR
OLD."type" != NEW."type" OR
OLD."category" != NEW."category"
)) THEN
-- Find all services that have this attribute and queue a recalculation job
FOR service_rec IN
SELECT DISTINCT sa."serviceId"
FROM "ServiceAttribute" sa
WHERE sa."attributeId" = NEW.id
LOOP
-- Insert a job into the queue table
-- ON CONFLICT clause ensures we don't queue the same service multiple times per transaction
INSERT INTO "ServiceScoreRecalculationJob" ("serviceId", "createdAt", "processedAt")
VALUES (service_rec."serviceId", NOW(), NULL)
ON CONFLICT ("serviceId") DO UPDATE SET "processedAt" = NULL, "createdAt" = NOW();
END LOOP;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create constraint trigger to queue score recalculation when attributes are updated
DROP TRIGGER IF EXISTS attribute_change_trigger ON "Attribute";
CREATE CONSTRAINT TRIGGER attribute_change_trigger
AFTER UPDATE
ON "Attribute"
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW
WHEN (pg_trigger_depth() < 2)
EXECUTE FUNCTION queue_service_score_recalculation_for_attribute();

View File

@@ -0,0 +1,57 @@
-- This script defines a PostgreSQL function and trigger to automatically calculate
-- and update the average user rating for services based on associated comments.
-- The average rating is recalculated whenever comments are added, updated, or deleted.
-- Drop existing triggers first
DROP TRIGGER IF EXISTS comment_average_rating_trigger ON "Comment";
-- Drop existing functions
DROP FUNCTION IF EXISTS calculate_average_rating();
-- Calculate average rating based on active comments with approved or verified status
CREATE OR REPLACE FUNCTION calculate_average_rating()
RETURNS TRIGGER AS $$
DECLARE
affected_service_id INT;
average_user_rating DECIMAL;
BEGIN
-- Determine which service ID to use based on the trigger context
IF TG_OP = 'DELETE' THEN
affected_service_id := OLD."serviceId";
ELSE -- INSERT or UPDATE
affected_service_id := NEW."serviceId";
END IF;
-- Calculate average rating from active comments with approved or verified status
-- Excluding suspicious comments and replies (comments with parentId not null)
SELECT AVG(rating) INTO average_user_rating
FROM "Comment"
WHERE "serviceId" = affected_service_id
AND "parentId" IS NULL
AND rating IS NOT NULL
AND (status = 'APPROVED' OR status = 'VERIFIED')
AND "ratingActive" = true
AND suspicious = false;
-- Update the service with the new average rating
UPDATE "Service"
SET "averageUserRating" = average_user_rating
WHERE "id" = affected_service_id;
-- Return the appropriate record based on operation
IF TG_OP = 'DELETE' THEN
RETURN OLD;
ELSE
RETURN NEW;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Create trigger to recalculate average rating when comments are created, updated, or deleted
CREATE TRIGGER comment_average_rating_trigger
AFTER INSERT OR UPDATE OR DELETE
ON "Comment"
FOR EACH ROW
WHEN (pg_trigger_depth() < 2) -- Prevent recursive triggering
EXECUTE FUNCTION calculate_average_rating();

View File

@@ -0,0 +1,48 @@
-- This script manages the `listedAt`, `verifiedAt`, and `isRecentlyListed` timestamps
-- for services based on changes to their `verificationStatus`. It ensures these timestamps
-- are set or cleared appropriately when a service's verification status is updated.
CREATE OR REPLACE FUNCTION manage_service_timestamps()
RETURNS TRIGGER AS $$
BEGIN
-- Manage listedAt timestamp
IF NEW."verificationStatus" IN ('APPROVED', 'VERIFICATION_SUCCESS') THEN
-- Set listedAt only on the first time status becomes APPROVED or VERIFICATION_SUCCESS
IF OLD."listedAt" IS NULL THEN
NEW."listedAt" := NOW();
NEW."isRecentlyListed" := TRUE;
END IF;
ELSIF OLD."verificationStatus" IN ('APPROVED', 'VERIFICATION_SUCCESS') THEN
-- Clear listedAt if the status changes FROM APPROVED or VERIFICATION_SUCCESS to something else
-- The trigger's WHEN clause ensures NEW."verificationStatus" is different.
NEW."listedAt" := NULL;
NEW."isRecentlyListed" := FALSE;
END IF;
-- Manage verifiedAt timestamp
IF NEW."verificationStatus" = 'VERIFICATION_SUCCESS' THEN
-- Set verifiedAt when status changes TO VERIFICATION_SUCCESS
NEW."verifiedAt" := NOW();
NEW."isRecentlyListed" := FALSE;
ELSIF OLD."verificationStatus" = 'VERIFICATION_SUCCESS' THEN
-- Clear verifiedAt when status changes FROM VERIFICATION_SUCCESS
-- The trigger's WHEN clause ensures NEW."verificationStatus" is different.
NEW."verifiedAt" := NULL;
NEW."isRecentlyListed" := FALSE;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Drop the old trigger first if it exists under the old name
DROP TRIGGER IF EXISTS trigger_set_service_listed_at ON "Service";
-- Drop the trigger if it exists under the new name
DROP TRIGGER IF EXISTS trigger_manage_service_timestamps ON "Service";
CREATE TRIGGER trigger_manage_service_timestamps
BEFORE UPDATE OF "verificationStatus" ON "Service"
FOR EACH ROW
-- Only execute the function if the verificationStatus value has actually changed
WHEN (OLD."verificationStatus" IS DISTINCT FROM NEW."verificationStatus")
EXECUTE FUNCTION manage_service_timestamps();

View File

@@ -0,0 +1,399 @@
-- Service Events Trigger
-- This trigger automatically creates events when services are updated
-- to track important changes over time
CREATE OR REPLACE FUNCTION trigger_service_events()
RETURNS TRIGGER AS $$
DECLARE
change_descriptions TEXT[] := '{}';
event_title TEXT;
event_content TEXT;
change_type TEXT := NULL;
event_time TIMESTAMP WITH TIME ZONE := transaction_timestamp();
currency_desc TEXT;
BEGIN
-- Only proceed if this is an UPDATE operation
IF TG_OP <> 'UPDATE' THEN
RETURN NEW;
END IF;
-- Check for domain/URL changes
IF OLD."serviceUrls" IS DISTINCT FROM NEW."serviceUrls" THEN
change_descriptions := array_append(change_descriptions,
'Service URLs updated from ' || array_to_string(OLD."serviceUrls", ', ') ||
' to ' || array_to_string(NEW."serviceUrls", ', ')
);
change_type := COALESCE(change_type, 'Domain change');
END IF;
-- Check for KYC level changes
IF OLD."kycLevel" IS DISTINCT FROM NEW."kycLevel" THEN
change_descriptions := array_append(change_descriptions,
'KYC level changed from ' || OLD."kycLevel"::TEXT || ' to ' || NEW."kycLevel"::TEXT
);
change_type := COALESCE(change_type, 'KYC update');
END IF;
-- Check for verification status changes
IF OLD."verificationStatus" IS DISTINCT FROM NEW."verificationStatus" THEN
change_descriptions := array_append(change_descriptions,
'Verification status changed from ' || OLD."verificationStatus"::TEXT || ' to ' || NEW."verificationStatus"::TEXT
);
change_type := COALESCE(change_type, 'Verification update');
END IF;
-- Check for description changes
IF OLD.description IS DISTINCT FROM NEW.description THEN
change_descriptions := array_append(change_descriptions, 'Description was updated');
change_type := COALESCE(change_type, 'Description update');
END IF;
-- Check for currency changes
IF OLD."acceptedCurrencies" IS DISTINCT FROM NEW."acceptedCurrencies" THEN
-- Find currencies added
WITH
old_currencies AS (SELECT unnest(OLD."acceptedCurrencies") AS currency),
new_currencies AS (SELECT unnest(NEW."acceptedCurrencies") AS currency),
added_currencies AS (
SELECT currency FROM new_currencies
EXCEPT
SELECT currency FROM old_currencies
),
removed_currencies AS (
SELECT currency FROM old_currencies
EXCEPT
SELECT currency FROM new_currencies
)
-- Temp variable for currency description
SELECT
CASE
WHEN (SELECT COUNT(*) FROM added_currencies) > 0 AND (SELECT COUNT(*) FROM removed_currencies) > 0 THEN
'Currencies updated: added ' || array_to_string(ARRAY(SELECT currency FROM added_currencies), ', ') ||
', removed ' || array_to_string(ARRAY(SELECT currency FROM removed_currencies), ', ')
WHEN (SELECT COUNT(*) FROM added_currencies) > 0 THEN
'Added currencies: ' || array_to_string(ARRAY(SELECT currency FROM added_currencies), ', ')
WHEN (SELECT COUNT(*) FROM removed_currencies) > 0 THEN
'Removed currencies: ' || array_to_string(ARRAY(SELECT currency FROM removed_currencies), ', ')
ELSE
'Currencies changed'
END
INTO currency_desc;
IF currency_desc IS NOT NULL AND currency_desc <> '' THEN
change_descriptions := array_append(change_descriptions, currency_desc);
change_type := COALESCE(change_type, 'Currency update');
END IF;
END IF;
-- If there are changes, create an event
IF array_length(change_descriptions, 1) > 0 THEN
-- Create a title based on number of changes
IF array_length(change_descriptions, 1) = 1 THEN
event_title := COALESCE(change_type, 'Service updated'); -- Ensure title is not null
ELSE
event_title := 'Service updated';
END IF;
-- Create content with all changes
event_content := array_to_string(change_descriptions, '. ');
-- Ensure content is not null or empty
IF event_content IS NULL OR event_content = '' THEN
event_content := 'Service details changed (content unavailable)';
END IF;
-- Insert the event
INSERT INTO "Event" (
"title",
"content",
"type",
"visible",
"startedAt",
"endedAt",
"serviceId"
) VALUES (
event_title,
event_content,
'UPDATE',
TRUE,
event_time,
event_time,
NEW.id
);
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create a trigger for service updates
DROP TRIGGER IF EXISTS service_events_trigger ON "Service";
CREATE TRIGGER service_events_trigger
AFTER UPDATE OF "serviceUrls", "kycLevel", "verificationStatus", "description", "acceptedCurrencies" ON "Service"
FOR EACH ROW
EXECUTE FUNCTION trigger_service_events();
-- Additional trigger to monitor changes to ServiceAttribute
CREATE OR REPLACE FUNCTION trigger_service_attribute_events()
RETURNS TRIGGER AS $$
DECLARE
attribute_name TEXT;
service_name TEXT;
event_title TEXT := 'Attribute change'; -- Default title
event_content TEXT;
event_time TIMESTAMP WITH TIME ZONE := transaction_timestamp();
target_service_id INT;
service_exists BOOLEAN;
service_created_at TIMESTAMP WITH TIME ZONE;
is_new_service BOOLEAN := FALSE;
BEGIN
-- Determine target service ID and operation type
IF TG_OP = 'INSERT' THEN
target_service_id := NEW."serviceId";
-- Check if this is a new service (created within the last minute)
-- This helps prevent events when attributes are initially added to a new service
SELECT "createdAt" INTO service_created_at FROM "Service" WHERE id = target_service_id;
IF service_created_at IS NOT NULL AND (event_time - service_created_at) < INTERVAL '1 minute' THEN
is_new_service := TRUE;
RETURN NEW; -- Skip event creation for new services
END IF;
SELECT title INTO attribute_name FROM "Attribute" WHERE id = NEW."attributeId";
SELECT name INTO service_name FROM "Service" WHERE id = target_service_id;
IF attribute_name IS NOT NULL AND service_name IS NOT NULL THEN
event_title := 'Attribute added';
event_content := 'Attribute "' || attribute_name || '" was added to ' || service_name;
ELSE
event_content := 'An attribute was added (details unavailable)';
END IF;
ELSIF TG_OP = 'DELETE' THEN
target_service_id := OLD."serviceId";
-- Check if the service still exists before trying to fetch its name or create an event
SELECT EXISTS (SELECT 1 FROM "Service" WHERE id = target_service_id) INTO service_exists;
IF service_exists THEN
SELECT title INTO attribute_name FROM "Attribute" WHERE id = OLD."attributeId";
SELECT name INTO service_name FROM "Service" WHERE id = target_service_id;
IF attribute_name IS NOT NULL AND service_name IS NOT NULL THEN
event_title := 'Attribute removed';
event_content := 'Attribute "' || attribute_name || '" was removed from ' || service_name;
ELSE
-- This case might happen if attribute was deleted concurrently
event_content := 'An attribute was removed (details unavailable)';
END IF;
ELSE
-- Service was deleted, don't create an event
RETURN OLD;
END IF;
END IF;
-- Ensure content is not null/empty and insert
IF event_content IS NOT NULL AND event_content <> '' AND target_service_id IS NOT NULL AND NOT is_new_service THEN
-- Re-check service existence right before insert just in case of concurrency on INSERT
IF TG_OP = 'INSERT' THEN
SELECT EXISTS (SELECT 1 FROM "Service" WHERE id = target_service_id) INTO service_exists;
END IF;
IF service_exists THEN
INSERT INTO "Event" (
"title",
"content",
"type",
"visible",
"startedAt",
"endedAt",
"serviceId"
) VALUES (
event_title,
event_content,
'UPDATE',
TRUE,
event_time,
event_time,
target_service_id
);
END IF;
END IF;
-- Return appropriate record
IF TG_OP = 'INSERT' THEN
RETURN NEW;
ELSE
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Create a trigger for service attribute changes
DROP TRIGGER IF EXISTS service_attribute_events_trigger ON "ServiceAttribute";
CREATE TRIGGER service_attribute_events_trigger
AFTER INSERT OR DELETE ON "ServiceAttribute"
FOR EACH ROW
EXECUTE FUNCTION trigger_service_attribute_events();
-- Additional trigger to monitor changes to service categories
CREATE OR REPLACE FUNCTION trigger_service_category_events()
RETURNS TRIGGER AS $$
DECLARE
category_name TEXT;
service_name TEXT;
event_title TEXT := 'Category change'; -- Default title
event_content TEXT;
event_time TIMESTAMP WITH TIME ZONE := transaction_timestamp();
target_service_id INT;
service_exists BOOLEAN;
service_created_at TIMESTAMP WITH TIME ZONE;
is_new_service BOOLEAN := FALSE;
BEGIN
-- Determine target service ID and operation type
IF TG_OP = 'INSERT' THEN
target_service_id := NEW."A";
-- Check if this is a new service (created within the last minute)
-- This helps prevent events when categories are initially added to a new service
SELECT "createdAt" INTO service_created_at FROM "Service" WHERE id = target_service_id;
IF service_created_at IS NOT NULL AND (event_time - service_created_at) < INTERVAL '1 minute' THEN
is_new_service := TRUE;
RETURN NEW; -- Skip event creation for new services
END IF;
SELECT name INTO category_name FROM "Category" WHERE id = NEW."B";
SELECT name INTO service_name FROM "Service" WHERE id = target_service_id;
IF category_name IS NOT NULL AND service_name IS NOT NULL THEN
event_title := 'Category added';
event_content := 'Category "' || category_name || '" was added to ' || service_name;
ELSE
event_content := 'A category was added (details unavailable)';
END IF;
ELSIF TG_OP = 'DELETE' THEN
target_service_id := OLD."A";
-- Check if the service still exists before trying to fetch its name or create an event
SELECT EXISTS (SELECT 1 FROM "Service" WHERE id = target_service_id) INTO service_exists;
IF service_exists THEN
SELECT name INTO category_name FROM "Category" WHERE id = OLD."B";
SELECT name INTO service_name FROM "Service" WHERE id = target_service_id;
IF category_name IS NOT NULL AND service_name IS NOT NULL THEN
event_title := 'Category removed';
event_content := 'Category "' || category_name || '" was removed from ' || service_name;
ELSE
-- This case might happen if category was deleted concurrently
event_content := 'A category was removed (details unavailable)';
END IF;
ELSE
-- Service was deleted, don't create an event
RETURN OLD;
END IF;
END IF;
-- Ensure content is not null/empty and insert
IF event_content IS NOT NULL AND event_content <> '' AND target_service_id IS NOT NULL AND NOT is_new_service THEN
-- Re-check service existence right before insert just in case of concurrency on INSERT
IF TG_OP = 'INSERT' THEN
SELECT EXISTS (SELECT 1 FROM "Service" WHERE id = target_service_id) INTO service_exists;
END IF;
IF service_exists THEN
INSERT INTO "Event" (
"title",
"content",
"type",
"visible",
"startedAt",
"endedAt",
"serviceId"
) VALUES (
event_title,
event_content,
'UPDATE',
TRUE,
event_time,
event_time,
target_service_id
);
END IF;
END IF;
-- Return appropriate record
IF TG_OP = 'INSERT' THEN
RETURN NEW;
ELSE
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Create a trigger for service category changes (on the junction table)
DROP TRIGGER IF EXISTS service_category_events_trigger ON "_ServiceToCategory";
CREATE TRIGGER service_category_events_trigger
AFTER INSERT OR DELETE ON "_ServiceToCategory"
FOR EACH ROW
EXECUTE FUNCTION trigger_service_category_events();
-- Verification Steps Trigger
-- This trigger creates events when verification steps are added or status changes
CREATE OR REPLACE FUNCTION trigger_verification_step_events()
RETURNS TRIGGER AS $$
DECLARE
service_name TEXT;
event_title TEXT;
event_content TEXT;
event_time TIMESTAMP WITH TIME ZONE := transaction_timestamp();
service_exists BOOLEAN;
BEGIN
-- Check if the service exists
SELECT EXISTS (SELECT 1 FROM "Service" WHERE id = NEW."serviceId") INTO service_exists;
IF NOT service_exists THEN
-- Service was deleted or doesn't exist, don't create an event
RETURN NEW;
END IF;
-- Get service name
SELECT name INTO service_name FROM "Service" WHERE id = NEW."serviceId";
-- Handle different operations
IF TG_OP = 'INSERT' THEN
event_title := 'Verification step added';
event_content := '"' || NEW.title || '" was added';
ELSIF TG_OP = 'UPDATE' AND OLD.status IS DISTINCT FROM NEW.status THEN
event_title := 'Verification step ' || replace(lower(NEW.status::TEXT), '_', ' ');
event_content := '"' || NEW.title || '" status changed from ' ||
replace(lower(OLD.status::TEXT), '_', ' ') || ' to ' || replace(lower(NEW.status::TEXT), '_', ' ');
ELSE
-- No relevant changes, exit
RETURN NEW;
END IF;
-- Insert the event
INSERT INTO "Event" (
"title",
"content",
"type",
"visible",
"startedAt",
"endedAt",
"serviceId"
) VALUES (
event_title,
event_content,
'UPDATE',
TRUE,
event_time,
event_time,
NEW."serviceId"
);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger for verification step changes
DROP TRIGGER IF EXISTS verification_step_events_trigger ON "VerificationStep";
CREATE TRIGGER verification_step_events_trigger
AFTER INSERT OR UPDATE OF status ON "VerificationStep"
FOR EACH ROW
EXECUTE FUNCTION trigger_verification_step_events();

View File

@@ -0,0 +1,227 @@
-- Function & Trigger for Root Comment Insertions (Approved/Verified)
CREATE OR REPLACE FUNCTION notify_root_comment_inserted()
RETURNS TRIGGER AS $$
DECLARE
watcher_count INT;
BEGIN
RAISE NOTICE '[notify_root_comment_inserted] Trigger fired for comment ID: %', NEW.id;
WITH watchers AS (
SELECT np."userId", np."enableNotifyPendingRepliesOnWatch"
FROM "_onRootCommentCreatedForServices" rc
JOIN "NotificationPreferences" np ON rc."A" = np."id"
WHERE rc."B" = NEW."serviceId"
AND np."userId" <> NEW."authorId"
)
INSERT INTO "Notification" ("userId", "type", "aboutCommentId")
SELECT w."userId",
'ROOT_COMMENT_CREATED',
NEW."id"
FROM watchers w
WHERE (
NEW.status IN ('APPROVED', 'VERIFIED')
OR (NEW.status = 'PENDING' AND w."enableNotifyPendingRepliesOnWatch")
)
ON CONFLICT DO NOTHING;
RAISE NOTICE '[notify_root_comment_inserted] Inserted % notifications for comment ID: %', FOUND, NEW.id;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_notify_root_comment_inserted ON "Comment";
CREATE TRIGGER trg_notify_root_comment_inserted
AFTER INSERT ON "Comment"
FOR EACH ROW
WHEN (NEW."parentId" IS NULL)
EXECUTE FUNCTION notify_root_comment_inserted();
-- Function & Trigger for Reply Comment Insertions
CREATE OR REPLACE FUNCTION notify_reply_comment_inserted()
RETURNS TRIGGER AS $$
BEGIN
WITH watchers AS (
SELECT np."userId", np."enableNotifyPendingRepliesOnWatch"
FROM "_watchedComments" w
JOIN "NotificationPreferences" np ON w."B" = np."id"
WHERE w."A" = NEW."parentId"
AND np."userId" <> NEW."authorId"
)
INSERT INTO "Notification" ("userId", "type", "aboutCommentId")
SELECT w."userId",
'REPLY_COMMENT_CREATED',
NEW."id"
FROM watchers w
WHERE (
NEW.status IN ('APPROVED', 'VERIFIED')
OR (NEW.status = 'PENDING' AND w."enableNotifyPendingRepliesOnWatch")
)
ON CONFLICT DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_notify_reply_comment_inserted ON "Comment";
CREATE TRIGGER trg_notify_reply_comment_inserted
AFTER INSERT ON "Comment"
FOR EACH ROW
WHEN (NEW."parentId" IS NOT NULL)
EXECUTE FUNCTION notify_reply_comment_inserted();
-- Function & Trigger for Reply Approval/Verification
CREATE OR REPLACE FUNCTION notify_reply_approved()
RETURNS TRIGGER AS $$
BEGIN
WITH watchers AS (
SELECT np."userId"
FROM "_watchedComments" w
JOIN "NotificationPreferences" np ON w."B" = np."id"
WHERE w."A" = NEW."parentId"
AND np."userId" <> NEW."authorId"
)
INSERT INTO "Notification" ("userId", "type", "aboutCommentId")
SELECT w."userId",
'REPLY_COMMENT_CREATED',
NEW."id"
FROM watchers w
ON CONFLICT DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_notify_reply_approved ON "Comment";
CREATE TRIGGER trg_notify_reply_approved
AFTER UPDATE OF status ON "Comment"
FOR EACH ROW
WHEN (NEW."parentId" IS NOT NULL AND NEW.status IN ('APPROVED', 'VERIFIED') AND OLD.status NOT IN ('APPROVED', 'VERIFIED'))
EXECUTE FUNCTION notify_reply_approved();
DROP TRIGGER IF EXISTS trg_notify_root_approved ON "Comment";
CREATE OR REPLACE FUNCTION notify_root_approved()
RETURNS TRIGGER AS $$
BEGIN
WITH watchers AS (
SELECT np."userId"
FROM "_onRootCommentCreatedForServices" rc
JOIN "NotificationPreferences" np ON rc."A" = np."id"
WHERE rc."B" = NEW."serviceId"
AND np."userId" <> NEW."authorId"
AND NOT np."enableNotifyPendingRepliesOnWatch"
)
INSERT INTO "Notification" ("userId", "type", "aboutCommentId")
SELECT w."userId",
'ROOT_COMMENT_CREATED',
NEW."id"
FROM watchers w
ON CONFLICT DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_notify_root_approved
AFTER UPDATE OF status ON "Comment"
FOR EACH ROW
WHEN (NEW."parentId" IS NULL AND NEW.status IN ('APPROVED', 'VERIFIED') AND OLD.status NOT IN ('APPROVED', 'VERIFIED'))
EXECUTE FUNCTION notify_root_approved();
-- Function & Trigger for Comment Status Changes (Status, Suspicious, AdminReview)
CREATE OR REPLACE FUNCTION notify_comment_status_changed()
RETURNS TRIGGER AS $$
DECLARE
v_status_change "CommentStatusChange" := NULL;
BEGIN
-- Determine the status change type
IF NEW.status <> OLD.status THEN
IF NEW.status = 'APPROVED' THEN v_status_change := 'STATUS_CHANGED_TO_APPROVED';
ELSIF NEW.status = 'VERIFIED' THEN v_status_change := 'STATUS_CHANGED_TO_VERIFIED';
ELSIF NEW.status = 'REJECTED' THEN v_status_change := 'STATUS_CHANGED_TO_REJECTED';
ELSIF (NEW.status = 'PENDING' OR NEW.status = 'HUMAN_PENDING') AND (OLD.status <> 'PENDING' AND OLD.status <> 'HUMAN_PENDING') THEN v_status_change := 'STATUS_CHANGED_TO_PENDING';
END IF;
ELSIF NEW.suspicious <> OLD.suspicious THEN
IF NEW.suspicious = true THEN v_status_change := 'MARKED_AS_SPAM';
ELSE v_status_change := 'UNMARKED_AS_SPAM';
END IF;
ELSIF NEW."requiresAdminReview" <> OLD."requiresAdminReview" THEN
IF NEW."requiresAdminReview" = true THEN v_status_change := 'MARKED_FOR_ADMIN_REVIEW';
ELSE v_status_change := 'UNMARKED_FOR_ADMIN_REVIEW';
END IF;
END IF;
-- If a relevant status change occurred, notify watchers of THIS comment
IF v_status_change IS NOT NULL THEN
WITH watchers AS (
-- Get all watchers excluding author
SELECT np."userId"
FROM "_watchedComments" w
JOIN "NotificationPreferences" np ON w."B" = np."id"
WHERE w."A" = NEW."id"
AND np."userId" <> NEW."authorId"
AND np."enableOnMyCommentStatusChange"
UNION ALL
-- Add author if they have enabled notifications for their own comments
SELECT np."userId"
FROM "NotificationPreferences" np
WHERE np."userId" = NEW."authorId"
AND np."enableOnMyCommentStatusChange"
)
INSERT INTO "Notification" ("userId", "type", "aboutCommentId", "aboutCommentStatusChange")
SELECT w."userId",
'COMMENT_STATUS_CHANGE',
NEW."id",
v_status_change
FROM watchers w
ON CONFLICT DO NOTHING;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_notify_comment_status_changed ON "Comment";
CREATE TRIGGER trg_notify_comment_status_changed
AFTER UPDATE OF status, suspicious, "requiresAdminReview" ON "Comment"
FOR EACH ROW
WHEN (NEW.status <> OLD.status OR NEW.suspicious <> OLD.suspicious OR NEW."requiresAdminReview" <> OLD."requiresAdminReview")
EXECUTE FUNCTION notify_comment_status_changed();
-- Function & Trigger for Community Note Added
CREATE OR REPLACE FUNCTION notify_community_note_added()
RETURNS TRIGGER AS $$
BEGIN
-- Notify watchers of this specific comment (excluding author)
WITH watchers AS (
SELECT np."userId"
FROM "_watchedComments" w
JOIN "NotificationPreferences" np ON w."B" = np."id"
WHERE w."A" = NEW."id"
AND np."userId" <> NEW."authorId"
)
INSERT INTO "Notification" ("userId", "type", "aboutCommentId")
SELECT w."userId",
'COMMUNITY_NOTE_ADDED',
NEW."id"
FROM watchers w
ON CONFLICT DO NOTHING;
-- Always notify the author
INSERT INTO "Notification" ("userId", "type", "aboutCommentId")
VALUES (NEW."authorId", 'COMMUNITY_NOTE_ADDED', NEW."id")
ON CONFLICT DO NOTHING;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS trg_notify_community_note_added ON "Comment";
CREATE TRIGGER trg_notify_community_note_added
AFTER UPDATE OF "communityNote" ON "Comment"
FOR EACH ROW
WHEN (NEW."communityNote" IS NOT NULL AND NEW."communityNote" <> '' AND (OLD."communityNote" IS NULL OR OLD."communityNote" = ''))
EXECUTE FUNCTION notify_community_note_added();
-- Remove the old monolithic trigger and function definition if they still exist
DROP TRIGGER IF EXISTS comment_notifications_trigger ON "Comment";
DROP FUNCTION IF EXISTS trigger_comment_notifications();

View File

@@ -0,0 +1,72 @@
CREATE OR REPLACE FUNCTION trigger_service_suggestion_notifications()
RETURNS TRIGGER AS $$
DECLARE
suggestion_status_change "ServiceSuggestionStatusChange";
BEGIN
IF TG_OP = 'INSERT' THEN -- Corresponds to ServiceSuggestionMessage insert
-- Notify suggestion author (if not the sender)
INSERT INTO "Notification" ("userId", "type", "aboutServiceSuggestionId", "aboutServiceSuggestionMessageId")
SELECT s."userId", 'SUGGESTION_MESSAGE', NEW."suggestionId", NEW."id"
FROM "ServiceSuggestion" s
WHERE s."id" = NEW."suggestionId"
AND s."userId" <> NEW."userId"
AND NOT EXISTS (
SELECT 1 FROM "Notification" n
WHERE n."userId" = s."userId"
AND n."type" = 'SUGGESTION_MESSAGE'
AND n."aboutServiceSuggestionMessageId" = NEW."id"
);
-- Notify all admins (except the sender), but only if sender is not admin
INSERT INTO "Notification" ("userId", "type", "aboutServiceSuggestionId", "aboutServiceSuggestionMessageId")
SELECT u."id", 'SUGGESTION_MESSAGE', NEW."suggestionId", NEW."id"
FROM "User" u
WHERE u."admin" = true
AND u."id" <> NEW."userId"
-- Only notify admins if the message sender is not an admin
AND NOT EXISTS (SELECT 1 FROM "User" WHERE "id" = NEW."userId" AND "admin" = true)
AND NOT EXISTS (
SELECT 1 FROM "Notification" n
WHERE n."userId" = u."id"
AND n."type" = 'SUGGESTION_MESSAGE'
AND n."aboutServiceSuggestionMessageId" = NEW."id"
);
ELSIF TG_OP = 'UPDATE' THEN -- Corresponds to ServiceSuggestion status update
-- Notify suggestion author about status change
IF NEW.status <> OLD.status THEN
IF NEW.status = 'PENDING' THEN
suggestion_status_change := 'STATUS_CHANGED_TO_PENDING';
ELSIF NEW.status = 'APPROVED' THEN
suggestion_status_change := 'STATUS_CHANGED_TO_APPROVED';
ELSIF NEW.status = 'REJECTED' THEN
suggestion_status_change := 'STATUS_CHANGED_TO_REJECTED';
ELSIF NEW.status = 'WITHDRAWN' THEN
suggestion_status_change := 'STATUS_CHANGED_TO_WITHDRAWN';
END IF;
INSERT INTO "Notification" ("userId", "type", "aboutServiceSuggestionId", "aboutSuggestionStatusChange")
VALUES (NEW."userId", 'SUGGESTION_STATUS_CHANGE', NEW."id", suggestion_status_change);
END IF;
END IF;
-- Use RETURN NULL for AFTER triggers as the return value is ignored.
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Trigger for new messages
DROP TRIGGER IF EXISTS service_suggestion_message_notifications_trigger ON "ServiceSuggestionMessage";
CREATE TRIGGER service_suggestion_message_notifications_trigger
AFTER INSERT ON "ServiceSuggestionMessage"
FOR EACH ROW
EXECUTE FUNCTION trigger_service_suggestion_notifications();
-- Trigger for status updates
DROP TRIGGER IF EXISTS service_suggestion_status_notifications_trigger ON "ServiceSuggestion";
CREATE TRIGGER service_suggestion_status_notifications_trigger
AFTER UPDATE OF status ON "ServiceSuggestion"
FOR EACH ROW
-- Only run the function if the status actually changed
WHEN (OLD.status IS DISTINCT FROM NEW.status)
EXECUTE FUNCTION trigger_service_suggestion_notifications();

View File

@@ -0,0 +1,28 @@
CREATE OR REPLACE FUNCTION trigger_service_events_notifications()
RETURNS TRIGGER AS $$
BEGIN
-- Handle new Event insertions
IF TG_TABLE_NAME = 'Event' AND TG_OP = 'INSERT' THEN
INSERT INTO "Notification" ("userId", "type", "aboutServiceId", "aboutEventId")
SELECT np."userId", 'EVENT_CREATED', NEW."serviceId", NEW.id
FROM "_onEventCreatedForServices" oes
JOIN "NotificationPreferences" np ON oes."A" = np.id
WHERE oes."B" = NEW."serviceId"
AND NOT EXISTS (
SELECT 1 FROM "Notification" n
WHERE n."userId" = np."userId"
AND n."type" = 'EVENT_CREATED'
AND n."aboutEventId" = NEW.id
);
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Trigger for new Events
DROP TRIGGER IF EXISTS eVENT_CREATED_notifications_trigger ON "Event";
CREATE TRIGGER eVENT_CREATED_notifications_trigger
AFTER INSERT ON "Event"
FOR EACH ROW
EXECUTE FUNCTION trigger_service_events_notifications();

View File

@@ -0,0 +1,37 @@
CREATE OR REPLACE FUNCTION trigger_service_verification_status_change_notifications()
RETURNS TRIGGER AS $$
DECLARE
v_status_change "ServiceVerificationStatusChange";
BEGIN
-- Check if verificationStatus actually changed
IF OLD."verificationStatus" IS DISTINCT FROM NEW."verificationStatus" THEN
-- Determine the correct ServiceVerificationStatusChange enum value
SELECT CASE NEW."verificationStatus"
WHEN 'COMMUNITY_CONTRIBUTED' THEN 'STATUS_CHANGED_TO_COMMUNITY_CONTRIBUTED'
WHEN 'APPROVED' THEN 'STATUS_CHANGED_TO_APPROVED'
WHEN 'VERIFICATION_SUCCESS' THEN 'STATUS_CHANGED_TO_VERIFICATION_SUCCESS'
WHEN 'VERIFICATION_FAILED' THEN 'STATUS_CHANGED_TO_VERIFICATION_FAILED'
ELSE NULL
END
INTO v_status_change;
-- Only insert if we determined a valid status change enum
IF v_status_change IS NOT NULL THEN
INSERT INTO "Notification" ("userId", "type", "aboutServiceId", "aboutServiceVerificationStatusChange")
SELECT np."userId", 'SERVICE_VERIFICATION_STATUS_CHANGE', NEW.id, v_status_change
FROM "_onVerificationChangeForServices" oes
JOIN "NotificationPreferences" np ON oes."A" = np.id -- A -> NotificationPreferences.id
WHERE oes."B" = NEW.id; -- B -> Service.id
END IF;
END IF;
RETURN NULL; -- Return NULL for AFTER trigger
END;
$$ LANGUAGE plpgsql;
-- Trigger for Service verificationStatus updates
DROP TRIGGER IF EXISTS service_verification_status_change_notifications_trigger ON "Service";
CREATE TRIGGER service_verification_status_change_notifications_trigger
AFTER UPDATE ON "Service"
FOR EACH ROW
EXECUTE FUNCTION trigger_service_verification_status_change_notifications();

View File

@@ -0,0 +1,62 @@
CREATE OR REPLACE FUNCTION trigger_user_status_change_notifications()
RETURNS TRIGGER AS $$
DECLARE
status_change "AccountStatusChange";
BEGIN
-- Check for admin status change
IF OLD.admin IS DISTINCT FROM NEW.admin THEN
IF NEW.admin = true THEN
status_change := 'ADMIN_TRUE';
ELSE
status_change := 'ADMIN_FALSE';
END IF;
INSERT INTO "Notification" ("userId", "type", "aboutAccountStatusChange")
VALUES (NEW.id, 'ACCOUNT_STATUS_CHANGE', status_change);
END IF;
-- Check for verified status change
IF OLD.verified IS DISTINCT FROM NEW.verified THEN
IF NEW.verified = true THEN
status_change := 'VERIFIED_TRUE';
ELSE
status_change := 'VERIFIED_FALSE';
END IF;
INSERT INTO "Notification" ("userId", "type", "aboutAccountStatusChange")
VALUES (NEW.id, 'ACCOUNT_STATUS_CHANGE', status_change);
END IF;
-- Check for verifier status change
IF OLD.verifier IS DISTINCT FROM NEW.verifier THEN
IF NEW.verifier = true THEN
status_change := 'VERIFIER_TRUE';
ELSE
status_change := 'VERIFIER_FALSE';
END IF;
INSERT INTO "Notification" ("userId", "type", "aboutAccountStatusChange")
VALUES (NEW.id, 'ACCOUNT_STATUS_CHANGE', status_change);
END IF;
-- Check for spammer status change
IF OLD.spammer IS DISTINCT FROM NEW.spammer THEN
IF NEW.spammer = true THEN
status_change := 'SPAMMER_TRUE';
ELSE
status_change := 'SPAMMER_FALSE';
END IF;
INSERT INTO "Notification" ("userId", "type", "aboutAccountStatusChange")
VALUES (NEW.id, 'ACCOUNT_STATUS_CHANGE', status_change);
END IF;
-- Return NULL for AFTER triggers as the return value is ignored.
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
-- Drop the trigger if it exists to ensure a clean setup
DROP TRIGGER IF EXISTS user_status_change_notifications_trigger ON "User";
-- Create the trigger to fire after updates on specific status columns
CREATE TRIGGER user_status_change_notifications_trigger
AFTER UPDATE OF admin, verified, verifier, spammer ON "User"
FOR EACH ROW
EXECUTE FUNCTION trigger_user_status_change_notifications();