♻️ Update house keeping (#1591)

This commit is contained in:
Luke Vella 2025-02-28 12:27:22 +00:00 committed by GitHub
parent b173cc78a0
commit c25ee09855
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 394 additions and 109 deletions

View file

@ -6,4 +6,5 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5450/rallly
SUPPORT_EMAIL=support@rallly.co SUPPORT_EMAIL=support@rallly.co
SMTP_HOST=0.0.0.0 SMTP_HOST=0.0.0.0
SMTP_PORT=1025 SMTP_PORT=1025
QUICK_CREATE_ENABLED=true QUICK_CREATE_ENABLED=true
API_SECRET=1234567890abcdef1234567890abcdef1234

View file

@ -1,6 +1,5 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

View file

@ -1,107 +0,0 @@
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: deletedPollCount } = await prisma.poll.deleteMany({
where: {
id: {
in: deletedPollIds,
},
},
});
return NextResponse.json({
success: true,
summary: {
deleted: {
polls: deletedPollCount,
},
},
});
}

View file

@ -0,0 +1,52 @@
import { prisma } from "@rallly/database";
import { NextResponse } from "next/server";
import { checkApiAuthorization } from "@/utils/api-auth";
/**
* 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.
* Only marks polls as deleted if they belong to users without an active subscription
* or if they don't have a user associated with them.
*/
export async function POST() {
const unauthorized = checkApiAuthorization();
if (unauthorized) return unauthorized;
// Mark inactive polls as deleted in a single query
const { count: markedDeleted } = await prisma.poll.updateMany({
where: {
touchedAt: {
lt: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
},
deleted: false,
options: {
none: {
startTime: {
gt: new Date(),
},
},
},
// Include polls without a user or with users that don't have an active subscription
OR: [
{ userId: null },
{
user: {
OR: [{ subscription: null }, { subscription: { active: false } }],
},
},
],
},
data: {
deleted: true,
deletedAt: new Date(),
},
});
return NextResponse.json({
success: true,
summary: {
markedDeleted,
},
});
}

View file

@ -0,0 +1,47 @@
import { prisma } from "@rallly/database";
import { NextResponse } from "next/server";
import { checkApiAuthorization } from "@/utils/api-auth";
/**
* Remove polls and corresponding data that have been marked deleted for more than 7 days.
*/
export async function POST(req: Request) {
const unauthorized = checkApiAuthorization();
if (unauthorized) return unauthorized;
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: deletedPollCount } = await prisma.poll.deleteMany({
where: {
id: {
in: deletedPollIds,
},
},
});
return NextResponse.json({
success: true,
summary: {
deleted: {
polls: deletedPollCount,
},
},
});
}

View file

@ -0,0 +1,20 @@
import { headers } from "next/headers";
import { NextResponse } from "next/server";
/**
* Checks if the request is authorized using the API secret
* @returns A NextResponse with 401 status if unauthorized, or null if authorized
*/
export function checkApiAuthorization() {
const headersList = headers();
const authorization = headersList.get("authorization");
if (authorization !== `Bearer ${process.env.API_SECRET}`) {
return NextResponse.json(
{ success: false, message: "Unauthorized" },
{ status: 401 },
);
}
return null;
}

View file

