feat: refactor session retrieval logic to utilize generic session queries, enhancing code maintainability and reducing duplication across session types
This commit is contained in:
@@ -1,210 +1,98 @@
|
||||
import { prisma } from '@/services/database';
|
||||
import { resolveCollaborator } from '@/services/auth';
|
||||
import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
|
||||
import type { ShareRole, YearReviewCategory } from '@prisma/client';
|
||||
import { getTeamMemberIdsForAdminTeams } from '@/services/teams';
|
||||
import { createSessionPermissionChecks } from '@/services/session-permissions';
|
||||
import { createShareAndEventHandlers } from '@/services/session-share-events';
|
||||
import {
|
||||
mergeSessionsByUserId,
|
||||
fetchTeamCollaboratorSessions,
|
||||
getSessionByIdGeneric,
|
||||
} from '@/services/session-queries';
|
||||
import type { YearReviewCategory } from '@prisma/client';
|
||||
|
||||
const yearReviewInclude = {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
shares: { include: { user: { select: { id: true, name: true, email: true } } } },
|
||||
_count: { select: { items: true } },
|
||||
};
|
||||
|
||||
// ============================================
|
||||
// Year Review Session CRUD
|
||||
// ============================================
|
||||
|
||||
export async function getYearReviewSessionsByUserId(userId: string) {
|
||||
// Get owned sessions + shared sessions
|
||||
const [owned, shared] = await Promise.all([
|
||||
prisma.yearReviewSession.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
shares: {
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
items: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
}),
|
||||
prisma.yRSessionShare.findMany({
|
||||
where: { userId },
|
||||
include: {
|
||||
session: {
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
shares: {
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
},
|
||||
_count: {
|
||||
select: {
|
||||
items: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Mark owned sessions and merge with shared
|
||||
const ownedWithRole = owned.map((s) => ({
|
||||
...s,
|
||||
isOwner: true as const,
|
||||
role: 'OWNER' as const,
|
||||
}));
|
||||
const sharedWithRole = shared.map((s) => ({
|
||||
...s.session,
|
||||
isOwner: false as const,
|
||||
role: s.role,
|
||||
sharedAt: s.createdAt,
|
||||
}));
|
||||
|
||||
const allSessions = [...ownedWithRole, ...sharedWithRole].sort(
|
||||
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
return mergeSessionsByUserId(
|
||||
(uid) =>
|
||||
prisma.yearReviewSession.findMany({
|
||||
where: { userId: uid },
|
||||
include: yearReviewInclude,
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
}),
|
||||
(uid) =>
|
||||
prisma.yRSessionShare.findMany({
|
||||
where: { userId: uid },
|
||||
include: { session: { include: yearReviewInclude } },
|
||||
}),
|
||||
userId,
|
||||
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
|
||||
);
|
||||
|
||||
// Resolve participants to users
|
||||
const sessionsWithResolved = await Promise.all(
|
||||
allSessions.map(async (s) => ({
|
||||
...s,
|
||||
resolvedParticipant: await resolveCollaborator(s.participant),
|
||||
}))
|
||||
);
|
||||
|
||||
return sessionsWithResolved;
|
||||
}
|
||||
|
||||
/** Sessions owned by team members (where user is team admin) that are NOT shared with the user. */
|
||||
export async function getTeamCollaboratorSessionsForAdmin(userId: string) {
|
||||
const teamMemberIds = await getTeamMemberIdsForAdminTeams(userId);
|
||||
if (teamMemberIds.length === 0) return [];
|
||||
|
||||
const sessions = await prisma.yearReviewSession.findMany({
|
||||
where: {
|
||||
userId: { in: teamMemberIds },
|
||||
shares: { none: { userId } },
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
shares: {
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
},
|
||||
_count: { select: { items: true } },
|
||||
},
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
});
|
||||
|
||||
const withRole = sessions.map((s) => ({
|
||||
...s,
|
||||
isOwner: false as const,
|
||||
role: 'VIEWER' as const,
|
||||
isTeamCollab: true as const,
|
||||
canEdit: true as const, // Admin has full rights on team member sessions
|
||||
}));
|
||||
|
||||
return Promise.all(
|
||||
withRole.map(async (s) => ({
|
||||
...s,
|
||||
resolvedParticipant: await resolveCollaborator(s.participant),
|
||||
}))
|
||||
return fetchTeamCollaboratorSessions(
|
||||
(teamMemberIds, uid) =>
|
||||
prisma.yearReviewSession.findMany({
|
||||
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
|
||||
include: yearReviewInclude,
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
}),
|
||||
getTeamMemberIdsForAdminTeams,
|
||||
userId,
|
||||
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
|
||||
);
|
||||
}
|
||||
|
||||
const yearReviewByIdInclude = {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
items: { orderBy: [...([{ category: 'asc' }, { order: 'asc' }] as const)] },
|
||||
shares: { include: { user: { select: { id: true, name: true, email: true } } } },
|
||||
};
|
||||
|
||||
export async function getYearReviewSessionById(sessionId: string, userId: string) {
|
||||
// Check if user owns the session, has it shared, or is team admin of owner
|
||||
let session = await prisma.yearReviewSession.findFirst({
|
||||
where: {
|
||||
id: sessionId,
|
||||
OR: [
|
||||
{ userId }, // Owner
|
||||
{ shares: { some: { userId } } }, // Shared with user
|
||||
],
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
items: {
|
||||
orderBy: [{ category: 'asc' }, { order: 'asc' }],
|
||||
},
|
||||
shares: {
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
const raw = await prisma.yearReviewSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
items: { orderBy: [{ category: 'asc' }, { order: 'asc' }] },
|
||||
shares: { include: { user: { select: { id: true, name: true, email: true } } } },
|
||||
},
|
||||
});
|
||||
if (!raw || !(await isAdminOfUser(raw.userId, userId))) return null;
|
||||
session = raw;
|
||||
}
|
||||
|
||||
// Determine user's role
|
||||
const isOwner = session.userId === userId;
|
||||
const share = session.shares.find((s) => s.userId === userId);
|
||||
const isAdminOfOwner = !isOwner && !share && (await isAdminOfUser(session.userId, userId));
|
||||
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
|
||||
const canEdit = isOwner || role === 'EDITOR' || isAdminOfOwner;
|
||||
|
||||
// Resolve participant to user if it's an email
|
||||
const resolvedParticipant = await resolveCollaborator(session.participant);
|
||||
|
||||
return { ...session, isOwner, role, canEdit, resolvedParticipant };
|
||||
return getSessionByIdGeneric(
|
||||
sessionId,
|
||||
userId,
|
||||
(sid, uid) =>
|
||||
prisma.yearReviewSession.findFirst({
|
||||
where: { id: sid, OR: [{ userId: uid }, { shares: { some: { userId: uid } } }] },
|
||||
include: yearReviewByIdInclude,
|
||||
}),
|
||||
(sid) =>
|
||||
prisma.yearReviewSession.findUnique({ where: { id: sid }, include: yearReviewByIdInclude }),
|
||||
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
|
||||
);
|
||||
}
|
||||
|
||||
// Check if user can access session (owner, shared, or team admin of owner)
|
||||
export async function canAccessYearReviewSession(sessionId: string, userId: string) {
|
||||
const count = await prisma.yearReviewSession.count({
|
||||
where: {
|
||||
id: sessionId,
|
||||
OR: [{ userId }, { shares: { some: { userId } } }],
|
||||
},
|
||||
});
|
||||
if (count > 0) return true;
|
||||
const session = await prisma.yearReviewSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
select: { userId: true },
|
||||
});
|
||||
return session ? isAdminOfUser(session.userId, userId) : false;
|
||||
}
|
||||
const yearReviewPermissions = createSessionPermissionChecks(prisma.yearReviewSession);
|
||||
|
||||
// Check if user can edit session (owner, EDITOR role, or team admin of owner)
|
||||
export async function canEditYearReviewSession(sessionId: string, userId: string) {
|
||||
const count = await prisma.yearReviewSession.count({
|
||||
where: {
|
||||
id: sessionId,
|
||||
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
|
||||
},
|
||||
});
|
||||
if (count > 0) return true;
|
||||
const session = await prisma.yearReviewSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
select: { userId: true },
|
||||
});
|
||||
return session ? isAdminOfUser(session.userId, userId) : false;
|
||||
}
|
||||
const yearReviewShareEvents = createShareAndEventHandlers<
|
||||
| 'ITEM_CREATED'
|
||||
| 'ITEM_UPDATED'
|
||||
| 'ITEM_DELETED'
|
||||
| 'ITEM_MOVED'
|
||||
| 'ITEMS_REORDERED'
|
||||
| 'SESSION_UPDATED'
|
||||
>(
|
||||
prisma.yearReviewSession,
|
||||
prisma.yRSessionShare,
|
||||
prisma.yRSessionEvent,
|
||||
yearReviewPermissions.canAccess
|
||||
);
|
||||
|
||||
// Check if user can delete session (owner or team admin only - NOT EDITOR)
|
||||
export async function canDeleteYearReviewSession(sessionId: string, userId: string) {
|
||||
const session = await prisma.yearReviewSession.findUnique({
|
||||
where: { id: sessionId },
|
||||
select: { userId: true },
|
||||
});
|
||||
if (!session) return false;
|
||||
return session.userId === userId || isAdminOfUser(session.userId, userId);
|
||||
}
|
||||
export const canAccessYearReviewSession = yearReviewPermissions.canAccess;
|
||||
export const canEditYearReviewSession = yearReviewPermissions.canEdit;
|
||||
export const canDeleteYearReviewSession = yearReviewPermissions.canDelete;
|
||||
|
||||
export async function createYearReviewSession(
|
||||
userId: string,
|
||||
@@ -319,81 +207,9 @@ export async function reorderYearReviewItems(
|
||||
// Session Sharing
|
||||
// ============================================
|
||||
|
||||
export async function shareYearReviewSession(
|
||||
sessionId: string,
|
||||
ownerId: string,
|
||||
targetEmail: string,
|
||||
role: ShareRole = 'EDITOR'
|
||||
) {
|
||||
// Verify owner
|
||||
const session = await prisma.yearReviewSession.findFirst({
|
||||
where: { id: sessionId, userId: ownerId },
|
||||
});
|
||||
if (!session) {
|
||||
throw new Error('Session not found or not owned');
|
||||
}
|
||||
|
||||
// Find target user
|
||||
const targetUser = await prisma.user.findUnique({
|
||||
where: { email: targetEmail },
|
||||
});
|
||||
if (!targetUser) {
|
||||
throw new Error('User not found');
|
||||
}
|
||||
|
||||
// Can't share with yourself
|
||||
if (targetUser.id === ownerId) {
|
||||
throw new Error('Cannot share session with yourself');
|
||||
}
|
||||
|
||||
// Create or update share
|
||||
return prisma.yRSessionShare.upsert({
|
||||
where: {
|
||||
sessionId_userId: { sessionId, userId: targetUser.id },
|
||||
},
|
||||
update: { role },
|
||||
create: {
|
||||
sessionId,
|
||||
userId: targetUser.id,
|
||||
role,
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function removeYearReviewShare(
|
||||
sessionId: string,
|
||||
ownerId: string,
|
||||
shareUserId: string
|
||||
) {
|
||||
// Verify owner
|
||||
const session = await prisma.yearReviewSession.findFirst({
|
||||
where: { id: sessionId, userId: ownerId },
|
||||
});
|
||||
if (!session) {
|
||||
throw new Error('Session not found or not owned');
|
||||
}
|
||||
|
||||
return prisma.yRSessionShare.deleteMany({
|
||||
where: { sessionId, userId: shareUserId },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getYearReviewSessionShares(sessionId: string, userId: string) {
|
||||
// Verify access
|
||||
if (!(await canAccessYearReviewSession(sessionId, userId))) {
|
||||
throw new Error('Access denied');
|
||||
}
|
||||
|
||||
return prisma.yRSessionShare.findMany({
|
||||
where: { sessionId },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
export const shareYearReviewSession = yearReviewShareEvents.share;
|
||||
export const removeYearReviewShare = yearReviewShareEvents.removeShare;
|
||||
export const getYearReviewSessionShares = yearReviewShareEvents.getShares;
|
||||
|
||||
// ============================================
|
||||
// Session Events (for real-time sync)
|
||||
@@ -407,41 +223,7 @@ export type YRSessionEventType =
|
||||
| 'ITEMS_REORDERED'
|
||||
| 'SESSION_UPDATED';
|
||||
|
||||
export async function createYearReviewSessionEvent(
|
||||
sessionId: string,
|
||||
userId: string,
|
||||
type: YRSessionEventType,
|
||||
payload: Record<string, unknown>
|
||||
) {
|
||||
return prisma.yRSessionEvent.create({
|
||||
data: {
|
||||
sessionId,
|
||||
userId,
|
||||
type,
|
||||
payload: JSON.stringify(payload),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getYearReviewSessionEvents(sessionId: string, since?: Date) {
|
||||
return prisma.yRSessionEvent.findMany({
|
||||
where: {
|
||||
sessionId,
|
||||
...(since && { createdAt: { gt: since } }),
|
||||
},
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'asc' },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getLatestYearReviewEventTimestamp(sessionId: string) {
|
||||
const event = await prisma.yRSessionEvent.findFirst({
|
||||
where: { sessionId },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
select: { createdAt: true },
|
||||
});
|
||||
return event?.createdAt;
|
||||
}
|
||||
export const createYearReviewSessionEvent = yearReviewShareEvents.createEvent;
|
||||
export const getYearReviewSessionEvents = yearReviewShareEvents.getEvents;
|
||||
export const getLatestYearReviewEventTimestamp = yearReviewShareEvents.getLatestEventTimestamp;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user