chore: Watchlist schema update (#24246)

* watchlist schema update

* changes addressed

* no backfill audit

* fix type

* fix flow of migration

* fix err

* type err fix

* add if exists check in migration
This commit is contained in:
Syed Ali Shahbaz
2025-10-04 01:44:37 +01:00
committed by GitHub
parent 654a865007
commit 7cd8dbf7d8
6 changed files with 200 additions and 77 deletions
@@ -1,9 +1,8 @@
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import type { NextApiRequest } from "next";
import { describe, expect, it, beforeEach } from "vitest";
import { WatchlistSeverity } from "@calcom/prisma/enums";
import { isLockedOrBlocked } from "../../../lib/utils/isLockedOrBlocked";
describe("isLockedOrBlocked", () => {
@@ -18,7 +17,6 @@ describe("isLockedOrBlocked", () => {
{
type: "DOMAIN",
value: "blocked.com",
severity: WatchlistSeverity.CRITICAL,
createdById: 1,
},
],
@@ -26,13 +24,13 @@ describe("isLockedOrBlocked", () => {
});
it("should return false if no user in request", async () => {
const req = { userId: null, user: null } as any;
const req = { userId: null, user: null } as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
it("should return false if user has no email", async () => {
const req = { userId: 123, user: { email: null } } as any;
const req = { userId: 123, user: { email: null } } as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
});
@@ -44,7 +42,7 @@ describe("isLockedOrBlocked", () => {
locked: true,
email: "test@example.com",
},
} as any;
} as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
@@ -57,7 +55,7 @@ describe("isLockedOrBlocked", () => {
locked: false,
email: "test@blocked.com",
},
} as any;
} as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
@@ -70,7 +68,7 @@ describe("isLockedOrBlocked", () => {
locked: false,
email: "test@example.com",
},
} as any;
} as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(false);
@@ -83,7 +81,7 @@ describe("isLockedOrBlocked", () => {
locked: false,
email: "test@BLOCKED.COM",
},
} as any;
} as unknown as NextApiRequest;
const result = await isLockedOrBlocked(req);
expect(result).toBe(true);
@@ -59,7 +59,7 @@ import { createWatchlistEntry } from "@calcom/features/watchlist/lib/testUtils";
import { WEBSITE_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { resetTestEmails } from "@calcom/lib/testEmails";
import { CreationSource, WatchlistSeverity, WatchlistType } from "@calcom/prisma/enums";
import { CreationSource, WatchlistType } from "@calcom/prisma/enums";
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
@@ -199,7 +199,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -263,6 +263,7 @@ describe("handleNewBooking", () => {
3. Should fallback to creating the booking in the first connected Calendar when neither event nor organizer has a destination calendar - This doesn't practically happen because organizer is always required to have a schedule set
3. Should trigger BOOKING_CREATED webhook
`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -363,7 +364,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -423,6 +424,7 @@ describe("handleNewBooking", () => {
3. Should fallback to create a booking in the Organizer Calendar if event doesn't have destination calendar
3. Should trigger BOOKING_CREATED webhook
`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -525,7 +527,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -578,6 +580,7 @@ describe("handleNewBooking", () => {
test(
`an error in creating a calendar event should not stop the booking creation - Current behaviour is wrong as the booking is created but no-one is notified of it`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -666,7 +669,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -699,6 +702,7 @@ describe("handleNewBooking", () => {
test(
"If destination calendar has no credential ID due to some reason, it should create the event in first connected calendar instead",
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -811,7 +815,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -865,6 +869,7 @@ describe("handleNewBooking", () => {
test(
"If destination calendar is there for Google Calendar but there are no Google Calendar credentials but there is an Apple Calendar credential connected, it should create the event in Apple Calendar",
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -970,7 +975,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -1022,6 +1027,7 @@ describe("handleNewBooking", () => {
describe("Event's first location should be used when location is unspecied", () => {
test(
`should create a successful booking with right location app when event has location option as video client`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -1115,6 +1121,7 @@ describe("handleNewBooking", () => {
`should create a successful booking with right location when event's location is not a conferencing app
`,
//test with inPerson event type
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -1198,6 +1205,7 @@ describe("handleNewBooking", () => {
);
test(
`should create a successful booking with organizer default conferencing app when event's location is not set`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -1292,6 +1300,7 @@ describe("handleNewBooking", () => {
describe("Video Meeting Creation", () => {
test(
`should create a successful booking with Zoom if used`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const subscriberUrl = "http://my-webhook.example.com";
@@ -1380,6 +1389,7 @@ describe("handleNewBooking", () => {
test(
`Booking should still be created using calvideo, if error occurs with zoom`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const subscriberUrl = "http://my-webhook.example.com";
@@ -1534,6 +1544,7 @@ describe("handleNewBooking", () => {
describe("UTM tracking tests", () => {
test(
"should create a booking with UTM tracking",
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -1609,7 +1620,6 @@ describe("handleNewBooking", () => {
bookingData: mockedBookingData,
});
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expectBookingTrackingToBeInDatabase(tracking, createdBooking.uid!);
},
timeout
@@ -1619,6 +1629,7 @@ describe("handleNewBooking", () => {
describe("Creation source tests", () => {
test(
`should create a booking with creation source as WEBAPP`,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -1700,7 +1711,7 @@ describe("handleNewBooking", () => {
() => {
test(
`should fail a booking if there is already a Cal.com booking overlapping the time`,
async ({}) => {
async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -1929,7 +1940,7 @@ describe("handleNewBooking", () => {
});
expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -2098,7 +2109,7 @@ describe("handleNewBooking", () => {
});
expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -2119,6 +2130,7 @@ describe("handleNewBooking", () => {
3. Should trigger BOOKING_REQUESTED webhook
4. Should trigger BOOKING_REQUESTED workflow
`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const subscriberUrl = "http://my-webhook.example.com";
@@ -2220,7 +2232,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING,
@@ -2256,6 +2268,7 @@ describe("handleNewBooking", () => {
3. Should trigger BOOKING_REQUESTED webhook
4. Should trigger BOOKING_REQUESTED workflow
`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const subscriberUrl = "http://my-webhook.example.com";
@@ -2345,7 +2358,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING,
@@ -2382,6 +2395,7 @@ describe("handleNewBooking", () => {
2. Should send emails to the booker as well as organizer
3. Should trigger BOOKING_CREATED webhook
`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -2479,7 +2493,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -2518,6 +2532,7 @@ describe("handleNewBooking", () => {
3. Should trigger BOOKING_REQUESTED webhook
4. Should trigger BOOKING_REQUESTED workflows
`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const subscriberUrl = "http://my-webhook.example.com";
@@ -2621,7 +2636,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING,
@@ -2648,7 +2663,7 @@ describe("handleNewBooking", () => {
// FIXME: We shouldn't throw error here, the behaviour should be fixed.
test(
`if booking with Cal Video(Daily Video) fails, booking creation fails with uncaught error`,
async ({}) => {
async () => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
email: "booker@example.org",
@@ -2716,6 +2731,7 @@ describe("handleNewBooking", () => {
2. Should send emails to the booker as well as organizer
3. Should trigger BOOKING_CREATED webhook
`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -2801,7 +2817,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -2842,6 +2858,7 @@ describe("handleNewBooking", () => {
6. Workflow should not trigger before payment is made
7. Workflow triggers once payment is successful
`,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -2963,7 +2980,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
location: BookingLocations.CalVideo,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING,
@@ -2984,7 +3001,7 @@ describe("handleNewBooking", () => {
organizer,
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
paymentId: createdBooking.paymentId!,
});
@@ -2993,7 +3010,7 @@ describe("handleNewBooking", () => {
expect(webhookResponse?.statusCode).toBe(200);
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -3024,6 +3041,7 @@ describe("handleNewBooking", () => {
6. Should trigger BOOKING_REQUESTED workflow
7. Booking should still stay in pending state
`,
async ({ emails }) => {
const bookingInitiatedEmail = "booking_initiated@workflow.com";
const handleNewBooking = getNewBookingHandler();
@@ -3145,7 +3163,7 @@ describe("handleNewBooking", () => {
);
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING,
@@ -3165,7 +3183,7 @@ describe("handleNewBooking", () => {
organizer,
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
paymentId: createdBooking.paymentId!,
});
expectWorkflowToBeTriggered({ emailsToReceive: [bookingInitiatedEmail], emails });
@@ -3178,7 +3196,7 @@ describe("handleNewBooking", () => {
expect(webhookResponse?.statusCode).toBe(200);
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.PENDING,
@@ -3203,6 +3221,7 @@ describe("handleNewBooking", () => {
);
test(
`cannot book same slot multiple times `,
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -3298,7 +3317,7 @@ describe("handleNewBooking", () => {
await expectBookingToBeInDatabase({
description: "",
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uid: createdBooking.uid!,
eventTypeId: mockBookingData.eventTypeId,
status: BookingStatus.ACCEPTED,
@@ -3351,6 +3370,7 @@ describe("handleNewBooking", () => {
test(
`Payment retry scenario - should return existing payment UID and prevent duplicate bookings when retrying canceled payment`,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async ({ emails }) => {
const handleNewBooking = getNewBookingHandler();
const booker = getBooker({
@@ -3400,7 +3420,7 @@ describe("handleNewBooking", () => {
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
});
const { paymentUid } = mockPaymentApp({
const { paymentUid: _paymentUid } = mockPaymentApp({
metadataLookupKey: "stripe",
appStoreLookupKey: "stripepayment",
});
@@ -3693,7 +3713,6 @@ describe("handleNewBooking", () => {
await createWatchlistEntry({
type: WatchlistType.USERNAME,
severity: WatchlistSeverity.CRITICAL,
value: organizer.username,
});
@@ -3755,7 +3774,6 @@ describe("handleNewBooking", () => {
await createWatchlistEntry({
type: WatchlistType.DOMAIN,
severity: WatchlistSeverity.CRITICAL,
value: "spammer.com",
});
@@ -3817,7 +3835,6 @@ describe("handleNewBooking", () => {
await createWatchlistEntry({
type: WatchlistType.EMAIL,
severity: WatchlistSeverity.CRITICAL,
value: "spam@spammer.com",
});
+1 -2
View File
@@ -1,9 +1,8 @@
import prismock from "../../../../tests/libs/__mocks__/prisma";
import type { WatchlistSeverity, WatchlistType } from "@calcom/prisma/enums";
import type { WatchlistType } from "@calcom/prisma/enums";
interface WatchlistInput {
severity: WatchlistSeverity;
type: WatchlistType;
value: string;
}
@@ -1,7 +1,7 @@
import { captureException } from "@sentry/nextjs";
import db from "@calcom/prisma";
import { WatchlistType, WatchlistSeverity } from "@calcom/prisma/enums";
import { WatchlistType } from "@calcom/prisma/enums";
import type { IWatchlistRepository } from "./watchlist.repository.interface";
@@ -11,7 +11,6 @@ export class WatchlistRepository implements IWatchlistRepository {
try {
const emailInWatchlist = await db.watchlist.findFirst({
where: {
severity: WatchlistSeverity.CRITICAL,
OR: [
{ type: WatchlistType.EMAIL, value: email },
{ type: WatchlistType.DOMAIN, value: domain },
@@ -53,7 +52,6 @@ export class WatchlistRepository implements IWatchlistRepository {
try {
const blockedRecords = await db.watchlist.findMany({
where: {
severity: WatchlistSeverity.CRITICAL,
OR: [
...(usernames.length > 0
? [
@@ -0,0 +1,113 @@
-- CreateEnum
CREATE TYPE "WatchlistSource" AS ENUM ('MANUAL', 'FREE_DOMAIN_POLICY');
-- AlterEnum
ALTER TYPE "WatchlistAction" ADD VALUE 'ALERT';
-- 1) Add a temporary UUID column `uid` and backfill it without extensions
ALTER TABLE "Watchlist"
ADD COLUMN IF NOT EXISTS "uid" UUID;
-- Backfill existing rows with UUIDs using gen_random_uuid()
UPDATE "Watchlist"
SET "uid" = COALESCE("uid", gen_random_uuid());
-- Enforce NOT NULL now that all rows have a value
ALTER TABLE "Watchlist"
ALTER COLUMN "uid" SET NOT NULL;
-- 2) Adjust uniques/indexes that reference old id? (none in your snippet)
-- If you had FKs or PK references elsewhere, you'd update them here.
-- 3) Switch primary key from old `id` to new `uid`:
-- Drop old PK (name might vary in your DB; adjust if needed)
DO $$ BEGIN
ALTER TABLE "Watchlist" DROP CONSTRAINT "Watchlist_pkey";
EXCEPTION
WHEN undefined_object THEN NULL;
END $$;
-- 4) Drop the old `id` column and rename `uid`→`id`
ALTER TABLE "Watchlist" DROP COLUMN "id";
ALTER TABLE "Watchlist" RENAME COLUMN "uid" TO "id";
-- 5) Add the new PK on `id` (UUID)
ALTER TABLE "Watchlist" ADD CONSTRAINT "Watchlist_pkey" PRIMARY KEY ("id");
-- DropForeignKey - Do this before modifying Watchlist structure
ALTER TABLE "Watchlist" DROP CONSTRAINT "Watchlist_createdById_fkey";
-- DropForeignKey
ALTER TABLE "Watchlist" DROP CONSTRAINT "Watchlist_updatedById_fkey";
-- Modify Watchlist table structure FIRST
-- AlterTable
ALTER TABLE "Watchlist" DROP COLUMN "createdAt",
DROP COLUMN "createdById",
DROP COLUMN "severity",
DROP COLUMN "updatedAt",
DROP COLUMN "updatedById",
ADD COLUMN "isGlobal" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "lastUpdatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "source" "WatchlistSource" NOT NULL DEFAULT 'MANUAL';
-- Create WatchlistAudit table AFTER Watchlist has UUID id
-- CreateTable
CREATE TABLE "WatchlistAudit" (
"id" UUID NOT NULL,
"type" "WatchlistType" NOT NULL,
"value" TEXT NOT NULL,
"description" TEXT,
"action" "WatchlistAction" NOT NULL DEFAULT 'REPORT',
"changedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"changedByUserId" INTEGER,
"watchlistId" UUID NOT NULL,
CONSTRAINT "WatchlistAudit_pkey" PRIMARY KEY ("id")
);
-- Drop the BlockedBookingLog table
-- DropTable
DROP TABLE "BlockedBookingLog";
-- Drop unused enum
-- DropEnum
DROP TYPE "WatchlistSeverity";
-- Create WatchlistEventAudit table
-- CreateTable
CREATE TABLE "WatchlistEventAudit" (
"id" UUID NOT NULL,
"watchlistId" UUID NOT NULL,
"eventTypeId" INTEGER NOT NULL,
"actionTaken" "WatchlistAction" NOT NULL,
"timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "WatchlistEventAudit_pkey" PRIMARY KEY ("id")
);
-- Create indexes for performance
-- CreateIndex
CREATE UNIQUE INDEX IF NOT EXISTS "WatchlistAudit_id_key" ON "WatchlistAudit"("id");
-- CreateIndex
CREATE INDEX IF NOT EXISTS "WatchlistAudit_watchlistId_changedAt_idx" ON "WatchlistAudit"("watchlistId", "changedAt");
-- CreateIndex - Main composite index matching schema
CREATE INDEX IF NOT EXISTS "Watchlist_type_value_organizationId_action_idx" ON "Watchlist"("type", "value", "organizationId", "action");
-- CreateIndex - For organization-specific and global queries
CREATE INDEX IF NOT EXISTS "Watchlist_organizationId_isGlobal_idx" ON "Watchlist"("organizationId", "isGlobal");
-- CreateIndex - For source-based queries
CREATE INDEX IF NOT EXISTS "Watchlist_source_idx" ON "Watchlist"("source");
-- CreateIndex - Add unique constraint matching schema
CREATE UNIQUE INDEX IF NOT EXISTS "Watchlist_type_value_organizationId_key" ON "Watchlist"("type", "value", "organizationId");
-- CreateIndex - Performance index for WatchlistEventAudit queries
CREATE INDEX IF NOT EXISTS "WatchlistEventAudit_watchlistId_timestamp_idx" ON "WatchlistEventAudit"("watchlistId", "timestamp");
-- CreateIndex - For eventType-based queries
CREATE INDEX IF NOT EXISTS "WatchlistEventAudit_eventTypeId_timestamp_idx" ON "WatchlistEventAudit"("eventTypeId", "timestamp");
+33 -35
View File
@@ -451,8 +451,6 @@ model User {
updatedAttributeToUsers AttributeToUser[] @relation("updatedBy")
createdTranslations EventTypeTranslation[] @relation("CreatedEventTypeTranslations")
updatedTranslations EventTypeTranslation[] @relation("UpdatedEventTypeTranslations")
createdWatchlists Watchlist[] @relation("CreatedWatchlists")
updatedWatchlists Watchlist[] @relation("UpdatedWatchlists")
BookingInternalNote BookingInternalNote[]
creationSource CreationSource?
createdOrganizationOnboardings OrganizationOnboarding[] @relation("CreatedOrganizationOnboardings")
@@ -2269,54 +2267,54 @@ enum WatchlistType {
USERNAME
}
enum WatchlistSeverity {
LOW
MEDIUM
HIGH
CRITICAL
}
enum WatchlistAction {
REPORT
BLOCK
ALERT
}
enum WatchlistSource {
MANUAL
FREE_DOMAIN_POLICY
}
model Watchlist {
id String @id @unique @default(cuid())
type WatchlistType
value String
description String?
id String @id @default(uuid()) @db.Uuid
type WatchlistType
value String
description String?
isGlobal Boolean @default(false)
organizationId Int?
action WatchlistAction @default(REPORT)
severity WatchlistSeverity @default(LOW)
createdAt DateTime @default(now())
createdBy User? @relation("CreatedWatchlists", onDelete: SetNull, fields: [createdById], references: [id])
createdById Int?
updatedAt DateTime @updatedAt
updatedBy User? @relation("UpdatedWatchlists", onDelete: SetNull, fields: [updatedById], references: [id])
updatedById Int?
action WatchlistAction @default(REPORT)
source WatchlistSource @default(MANUAL)
lastUpdatedAt DateTime @default(now())
@@unique([type, value, organizationId])
@@index([type, value, organizationId, action])
}
model BlockedBookingLog {
id String @id @default(uuid())
email String
eventTypeId Int?
model WatchlistAudit {
id String @id @default(uuid(7)) @db.Uuid
type WatchlistType
value String
description String?
organizationId Int?
action WatchlistAction @default(REPORT)
bookingData Json?
createdAt DateTime @default(now())
watchlistId String?
changedAt DateTime @default(now())
changedByUserId Int?
@@index([organizationId, createdAt])
@@index([email])
@@index([watchlistId])
watchlistId String @db.Uuid
@@index([watchlistId, changedAt])
}
model WatchlistEventAudit {
id String @id @default(uuid(7)) @db.Uuid
watchlistId String @db.Uuid
eventTypeId Int
actionTaken WatchlistAction
timestamp DateTime @default(now())
}
enum BillingPeriod {