diff --git a/src/export/export.controller.ts b/src/export/export.controller.ts new file mode 100644 index 0000000..fcaaba9 --- /dev/null +++ b/src/export/export.controller.ts @@ -0,0 +1,130 @@ +import { Body, Controller, Get, IsString, Post } from '@nestjs/common'; +import { eq, isNull, and } from 'drizzle-orm'; +import { CurrentSession } from '../auth/session.decorator'; +import type { AuthenticatedSession } from '../auth/tenant.guard'; +import { db } from '../db/client'; +import { + organizations, + tasks, + goals, + decisions, + transactions, + aiRequests, + consentRecords, + notifications, + researchBriefs, + savedSegments, + opportunities, +} from '../db/schema'; + +class DeletionRequestDto { + @IsString() reason!: string; +} + +@Controller('export') +export class ExportController { + @Get('data') + async exportData(@CurrentSession() session: AuthenticatedSession) { + const tid = session.tenantId; + const uid = session.userId; + + const [ + orgs, + tks, + gls, + decs, + txs, + aiReqs, + consents, + notifs, + briefs, + segs, + opps, + ] = await Promise.all([ + db.query.organizations.findMany({ + where: and(eq(organizations.tenantId, tid), isNull(organizations.deletedAt)), + }), + db.query.tasks.findMany({ where: eq(tasks.tenantId, tid) }), + db.query.goals.findMany({ where: eq(goals.tenantId, tid) }), + db.query.decisions.findMany({ where: eq(decisions.tenantId, tid) }), + db.query.transactions.findMany({ where: eq(transactions.tenantId, tid) }), + db.query.aiRequests.findMany({ + where: eq(aiRequests.tenantId, tid), + columns: { contextManifest: false }, + }), + db.query.consentRecords.findMany({ + where: and( + eq(consentRecords.tenantId, tid), + eq(consentRecords.userId, uid), + ), + }), + db.query.notifications.findMany({ where: eq(notifications.tenantId, tid) }), + db.query.researchBriefs.findMany({ where: eq(researchBriefs.tenantId, tid) }), + db.query.savedSegments.findMany({ where: eq(savedSegments.tenantId, tid) }), + db.query.opportunities.findMany({ where: eq(opportunities.tenantId, tid) }), + ]); + + return { + exportedAt: new Date().toISOString(), + tenantId: tid, + userId: uid, + schema: '1.0', + data: { + organizations: orgs, + tasks: tks, + goals: gls, + decisions: decs, + transactions: txs, + aiRequests: aiReqs, + consentRecords: consents, + notifications: notifs, + researchBriefs: briefs, + savedSegments: segs, + opportunities: opps, + }, + counts: { + organizations: orgs.length, + tasks: tks.length, + goals: gls.length, + decisions: decs.length, + transactions: txs.length, + aiRequests: aiReqs.length, + consentRecords: consents.length, + notifications: notifs.length, + researchBriefs: briefs.length, + savedSegments: segs.length, + opportunities: opps.length, + }, + }; + } + + @Post('deletion-request') + async deletionRequest( + @CurrentSession() session: AuthenticatedSession, + @Body() dto: DeletionRequestDto, + ) { + // Record deletion request as a consent record with special purpose + const existing = await db.query.consentRecords.findFirst({ + where: and( + eq(consentRecords.tenantId, session.tenantId), + eq(consentRecords.userId, session.userId), + eq(consentRecords.purpose, 'account_deletion_requested'), + isNull(consentRecords.revokedAt), + ), + }); + if (existing) { + return { submitted: true, alreadyPending: true, submittedAt: existing.grantedAt }; + } + const [record] = await db + .insert(consentRecords) + .values({ + tenantId: session.tenantId, + userId: session.userId, + purpose: 'account_deletion_requested', + grantedAt: new Date(), + metadata: { reason: dto.reason }, + }) + .returning(); + return { submitted: true, alreadyPending: false, submittedAt: record.grantedAt }; + } +}