feat(CC-060): add ExportController (GET /export/data, POST /export/deletion-request)

This commit is contained in:
admin-valentin 2026-08-01 21:09:50 +00:00
parent 8f69f9d95c
commit eca3f4a48b

View file

@ -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 };
}
}