import { Prisma } from '@prisma/client'; import type { PrismaClient } from '@prisma/client'; import { deletionActor, skipDeletionArchive } from './deletion-context'; const SKIP_MODELS = new Set([ 'DeletionArchive', 'AuditLog', 'Notification', 'PortalOtp', 'AutomationCursor', 'MessageAutomationJob', 'AccountingSyncJob', 'LeadImportJob', 'WebhookDelivery', ]); const MODEL_LABELS: Record = { Person: 'مشتری', Company: 'شرکت', Lead: 'لید', Deal: 'معامله', Pipeline: 'مسیر فروش', PipelineStage: 'مرحله مسیر', Activity: 'فعالیت', Note: 'یادداشت', Quote: 'پیش‌فاکتور', Invoice: 'فاکتور', Ticket: 'تیکت', ProjectCard: 'کارت پروژه', ProjectBoard: 'تخته پروژه', Product: 'محصول', RoutineTask: 'کار روتین', Routine: 'روتین', LeadForm: 'فرم', User: 'کاربر', Team: 'تیم', }; type Relink = { model: string; field: string; ids: string[] }; type Captured = { row: Record; children: Array<{ model: string; item: Captured }>; relink: Relink[]; }; type ModelField = { name: string; kind: string; type: string; isList: boolean; relationName?: string; relationFromFields?: string[]; relationOnDelete?: string; }; type ModelMeta = { name: string; fields: ModelField[] }; function models(): ModelMeta[] { return Prisma.dmmf.datamodel.models as unknown as ModelMeta[]; } function delegateName(model: string) { return model.charAt(0).toLowerCase() + model.slice(1); } function delegate(client: PrismaClient, model: string) { return (client as unknown as Record Promise>>; findFirst: (args: unknown) => Promise | null>; create: (args: unknown) => Promise; updateMany: (args: unknown) => Promise; deleteMany: (args: unknown) => Promise; }>)[delegateName(model)]; } function plain(value: unknown): Record { return JSON.parse(JSON.stringify(value)) as Record; } export function modelLabel(model: string) { return MODEL_LABELS[model] ?? model; } export function rowLabel(row: Record) { const person = [row.firstName, row.lastName].filter(Boolean).join(' ').trim(); const text = row.title || row.name || row.fullName || row.subject || person || row.number || row.mobile || row.email || row.id; return String(text || 'بدون عنوان').slice(0, 180); } function cascadeLinks(parentModel: string) { const parent = models().find((item) => item.name === parentModel); if (!parent) return []; const links: Array<{ model: string; fk: string }> = []; for (const field of parent.fields) { if (field.kind !== 'object' || !field.isList) continue; const child = models().find((item) => item.name === field.type); const back = child?.fields.find( (item) => item.relationName === field.relationName && (item.relationFromFields?.length ?? 0) > 0, ); if (!back || back.relationOnDelete !== 'Cascade') continue; if (SKIP_MODELS.has(field.type)) continue; links.push({ model: field.type, fk: back.relationFromFields![0] }); } return links; } async function setNullRefs( client: PrismaClient, parentModel: string, id: string, ): Promise { const refs: Relink[] = []; for (const model of models()) { if (SKIP_MODELS.has(model.name)) continue; for (const field of model.fields) { if (field.kind !== 'object' || field.type !== parentModel) continue; if (field.relationOnDelete !== 'SetNull') continue; const fk = field.relationFromFields?.[0]; if (!fk) continue; const rows = await delegate(client, model.name).findMany({ where: { [fk]: id }, select: { id: true }, }); if (!rows.length) continue; refs.push({ model: model.name, field: fk, ids: rows.map((row) => String(row.id)), }); } } return refs; } async function captureRow( client: PrismaClient, model: string, row: Record, depth: number, ): Promise { const id = String(row.id); const relink = depth > 3 ? [] : await setNullRefs(client, model, id); const children: Captured['children'] = []; if (depth < 4) { for (const link of cascadeLinks(model)) { const nested = await delegate(client, link.model).findMany({ where: { [link.fk]: id }, }); for (const child of nested) { children.push({ model: link.model, item: await captureRow(client, link.model, child, depth + 1), }); } } } return { row: plain(row), children, relink }; } async function writeArchives( client: PrismaClient, model: string, captured: Captured[], ) { const actor = deletionActor(); for (const item of captured) { const organizationId = typeof item.row.organizationId === 'string' ? item.row.organizationId : actor?.organizationId; if (!organizationId) continue; const deletedById = actor?.userId && actor.organizationId === organizationId ? actor.userId : null; await client.deletionArchive.create({ data: { organizationId, model, entityId: String(item.row.id), label: rowLabel(item.row), snapshot: item as unknown as Prisma.InputJsonValue, deletedById, }, }); } } async function snapshotDelete( client: PrismaClient, model: string, action: 'delete' | 'deleteMany', where: Record | undefined, ) { if (!where || model === 'Organization' || SKIP_MODELS.has(model) || deletionActor()?.skip) { return []; } const rows = action === 'delete' ? [await delegate(client, model).findFirst({ where })].filter( (row): row is Record => !!row, ) : await delegate(client, model).findMany({ where }); if (!rows.length) return []; return Promise.all(rows.map((row) => captureRow(client, model, row, 0))); } export function withDeletionArchive(base: T) { return base.$extends({ query: { $allModels: { async delete({ model, args, query }) { const captured = await snapshotDelete( base, model, 'delete', args.where as Record | undefined, ).catch((error) => { console.error('deletion archive snapshot failed', error); return [] as Captured[]; }); const result = await query(args); if (captured.length) { await writeArchives(base, model, captured).catch((error) => { console.error('deletion archive write failed', error); }); } return result; }, async deleteMany({ model, args, query }) { const captured = await snapshotDelete( base, model, 'deleteMany', args.where as Record | undefined, ).catch((error) => { console.error('deletion archive snapshot failed', error); return [] as Captured[]; }); const result = await query(args); if (captured.length) { await writeArchives(base, model, captured).catch((error) => { console.error('deletion archive write failed', error); }); } return result; }, }, }, }); } export async function restoreArchive( client: PrismaClient, snapshot: Captured, model: string, ) { await skipDeletionArchive(async () => { await restoreNode(client, model, snapshot); }); } async function restoreNode(client: PrismaClient, model: string, node: Captured) { const existing = await delegate(client, model).findFirst({ where: { id: node.row.id }, select: { id: true }, }); if (existing) { throw new Error('این مورد هنوز در سامانه وجود دارد.'); } try { await delegate(client, model).create({ data: node.row }); } catch (error) { const code = (error as { code?: string }).code; if (code === 'P2002') { throw new Error('این مورد یا یک مقدار یکتای آن از قبل وجود دارد.'); } if (code === 'P2003') { throw new Error('وابستگی این مورد هنوز وجود ندارد. اول مورد اصلی را برگردانید.'); } throw error; } for (const child of node.children) { await restoreNode(client, child.model, child.item); } for (const link of node.relink) { if (!link.ids.length || SKIP_MODELS.has(link.model)) continue; await delegate(client, link.model).updateMany({ where: { id: { in: link.ids } }, data: { [link.field]: node.row.id }, }); } }