feat(CC-060): add ExportController (GET /export/data, POST /export/deletion-request)
This commit is contained in:
parent
8f69f9d95c
commit
eca3f4a48b
1 changed files with 130 additions and 0 deletions
130
src/export/export.controller.ts
Normal file
130
src/export/export.controller.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue