happy-server/sources/app/session/sessionDelete.ts
Steve Korshakov 014473a7ac feat: implement session deletion with cascade and socket notifications
- Add sessionDelete action to handle deletion of sessions and all related data
- Delete session messages, usage reports, and access keys in proper order
- Add delete-session event type to eventRouter for real-time notifications
- Add DELETE /v1/sessions/:sessionId endpoint with ownership verification
- Send socket notification to all user connections after successful deletion
- Add idempotency rule to CLAUDE.md for API operations

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2025-09-20 21:17:48 -07:00

108 lines
3.6 KiB
TypeScript

import { Context } from "@/context";
import { inTx, afterTx } from "@/storage/inTx";
import { eventRouter, buildDeleteSessionUpdate } from "@/app/events/eventRouter";
import { allocateUserSeq } from "@/storage/seq";
import { randomKeyNaked } from "@/utils/randomKeyNaked";
import { log } from "@/utils/log";
/**
* Delete a session and all its related data.
* Handles:
* - Deleting all session messages
* - Deleting all usage reports for the session
* - Deleting all access keys for the session
* - Deleting the session itself
* - Sending socket notification to all connected clients
*
* @param ctx - Context with user information
* @param sessionId - ID of the session to delete
* @returns true if deletion was successful, false if session not found or not owned by user
*/
export async function sessionDelete(ctx: Context, sessionId: string): Promise<boolean> {
return await inTx(async (tx) => {
// Verify session exists and belongs to the user
const session = await tx.session.findFirst({
where: {
id: sessionId,
accountId: ctx.uid
}
});
if (!session) {
log({
module: 'session-delete',
userId: ctx.uid,
sessionId
}, `Session not found or not owned by user`);
return false;
}
// Delete all related data
// Note: Order matters to avoid foreign key constraint violations
// 1. Delete session messages
const deletedMessages = await tx.sessionMessage.deleteMany({
where: { sessionId }
});
log({
module: 'session-delete',
userId: ctx.uid,
sessionId,
deletedCount: deletedMessages.count
}, `Deleted ${deletedMessages.count} session messages`);
// 2. Delete usage reports
const deletedReports = await tx.usageReport.deleteMany({
where: { sessionId }
});
log({
module: 'session-delete',
userId: ctx.uid,
sessionId,
deletedCount: deletedReports.count
}, `Deleted ${deletedReports.count} usage reports`);
// 3. Delete access keys
const deletedAccessKeys = await tx.accessKey.deleteMany({
where: { sessionId }
});
log({
module: 'session-delete',
userId: ctx.uid,
sessionId,
deletedCount: deletedAccessKeys.count
}, `Deleted ${deletedAccessKeys.count} access keys`);
// 4. Delete the session itself
await tx.session.delete({
where: { id: sessionId }
});
log({
module: 'session-delete',
userId: ctx.uid,
sessionId
}, `Session deleted successfully`);
// Send notification after transaction commits
afterTx(tx, async () => {
const updSeq = await allocateUserSeq(ctx.uid);
const updatePayload = buildDeleteSessionUpdate(sessionId, updSeq, randomKeyNaked(12));
log({
module: 'session-delete',
userId: ctx.uid,
sessionId,
updateType: 'delete-session',
updatePayload: JSON.stringify(updatePayload)
}, `Emitting delete-session update to all user connections`);
eventRouter.emitUpdate({
userId: ctx.uid,
payload: updatePayload,
recipientFilter: { type: 'all-user-authenticated-connections' }
});
});
return true;
});
}