feat: refactor session retrieval logic to utilize generic session queries, enhancing code maintainability and reducing duplication across session types

This commit is contained in:
Julien Froidefond
2026-02-17 14:38:54 +01:00
parent aad4b7f111
commit 4d04d3ede8
11 changed files with 777 additions and 1499 deletions

View File

@@ -1,210 +1,93 @@
import { prisma } from '@/services/database';
import { resolveCollaborator } from '@/services/auth';
import { getTeamMemberIdsForAdminTeams, isAdminOfUser } from '@/services/teams';
import type { ShareRole, MotivatorType } 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 { MotivatorType } from '@prisma/client';
const motivatorInclude = {
user: { select: { id: true, name: true, email: true } },
shares: { include: { user: { select: { id: true, name: true, email: true } } } },
_count: { select: { cards: true } },
};
// ============================================
// Moving Motivators Session CRUD
// ============================================
export async function getMotivatorSessionsByUserId(userId: string) {
// Get owned sessions + shared sessions
const [owned, shared] = await Promise.all([
prisma.movingMotivatorsSession.findMany({
where: { userId },
include: {
user: { select: { id: true, name: true, email: true } },
shares: {
include: {
user: { select: { id: true, name: true, email: true } },
},
},
_count: {
select: {
cards: true,
},
},
},
orderBy: { updatedAt: 'desc' },
}),
prisma.mMSessionShare.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: {
cards: 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.movingMotivatorsSession.findMany({
where: { userId: uid },
include: motivatorInclude,
orderBy: { updatedAt: 'desc' },
}),
(uid) =>
prisma.mMSessionShare.findMany({
where: { userId: uid },
include: { session: { include: motivatorInclude } },
}),
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.movingMotivatorsSession.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: { cards: 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.movingMotivatorsSession.findMany({
where: { userId: { in: teamMemberIds }, shares: { none: { userId: uid } } },
include: motivatorInclude,
orderBy: { updatedAt: 'desc' },
}),
getTeamMemberIdsForAdminTeams,
userId,
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
);
}
const motivatorByIdInclude = {
user: { select: { id: true, name: true, email: true } },
cards: { orderBy: { orderIndex: 'asc' } as const },
shares: { include: { user: { select: { id: true, name: true, email: true } } } },
};
export async function getMotivatorSessionById(sessionId: string, userId: string) {
// Check if user owns the session, has it shared, or is team admin of owner
let session = await prisma.movingMotivatorsSession.findFirst({
where: {
id: sessionId,
OR: [
{ userId }, // Owner
{ shares: { some: { userId } } }, // Shared with user
],
},
include: {
user: { select: { id: true, name: true, email: true } },
cards: {
orderBy: { orderIndex: 'asc' },
},
shares: {
include: {
user: { select: { id: true, name: true, email: true } },
},
},
},
});
if (!session) {
const raw = await prisma.movingMotivatorsSession.findUnique({
where: { id: sessionId },
include: {
user: { select: { id: true, name: true, email: true } },
cards: { orderBy: { orderIndex: '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.movingMotivatorsSession.findFirst({
where: { id: sid, OR: [{ userId: uid }, { shares: { some: { userId: uid } } }] },
include: motivatorByIdInclude,
}),
(sid) =>
prisma.movingMotivatorsSession.findUnique({ where: { id: sid }, include: motivatorByIdInclude }),
(s) => resolveCollaborator(s.participant).then((r) => ({ resolvedParticipant: r }))
);
}
// Check if user can access session (owner, shared, or team admin of owner)
export async function canAccessMotivatorSession(sessionId: string, userId: string) {
const count = await prisma.movingMotivatorsSession.count({
where: {
id: sessionId,
OR: [{ userId }, { shares: { some: { userId } } }],
},
});
if (count > 0) return true;
const session = await prisma.movingMotivatorsSession.findUnique({
where: { id: sessionId },
select: { userId: true },
});
return session ? isAdminOfUser(session.userId, userId) : false;
}
const motivatorPermissions = createSessionPermissionChecks(prisma.movingMotivatorsSession);
// Check if user can edit session (owner, EDITOR role, or team admin of owner)
export async function canEditMotivatorSession(sessionId: string, userId: string) {
const count = await prisma.movingMotivatorsSession.count({
where: {
id: sessionId,
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
},
});
if (count > 0) return true;
const session = await prisma.movingMotivatorsSession.findUnique({
where: { id: sessionId },
select: { userId: true },
});
return session ? isAdminOfUser(session.userId, userId) : false;
}
const motivatorShareEvents = createShareAndEventHandlers<
'CARD_MOVED' | 'CARD_INFLUENCE_CHANGED' | 'CARDS_REORDERED' | 'SESSION_UPDATED'
>(
prisma.movingMotivatorsSession,
prisma.mMSessionShare,
prisma.mMSessionEvent,
motivatorPermissions.canAccess
);
// Check if user can delete session (owner or team admin only - NOT EDITOR)
export async function canDeleteMotivatorSession(sessionId: string, userId: string) {
const session = await prisma.movingMotivatorsSession.findUnique({
where: { id: sessionId },
select: { userId: true },
});
if (!session) return false;
return session.userId === userId || isAdminOfUser(session.userId, userId);
}
export const canAccessMotivatorSession = motivatorPermissions.canAccess;
export const canEditMotivatorSession = motivatorPermissions.canEdit;
export const canDeleteMotivatorSession = motivatorPermissions.canDelete;
const DEFAULT_MOTIVATOR_TYPES: MotivatorType[] = [
'STATUS',
@@ -305,81 +188,9 @@ export async function updateCardInfluence(cardId: string, influence: number) {
// Session Sharing
// ============================================
export async function shareMotivatorSession(
sessionId: string,
ownerId: string,
targetEmail: string,
role: ShareRole = 'EDITOR'
) {
// Verify owner
const session = await prisma.movingMotivatorsSession.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.mMSessionShare.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 removeMotivatorShare(
sessionId: string,
ownerId: string,
shareUserId: string
) {
// Verify owner
const session = await prisma.movingMotivatorsSession.findFirst({
where: { id: sessionId, userId: ownerId },
});
if (!session) {
throw new Error('Session not found or not owned');
}
return prisma.mMSessionShare.deleteMany({
where: { sessionId, userId: shareUserId },
});
}
export async function getMotivatorSessionShares(sessionId: string, userId: string) {
// Verify access
if (!(await canAccessMotivatorSession(sessionId, userId))) {
throw new Error('Access denied');
}
return prisma.mMSessionShare.findMany({
where: { sessionId },
include: {
user: { select: { id: true, name: true, email: true } },
},
});
}
export const shareMotivatorSession = motivatorShareEvents.share;
export const removeMotivatorShare = motivatorShareEvents.removeShare;
export const getMotivatorSessionShares = motivatorShareEvents.getShares;
// ============================================
// Session Events (for real-time sync)
@@ -391,40 +202,6 @@ export type MMSessionEventType =
| 'CARDS_REORDERED'
| 'SESSION_UPDATED';
export async function createMotivatorSessionEvent(
sessionId: string,
userId: string,
type: MMSessionEventType,
payload: Record<string, unknown>
) {
return prisma.mMSessionEvent.create({
data: {
sessionId,
userId,
type,
payload: JSON.stringify(payload),
},
});
}
export async function getMotivatorSessionEvents(sessionId: string, since?: Date) {
return prisma.mMSessionEvent.findMany({
where: {
sessionId,
...(since && { createdAt: { gt: since } }),
},
include: {
user: { select: { id: true, name: true, email: true } },
},
orderBy: { createdAt: 'asc' },
});
}
export async function getLatestMotivatorEventTimestamp(sessionId: string) {
const event = await prisma.mMSessionEvent.findFirst({
where: { sessionId },
orderBy: { createdAt: 'desc' },
select: { createdAt: true },
});
return event?.createdAt;
}
export const createMotivatorSessionEvent = motivatorShareEvents.createEvent;
export const getMotivatorSessionEvents = motivatorShareEvents.getEvents;
export const getLatestMotivatorEventTimestamp = motivatorShareEvents.getLatestEventTimestamp;