mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-30 02:36:30 +02:00
♻️ Improvements to house-keeping webhook (#1063)
This commit is contained in:
parent
0d82c158ce
commit
90f0d90fab
9 changed files with 173 additions and 104 deletions
21
.github/workflows/house-keeping.yml
vendored
21
.github/workflows/house-keeping.yml
vendored
|
@ -1,21 +1,22 @@
|
||||||
name: House Keeping
|
name: House Keeping
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
|
||||||
query:
|
|
||||||
description: "Query params for house keeping endpoint"
|
|
||||||
required: false
|
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 6 * * *" # Every day at 6:00am UTC
|
- cron: "0 6 * * *" # Every day at 6:00am UTC
|
||||||
|
env:
|
||||||
|
API_SECRET: ${{ secrets.API_SECRET }}
|
||||||
jobs:
|
jobs:
|
||||||
clean:
|
clean:
|
||||||
name: "Clean"
|
name: "Clean"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Call house keeping
|
- name: Mark inactive polls as deleted
|
||||||
env:
|
|
||||||
API_SECRET: ${{ secrets.API_SECRET }}
|
|
||||||
run: |
|
run: |
|
||||||
curl -X "POST" --fail "https://app.rallly.co/api/house-keeping${{ github.event.inputs.query }}" \
|
curl -X "POST" --fail "https://app.rallly.co/api/house-keeping/delete-inactive-polls" \
|
||||||
-H "Authorization: Bearer ${API_SECRET}"
|
-H "Authorization: Bearer ${API_SECRET}" \
|
||||||
|
- name: Remove deleted polls
|
||||||
|
run: |
|
||||||
|
curl -X "POST" --fail "https://app.rallly.co/api/house-keeping/remove-deleted-polls" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer ${API_SECRET}" \
|
||||||
|
-d '{"take": 1000}'
|
||||||
|
|
153
apps/web/src/app/api/house-keeping/[task]/route.ts
Normal file
153
apps/web/src/app/api/house-keeping/[task]/route.ts
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
import { prisma } from "@rallly/database";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export async function POST(req: Request, ctx: { params: { task: string } }) {
|
||||||
|
const headersList = headers();
|
||||||
|
const authorization = headersList.get("authorization");
|
||||||
|
|
||||||
|
if (authorization !== `Bearer ${process.env.API_SECRET}`) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ success: false },
|
||||||
|
{
|
||||||
|
status: 401,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (ctx.params.task) {
|
||||||
|
case "delete-inactive-polls": {
|
||||||
|
return await deleteInactivePolls();
|
||||||
|
}
|
||||||
|
case "remove-deleted-polls": {
|
||||||
|
return await removeDeletedPolls(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks inactive polls as deleted. Polls are inactive if they have not been
|
||||||
|
* touched in the last 30 days and all dates are in the past.
|
||||||
|
*/
|
||||||
|
async function deleteInactivePolls() {
|
||||||
|
const markedDeleted = await prisma.$executeRaw`
|
||||||
|
UPDATE polls p
|
||||||
|
SET
|
||||||
|
deleted = true,
|
||||||
|
deleted_at = NOW()
|
||||||
|
WHERE touched_at < NOW() - INTERVAL '30 days'
|
||||||
|
AND deleted = false
|
||||||
|
AND id NOT IN (
|
||||||
|
SELECT poll_id
|
||||||
|
FROM options
|
||||||
|
WHERE poll_id = p.id
|
||||||
|
AND start_time > NOW()
|
||||||
|
)
|
||||||
|
AND user_id NOT IN (
|
||||||
|
SELECT id
|
||||||
|
FROM users
|
||||||
|
WHERE id IN (
|
||||||
|
SELECT user_id
|
||||||
|
FROM user_payment_data
|
||||||
|
WHERE end_date > NOW()
|
||||||
|
)
|
||||||
|
OR subscription_id IN (
|
||||||
|
SELECT subscription_id
|
||||||
|
FROM subscriptions
|
||||||
|
WHERE active = true
|
||||||
|
)
|
||||||
|
);
|
||||||
|
`;
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
summary: {
|
||||||
|
markedDeleted,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove polls and corresponding data that have been marked deleted for more than 7 days.
|
||||||
|
*/
|
||||||
|
async function removeDeletedPolls(req: Request) {
|
||||||
|
const options = (await req.json()) as { take?: number } | undefined;
|
||||||
|
// First get the ids of all the polls that have been marked as deleted for at least 7 days
|
||||||
|
const deletedPolls = await prisma.poll.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
deleted: true,
|
||||||
|
deletedAt: {
|
||||||
|
lt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
take: options?.take ?? 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const deletedPollIds = deletedPolls.map((poll) => poll.id);
|
||||||
|
|
||||||
|
const { count: deletedWatcherCount } = await prisma.watcher.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: deletedPollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { count: deletedVoteCount } = await prisma.vote.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: deletedPollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { count: deletedParticipantCount } =
|
||||||
|
await prisma.participant.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: deletedPollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { count: deletedOptionCount } = await prisma.option.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: deletedPollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { count: deletedCommentCount } = await prisma.comment.deleteMany({
|
||||||
|
where: {
|
||||||
|
pollId: {
|
||||||
|
in: deletedPollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { count: deletedPollCount } = await prisma.poll.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: deletedPollIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
summary: {
|
||||||
|
deleted: {
|
||||||
|
votes: deletedVoteCount,
|
||||||
|
options: deletedOptionCount,
|
||||||
|
participants: deletedParticipantCount,
|
||||||
|
comments: deletedCommentCount,
|
||||||
|
watchers: deletedWatcherCount,
|
||||||
|
polls: deletedPollCount,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,53 +0,0 @@
|
||||||
import { prisma } from "@rallly/database";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { authorization } = req.headers;
|
|
||||||
|
|
||||||
if (authorization !== `Bearer ${process.env.API_SECRET}`) {
|
|
||||||
res.status(401).json({ success: false });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const deletedPolls = await prisma.$executeRaw`
|
|
||||||
UPDATE polls p
|
|
||||||
SET
|
|
||||||
deleted = true,
|
|
||||||
deleted_at = NOW()
|
|
||||||
WHERE touched_at < NOW() - INTERVAL '30 days'
|
|
||||||
AND deleted = false
|
|
||||||
AND id NOT IN (
|
|
||||||
SELECT poll_id
|
|
||||||
FROM options
|
|
||||||
WHERE poll_id = p.id
|
|
||||||
AND start_time > NOW()
|
|
||||||
)
|
|
||||||
AND user_id NOT IN (
|
|
||||||
SELECT id
|
|
||||||
FROM users
|
|
||||||
WHERE id IN (
|
|
||||||
SELECT user_id
|
|
||||||
FROM user_payment_data
|
|
||||||
WHERE end_date > NOW()
|
|
||||||
)
|
|
||||||
OR subscription_id IN (
|
|
||||||
SELECT subscription_id
|
|
||||||
FROM subscriptions
|
|
||||||
WHERE active = true
|
|
||||||
)
|
|
||||||
);
|
|
||||||
`;
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
softDeleted: deletedPolls,
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -237,7 +237,10 @@ export const polls = router({
|
||||||
)
|
)
|
||||||
.mutation(async ({ input: { urlId } }) => {
|
.mutation(async ({ input: { urlId } }) => {
|
||||||
const pollId = await getPollIdFromAdminUrlId(urlId);
|
const pollId = await getPollIdFromAdminUrlId(urlId);
|
||||||
await prisma.poll.delete({ where: { id: pollId } });
|
await prisma.poll.update({
|
||||||
|
where: { id: pollId },
|
||||||
|
data: { deleted: true, deletedAt: new Date() },
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
touch: publicProcedure
|
touch: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { PrismaClient } from "@rallly/database";
|
import { PrismaClient } from "@rallly/database";
|
||||||
|
|
||||||
import { softDeleteMiddleware } from "./middleware/soft-delete-middleware";
|
|
||||||
|
|
||||||
export * from "@prisma/client";
|
export * from "@prisma/client";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -12,6 +10,4 @@ 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;
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { Prisma, PrismaClient } from "@rallly/database";
|
|
||||||
|
|
||||||
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, deletedAt: new Date() };
|
|
||||||
}
|
|
||||||
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, deletedAt: new Date() };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (params.action === "findFirst") {
|
|
||||||
// Add 'deleted' filter
|
|
||||||
// ID filter maintained
|
|
||||||
params.args.where["deleted"] = params.args.where["deleted"] || false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return next(params);
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "votes_poll_id_idx" ON "votes" USING HASH ("poll_id");
|
|
@ -226,6 +226,7 @@ model Vote {
|
||||||
|
|
||||||
@@index([participantId], type: Hash)
|
@@index([participantId], type: Hash)
|
||||||
@@index([optionId], type: Hash)
|
@@index([optionId], type: Hash)
|
||||||
|
@@index([pollId], type: Hash)
|
||||||
@@map("votes")
|
@@map("votes")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { cn } from "@rallly/ui";
|
||||||
|
|
||||||
const buttonVariants = cva(
|
const buttonVariants = cva(
|
||||||
cn(
|
cn(
|
||||||
"inline-flex border font-medium disabled:text-muted-foreground disabled:bg-muted disabled:pointer-events-none select-none items-center justify-center whitespace-nowrap rounded-md border",
|
"inline-flex shadow-[inset_0_0.5px_0_0.5px_rgb(255,255,255,0.4),_inset_0_-0.5px_0_0.5px_rgb(0,0,0,0.1)] border font-medium disabled:text-muted-foreground disabled:bg-muted disabled:pointer-events-none select-none items-center justify-center whitespace-nowrap rounded-md border",
|
||||||
"focus-visible:ring-offset-input-background focus-visible:border-primary-400 focus-visible:ring-2 focus-visible:ring-indigo-100",
|
"focus-visible:ring-offset-input-background focus-visible:border-primary-400 focus-visible:ring-2 focus-visible:ring-indigo-100",
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
|
@ -19,7 +19,7 @@ const buttonVariants = cva(
|
||||||
destructive:
|
destructive:
|
||||||
"bg-destructive text-destructive-foreground focus:ring-offset-1 active:bg-destructive border-destructive hover:bg-destructive/90",
|
"bg-destructive text-destructive-foreground focus:ring-offset-1 active:bg-destructive border-destructive hover:bg-destructive/90",
|
||||||
default:
|
default:
|
||||||
"rounded-md px-3.5 py-2.5 data-[state=open]:shadow-none data-[state=open]:bg-gray-100 active:bg-gray-200 focus:border-gray-300 hover:bg-gray-100 bg-gray-50",
|
"rounded-md px-3.5 shadow-inner py-2.5 data-[state=open]:shadow-none data-[state=open]:bg-gray-100 active:bg-gray-200 focus:border-gray-300 hover:bg-gray-100 bg-gray-50",
|
||||||
secondary:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
ghost: "border-transparent hover:bg-gray-200 active:bg-gray-300",
|
ghost: "border-transparent hover:bg-gray-200 active:bg-gray-300",
|
||||||
|
|
Loading…
Add table
Reference in a new issue