Study/src/services/shareService.ts

227 lines
6.9 KiB
TypeScript

import { prisma } from "@/lib/db";
export async function getShareLink(token: string) {
const link = await prisma.shareLink.findUnique({
where: { id: token },
include: {
deck: {
include: {
cards: { orderBy: { sortOrder: "asc" } },
class: { select: { slug: true, name: true } },
},
},
quizSet: {
include: {
questions: {
orderBy: { sortOrder: "asc" },
include: {
options: { orderBy: { sortOrder: "asc" } },
},
},
class: { select: { slug: true, name: true } },
},
},
group: {
include: {
class: { select: { slug: true, name: true } },
decks: {
include: {
cards: { orderBy: { sortOrder: "asc" } },
class: { select: { slug: true, name: true } },
},
orderBy: { sortOrder: "asc" },
},
quizSets: {
include: {
questions: {
orderBy: { sortOrder: "asc" },
include: {
options: { orderBy: { sortOrder: "asc" } },
},
},
class: { select: { slug: true, name: true } },
},
orderBy: { sortOrder: "asc" },
},
}
}
},
});
if (!link) return null;
const targets = [link.deck, link.quizSet, link.group].filter(Boolean);
const agrees =
(link.targetType === "DECK" && Boolean(link.deck)) ||
(link.targetType === "QUIZ" && Boolean(link.quizSet)) ||
(link.targetType === "GROUP" && Boolean(link.group));
if (targets.length !== 1 || !agrees) return null;
if (!link.group) return link;
return {
...link,
group: {
...link.group,
decks: link.group.decks.filter(
(deck) =>
link.group?.type === "DECK" &&
deck.class.slug === link.group.class.slug
),
quizSets: link.group.quizSets.filter(
(quiz) =>
link.group?.type === "QUIZ" &&
quiz.class.slug === link.group.class.slug
),
},
};
}
/**
* Names, descriptions and counts only — enough to build social/link-preview
* metadata without loading every card, question and option.
*/
export async function getShareLinkMeta(token: string) {
const link = await prisma.shareLink.findUnique({
where: { id: token },
select: {
targetType: true,
deck: {
select: {
name: true,
description: true,
class: { select: { slug: true, name: true } },
_count: { select: { cards: true } },
},
},
quizSet: {
select: {
name: true,
description: true,
class: { select: { slug: true, name: true } },
_count: { select: { questions: true } },
},
},
group: {
select: {
name: true,
type: true,
class: { select: { slug: true, name: true } },
decks: {
select: {
id: true,
name: true,
description: true,
class: { select: { slug: true, name: true } },
_count: { select: { cards: true } },
},
orderBy: { sortOrder: "asc" },
},
quizSets: {
select: {
id: true,
name: true,
description: true,
class: { select: { slug: true, name: true } },
_count: { select: { questions: true } },
},
orderBy: { sortOrder: "asc" },
},
},
},
},
});
if (!link) return null;
const targets = [link.deck, link.quizSet, link.group].filter(Boolean);
const agrees =
(link.targetType === "DECK" && Boolean(link.deck)) ||
(link.targetType === "QUIZ" && Boolean(link.quizSet)) ||
(link.targetType === "GROUP" && Boolean(link.group));
if (targets.length !== 1 || !agrees) return null;
if (!link.group) return link;
return {
...link,
group: {
...link.group,
decks: link.group.decks.filter(
(deck) =>
link.group?.type === "DECK" &&
deck.class.slug === link.group.class.slug
),
quizSets: link.group.quizSets.filter(
(quiz) =>
link.group?.type === "QUIZ" &&
quiz.class.slug === link.group.class.slug
),
},
};
}
export async function getShareLinkForContent(targetType: "DECK" | "QUIZ" | "GROUP", contentId: string) {
if (targetType === "DECK") {
return prisma.shareLink.findUnique({ where: { deckId: contentId } });
}
if (targetType === "GROUP") {
return prisma.shareLink.findUnique({ where: { groupId: contentId } });
}
return prisma.shareLink.findUnique({ where: { quizSetId: contentId } });
}
export async function toggleShareLink(targetType: "DECK" | "QUIZ" | "GROUP", contentId: string) {
return prisma.$transaction(async (transaction) => {
const target =
targetType === "DECK"
? await transaction.deck.findUnique({
where: { id: contentId },
select: { id: true },
})
: targetType === "QUIZ"
? await transaction.quizSet.findUnique({
where: { id: contentId },
select: { id: true },
})
: await transaction.materialGroup.findUnique({
where: { id: contentId },
select: { id: true },
});
if (!target) throw new ShareTargetNotFoundError("Share target not found");
const existing =
targetType === "DECK"
? await transaction.shareLink.findUnique({ where: { deckId: contentId } })
: targetType === "QUIZ"
? await transaction.shareLink.findUnique({ where: { quizSetId: contentId } })
: await transaction.shareLink.findUnique({ where: { groupId: contentId } });
if (existing) {
await transaction.shareLink.delete({ where: { id: existing.id } });
return null;
}
return transaction.shareLink.create({
data: {
targetType,
deckId: targetType === "DECK" ? contentId : null,
quizSetId: targetType === "QUIZ" ? contentId : null,
groupId: targetType === "GROUP" ? contentId : null,
},
});
});
}
export class ShareTargetNotFoundError extends Error {}
export async function isContentSharedViaGroup(targetType: "DECK" | "QUIZ", contentId: string) {
let groupId: string | null = null;
if (targetType === "DECK") {
const deck = await prisma.deck.findUnique({ where: { id: contentId }, select: { groupId: true } });
groupId = deck?.groupId || null;
} else {
const quiz = await prisma.quizSet.findUnique({ where: { id: contentId }, select: { groupId: true } });
groupId = quiz?.groupId || null;
}
if (!groupId) return null;
const groupLink = await prisma.shareLink.findUnique({
where: { groupId },
include: { group: true },
});
return groupLink ? { token: groupLink.id, groupName: groupLink.group?.name } : null;
}