feat: refactor session retrieval logic to utilize generic session queries, enhancing code maintainability and reducing duplication across session types
This commit is contained in:
172
src/services/session-share-events.ts
Normal file
172
src/services/session-share-events.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user