bunker-admin 902adce646 Add Straw Polls feature: quick opinion polling with public landers, MkDocs widgets, and social integration
Full-stack implementation across 7 sprints:
- Backend: 5 Prisma models (StrawPoll, Option, Vote, Comment, Challenge), 4 enums, POLLS_ADMIN role,
  admin CRUD routes, public voting/SSE/widget endpoints, BullMQ auto-close queue, rate limiting
- Admin: StrawPollsPage with inline drawers (campaigns pattern), PollResults bar chart, sidebar under Advocacy
- Public: dedicated poll lander with real-time SSE updates, browse page, anonymous voting with token dedup
- MkDocs: straw-poll-widget.js hydration (inline vote + card link modes), GrapesJS block types
- Social: feed activity (poll_voted), friend badge integration, challenge notifications, notification preferences
- Feature flag: enablePolls toggle in Settings, FeatureGate, Zod schema

Bunker Admin
2026-03-31 10:16:56 -06:00

169 lines
6.4 KiB
SQL

-- CreateEnum
CREATE TYPE "StrawPollType" AS ENUM ('SINGLE_CHOICE', 'YES_NO_ABSTAIN');
-- CreateEnum
CREATE TYPE "StrawPollStatus" AS ENUM ('DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED');
-- CreateEnum
CREATE TYPE "StrawPollIdentityMode" AS ENUM ('ANONYMOUS', 'TOKEN_GATED', 'AUTHENTICATED', 'MIXED');
-- CreateEnum
CREATE TYPE "StrawPollResultVisibility" AS ENUM ('LIVE', 'AFTER_VOTE', 'AFTER_CLOSE', 'CREATOR_ONLY', 'PUBLIC_ALWAYS');
-- AlterEnum
-- This migration adds more than one value to an enum.
-- With PostgreSQL versions 11 and earlier, this is not possible
-- in a single migration. This can be worked around by creating
-- multiple migrations, each migration adding only one value to
-- the enum.
ALTER TYPE "NotificationType" ADD VALUE 'poll_closed';
ALTER TYPE "NotificationType" ADD VALUE 'poll_results_available';
ALTER TYPE "NotificationType" ADD VALUE 'poll_challenge';
-- AlterEnum
ALTER TYPE "UserRole" ADD VALUE 'POLLS_ADMIN';
-- AlterTable
ALTER TABLE "site_settings" ADD COLUMN "enable_polls" BOOLEAN NOT NULL DEFAULT false;
-- CreateTable
CREATE TABLE "straw_polls" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"title" VARCHAR(200) NOT NULL,
"description" TEXT,
"type" "StrawPollType" NOT NULL,
"status" "StrawPollStatus" NOT NULL DEFAULT 'DRAFT',
"identity_mode" "StrawPollIdentityMode" NOT NULL DEFAULT 'ANONYMOUS',
"result_visibility" "StrawPollResultVisibility" NOT NULL DEFAULT 'LIVE',
"allow_comments" BOOLEAN NOT NULL DEFAULT true,
"closes_at" TIMESTAMP(3),
"close_threshold" INTEGER,
"auto_close_job_id" TEXT,
"is_private" BOOLEAN NOT NULL DEFAULT false,
"created_by_user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "straw_polls_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "straw_poll_options" (
"id" TEXT NOT NULL,
"poll_id" TEXT NOT NULL,
"label" VARCHAR(500) NOT NULL,
"sort_order" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "straw_poll_options_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "straw_poll_votes" (
"id" TEXT NOT NULL,
"poll_id" TEXT NOT NULL,
"option_id" TEXT NOT NULL,
"user_id" TEXT,
"voter_name" VARCHAR(100),
"voter_token" TEXT,
"voter_ip" TEXT,
"contact_id" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "straw_poll_votes_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "straw_poll_comments" (
"id" TEXT NOT NULL,
"poll_id" TEXT NOT NULL,
"user_id" TEXT,
"author_name" VARCHAR(100) NOT NULL,
"content" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "straw_poll_comments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "straw_poll_challenges" (
"id" TEXT NOT NULL,
"poll_id" TEXT NOT NULL,
"challenger_user_id" TEXT NOT NULL,
"challenged_user_id" TEXT NOT NULL,
"completed_at" TIMESTAMP(3),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "straw_poll_challenges_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "straw_polls_slug_key" ON "straw_polls"("slug");
-- CreateIndex
CREATE INDEX "straw_polls_created_by_user_id_idx" ON "straw_polls"("created_by_user_id");
-- CreateIndex
CREATE INDEX "straw_polls_status_idx" ON "straw_polls"("status");
-- CreateIndex
CREATE INDEX "straw_poll_options_poll_id_idx" ON "straw_poll_options"("poll_id");
-- CreateIndex
CREATE INDEX "straw_poll_votes_poll_id_idx" ON "straw_poll_votes"("poll_id");
-- CreateIndex
CREATE INDEX "straw_poll_votes_option_id_idx" ON "straw_poll_votes"("option_id");
-- CreateIndex
CREATE UNIQUE INDEX "straw_poll_votes_poll_id_user_id_key" ON "straw_poll_votes"("poll_id", "user_id");
-- CreateIndex
CREATE UNIQUE INDEX "straw_poll_votes_poll_id_voter_token_key" ON "straw_poll_votes"("poll_id", "voter_token");
-- CreateIndex
CREATE UNIQUE INDEX "straw_poll_votes_poll_id_voter_ip_key" ON "straw_poll_votes"("poll_id", "voter_ip");
-- CreateIndex
CREATE INDEX "straw_poll_comments_poll_id_idx" ON "straw_poll_comments"("poll_id");
-- CreateIndex
CREATE UNIQUE INDEX "straw_poll_challenges_poll_id_challenger_user_id_challenged_key" ON "straw_poll_challenges"("poll_id", "challenger_user_id", "challenged_user_id");
-- AddForeignKey
ALTER TABLE "straw_polls" ADD CONSTRAINT "straw_polls_created_by_user_id_fkey" FOREIGN KEY ("created_by_user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_options" ADD CONSTRAINT "straw_poll_options_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "straw_polls"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_votes" ADD CONSTRAINT "straw_poll_votes_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "straw_polls"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_votes" ADD CONSTRAINT "straw_poll_votes_option_id_fkey" FOREIGN KEY ("option_id") REFERENCES "straw_poll_options"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_votes" ADD CONSTRAINT "straw_poll_votes_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_votes" ADD CONSTRAINT "straw_poll_votes_contact_id_fkey" FOREIGN KEY ("contact_id") REFERENCES "contacts"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_comments" ADD CONSTRAINT "straw_poll_comments_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "straw_polls"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_comments" ADD CONSTRAINT "straw_poll_comments_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_challenges" ADD CONSTRAINT "straw_poll_challenges_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "straw_polls"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_challenges" ADD CONSTRAINT "straw_poll_challenges_challenger_user_id_fkey" FOREIGN KEY ("challenger_user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "straw_poll_challenges" ADD CONSTRAINT "straw_poll_challenges_challenged_user_id_fkey" FOREIGN KEY ("challenged_user_id") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;