123 lines
3.9 KiB
JavaScript
123 lines
3.9 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { PrismaClient } from "@prisma/client";
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
const ONE_MINUTE_MS = 60_000;
|
|
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
|
|
const now = new Date();
|
|
const consumedRetentionHours = getPositiveNumber(process.env.AUTH_TOKEN_CONSUMED_RETENTION_HOURS, 24);
|
|
const staleLockMinutes = getPositiveNumber(process.env.CAMPAIGN_RETRY_STALE_LOCK_MINUTES, 120);
|
|
const webhookRetentionDays = getPositiveNumber(process.env.WEBHOOK_EVENT_RETENTION_DAYS, 30);
|
|
const auditRetentionDays = getPositiveNumber(process.env.AUDIT_LOG_RETENTION_DAYS, 365);
|
|
|
|
function getPositiveNumber(raw, fallback) {
|
|
const parsed = Number(raw);
|
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
return fallback;
|
|
}
|
|
return parsed;
|
|
}
|
|
|
|
function printLine(message) {
|
|
console.log(`[ops-maintenance] ${message}`);
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
const consumedCutoff = new Date(now.getTime() - consumedRetentionHours * 60 * 60 * 1000);
|
|
const staleLockCutoff = new Date(now.getTime() - staleLockMinutes * ONE_MINUTE_MS);
|
|
const webhookCutoff = new Date(now.getTime() - webhookRetentionDays * ONE_DAY_MS);
|
|
const auditCutoff = new Date(now.getTime() - auditRetentionDays * ONE_DAY_MS);
|
|
|
|
const expiredTokens = await prisma.authToken.deleteMany({
|
|
where: {
|
|
expiresAt: { lte: now },
|
|
consumedAt: null
|
|
}
|
|
});
|
|
|
|
const consumedTokens = await prisma.authToken.deleteMany({
|
|
where: {
|
|
consumedAt: { lte: consumedCutoff }
|
|
}
|
|
});
|
|
|
|
const recoveredLock = await prisma.backgroundJobState.updateMany({
|
|
where: {
|
|
lockedUntil: { lte: staleLockCutoff },
|
|
lastRunStatus: "running"
|
|
},
|
|
data: {
|
|
lockedUntil: null,
|
|
lastRunStatus: "stale",
|
|
lastError: "Recovered stale lock from maintenance cleanup",
|
|
lastFailureAt: now,
|
|
lastRunCompletedAt: now,
|
|
consecutiveFailures: { increment: 1 }
|
|
}
|
|
});
|
|
|
|
const removedWebhookEvents = await prisma.webhookEvent.deleteMany({
|
|
where: {
|
|
createdAt: { lte: webhookCutoff },
|
|
processStatus: { not: "skipped" }
|
|
}
|
|
});
|
|
|
|
const removedAuditLogs = await prisma.auditLog.deleteMany({
|
|
where: {
|
|
createdAt: { lte: auditCutoff }
|
|
}
|
|
});
|
|
|
|
printLine(`Expired auth tokens deleted: ${expiredTokens.count}`);
|
|
printLine(`Consumed auth tokens deleted (older than ${consumedRetentionHours}h): ${consumedTokens.count}`);
|
|
printLine(`Recovered stale background locks: ${recoveredLock.count}`);
|
|
printLine(`Webhook events deleted (older than ${webhookRetentionDays}d): ${removedWebhookEvents.count}`);
|
|
printLine(`Audit logs deleted (older than ${auditRetentionDays}d): ${removedAuditLogs.count}`);
|
|
|
|
const sampleTenant = await prisma.tenant.findFirst({ select: { id: true } });
|
|
const shouldWriteAudit = Boolean(sampleTenant) && (
|
|
expiredTokens.count > 0 ||
|
|
consumedTokens.count > 0 ||
|
|
recoveredLock.count > 0
|
|
);
|
|
|
|
if (shouldWriteAudit) {
|
|
const tenantId = sampleTenant.id;
|
|
await prisma.auditLog.create({
|
|
data: {
|
|
tenantId,
|
|
actorUserId: null,
|
|
entityType: "system",
|
|
entityId: "maintenance",
|
|
action: "ops_maintenance_cleanup",
|
|
metadataJson: JSON.stringify({
|
|
expiredAuthTokens: expiredTokens.count,
|
|
consumedAuthTokens: consumedTokens.count,
|
|
recoveredLocks: recoveredLock.count,
|
|
webhookEventsDeleted: removedWebhookEvents.count,
|
|
auditLogsDeleted: removedAuditLogs.count,
|
|
consumedRetentionHours,
|
|
staleLockMinutes,
|
|
webhookRetentionDays,
|
|
auditRetentionDays
|
|
})
|
|
}
|
|
});
|
|
}
|
|
|
|
printLine("maintenance completed");
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.error(`[ops-maintenance] failed: ${message}`);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
await prisma.$disconnect();
|
|
}
|
|
}
|
|
|
|
await main();
|