155 lines
4.6 KiB
TypeScript
155 lines
4.6 KiB
TypeScript
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,
|
|
deletedById?: string,
|
|
) {
|
|
const take = Math.min(100, Math.max(1, limit));
|
|
const skip = (Math.max(1, page) - 1) * take;
|
|
const query = q?.trim();
|
|
const actorId = deletedById?.trim();
|
|
const where = {
|
|
organizationId,
|
|
...(actorId ? { deletedById: actorId } : {}),
|
|
...(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;
|
|
}
|
|
}
|