mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-30 10:46:35 +02:00
Soft delete and house keeping endpoint (#184)
This commit is contained in:
parent
97e189132f
commit
3299ba2030
14 changed files with 326 additions and 14 deletions
|
@ -1,5 +1,7 @@
|
||||||
import { PrismaClient } from "@prisma/client";
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
import { softDeleteMiddleware } from "./middlewares/softDeleteMiddleware";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// allow global `var` declarations
|
// allow global `var` declarations
|
||||||
// eslint-disable-next-line no-var
|
// eslint-disable-next-line no-var
|
||||||
|
@ -8,4 +10,6 @@ declare global {
|
||||||
|
|
||||||
export const prisma = global.prisma || new PrismaClient();
|
export const prisma = global.prisma || new PrismaClient();
|
||||||
|
|
||||||
|
softDeleteMiddleware(prisma, "Poll");
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== "production") global.prisma = prisma;
|
if (process.env.NODE_ENV !== "production") global.prisma = prisma;
|
||||||
|
|
48
prisma/middlewares/softDeleteMiddleware.ts
Normal file
48
prisma/middlewares/softDeleteMiddleware.ts
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import { Prisma, PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
|
export const softDeleteMiddleware = (
|
||||||
|
prisma: PrismaClient,
|
||||||
|
model: Prisma.ModelName,
|
||||||
|
) => {
|
||||||
|
prisma.$use(async (params, next) => {
|
||||||
|
// We use middleware to handle soft deletes
|
||||||
|
// See: https://www.prisma.io/docs/concepts/components/prisma-client/middleware/soft-delete-middleware
|
||||||
|
if (params.model === model) {
|
||||||
|
if (params.action === "delete") {
|
||||||
|
// Delete queries
|
||||||
|
// Change action to an update
|
||||||
|
params.action = "update";
|
||||||
|
params.args["data"] = { deleted: true };
|
||||||
|
}
|
||||||
|
if (params.action == "deleteMany") {
|
||||||
|
// Delete many queries
|
||||||
|
params.action = "updateMany";
|
||||||
|
if (params.args.data != undefined) {
|
||||||
|
params.args.data["deleted"] = true;
|
||||||
|
} else {
|
||||||
|
params.args["data"] = { deleted: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (params.action === "findUnique" || params.action === "findFirst") {
|
||||||
|
// Change to findFirst - you cannot filter
|
||||||
|
// by anything except ID / unique with findUnique
|
||||||
|
params.action = "findFirst";
|
||||||
|
// Add 'deleted' filter
|
||||||
|
// ID filter maintained
|
||||||
|
params.args.where["deleted"] = false;
|
||||||
|
}
|
||||||
|
if (params.action === "findMany") {
|
||||||
|
// Find many queries
|
||||||
|
if (params.args.where) {
|
||||||
|
if (params.args.where.deleted == undefined) {
|
||||||
|
// Exclude deleted records if they have not been explicitly requested
|
||||||
|
params.args.where["deleted"] = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params.args["where"] = { deleted: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next(params);
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,44 @@
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "comments" DROP CONSTRAINT "comments_poll_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "links" DROP CONSTRAINT "links_poll_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "options" DROP CONSTRAINT "options_poll_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "participants" DROP CONSTRAINT "participants_poll_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "votes" DROP CONSTRAINT "votes_option_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "votes" DROP CONSTRAINT "votes_participant_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "votes" DROP CONSTRAINT "votes_poll_id_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "polls" ADD COLUMN "deleted" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "links" ADD CONSTRAINT "links_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "polls"("url_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "participants" ADD CONSTRAINT "participants_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "polls"("url_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "options" ADD CONSTRAINT "options_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "polls"("url_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "votes" ADD CONSTRAINT "votes_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "polls"("url_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "votes" ADD CONSTRAINT "votes_participant_id_fkey" FOREIGN KEY ("participant_id") REFERENCES "participants"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "votes" ADD CONSTRAINT "votes_option_id_fkey" FOREIGN KEY ("option_id") REFERENCES "options"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "comments" ADD CONSTRAINT "comments_poll_id_fkey" FOREIGN KEY ("poll_id") REFERENCES "polls"("url_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "polls" ADD COLUMN "touched_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
|
|
@ -49,6 +49,8 @@ model Poll {
|
||||||
legacy Boolean @default(false)
|
legacy Boolean @default(false)
|
||||||
closed Boolean @default(false)
|
closed Boolean @default(false)
|
||||||
notifications Boolean @default(false)
|
notifications Boolean @default(false)
|
||||||
|
deleted Boolean @default(false)
|
||||||
|
touchedAt DateTime @default(now()) @map("touched_at")
|
||||||
|
|
||||||
@@map("polls")
|
@@map("polls")
|
||||||
}
|
}
|
||||||
|
@ -64,7 +66,7 @@ model Link {
|
||||||
urlId String @id @unique @map("url_id")
|
urlId String @id @unique @map("url_id")
|
||||||
role Role
|
role Role
|
||||||
pollId String @map("poll_id")
|
pollId String @map("poll_id")
|
||||||
poll Poll @relation(fields: [pollId], references: [urlId], onDelete: Cascade)
|
poll Poll @relation(fields: [pollId], references: [urlId])
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
@@unique([pollId, role])
|
@@unique([pollId, role])
|
||||||
|
@ -77,7 +79,7 @@ model Participant {
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation(fields: [userId], references: [id])
|
||||||
userId String? @map("user_id")
|
userId String? @map("user_id")
|
||||||
guestId String? @map("guest_id")
|
guestId String? @map("guest_id")
|
||||||
poll Poll @relation(fields: [pollId], references: [urlId], onDelete: Cascade)
|
poll Poll @relation(fields: [pollId], references: [urlId])
|
||||||
pollId String @map("poll_id")
|
pollId String @map("poll_id")
|
||||||
votes Vote[]
|
votes Vote[]
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
@ -91,7 +93,7 @@ model Option {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
value String
|
value String
|
||||||
pollId String @map("poll_id")
|
pollId String @map("poll_id")
|
||||||
poll Poll @relation(fields: [pollId], references: [urlId], onDelete: Cascade)
|
poll Poll @relation(fields: [pollId], references: [urlId])
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
updatedAt DateTime? @updatedAt @map("updated_at")
|
updatedAt DateTime? @updatedAt @map("updated_at")
|
||||||
votes Vote[]
|
votes Vote[]
|
||||||
|
@ -109,11 +111,11 @@ enum VoteType {
|
||||||
|
|
||||||
model Vote {
|
model Vote {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
participant Participant @relation(fields: [participantId], references: [id], onDelete: Cascade)
|
participant Participant @relation(fields: [participantId], references: [id])
|
||||||
participantId String @map("participant_id")
|
participantId String @map("participant_id")
|
||||||
option Option @relation(fields: [optionId], references: [id], onDelete: Cascade)
|
option Option @relation(fields: [optionId], references: [id])
|
||||||
optionId String @map("option_id")
|
optionId String @map("option_id")
|
||||||
poll Poll @relation(fields: [pollId], references: [urlId], onDelete: Cascade)
|
poll Poll @relation(fields: [pollId], references: [urlId])
|
||||||
pollId String @map("poll_id")
|
pollId String @map("poll_id")
|
||||||
type VoteType @default(yes)
|
type VoteType @default(yes)
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
@ -125,7 +127,7 @@ model Vote {
|
||||||
model Comment {
|
model Comment {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
content String
|
content String
|
||||||
poll Poll @relation(fields: [pollId], references: [urlId], onDelete: Cascade)
|
poll Poll @relation(fields: [pollId], references: [urlId])
|
||||||
pollId String @map("poll_id")
|
pollId String @map("poll_id")
|
||||||
authorName String @map("author_name")
|
authorName String @map("author_name")
|
||||||
user User? @relation(fields: [userId], references: [id])
|
user User? @relation(fields: [userId], references: [id])
|
||||||
|
|
|
@ -56,5 +56,6 @@
|
||||||
"ifNeedBe": "If need be",
|
"ifNeedBe": "If need be",
|
||||||
"areYouSure": "Are you sure?",
|
"areYouSure": "Are you sure?",
|
||||||
"deletePollDescription": "All data related to this poll will be deleted. This action <b>cannot be undone</b>. To confirm, please type <s>“{{confirmText}}”</s> in to the input below:",
|
"deletePollDescription": "All data related to this poll will be deleted. This action <b>cannot be undone</b>. To confirm, please type <s>“{{confirmText}}”</s> in to the input below:",
|
||||||
"deletePoll": "Delete poll"
|
"deletePoll": "Delete poll",
|
||||||
|
"demoPollNotice": "Demo polls are automatically deleted after 1 day"
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,19 @@ import React from "react";
|
||||||
|
|
||||||
const Badge: React.VoidFunctionComponent<{
|
const Badge: React.VoidFunctionComponent<{
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
color?: "gray" | "amber" | "green";
|
color?: "gray" | "amber" | "green" | "red" | "blue";
|
||||||
className?: string;
|
className?: string;
|
||||||
}> = ({ children, color = "gray", className }) => {
|
}> = ({ children, color = "gray", className }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"inline-flex h-5 items-center rounded-md px-1 text-xs",
|
"inline-flex h-5 cursor-default items-center rounded-md px-1 text-xs",
|
||||||
{
|
{
|
||||||
"bg-slate-200 text-slate-500": color === "gray",
|
"bg-slate-200 text-slate-500": color === "gray",
|
||||||
"bg-amber-100 text-amber-500": color === "amber",
|
"bg-amber-100 text-amber-500": color === "amber",
|
||||||
"bg-green-100/50 text-green-500": color === "green",
|
"bg-green-100/50 text-green-500": color === "green",
|
||||||
|
"bg-rose-100/50 text-rose-500": color === "red",
|
||||||
|
"bg-cyan-100/50 text-cyan-500": color === "blue",
|
||||||
},
|
},
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -19,6 +19,7 @@ import { useUpdatePollMutation } from "./poll/mutations";
|
||||||
import NotificationsToggle from "./poll/notifications-toggle";
|
import NotificationsToggle from "./poll/notifications-toggle";
|
||||||
import PollSubheader from "./poll/poll-subheader";
|
import PollSubheader from "./poll/poll-subheader";
|
||||||
import TruncatedLinkify from "./poll/truncated-linkify";
|
import TruncatedLinkify from "./poll/truncated-linkify";
|
||||||
|
import { useTouchBeacon } from "./poll/use-touch-beacon";
|
||||||
import { UserAvatarProvider } from "./poll/user-avatar";
|
import { UserAvatarProvider } from "./poll/user-avatar";
|
||||||
import VoteIcon from "./poll/vote-icon";
|
import VoteIcon from "./poll/vote-icon";
|
||||||
import { usePoll } from "./poll-context";
|
import { usePoll } from "./poll-context";
|
||||||
|
@ -37,6 +38,8 @@ const PollPage: NextPage = () => {
|
||||||
const { participants } = useParticipants();
|
const { participants } = useParticipants();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
useTouchBeacon(poll.pollId);
|
||||||
|
|
||||||
useMount(() => {
|
useMount(() => {
|
||||||
const path = poll.role === "admin" ? "admin" : "p";
|
const path = poll.role === "admin" ? "admin" : "p";
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ const PollSubheader: React.VoidFunctionComponent = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<span className="inline-flex items-center space-x-1">
|
<span className="inline-flex items-center space-x-1">
|
||||||
{poll.role === "admin" ? (
|
{poll.role === "admin" && !poll.demo ? (
|
||||||
poll.verified ? (
|
poll.verified ? (
|
||||||
<Badge color="green">Verified</Badge>
|
<Badge color="green">Verified</Badge>
|
||||||
) : (
|
) : (
|
||||||
|
@ -86,6 +86,11 @@ const PollSubheader: React.VoidFunctionComponent = () => {
|
||||||
<Badge color="amber">Legacy</Badge>
|
<Badge color="amber">Legacy</Badge>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
|
{poll.demo ? (
|
||||||
|
<Tooltip content={<Trans t={t} i18nKey="demoPollNotice" />}>
|
||||||
|
<Badge color="blue">Demo</Badge>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="hidden md:inline"> • </span>
|
<span className="hidden md:inline"> • </span>
|
||||||
|
|
15
src/components/poll/use-touch-beacon.ts
Normal file
15
src/components/poll/use-touch-beacon.ts
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import { useMount } from "react-use";
|
||||||
|
|
||||||
|
import { trpc } from "../../utils/trpc";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Touching a poll updates a column with the current date. This information is used to
|
||||||
|
* find polls that haven't been accessed for some time so that they can be deleted by house keeping.
|
||||||
|
*/
|
||||||
|
export const useTouchBeacon = (pollId: string) => {
|
||||||
|
const touchMutation = trpc.useMutation(["polls.touch"]);
|
||||||
|
|
||||||
|
useMount(() => {
|
||||||
|
touchMutation.mutate({ pollId });
|
||||||
|
});
|
||||||
|
};
|
153
src/pages/api/house-keeping.ts
Normal file
153
src/pages/api/house-keeping.ts
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
import { addDays } from "date-fns";
|
||||||
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { prisma } from "~/prisma/db";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This endpoint will permanently delete polls that:
|
||||||
|
* * have been soft deleted OR
|
||||||
|
* * are demo polls that are older than 1 day
|
||||||
|
* All dependant records are also deleted.
|
||||||
|
*/
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
res.setHeader("Allow", "POST");
|
||||||
|
res.status(405).end("Method not allowed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Batch size is the max number of polls we will attempt to delete
|
||||||
|
// We can adjust this value through the batchSize query param
|
||||||
|
const parsedBatchSizeQueryParam =
|
||||||
|
typeof req.query["batchSize"] === "string"
|
||||||
|
? parseInt(req.query["batchSize"])
|
||||||
|
: NaN;
|
||||||
|
|
||||||
|
// If not specified we default to a max of 500 polls
|
||||||
|
const batchSize = !isNaN(parsedBatchSizeQueryParam)
|
||||||
|
? parsedBatchSizeQueryParam
|
||||||
|
: 500;
|
||||||
|
|
||||||
|
const { authorization } = req.headers;
|
||||||
|
|
||||||
|
if (authorization !== `Bearer ${process.env.API_SECRET}`) {
|
||||||
|
res.status(401).json({ success: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pollIds = (
|
||||||
|
await prisma.poll.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{
|
||||||
|
deleted: true,
|
||||||
|
},
|
||||||
|
// demo polls that are 1 day old
|
||||||
|
{
|
||||||
|
demo: true,
|
||||||
|
createdAt: {
|
||||||
|
lte: addDays(new Date(), -1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// polls that have not been accessed for over 30 days
|
||||||
|
{
|
||||||
|
touchedAt: {
|
||||||
|
lte: addDays(new Date(), -30),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
urlId: true,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "asc", // oldest first
|
||||||
|
},
|
||||||
|
take: batchSize,
|
||||||
|
})
|
||||||
|
).map(({ urlId }) => urlId);
|
||||||
|
|
||||||
|
if (pollIds.length !== 0) {
|
||||||
|
// Delete links
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE links DISABLE TRIGGER ALL"),
|
||||||
|
prisma.link.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: pollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE links ENABLE TRIGGER ALL"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete comments
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE comments DISABLE TRIGGER ALL"),
|
||||||
|
prisma.comment.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: pollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE comments ENABLE TRIGGER ALL"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE votes DISABLE TRIGGER ALL"),
|
||||||
|
prisma.vote.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: pollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE votes ENABLE TRIGGER ALL"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete participants
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE participants DISABLE TRIGGER ALL"),
|
||||||
|
prisma.participant.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: pollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE participants ENABLE TRIGGER ALL"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete options
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE options DISABLE TRIGGER ALL"),
|
||||||
|
prisma.option.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: pollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE options ENABLE TRIGGER ALL"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Delete polls
|
||||||
|
await prisma.$transaction([
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE polls DISABLE TRIGGER ALL"),
|
||||||
|
// Using raw query to bypass soft delete middleware
|
||||||
|
prisma.$executeRaw`DELETE FROM polls WHERE url_id IN (${Prisma.join(
|
||||||
|
pollIds,
|
||||||
|
)})`,
|
||||||
|
prisma.$executeRawUnsafe("ALTER TABLE polls ENABLE TRIGGER ALL"),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
deleted: pollIds,
|
||||||
|
});
|
||||||
|
}
|
|
@ -217,4 +217,19 @@ export const polls = createRouter()
|
||||||
|
|
||||||
await prisma.poll.delete({ where: { urlId: link.pollId } });
|
await prisma.poll.delete({ where: { urlId: link.pollId } });
|
||||||
},
|
},
|
||||||
|
})
|
||||||
|
.mutation("touch", {
|
||||||
|
input: z.object({
|
||||||
|
pollId: z.string(),
|
||||||
|
}),
|
||||||
|
resolve: async ({ input: { pollId } }) => {
|
||||||
|
await prisma.poll.update({
|
||||||
|
where: {
|
||||||
|
urlId: pollId,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
touchedAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,10 +10,19 @@ export const session = createRouter()
|
||||||
where: { id: ctx.session.user.id },
|
where: { id: ctx.session.user.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
return user
|
if (!user) {
|
||||||
? { id: user.id, name: user.name, email: user.email, isGuest: false }
|
ctx.session.destroy();
|
||||||
: null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
isGuest: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return ctx.session.user;
|
return ctx.session.user;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
test("should be able to vote and comment on a poll", async ({ page }) => {
|
test("should be able to vote and comment on a poll", async ({ page }) => {
|
||||||
|
const touchRequest = page.waitForRequest(
|
||||||
|
(request) =>
|
||||||
|
request.method() === "POST" &&
|
||||||
|
request.url().includes("/api/trpc/polls.touch"),
|
||||||
|
);
|
||||||
|
|
||||||
await page.goto("/demo");
|
await page.goto("/demo");
|
||||||
|
|
||||||
await expect(page.locator('text="Lunch Meeting Demo"')).toBeVisible();
|
await expect(page.locator('text="Lunch Meeting Demo"')).toBeVisible();
|
||||||
|
@ -26,4 +32,7 @@ test("should be able to vote and comment on a poll", async ({ page }) => {
|
||||||
const comment = page.locator("data-testid=comment");
|
const comment = page.locator("data-testid=comment");
|
||||||
await expect(comment.locator("text='This is a comment!'")).toBeVisible();
|
await expect(comment.locator("text='This is a comment!'")).toBeVisible();
|
||||||
await expect(comment.locator("text=You")).toBeVisible();
|
await expect(comment.locator("text=You")).toBeVisible();
|
||||||
|
|
||||||
|
// make sure call to touch RPC is made
|
||||||
|
await touchRequest;
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue