/** * Shared share + realtime event logic for workshop sessions. * Used by: sessions, moving-motivators, year-review, weekly-checkin, weather. */ import { prisma } from '@/services/database'; import type { ShareRole } from '@prisma/client'; const userSelect = { id: true, name: true, email: true } as const; export type SessionEventWithUser = { id: string; sessionId: string; userId: string; type: string; payload: string; createdAt: Date; user: { id: string; name: string | null; email: string }; }; type ShareInclude = { user: { select: typeof userSelect } }; type EventInclude = { user: { select: typeof userSelect } }; type ShareDelegate = { upsert: (args: { where: { sessionId_userId: { sessionId: string; userId: string } }; update: { role: ShareRole }; create: { sessionId: string; userId: string; role: ShareRole }; include: ShareInclude; }) => Promise; deleteMany: (args: { where: { sessionId: string; userId: string } }) => Promise; findMany: (args: { where: { sessionId: string }; include: ShareInclude; }) => Promise; }; type EventDelegate = { create: (args: { data: { sessionId: string; userId: string; type: string; payload: string }; }) => Promise; findMany: (args: { where: { sessionId: string } | { sessionId: string; createdAt: { gt: Date } }; include: EventInclude; orderBy: { createdAt: 'asc' }; }) => Promise; findFirst: (args: { where: { sessionId: string }; orderBy: { createdAt: 'desc' }; select: { createdAt: true }; }) => Promise<{ createdAt: Date } | null>; deleteMany: (args: { where: { createdAt: { lt: Date } }; }) => Promise; }; type SessionDelegate = { findFirst: (args: { where: { id: string; userId: string } }) => Promise; }; export function createShareAndEventHandlers( sessionModel: SessionDelegate, shareModel: ShareDelegate, eventModel: EventDelegate, canAccessSession: (sessionId: string, userId: string) => Promise ) { return { async share( sessionId: string, ownerId: string, targetEmail: string, role: ShareRole = 'EDITOR' ) { const session = await sessionModel.findFirst({ where: { id: sessionId, userId: ownerId }, }); if (!session) { throw new Error('Session not found or not owned'); } const targetUser = await prisma.user.findUnique({ where: { email: targetEmail }, }); if (!targetUser) { throw new Error('User not found'); } if (targetUser.id === ownerId) { throw new Error('Cannot share session with yourself'); } return shareModel.upsert({ where: { sessionId_userId: { sessionId, userId: targetUser.id }, }, update: { role }, create: { sessionId, userId: targetUser.id, role, }, include: { user: { select: userSelect }, }, }); }, async removeShare( sessionId: string, ownerId: string, shareUserId: string ) { const session = await sessionModel.findFirst({ where: { id: sessionId, userId: ownerId }, }); if (!session) { throw new Error('Session not found or not owned'); } return shareModel.deleteMany({ where: { sessionId, userId: shareUserId }, }); }, async getShares(sessionId: string, userId: string) { if (!(await canAccessSession(sessionId, userId))) { throw new Error('Access denied'); } return shareModel.findMany({ where: { sessionId }, include: { user: { select: userSelect }, }, }); }, async createEvent( sessionId: string, userId: string, type: TEventType, payload: Record ): Promise { return eventModel.create({ data: { sessionId, userId, type, payload: JSON.stringify(payload), }, }) as Promise; }, async getEvents(sessionId: string, since?: Date): Promise { return eventModel.findMany({ where: { sessionId, ...(since && { createdAt: { gt: since } }), }, include: { user: { select: userSelect }, }, orderBy: { createdAt: 'asc' }, }) as Promise; }, async getLatestEventTimestamp(sessionId: string) { const event = await eventModel.findFirst({ where: { sessionId }, orderBy: { createdAt: 'desc' }, select: { createdAt: true }, }); return event?.createdAt; }, /** Delete events older than the given number of hours (default: 24h) */ async cleanupOldEvents(maxAgeHours = 24) { const cutoff = new Date(Date.now() - maxAgeHours * 60 * 60 * 1000); return eventModel.deleteMany({ where: { createdAt: { lt: cutoff } }, }); }, }; }