173 lines
4.6 KiB
TypeScript
173 lines
4.6 KiB
TypeScript
/**
|
|
* 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<unknown>;
|
|
deleteMany: (args: { where: { sessionId: string; userId: string } }) => Promise<unknown>;
|
|
findMany: (args: {
|
|
where: { sessionId: string };
|
|
include: ShareInclude;
|
|
}) => Promise<unknown>;
|
|
};
|
|
|
|
type EventDelegate = {
|
|
create: (args: {
|
|
data: { sessionId: string; userId: string; type: string; payload: string };
|
|
}) => Promise<unknown>;
|
|
findMany: (args: {
|
|
where: { sessionId: string } | { sessionId: string; createdAt: { gt: Date } };
|
|
include: EventInclude;
|
|
orderBy: { createdAt: 'asc' };
|
|
}) => Promise<unknown>;
|
|
findFirst: (args: {
|
|
where: { sessionId: string };
|
|
orderBy: { createdAt: 'desc' };
|
|
select: { createdAt: true };
|
|
}) => Promise<{ createdAt: Date } | null>;
|
|
};
|
|
|
|
type SessionDelegate = {
|
|
findFirst: (args: { where: { id: string; userId: string } }) => Promise<unknown>;
|
|
};
|
|
|
|
export function createShareAndEventHandlers<TEventType extends string>(
|
|
sessionModel: SessionDelegate,
|
|
shareModel: ShareDelegate,
|
|
eventModel: EventDelegate,
|
|
canAccessSession: (sessionId: string, userId: string) => Promise<boolean>
|
|
) {
|
|
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<string, unknown>
|
|
): Promise<SessionEventWithUser> {
|
|
return eventModel.create({
|
|
data: {
|
|
sessionId,
|
|
userId,
|
|
type,
|
|
payload: JSON.stringify(payload),
|
|
},
|
|
}) as Promise<SessionEventWithUser>;
|
|
},
|
|
|
|
async getEvents(sessionId: string, since?: Date): Promise<SessionEventWithUser[]> {
|
|
return eventModel.findMany({
|
|
where: {
|
|
sessionId,
|
|
...(since && { createdAt: { gt: since } }),
|
|
},
|
|
include: {
|
|
user: { select: userSelect },
|
|
},
|
|
orderBy: { createdAt: 'asc' },
|
|
}) as Promise<SessionEventWithUser[]>;
|
|
},
|
|
|
|
async getLatestEventTimestamp(sessionId: string) {
|
|
const event = await eventModel.findFirst({
|
|
where: { sessionId },
|
|
orderBy: { createdAt: 'desc' },
|
|
select: { createdAt: true },
|
|
});
|
|
return event?.createdAt;
|
|
},
|
|
};
|
|
}
|