@ -0,0 +1,273 @@
import { expect, test } from "@playwright/test";
import { prisma } from "@rallly/database";
import dayjs from "dayjs";
/**
* This test suite tests the house-keeping API endpoints:
* 1. delete-inactive-polls: Marks inactive polls as deleted
* 2. remove-deleted-polls: Permanently removes polls that have been marked as deleted for more than 7 days
*/
test.describe("House-keeping API", () => {
// Store created poll IDs for cleanup
const createdPollIds: string[] = [];
const createdUserIds: string[] = [];
// API Secret for authentication
const API_SECRET = process.env.API_SECRET;
test.beforeAll(async () => {
// Clean up any existing test data
await cleanup();
});
test.afterAll(async () => {
// Clean up test data
await cleanup();
});
async function cleanup() {
// Delete test users - related polls will be deleted automatically due to cascade
if (createdUserIds.length > 0) {
await prisma.user.deleteMany({
where: {
id: {
in: createdUserIds,
},
},
});
createdUserIds.length = 0;
}
// Delete polls that don't have a user (not covered by cascade delete)
if (createdPollIds.length > 0) {
await prisma.poll.deleteMany({
where: {
id: {
in: createdPollIds,
},
userId: null,
},
});
createdPollIds.length = 0;
}
}
test("should mark inactive polls as deleted for users without active subscriptions", async ({
request,
baseURL,
}) => {
// Create test users
const regularUser = await prisma.user.create({
data: {
name: "Regular User",
email: "regular-user@example.com",
},
});
createdUserIds.push(regularUser.id);
const proUser = await prisma.user.create({
data: {
name: "Pro User",
email: "pro-user@example.com",
subscription: {
create: {
id: "sub_test_pro",
priceId: "price_test",
amount: 1000,
status: "active",
active: true,
currency: "USD",
interval: "month",
periodStart: new Date(),
periodEnd: dayjs().add(30, "day").toDate(),
},
},
},
});
createdUserIds.push(proUser.id);
// Create test polls
// 1. Old poll from regular user (should be marked as deleted)
const oldPollRegularUser = await prisma.poll.create({
data: {
id: "old-poll-regular-user",
title: "Old Poll Regular User",
participantUrlId: "old-poll-regular-user-participant",
adminUrlId: "old-poll-regular-user-admin",
userId: regularUser.id,
touchedAt: dayjs().subtract(35, "day").toDate(), // 35 days old
},
});
createdPollIds.push(oldPollRegularUser.id);
// 2. Old poll from pro user (should NOT be marked as deleted)
const oldPollProUser = await prisma.poll.create({
data: {
id: "old-poll-pro-user",
title: "Old Poll Pro User",
participantUrlId: "old-poll-pro-user-participant",
adminUrlId: "old-poll-pro-user-admin",
userId: proUser.id,
touchedAt: dayjs().subtract(35, "day").toDate(), // 35 days old
},
});
createdPollIds.push(oldPollProUser.id);
// 3. Recent poll from regular user (should NOT be marked as deleted)
const recentPollRegularUser = await prisma.poll.create({
data: {
id: "recent-poll-regular-user",
title: "Recent Poll Regular User",
participantUrlId: "recent-poll-regular-user-participant",
adminUrlId: "recent-poll-regular-user-admin",
userId: regularUser.id,
touchedAt: dayjs().subtract(15, "day").toDate(), // 15 days old
},
});
createdPollIds.push(recentPollRegularUser.id);
// 4. Old poll with future options from regular user (should NOT be marked as deleted)
const oldPollWithFutureOptions = await prisma.poll.create({
data: {
id: "old-poll-with-future-options",
title: "Old Poll With Future Options",
participantUrlId: "old-poll-with-future-options-participant",
adminUrlId: "old-poll-with-future-options-admin",
userId: regularUser.id,
touchedAt: dayjs().subtract(35, "day").toDate(), // 35 days old
options: {
create: {
startTime: dayjs().add(10, "day").toDate(), // Future date
duration: 60,
},
},
},
});
createdPollIds.push(oldPollWithFutureOptions.id);
// 5. Old poll without a user (should be marked as deleted)
const oldPollNoUser = await prisma.poll.create({
data: {
id: "old-poll-no-user",
title: "Old Poll No User",
participantUrlId: "old-poll-no-user-participant",
adminUrlId: "old-poll-no-user-admin",
touchedAt: dayjs().subtract(35, "day").toDate(), // 35 days old
},
});
createdPollIds.push(oldPollNoUser.id);
// Call the delete-inactive-polls endpoint
const response = await request.post(
`${baseURL}/api/house-keeping/delete-inactive-polls`,
{
headers: {
Authorization: `Bearer ${API_SECRET}`,
},
},
);
expect(response.ok()).toBeTruthy();
const responseData = await response.json();
expect(responseData.success).toBeTruthy();
// We expect 2 polls to be marked as deleted:
// - Old poll from regular user
// - Old poll without a user
expect(responseData.summary.markedDeleted).toBe(2);
// Verify the state of each poll
const updatedOldPollRegularUser = await prisma.poll.findUnique({
where: { id: oldPollRegularUser.id },
});
expect(updatedOldPollRegularUser?.deleted).toBe(true);
expect(updatedOldPollRegularUser?.deletedAt).not.toBeNull();
const updatedOldPollProUser = await prisma.poll.findUnique({
where: { id: oldPollProUser.id },
});
expect(updatedOldPollProUser?.deleted).toBe(false);
expect(updatedOldPollProUser?.deletedAt).toBeNull();
const updatedRecentPollRegularUser = await prisma.poll.findUnique({
where: { id: recentPollRegularUser.id },
});
expect(updatedRecentPollRegularUser?.deleted).toBe(false);
expect(updatedRecentPollRegularUser?.deletedAt).toBeNull();
const updatedOldPollWithFutureOptions = await prisma.poll.findUnique({
where: { id: oldPollWithFutureOptions.id },
});
expect(updatedOldPollWithFutureOptions?.deleted).toBe(false);
expect(updatedOldPollWithFutureOptions?.deletedAt).toBeNull();
const updatedOldPollNoUser = await prisma.poll.findUnique({
where: { id: oldPollNoUser.id },
});
expect(updatedOldPollNoUser?.deleted).toBe(true);
expect(updatedOldPollNoUser?.deletedAt).not.toBeNull();
});
test("should permanently remove polls that have been marked as deleted for more than 7 days", async ({
request,
baseURL,
}) => {
// Create a poll that was marked as deleted more than 7 days ago
const oldDeletedPoll = await prisma.poll.create({
data: {
id: "old-deleted-poll",
title: "Old Deleted Poll",
participantUrlId: "old-deleted-poll-participant",
adminUrlId: "old-deleted-poll-admin",
deleted: true,
deletedAt: dayjs().subtract(8, "day").toDate(), // Deleted 8 days ago
},
});
createdPollIds.push(oldDeletedPoll.id);
// Create a poll that was marked as deleted less than 7 days ago
const recentDeletedPoll = await prisma.poll.create({
data: {
id: "recent-deleted-poll",
title: "Recent Deleted Poll",
participantUrlId: "recent-deleted-poll-participant",
adminUrlId: "recent-deleted-poll-admin",
deleted: true,
deletedAt: dayjs().subtract(3, "day").toDate(), // Deleted 3 days ago
},
});
createdPollIds.push(recentDeletedPoll.id);
// Call the remove-deleted-polls endpoint
const response = await request.post(
`${baseURL}/api/house-keeping/remove-deleted-polls`,
{
headers: {
Authorization: `Bearer ${API_SECRET}`,
},
data: {}, // Empty JSON body
},
);
expect(response.ok()).toBeTruthy();
const responseData = await response.json();
expect(responseData.success).toBeTruthy();
// We expect 1 poll to be permanently deleted
expect(responseData.summary.deleted.polls).toBe(1);
// Verify that the old deleted poll is gone
const checkOldDeletedPoll = await prisma.poll.findUnique({
where: { id: oldDeletedPoll.id },
});
expect(checkOldDeletedPoll).toBeNull();
// Verify that the recent deleted poll still exists
const checkRecentDeletedPoll = await prisma.poll.findUnique({
where: { id: recentDeletedPoll.id },
});
expect(checkRecentDeletedPoll).not.toBeNull();
expect(checkRecentDeletedPoll?.deleted).toBe(true);
});
});