feat: keep deletions restorable and confirm a pipeline's deals before it is removed
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Cron } from '@nestjs/schedule';
|
||||
import type { DeletionRetention } from '@mento/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { modelLabel, restoreArchive } from './deletion-archive';
|
||||
|
||||
const RETENTIONS = new Set<DeletionRetention>([
|
||||
'month',
|
||||
'two_months',
|
||||
'three_months',
|
||||
'six_months',
|
||||
'year',
|
||||
'never',
|
||||
]);
|
||||
|
||||
const RETENTION_DAYS: Record<DeletionRetention, number | null> = {
|
||||
month: 30,
|
||||
two_months: 60,
|
||||
three_months: 90,
|
||||
six_months: 180,
|
||||
year: 365,
|
||||
never: null,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class DeletionsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(organizationId: string, page = 1, limit = 30, q?: string) {
|
||||
const take = Math.min(100, Math.max(1, limit));
|
||||
const skip = (Math.max(1, page) - 1) * take;
|
||||
const query = q?.trim();
|
||||
const where = {
|
||||
organizationId,
|
||||
...(query
|
||||
? {
|
||||
OR: [
|
||||
{ label: { contains: query, mode: 'insensitive' as const } },
|
||||
{ model: { contains: query, mode: 'insensitive' as const } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
const [rows, total, org] = await Promise.all([
|
||||
this.prisma.deletionArchive.findMany({
|
||||
where,
|
||||
include: { deletedBy: { select: { id: true, fullName: true } } },
|
||||
orderBy: { deletedAt: 'desc' },
|
||||
skip,
|
||||
take,
|
||||
}),
|
||||
this.prisma.deletionArchive.count({ where }),
|
||||
this.prisma.organization.findUnique({
|
||||
where: { id: organizationId },
|
||||
select: { deletionRetention: true },
|
||||
}),
|
||||
]);
|
||||
return {
|
||||
items: rows.map((row) => ({
|
||||
id: row.id,
|
||||
model: row.model,
|
||||
modelLabel: modelLabel(row.model),
|
||||
entityId: row.entityId,
|
||||
label: row.label,
|
||||
deletedById: row.deletedById,
|
||||
deletedByName: row.deletedBy?.fullName ?? null,
|
||||
deletedAt: row.deletedAt.toISOString(),
|
||||
})),
|
||||
total,
|
||||
page: Math.max(1, page),
|
||||
limit: take,
|
||||
retention: this.retentionOf(org?.deletionRetention),
|
||||
};
|
||||
}
|
||||
|
||||
async updateRetention(organizationId: string, retention: string) {
|
||||
if (!RETENTIONS.has(retention as DeletionRetention)) {
|
||||
throw new BadRequestException('مدت نگهداری معتبر نیست.');
|
||||
}
|
||||
await this.prisma.organization.update({
|
||||
where: { id: organizationId },
|
||||
data: { deletionRetention: retention },
|
||||
});
|
||||
return { retention: retention as DeletionRetention };
|
||||
}
|
||||
|
||||
async restore(organizationId: string, id: string) {
|
||||
const row = await this.owned(organizationId, id);
|
||||
try {
|
||||
await restoreArchive(
|
||||
this.prisma,
|
||||
row.snapshot as never,
|
||||
row.model,
|
||||
);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : '';
|
||||
if (message === 'این مورد هنوز در سامانه وجود دارد.') {
|
||||
await this.prisma.deletionArchive.delete({ where: { id: row.id } });
|
||||
}
|
||||
throw new BadRequestException(message || 'بازگردانی ناموفق بود.');
|
||||
}
|
||||
await this.prisma.deletionArchive.delete({ where: { id: row.id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async purge(organizationId: string, id: string) {
|
||||
const row = await this.owned(organizationId, id);
|
||||
await this.prisma.deletionArchive.delete({ where: { id: row.id } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
@Cron('15 3 * * *', { timeZone: 'Asia/Tehran' })
|
||||
async purgeExpired() {
|
||||
const orgs = await this.prisma.organization.findMany({
|
||||
select: { id: true, deletionRetention: true },
|
||||
});
|
||||
const now = Date.now();
|
||||
for (const org of orgs) {
|
||||
const days = RETENTION_DAYS[this.retentionOf(org.deletionRetention)];
|
||||
if (!days) continue;
|
||||
const cutoff = new Date(now - days * 24 * 60 * 60 * 1000);
|
||||
await this.prisma.deletionArchive.deleteMany({
|
||||
where: { organizationId: org.id, deletedAt: { lt: cutoff } },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private retentionOf(value: string | null | undefined): DeletionRetention {
|
||||
if (value && RETENTIONS.has(value as DeletionRetention)) {
|
||||
return value as DeletionRetention;
|
||||
}
|
||||
return 'never';
|
||||
}
|
||||
|
||||
private async owned(organizationId: string, id: string) {
|
||||
const row = await this.prisma.deletionArchive.findFirst({
|
||||
where: { id, organizationId },
|
||||
});
|
||||
if (!row) throw new NotFoundException('این حذف در فهرست پیدا نشد.');
|
||||
return row;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user