diff --git a/apps/api/prisma/migrations/20260613192706_add_p1_rule_action_and_digest_models/migration.sql b/apps/api/prisma/migrations/20260613192706_add_p1_rule_action_and_digest_models/migration.sql new file mode 100644 index 0000000..841adcc --- /dev/null +++ b/apps/api/prisma/migrations/20260613192706_add_p1_rule_action_and_digest_models/migration.sql @@ -0,0 +1,45 @@ +-- AlterEnum +ALTER TYPE "RuleAction" ADD VALUE 'P1'; + +-- CreateTable +CREATE TABLE "DigestConfig" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL, + "targetGroupJid" TEXT NOT NULL, + "targetAccountId" TEXT NOT NULL, + "scheduleHour" INTEGER NOT NULL DEFAULT 20, + "scheduleMinute" INTEGER NOT NULL DEFAULT 0, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "lastSentAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "DigestConfig_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Digest" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL, + "content" TEXT NOT NULL, + "messageIds" TEXT[], + "digestDate" TIMESTAMP(3) NOT NULL, + "sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Digest_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "DigestConfig_tenantId_key" ON "DigestConfig"("tenantId"); + +-- CreateIndex +CREATE INDEX "Digest_tenantId_idx" ON "Digest"("tenantId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Digest_tenantId_digestDate_key" ON "Digest"("tenantId", "digestDate"); + +-- AddForeignKey +ALTER TABLE "DigestConfig" ADD CONSTRAINT "DigestConfig_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Digest" ADD CONSTRAINT "Digest_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260616095334_add_hybrid_ai_architecture/migration.sql b/apps/api/prisma/migrations/20260616095334_add_hybrid_ai_architecture/migration.sql new file mode 100644 index 0000000..7a3442d --- /dev/null +++ b/apps/api/prisma/migrations/20260616095334_add_hybrid_ai_architecture/migration.sql @@ -0,0 +1,41 @@ +-- AlterEnum +-- Adding RAW and DNC values to MessageStatus enum. +-- NOTE: SET DEFAULT 'RAW' intentionally omitted — PostgreSQL cannot use new enum values +-- as column defaults in the same transaction. Status is always set explicitly in code. + +ALTER TYPE "MessageStatus" ADD VALUE IF NOT EXISTS 'RAW'; +ALTER TYPE "MessageStatus" ADD VALUE IF NOT EXISTS 'DNC'; + +-- AlterTable: add new columns to Message +ALTER TABLE "Message" ADD COLUMN IF NOT EXISTS "expiresAt" TIMESTAMP(3), +ADD COLUMN IF NOT EXISTS "quotedPlatformMsgId" TEXT, +ADD COLUMN IF NOT EXISTS "threadId" TEXT; + +-- CreateTable: Thread +CREATE TABLE IF NOT EXISTS "Thread" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL, + "sourceGroupId" TEXT NOT NULL, + "lastActivityAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "messageCount" INTEGER NOT NULL DEFAULT 0, + "topic" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Thread_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX IF NOT EXISTS "Thread_tenantId_idx" ON "Thread"("tenantId"); +CREATE INDEX IF NOT EXISTS "Thread_sourceGroupId_lastActivityAt_idx" ON "Thread"("sourceGroupId", "lastActivityAt"); +CREATE INDEX IF NOT EXISTS "Message_status_expiresAt_idx" ON "Message"("status", "expiresAt"); +CREATE INDEX IF NOT EXISTS "Message_threadId_idx" ON "Message"("threadId"); + +-- AddForeignKey +ALTER TABLE "Message" DROP CONSTRAINT IF EXISTS "Message_threadId_fkey"; +ALTER TABLE "Message" ADD CONSTRAINT "Message_threadId_fkey" FOREIGN KEY ("threadId") REFERENCES "Thread"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "Thread" DROP CONSTRAINT IF EXISTS "Thread_tenantId_fkey"; +ALTER TABLE "Thread" ADD CONSTRAINT "Thread_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +ALTER TABLE "Thread" DROP CONSTRAINT IF EXISTS "Thread_sourceGroupId_fkey"; +ALTER TABLE "Thread" ADD CONSTRAINT "Thread_sourceGroupId_fkey" FOREIGN KEY ("sourceGroupId") REFERENCES "Group"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260617000000_add_outbox_event/migration.sql b/apps/api/prisma/migrations/20260617000000_add_outbox_event/migration.sql new file mode 100644 index 0000000..c72cad4 --- /dev/null +++ b/apps/api/prisma/migrations/20260617000000_add_outbox_event/migration.sql @@ -0,0 +1,29 @@ +-- CreateTable +CREATE TABLE "OutboxEvent" ( + "id" TEXT NOT NULL, + "type" TEXT NOT NULL, + "payload" JSONB NOT NULL, + "attempts" INTEGER NOT NULL DEFAULT 0, + "lastError" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "OutboxEvent_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "OutboxEvent_attempts_createdAt_idx" ON "OutboxEvent"("attempts", "createdAt"); + +-- LISTEN/NOTIFY trigger: fires pg_notify('outbox_event', NEW.id) after each INSERT +-- The worker holds an open pg.Client with LISTEN outbox_event to receive these events. +-- AFTER INSERT ensures NOTIFY fires only after the transaction commits. +CREATE OR REPLACE FUNCTION notify_outbox_event() +RETURNS TRIGGER AS $$ +BEGIN + PERFORM pg_notify('outbox_event', NEW.id); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER outbox_event_notify +AFTER INSERT ON "OutboxEvent" +FOR EACH ROW EXECUTE FUNCTION notify_outbox_event(); diff --git a/apps/api/prisma/migrations/20260617100000_add_organization_layer/migration.sql b/apps/api/prisma/migrations/20260617100000_add_organization_layer/migration.sql new file mode 100644 index 0000000..5ffd119 --- /dev/null +++ b/apps/api/prisma/migrations/20260617100000_add_organization_layer/migration.sql @@ -0,0 +1,63 @@ +-- CreateEnum +CREATE TYPE "OrgAdminRole" AS ENUM ('ORG_OWNER', 'ORG_ADMIN'); + +-- CreateTable: Organization +CREATE TABLE "Organization" ( + "id" TEXT NOT NULL, + "slug" TEXT NOT NULL, + "name" TEXT NOT NULL, + "settings" JSONB NOT NULL DEFAULT '{}', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Organization_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "Organization_slug_key" ON "Organization"("slug"); + +-- CreateTable: OrgAdmin +CREATE TABLE "OrgAdmin" ( + "id" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "name" TEXT, + "role" "OrgAdminRole" NOT NULL DEFAULT 'ORG_ADMIN', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OrgAdmin_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "OrgAdmin_organizationId_email_key" ON "OrgAdmin"("organizationId", "email"); +CREATE INDEX "OrgAdmin_organizationId_idx" ON "OrgAdmin"("organizationId"); + +ALTER TABLE "OrgAdmin" ADD CONSTRAINT "OrgAdmin_organizationId_fkey" + FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- CreateTable: OrgRule +CREATE TABLE "OrgRule" ( + "id" TEXT NOT NULL, + "organizationId" TEXT NOT NULL, + "matchType" "RuleMatchType" NOT NULL, + "matchValue" TEXT NOT NULL, + "action" "RuleAction" NOT NULL, + "priority" INTEGER NOT NULL DEFAULT 0, + "isActive" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OrgRule_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "OrgRule_organizationId_matchType_matchValue_key" ON "OrgRule"("organizationId", "matchType", "matchValue"); +CREATE INDEX "OrgRule_organizationId_isActive_idx" ON "OrgRule"("organizationId", "isActive"); + +ALTER TABLE "OrgRule" ADD CONSTRAINT "OrgRule_organizationId_fkey" + FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AlterTable: Tenant — add nullable organizationId FK +ALTER TABLE "Tenant" ADD COLUMN "organizationId" TEXT; + +ALTER TABLE "Tenant" ADD CONSTRAINT "Tenant_organizationId_fkey" + FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE SET NULL ON UPDATE CASCADE;