Skip to content

Commit

Permalink
make polls anonymous
Browse files Browse the repository at this point in the history
Introduce a `PollBlindVote` DB table that tracks when a user votes in a poll,
but does not track which choice they made.

Alter the `PollVote` DB table to remove the `userId` column, meaning `PollVote`
now tracks poll votes anonymously - it captures votes per poll option,
but does not track which user submitted the vote.

Update the `poll_vote` DB function to work with both tables now.

Update the `item.poll` resolver to calculate `meVoted` based on the `PollBlindVote`
table instead of `PollVote`.
  • Loading branch information
SatsAllDay committed May 27, 2024
1 parent 9c5bec0 commit b04a6ce
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 8 deletions.
12 changes: 9 additions & 3 deletions api/resolvers/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -1034,18 +1034,24 @@ export default {
}

const options = await models.$queryRaw`
SELECT "PollOption".id, option, count("PollVote"."userId")::INTEGER as count,
coalesce(bool_or("PollVote"."userId" = ${me?.id}), 'f') as "meVoted"
SELECT "PollOption".id, option, count("PollVote".id)::INTEGER as count
FROM "PollOption"
LEFT JOIN "PollVote" on "PollVote"."pollOptionId" = "PollOption".id
WHERE "PollOption"."itemId" = ${item.id}
GROUP BY "PollOption".id
ORDER BY "PollOption".id ASC
`

const meVoted = await models.pollBlindVote.findFirst({
where: {
userId: me?.id,
itemId: item.id
}
})

const poll = {}
poll.options = options
poll.meVoted = options.some(o => o.meVoted)
poll.meVoted = !!meVoted
poll.count = options.reduce((t, o) => t + o.count, 0)

return poll
Expand Down
92 changes: 92 additions & 0 deletions prisma/migrations/20240527175411_anon_poll_vote/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
-- CreateTable
CREATE TABLE "PollBlindVote" (
"id" SERIAL NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"itemId" INTEGER NOT NULL,
"userId" INTEGER NOT NULL,

CONSTRAINT "PollBlindVote_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "PollBlindVote" ON "PollBlindVote"("userId");

-- CreateIndex
CREATE UNIQUE INDEX "PollBlindVote.itemId_userId_unique" ON "PollBlindVote"("itemId", "userId");

-- AddForeignKey
ALTER TABLE "PollBlindVote" ADD CONSTRAINT "PollBlindVote_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "PollBlindVote" ADD CONSTRAINT "PollBlindVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- migrate existing poll votes
INSERT INTO "PollBlindVote" ("itemId", "userId")
SELECT "itemId", "userId" FROM "PollVote";

/*
Warnings:
- You are about to drop the column `userId` on the `PollVote` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "PollVote" DROP CONSTRAINT "PollVote_userId_fkey";

-- DropIndex
DROP INDEX "PollVote.itemId_userId_unique";

-- DropIndex
DROP INDEX "PollVote.userId_index";

-- AlterTable
ALTER TABLE "PollVote" DROP COLUMN "userId";

-- update `poll_vote` function to update both "PollVote" and "PollBlindVote" tables
-- create poll vote
-- if user hasn't already voted
-- charges user item.pollCost
-- adds POLL to ItemAct
-- adds PollVote
-- adds PollBlindVote
CREATE OR REPLACE FUNCTION poll_vote(option_id INTEGER, user_id INTEGER)
RETURNS "Item"
LANGUAGE plpgsql
AS $$
DECLARE
item "Item";
option "PollOption";
BEGIN
PERFORM ASSERT_SERIALIZED();

SELECT * INTO option FROM "PollOption" where id = option_id;
IF option IS NULL THEN
RAISE EXCEPTION 'INVALID_POLL_OPTION';
END IF;

SELECT * INTO item FROM "Item" where id = option."itemId";
IF item IS NULL THEN
RAISE EXCEPTION 'POLL_DOES_NOT_EXIST';
END IF;

IF item."userId" = user_id THEN
RAISE EXCEPTION 'POLL_OWNER_CANT_VOTE';
END IF;

-- no longer check `PollVote` to see if a user has voted. Instead, check `PollBlindVote`
IF EXISTS (SELECT 1 FROM "PollBlindVote" WHERE "itemId" = item.id AND "userId" = user_id) THEN
RAISE EXCEPTION 'POLL_VOTE_ALREADY_EXISTS';
END IF;

PERFORM item_act(item.id, user_id, 'POLL', item."pollCost");

INSERT INTO "PollVote" (created_at, updated_at, "itemId", "pollOptionId")
VALUES (now_utc(), now_utc(), item.id, option_id);

INSERT INTO "PollBlindVote" (created_at, updated_at, "itemId", "userId")
VALUES (now_utc(), now_utc(), item.id, user_id);

RETURN item;
END;
$$;
20 changes: 15 additions & 5 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,6 @@ model User {
actions ItemAct[]
mentions Mention[]
messages Message[]
PollVote PollVote[]
PushSubscriptions PushSubscription[]
ReferralAct ReferralAct[]
Streak Streak[]
Expand Down Expand Up @@ -125,6 +124,7 @@ model User {
Replies Reply[]
walletLogs WalletLog[]
Reminder Reminder[]
PollBlindVote PollBlindVote[]
@@index([photoId])
@@index([createdAt], map: "users.created_at_index")
Expand Down Expand Up @@ -423,6 +423,7 @@ model Item {
Ancestors Reply[] @relation("AncestorReplyItem")
Replies Reply[]
Reminder Reminder[]
PollBlindVote PollBlindVote[]
@@index([uploadId])
@@index([lastZapAt])
Expand Down Expand Up @@ -500,16 +501,25 @@ model PollVote {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId Int
itemId Int
pollOptionId Int
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
pollOption PollOption @relation(fields: [pollOptionId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([itemId, userId], map: "PollVote.itemId_userId_unique")
@@index([pollOptionId], map: "PollVote.pollOptionId_index")
@@index([userId], map: "PollVote.userId_index")
}

model PollBlindVote {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
itemId Int
userId Int
item Item @relation(fields: [itemId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([itemId, userId], map: "PollBlindVote.itemId_userId_unique")
@@index([userId], map: "PollBlindVote")
}

enum BillingType {
Expand Down

0 comments on commit b04a6ce

Please sign in to comment.