Download packages/api/src/routers/combo.ts from Atmint/cpns: direct link, hf CLI and curl.
- Browser
- Download file 13.7 kB
-
https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/routers/combo.ts
- Command line
-
hf download hf://spaces/Atmint/cpns/packages/api/src/routers/combo.ts
-
curl -L -o combo.ts https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/routers/combo.ts
13.7 kB
| import { z } from "zod"; | |
| import { eq, and, desc, sql, inArray } from "drizzle-orm"; | |
| import { router, protectedProcedure, publicProcedure } from "../index"; | |
| import { db } from "@pram/db"; | |
| import { | |
| comboPackage, | |
| comboSection, | |
| testPackage, | |
| packageSection, | |
| sectionQuestion, | |
| question, | |
| examType, | |
| sectionType, | |
| user, | |
| } from "@pram/db"; | |
| import { paginationSchema, paginateDefaults } from "../lib/pagination"; | |
| import { assertOwnership } from "../lib/ownership"; | |
| import { buildVisibilityCondition } from "../lib/visibility"; | |
| import { throwBadRequest } from "../lib/errors"; | |
| export const comboRouter = router({ | |
| list: publicProcedure | |
| .input( | |
| z | |
| .object({ | |
| isPublic: z.boolean().optional(), | |
| search: z.string().optional(), | |
| ...paginationSchema.shape, | |
| }) | |
| .optional(), | |
| ) | |
| .query(async ({ ctx, input }) => { | |
| const userId = ctx.session?.user.id; | |
| const { limit, offset } = paginateDefaults(input); | |
| const conditions = []; | |
| if (input?.isPublic !== undefined) { | |
| conditions.push(eq(comboPackage.isPublic, input.isPublic)); | |
| } else { | |
| const vis = buildVisibilityCondition(comboPackage, userId); | |
| if (vis) conditions.push(vis); | |
| } | |
| const where = conditions.length > 0 ? and(...conditions) : undefined; | |
| const rows = await db | |
| .select({ | |
| id: comboPackage.id, | |
| title: comboPackage.title, | |
| description: comboPackage.description, | |
| creatorUserId: comboPackage.creatorUserId, | |
| isPublic: comboPackage.isPublic, | |
| createdAt: comboPackage.createdAt, | |
| updatedAt: comboPackage.updatedAt, | |
| creatorName: user.name, | |
| }) | |
| .from(comboPackage) | |
| .leftJoin(user, eq(comboPackage.creatorUserId, user.id)) | |
| .where(where) | |
| .orderBy(desc(comboPackage.createdAt)) | |
| .limit(limit) | |
| .offset(offset); | |
| const [countResult] = await db | |
| .select({ count: sql<number>`count(*)` }) | |
| .from(comboPackage) | |
| .where(where); | |
| return { combos: rows, total: Number(countResult?.count ?? 0) }; | |
| }), | |
| myCombos: protectedProcedure | |
| .input( | |
| z | |
| .object({ | |
| search: z.string().optional(), | |
| ...paginationSchema.shape, | |
| }) | |
| .optional(), | |
| ) | |
| .query(async ({ ctx, input }) => { | |
| const userId = ctx.session.user.id; | |
| const { limit, offset } = paginateDefaults(input); | |
| const conditions = [eq(comboPackage.creatorUserId, userId)]; | |
| const where = and(...conditions); | |
| const rows = await db | |
| .select({ | |
| id: comboPackage.id, | |
| title: comboPackage.title, | |
| description: comboPackage.description, | |
| isPublic: comboPackage.isPublic, | |
| createdAt: comboPackage.createdAt, | |
| updatedAt: comboPackage.updatedAt, | |
| }) | |
| .from(comboPackage) | |
| .where(where) | |
| .orderBy(desc(comboPackage.createdAt)) | |
| .limit(limit) | |
| .offset(offset); | |
| const [countResult] = await db | |
| .select({ count: sql<number>`count(*)` }) | |
| .from(comboPackage) | |
| .where(where); | |
| return { combos: rows, total: Number(countResult?.count ?? 0) }; | |
| }), | |
| getById: publicProcedure | |
| .input(z.object({ id: z.string().uuid() })) | |
| .query(async ({ ctx, input }) => { | |
| const userId = ctx.session?.user.id; | |
| const [combo] = await db | |
| .select({ | |
| id: comboPackage.id, | |
| title: comboPackage.title, | |
| description: comboPackage.description, | |
| creatorUserId: comboPackage.creatorUserId, | |
| isPublic: comboPackage.isPublic, | |
| createdAt: comboPackage.createdAt, | |
| updatedAt: comboPackage.updatedAt, | |
| creatorName: user.name, | |
| }) | |
| .from(comboPackage) | |
| .leftJoin(user, eq(comboPackage.creatorUserId, user.id)) | |
| .where(eq(comboPackage.id, input.id)) | |
| .limit(1); | |
| if (!combo) return null; | |
| if (!combo.isPublic && combo.creatorUserId !== userId) return null; | |
| // Fetch combo sections with source data | |
| const sections = await db | |
| .select({ | |
| id: comboSection.id, | |
| comboId: comboSection.comboId, | |
| sourcePackageId: comboSection.sourcePackageId, | |
| sourceSectionId: comboSection.sourceSectionId, | |
| orderIndex: comboSection.orderIndex, | |
| packageTitle: testPackage.title, | |
| sectionTitle: packageSection.title, | |
| sectionTypeName: sectionType.name, | |
| examTypeName: examType.name, | |
| }) | |
| .from(comboSection) | |
| .leftJoin(testPackage, eq(comboSection.sourcePackageId, testPackage.id)) | |
| .leftJoin(packageSection, eq(comboSection.sourceSectionId, packageSection.id)) | |
| .leftJoin(sectionType, eq(packageSection.sectionTypeId, sectionType.id)) | |
| .leftJoin(examType, eq(testPackage.examTypeId, examType.id)) | |
| .where(eq(comboSection.comboId, input.id)) | |
| .orderBy(comboSection.orderIndex); | |
| // Fetch questions for each section | |
| const sectionIds = sections.map((s) => s.sourceSectionId); | |
| let sectionQuestions: Array<{ | |
| sectionId: string; | |
| questionId: string; | |
| orderIndex: number; | |
| question: any; | |
| }> = []; | |
| if (sectionIds.length > 0) { | |
| const sqs = await db | |
| .select({ | |
| sectionId: sectionQuestion.sectionId, | |
| questionId: sectionQuestion.questionId, | |
| orderIndex: sectionQuestion.orderIndex, | |
| }) | |
| .from(sectionQuestion) | |
| .where(inArray(sectionQuestion.sectionId, sectionIds)); | |
| if (sqs.length > 0) { | |
| const questionIds = sqs.map((sq) => sq.questionId); | |
| const qs = await db | |
| .select() | |
| .from(question) | |
| .where(inArray(question.id, questionIds)); | |
| const questionMap = new Map(qs.map((q) => [q.id, q])); | |
| sectionQuestions = sqs.map((sq) => ({ | |
| ...sq, | |
| question: questionMap.get(sq.questionId), | |
| })); | |
| } | |
| } | |
| const isOwner = combo.creatorUserId === userId; | |
| const sectionsWithQuestions = sections.map((section) => ({ | |
| ...section, | |
| questions: sectionQuestions | |
| .filter((sq) => sq.sectionId === section.sourceSectionId) | |
| .sort((a, b) => a.orderIndex - b.orderIndex) | |
| .map((sq) => { | |
| if (isOwner) return sq.question; | |
| const { correctAnswer, explanation, ...rest } = sq.question; | |
| return rest; | |
| }), | |
| })); | |
| return { ...combo, sections: sectionsWithQuestions }; | |
| }), | |
| create: protectedProcedure | |
| .input( | |
| z.object({ | |
| title: z.string().min(1).max(200), | |
| description: z.string().optional(), | |
| isPublic: z.boolean().default(false), | |
| sections: z.array( | |
| z.object({ | |
| sourcePackageId: z.string().uuid(), | |
| sourceSectionId: z.string().uuid(), | |
| orderIndex: z.number().default(0), | |
| }), | |
| ).min(1), | |
| }), | |
| ) | |
| .mutation(async ({ ctx, input }) => { | |
| const { sections, ...pkgData } = input; | |
| // 1. Create combo record | |
| const [combo] = await db | |
| .insert(comboPackage) | |
| .values({ | |
| ...pkgData, | |
| creatorUserId: ctx.session.user.id, | |
| }) | |
| .returning(); | |
| if (!combo) { | |
| throwBadRequest("Failed to create combo package"); | |
| } | |
| await db.insert(comboSection).values( | |
| sections.map((s) => ({ | |
| comboId: combo.id, | |
| ...s, | |
| })), | |
| ); | |
| // 2. Derive examTypeId from the first source package | |
| const firstSourcePkgId = sections[0]?.sourcePackageId; | |
| let examTypeId = ""; | |
| if (firstSourcePkgId) { | |
| const [srcPkg] = await db | |
| .select({ examTypeId: testPackage.examTypeId }) | |
| .from(testPackage) | |
| .where(eq(testPackage.id, firstSourcePkgId)) | |
| .limit(1); | |
| examTypeId = srcPkg?.examTypeId ?? ""; | |
| } | |
| // 3. Also create a testPackage so the combo appears in /packages | |
| const [pkg] = await db | |
| .insert(testPackage) | |
| .values({ | |
| title: pkgData.title, | |
| description: pkgData.description, | |
| examTypeId, | |
| creatorUserId: ctx.session.user.id, | |
| isPublic: pkgData.isPublic, | |
| totalQuestions: 0, | |
| totalSections: 0, | |
| estimatedDurationMin: sections.length * 20, // rough estimate | |
| }) | |
| .returning(); | |
| if (pkg) { | |
| // 4. Duplicate each source section into the new package | |
| const sourceSectionIds = sections.map((s) => s.sourceSectionId); | |
| const sourceSections = sourceSectionIds.length > 0 | |
| ? await db | |
| .select() | |
| .from(packageSection) | |
| .where(inArray(packageSection.id, sourceSectionIds)) | |
| : []; | |
| const sourceSectionMap = new Map(sourceSections.map((s) => [s.id, s])); | |
| let totalQuestions = 0; | |
| for (const secInput of sections) { | |
| const srcSec = sourceSectionMap.get(secInput.sourceSectionId); | |
| if (!srcSec) continue; | |
| const [newSec] = await db | |
| .insert(packageSection) | |
| .values({ | |
| packageId: pkg.id, | |
| sectionTypeId: srcSec.sectionTypeId, | |
| title: srcSec.title, | |
| orderIndex: secInput.orderIndex, | |
| }) | |
| .returning(); | |
| // Copy questions from source section | |
| const srcQuestions = await db | |
| .select() | |
| .from(sectionQuestion) | |
| .where(eq(sectionQuestion.sectionId, secInput.sourceSectionId)) | |
| .orderBy(sectionQuestion.orderIndex); | |
| if (newSec && srcQuestions.length > 0) { | |
| await db.insert(sectionQuestion).values( | |
| srcQuestions.map((sq, idx) => ({ | |
| sectionId: newSec.id, | |
| questionId: sq.questionId, | |
| orderIndex: idx, | |
| })), | |
| ); | |
| totalQuestions += srcQuestions.length; | |
| } | |
| } | |
| // Update totals | |
| await db | |
| .update(testPackage) | |
| .set({ | |
| totalSections: sections.length, | |
| totalQuestions, | |
| }) | |
| .where(eq(testPackage.id, pkg.id)); | |
| } | |
| return { ...combo, packageId: pkg?.id }; | |
| }), | |
| update: protectedProcedure | |
| .input( | |
| z.object({ | |
| id: z.string().uuid(), | |
| title: z.string().min(1).max(200).optional(), | |
| description: z.string().optional(), | |
| isPublic: z.boolean().optional(), | |
| }), | |
| ) | |
| .mutation(async ({ ctx, input }) => { | |
| const { id, ...data } = input; | |
| const [combo] = await db | |
| .select() | |
| .from(comboPackage) | |
| .where(eq(comboPackage.id, id)) | |
| .limit(1); | |
| assertOwnership(combo, ctx.session.user.id, "Combo"); | |
| const [updated] = await db | |
| .update(comboPackage) | |
| .set(data) | |
| .where(eq(comboPackage.id, id)) | |
| .returning(); | |
| return updated ?? null; | |
| }), | |
| delete: protectedProcedure | |
| .input(z.object({ id: z.string().uuid() })) | |
| .mutation(async ({ ctx, input }) => { | |
| const [combo] = await db | |
| .select() | |
| .from(comboPackage) | |
| .where(eq(comboPackage.id, input.id)) | |
| .limit(1); | |
| assertOwnership(combo, ctx.session.user.id, "Combo"); | |
| await db.delete(comboPackage).where(eq(comboPackage.id, input.id)); | |
| return { success: true }; | |
| }), | |
| // Get available sections from packages for combiner | |
| availableSections: protectedProcedure | |
| .input( | |
| z.object({ | |
| examTypeId: z.string().optional(), | |
| search: z.string().optional(), | |
| ...paginationSchema.shape, | |
| }).optional(), | |
| ) | |
| .query(async ({ ctx, input }) => { | |
| const userId = ctx.session.user.id; | |
| const { limit, offset } = paginateDefaults(input); | |
| const pkgConditions = []; | |
| const vis = buildVisibilityCondition(testPackage, userId); | |
| if (vis) pkgConditions.push(vis); | |
| if (input?.examTypeId) { | |
| pkgConditions.push(eq(testPackage.examTypeId, input.examTypeId)); | |
| } | |
| const pkgWhere = pkgConditions.length > 0 ? and(...pkgConditions) : undefined; | |
| const packages = await db | |
| .select({ | |
| id: testPackage.id, | |
| title: testPackage.title, | |
| examTypeId: testPackage.examTypeId, | |
| creatorUserId: testPackage.creatorUserId, | |
| isPublic: testPackage.isPublic, | |
| examTypeName: examType.name, | |
| }) | |
| .from(testPackage) | |
| .leftJoin(examType, eq(testPackage.examTypeId, examType.id)) | |
| .where(pkgWhere) | |
| .limit(limit) | |
| .offset(offset); | |
| const packageIds = packages.map((p) => p.id); | |
| const sections = await db | |
| .select({ | |
| id: packageSection.id, | |
| packageId: packageSection.packageId, | |
| sectionTypeId: packageSection.sectionTypeId, | |
| title: packageSection.title, | |
| orderIndex: packageSection.orderIndex, | |
| packageTitle: testPackage.title, | |
| examTypeName: examType.name, | |
| sectionTypeName: sectionType.name, | |
| }) | |
| .from(packageSection) | |
| .leftJoin(testPackage, eq(packageSection.packageId, testPackage.id)) | |
| .leftJoin(examType, eq(testPackage.examTypeId, examType.id)) | |
| .leftJoin(sectionType, eq(packageSection.sectionTypeId, sectionType.id)) | |
| .where(inArray(packageSection.packageId, packageIds)) | |
| .orderBy(testPackage.title, packageSection.orderIndex); | |
| return { packages, sections }; | |
| }), | |
| }); | |