454 lines
10 KiB
TypeScript
454 lines
10 KiB
TypeScript
import { prisma } from '@/services/database';
|
|
import type { SwotCategory, ShareRole } from '@prisma/client';
|
|
|
|
// ============================================
|
|
// Session CRUD
|
|
// ============================================
|
|
|
|
export async function getSessionsByUserId(userId: string) {
|
|
// Get owned sessions + shared sessions
|
|
const [owned, shared] = await Promise.all([
|
|
prisma.session.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,
|
|
actions: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: { updatedAt: 'desc' },
|
|
}),
|
|
prisma.sessionShare.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,
|
|
actions: 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,
|
|
}));
|
|
|
|
return [...ownedWithRole, ...sharedWithRole].sort(
|
|
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
);
|
|
}
|
|
|
|
export async function getSessionById(sessionId: string, userId: string) {
|
|
// Check if user owns the session OR has it shared
|
|
const session = await prisma.session.findFirst({
|
|
where: {
|
|
id: sessionId,
|
|
OR: [
|
|
{ userId }, // Owner
|
|
{ shares: { some: { userId } } }, // Shared with user
|
|
],
|
|
},
|
|
include: {
|
|
user: { select: { id: true, name: true, email: true } },
|
|
items: {
|
|
orderBy: { order: 'asc' },
|
|
},
|
|
actions: {
|
|
include: {
|
|
links: {
|
|
include: {
|
|
swotItem: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: { createdAt: 'asc' },
|
|
},
|
|
shares: {
|
|
include: {
|
|
user: { select: { id: true, name: true, email: true } },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!session) return null;
|
|
|
|
// Determine user's role
|
|
const isOwner = session.userId === userId;
|
|
const share = session.shares.find((s) => s.userId === userId);
|
|
const role = isOwner ? ('OWNER' as const) : share?.role || ('VIEWER' as const);
|
|
const canEdit = isOwner || role === 'EDITOR';
|
|
|
|
return { ...session, isOwner, role, canEdit };
|
|
}
|
|
|
|
// Check if user can access session (owner or shared)
|
|
export async function canAccessSession(sessionId: string, userId: string) {
|
|
const count = await prisma.session.count({
|
|
where: {
|
|
id: sessionId,
|
|
OR: [{ userId }, { shares: { some: { userId } } }],
|
|
},
|
|
});
|
|
return count > 0;
|
|
}
|
|
|
|
// Check if user can edit session (owner or EDITOR role)
|
|
export async function canEditSession(sessionId: string, userId: string) {
|
|
const count = await prisma.session.count({
|
|
where: {
|
|
id: sessionId,
|
|
OR: [{ userId }, { shares: { some: { userId, role: 'EDITOR' } } }],
|
|
},
|
|
});
|
|
return count > 0;
|
|
}
|
|
|
|
export async function createSession(userId: string, data: { title: string; collaborator: string }) {
|
|
return prisma.session.create({
|
|
data: {
|
|
...data,
|
|
userId,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function updateSession(
|
|
sessionId: string,
|
|
userId: string,
|
|
data: { title?: string; collaborator?: string }
|
|
) {
|
|
return prisma.session.updateMany({
|
|
where: { id: sessionId, userId },
|
|
data,
|
|
});
|
|
}
|
|
|
|
export async function deleteSession(sessionId: string, userId: string) {
|
|
return prisma.session.deleteMany({
|
|
where: { id: sessionId, userId },
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// SWOT Items CRUD
|
|
// ============================================
|
|
|
|
export async function createSwotItem(
|
|
sessionId: string,
|
|
data: { content: string; category: SwotCategory }
|
|
) {
|
|
// Get max order for this category
|
|
const maxOrder = await prisma.swotItem.aggregate({
|
|
where: { sessionId, category: data.category },
|
|
_max: { order: true },
|
|
});
|
|
|
|
return prisma.swotItem.create({
|
|
data: {
|
|
...data,
|
|
sessionId,
|
|
order: (maxOrder._max.order ?? -1) + 1,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function updateSwotItem(
|
|
itemId: string,
|
|
data: { content?: string; category?: SwotCategory; order?: number }
|
|
) {
|
|
return prisma.swotItem.update({
|
|
where: { id: itemId },
|
|
data,
|
|
});
|
|
}
|
|
|
|
export async function deleteSwotItem(itemId: string) {
|
|
return prisma.swotItem.delete({
|
|
where: { id: itemId },
|
|
});
|
|
}
|
|
|
|
export async function duplicateSwotItem(itemId: string) {
|
|
const original = await prisma.swotItem.findUnique({
|
|
where: { id: itemId },
|
|
});
|
|
|
|
if (!original) {
|
|
throw new Error('Item not found');
|
|
}
|
|
|
|
// Get max order for this category
|
|
const maxOrder = await prisma.swotItem.aggregate({
|
|
where: { sessionId: original.sessionId, category: original.category },
|
|
_max: { order: true },
|
|
});
|
|
|
|
return prisma.swotItem.create({
|
|
data: {
|
|
content: original.content,
|
|
category: original.category,
|
|
sessionId: original.sessionId,
|
|
order: (maxOrder._max.order ?? -1) + 1,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function reorderSwotItems(
|
|
sessionId: string,
|
|
category: SwotCategory,
|
|
itemIds: string[]
|
|
) {
|
|
const updates = itemIds.map((id, index) =>
|
|
prisma.swotItem.update({
|
|
where: { id },
|
|
data: { order: index },
|
|
})
|
|
);
|
|
|
|
return prisma.$transaction(updates);
|
|
}
|
|
|
|
export async function moveSwotItem(
|
|
itemId: string,
|
|
newCategory: SwotCategory,
|
|
newOrder: number
|
|
) {
|
|
return prisma.swotItem.update({
|
|
where: { id: itemId },
|
|
data: {
|
|
category: newCategory,
|
|
order: newOrder,
|
|
},
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// Actions CRUD
|
|
// ============================================
|
|
|
|
export async function createAction(
|
|
sessionId: string,
|
|
data: {
|
|
title: string;
|
|
description?: string;
|
|
priority?: number;
|
|
linkedItemIds: string[];
|
|
}
|
|
) {
|
|
return prisma.action.create({
|
|
data: {
|
|
title: data.title,
|
|
description: data.description,
|
|
priority: data.priority ?? 0,
|
|
sessionId,
|
|
links: {
|
|
create: data.linkedItemIds.map((swotItemId) => ({
|
|
swotItemId,
|
|
})),
|
|
},
|
|
},
|
|
include: {
|
|
links: {
|
|
include: {
|
|
swotItem: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function updateAction(
|
|
actionId: string,
|
|
data: {
|
|
title?: string;
|
|
description?: string;
|
|
priority?: number;
|
|
status?: string;
|
|
dueDate?: Date | null;
|
|
}
|
|
) {
|
|
return prisma.action.update({
|
|
where: { id: actionId },
|
|
data,
|
|
});
|
|
}
|
|
|
|
export async function deleteAction(actionId: string) {
|
|
return prisma.action.delete({
|
|
where: { id: actionId },
|
|
});
|
|
}
|
|
|
|
export async function linkItemToAction(actionId: string, swotItemId: string) {
|
|
return prisma.actionLink.create({
|
|
data: {
|
|
actionId,
|
|
swotItemId,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function unlinkItemFromAction(actionId: string, swotItemId: string) {
|
|
return prisma.actionLink.deleteMany({
|
|
where: {
|
|
actionId,
|
|
swotItemId,
|
|
},
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// Session Sharing
|
|
// ============================================
|
|
|
|
export async function shareSession(
|
|
sessionId: string,
|
|
ownerId: string,
|
|
targetEmail: string,
|
|
role: ShareRole = 'EDITOR'
|
|
) {
|
|
// Verify owner
|
|
const session = await prisma.session.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.sessionShare.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 removeShare(sessionId: string, ownerId: string, shareUserId: string) {
|
|
// Verify owner
|
|
const session = await prisma.session.findFirst({
|
|
where: { id: sessionId, userId: ownerId },
|
|
});
|
|
if (!session) {
|
|
throw new Error('Session not found or not owned');
|
|
}
|
|
|
|
return prisma.sessionShare.deleteMany({
|
|
where: { sessionId, userId: shareUserId },
|
|
});
|
|
}
|
|
|
|
export async function getSessionShares(sessionId: string, userId: string) {
|
|
// Verify access
|
|
if (!(await canAccessSession(sessionId, userId))) {
|
|
throw new Error('Access denied');
|
|
}
|
|
|
|
return prisma.sessionShare.findMany({
|
|
where: { sessionId },
|
|
include: {
|
|
user: { select: { id: true, name: true, email: true } },
|
|
},
|
|
});
|
|
}
|
|
|
|
// ============================================
|
|
// Session Events (for real-time sync)
|
|
// ============================================
|
|
|
|
export type SessionEventType =
|
|
| 'ITEM_CREATED'
|
|
| 'ITEM_UPDATED'
|
|
| 'ITEM_DELETED'
|
|
| 'ITEM_MOVED'
|
|
| 'ACTION_CREATED'
|
|
| 'ACTION_UPDATED'
|
|
| 'ACTION_DELETED'
|
|
| 'SESSION_UPDATED';
|
|
|
|
export async function createSessionEvent(
|
|
sessionId: string,
|
|
userId: string,
|
|
type: SessionEventType,
|
|
payload: Record<string, unknown>
|
|
) {
|
|
return prisma.sessionEvent.create({
|
|
data: {
|
|
sessionId,
|
|
userId,
|
|
type,
|
|
payload: JSON.stringify(payload),
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function getSessionEvents(sessionId: string, since?: Date) {
|
|
return prisma.sessionEvent.findMany({
|
|
where: {
|
|
sessionId,
|
|
...(since && { createdAt: { gt: since } }),
|
|
},
|
|
include: {
|
|
user: { select: { id: true, name: true, email: true } },
|
|
},
|
|
orderBy: { createdAt: 'asc' },
|
|
});
|
|
}
|
|
|
|
export async function getLatestEventTimestamp(sessionId: string) {
|
|
const event = await prisma.sessionEvent.findFirst({
|
|
where: { sessionId },
|
|
orderBy: { createdAt: 'desc' },
|
|
select: { createdAt: true },
|
|
});
|
|
return event?.createdAt;
|
|
}
|
|
|