Download packages/api/src/routers/question.ts from Atmint/cpns: direct link, hf CLI and curl.
- Browser
- Download file 10.5 kB
-
https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/routers/question.ts
- Command line
-
hf download hf://spaces/Atmint/cpns/packages/api/src/routers/question.ts
-
curl -L -o question.ts https://huggingface.co/spaces/Atmint/cpns/resolve/main/packages/api/src/routers/question.ts
10.5 kB
| import { z } from "zod"; | |
| import { eq, and, desc, sql, like, or, inArray } from "drizzle-orm"; | |
| import { router, protectedProcedure, publicProcedure } from "../index"; | |
| import { db } from "@pram/db"; | |
| import { question, examType, sectionType, user } from "@pram/db"; | |
| import { paginationSchema, paginateDefaults } from "../lib/pagination"; | |
| import { buildVisibilityCondition } from "../lib/visibility"; | |
| import { assertOwnership } from "../lib/ownership"; | |
| import { throwNotFound } from "../lib/errors"; | |
| const questionListSelect = { | |
| id: question.id, | |
| examTypeId: question.examTypeId, | |
| sectionTypeId: question.sectionTypeId, | |
| format: question.format, | |
| passageText: question.passageText, | |
| questionText: question.questionText, | |
| options: question.options, | |
| correctAnswer: question.correctAnswer, | |
| explanation: question.explanation, | |
| difficulty: question.difficulty, | |
| skillTags: question.skillTags, | |
| source: question.source, | |
| aiModel: question.aiModel, | |
| creatorUserId: question.creatorUserId, | |
| isPublic: question.isPublic, | |
| usageCount: question.usageCount, | |
| avgRating: question.avgRating, | |
| createdAt: question.createdAt, | |
| updatedAt: question.updatedAt, | |
| creatorName: user.name, | |
| examTypeName: examType.name, | |
| sectionTypeName: sectionType.name, | |
| }; | |
| const questionDetailSelect = { | |
| ...questionListSelect, | |
| }; | |
| function buildSearchCondition(term: string) { | |
| const pattern = `%${term}%`; | |
| return or( | |
| like(question.passageText, pattern), | |
| like(question.questionText, pattern), | |
| like(question.explanation, pattern), | |
| ); | |
| } | |
| function stripAnswer<T extends { correctAnswer?: string; explanation?: string | null }>(row: T) { | |
| const { correctAnswer, explanation, ...rest } = row; | |
| return rest as Omit<T, "correctAnswer" | "explanation">; | |
| } | |
| export const questionRouter = router({ | |
| list: publicProcedure | |
| .input( | |
| z | |
| .object({ | |
| examTypeId: z.string().optional(), | |
| sectionTypeId: z.string().optional(), | |
| format: z.string().optional(), | |
| difficulty: z.number().optional(), | |
| isPublic: z.boolean().optional(), | |
| creatorUserId: z.string().optional(), | |
| search: z.string().optional(), | |
| skillTags: z.array(z.string()).optional(), | |
| ...paginationSchema?.shape, | |
| }) | |
| .optional(), | |
| ) | |
| .query(async ({ ctx, input }) => { | |
| const userId = ctx.session?.user.id; | |
| const conditions = []; | |
| if (input?.examTypeId) conditions.push(eq(question.examTypeId, input.examTypeId)); | |
| if (input?.sectionTypeId) conditions.push(eq(question.sectionTypeId, input.sectionTypeId)); | |
| if (input?.format) conditions.push(eq(question.format, input.format)); | |
| if (input?.difficulty) conditions.push(eq(question.difficulty, input.difficulty)); | |
| if (input?.skillTags?.length) { | |
| conditions.push(sql`${question.skillTags} && ${input.skillTags}`); | |
| } | |
| if (input?.creatorUserId) { | |
| conditions.push(eq(question.creatorUserId, input.creatorUserId)); | |
| } | |
| if (input?.isPublic !== undefined) { | |
| conditions.push(eq(question.isPublic, input.isPublic)); | |
| } else if (!input?.creatorUserId) { | |
| const vis = buildVisibilityCondition(question, userId); | |
| if (vis) conditions.push(vis); | |
| } | |
| if (input?.search) { | |
| const searchCond = buildSearchCondition(input.search); | |
| if (searchCond) conditions.push(searchCond); | |
| } | |
| const where = conditions.length > 0 ? and(...conditions) : undefined; | |
| const { limit, offset } = paginateDefaults(input); | |
| const rows = await db | |
| .select(questionListSelect) | |
| .from(question) | |
| .leftJoin(user, eq(question.creatorUserId, user.id)) | |
| .leftJoin(examType, eq(question.examTypeId, examType.id)) | |
| .leftJoin(sectionType, eq(question.sectionTypeId, sectionType.id)) | |
| .where(where) | |
| .orderBy(desc(question.createdAt)) | |
| .limit(limit) | |
| .offset(offset); | |
| const [countResult] = await db | |
| .select({ count: sql<number>`count(*)` }) | |
| .from(question) | |
| .where(where); | |
| return { questions: rows.map(stripAnswer), total: Number(countResult?.count ?? 0) }; | |
| }), | |
| myQuestions: protectedProcedure | |
| .input( | |
| z | |
| .object({ | |
| search: z.string().optional(), | |
| ...paginationSchema?.shape, | |
| }) | |
| .optional(), | |
| ) | |
| .query(async ({ ctx, input }) => { | |
| const userId = ctx.session.user.id; | |
| const conditions = [eq(question.creatorUserId, userId)]; | |
| if (input?.search) { | |
| const searchCond = buildSearchCondition(input.search); | |
| if (searchCond) conditions.push(searchCond); | |
| } | |
| const where = and(...conditions); | |
| const { limit, offset } = paginateDefaults(input); | |
| const rows = await db | |
| .select({ | |
| id: question.id, | |
| examTypeId: question.examTypeId, | |
| sectionTypeId: question.sectionTypeId, | |
| format: question.format, | |
| passageText: question.passageText, | |
| questionText: question.questionText, | |
| options: question.options, | |
| correctAnswer: question.correctAnswer, | |
| explanation: question.explanation, | |
| difficulty: question.difficulty, | |
| skillTags: question.skillTags, | |
| source: question.source, | |
| isPublic: question.isPublic, | |
| usageCount: question.usageCount, | |
| avgRating: question.avgRating, | |
| createdAt: question.createdAt, | |
| updatedAt: question.updatedAt, | |
| examTypeName: examType.name, | |
| sectionTypeName: sectionType.name, | |
| }) | |
| .from(question) | |
| .leftJoin(examType, eq(question.examTypeId, examType.id)) | |
| .leftJoin(sectionType, eq(question.sectionTypeId, sectionType.id)) | |
| .where(where) | |
| .orderBy(desc(question.createdAt)) | |
| .limit(limit) | |
| .offset(offset); | |
| const [countResult] = await db | |
| .select({ count: sql<number>`count(*)` }) | |
| .from(question) | |
| .where(where); | |
| return { questions: 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 [row] = await db | |
| .select(questionDetailSelect) | |
| .from(question) | |
| .leftJoin(user, eq(question.creatorUserId, user.id)) | |
| .leftJoin(examType, eq(question.examTypeId, examType.id)) | |
| .leftJoin(sectionType, eq(question.sectionTypeId, sectionType.id)) | |
| .where(eq(question.id, input.id)) | |
| .limit(1); | |
| if (!row) return null; | |
| if (!row.isPublic && row.creatorUserId !== userId) return null; | |
| if (row.creatorUserId === userId) return row; | |
| return stripAnswer(row); | |
| }), | |
| create: protectedProcedure | |
| .input( | |
| z.object({ | |
| examTypeId: z.string(), | |
| sectionTypeId: z.string(), | |
| format: z.string(), | |
| passageText: z.string().min(1), | |
| questionText: z.string().min(1), | |
| options: z.any().optional(), | |
| correctAnswer: z.string(), | |
| explanation: z.string().optional(), | |
| difficulty: z.number().min(1).max(5).default(3), | |
| isCaseSensitive: z.boolean().default(false), | |
| skillTags: z.array(z.string()).default([]), | |
| isPublic: z.boolean().default(false), | |
| }), | |
| ) | |
| .mutation(async ({ ctx, input }) => { | |
| const [row] = await db | |
| .insert(question) | |
| .values({ | |
| ...input, | |
| source: "manual", | |
| creatorUserId: ctx.session.user.id, | |
| }) | |
| .returning(); | |
| return row; | |
| }), | |
| update: protectedProcedure | |
| .input( | |
| z.object({ | |
| id: z.string().uuid(), | |
| examTypeId: z.string().optional(), | |
| sectionTypeId: z.string().optional(), | |
| format: z.string().optional(), | |
| passageText: z.string().min(1).optional(), | |
| questionText: z.string().min(1).optional(), | |
| options: z.any().optional(), | |
| correctAnswer: z.string().optional(), | |
| explanation: z.string().optional(), | |
| difficulty: z.number().min(1).max(5).optional(), | |
| skillTags: z.array(z.string()).optional(), | |
| isPublic: z.boolean().optional(), | |
| }), | |
| ) | |
| .mutation(async ({ ctx, input }) => { | |
| const { id, ...data } = input; | |
| const [row] = await db | |
| .update(question) | |
| .set(data) | |
| .where(and(eq(question.id, id), eq(question.creatorUserId, ctx.session.user.id))) | |
| .returning(); | |
| return row ?? null; | |
| }), | |
| togglePublic: protectedProcedure | |
| .input(z.object({ id: z.string().uuid() })) | |
| .mutation(async ({ ctx, input }) => { | |
| const [existing] = await db | |
| .select({ id: question.id, isPublic: question.isPublic, creatorUserId: question.creatorUserId }) | |
| .from(question) | |
| .where(eq(question.id, input.id)) | |
| .limit(1); | |
| if (!existing) { | |
| throwNotFound("Question"); | |
| } | |
| assertOwnership(existing, ctx.session.user.id, "Question"); | |
| const [row] = await db | |
| .update(question) | |
| .set({ isPublic: !existing.isPublic }) | |
| .where(eq(question.id, input.id)) | |
| .returning(); | |
| return row; | |
| }), | |
| delete: protectedProcedure | |
| .input(z.object({ id: z.string().uuid() })) | |
| .mutation(async ({ ctx, input }) => { | |
| const [existing] = await db | |
| .select({ id: question.id, creatorUserId: question.creatorUserId }) | |
| .from(question) | |
| .where(eq(question.id, input.id)) | |
| .limit(1); | |
| assertOwnership(existing, ctx.session.user.id, "Question"); | |
| await db.delete(question).where(eq(question.id, input.id)); | |
| return { success: true }; | |
| }), | |
| bulkPublish: protectedProcedure | |
| .input(z.object({ ids: z.array(z.string().uuid()) })) | |
| .mutation(async ({ ctx, input }) => { | |
| const rows = await db | |
| .select({ id: question.id, creatorUserId: question.creatorUserId, isPublic: question.isPublic }) | |
| .from(question) | |
| .where(inArray(question.id, input.ids)); | |
| const ownRows = rows.filter((r) => r.creatorUserId === ctx.session.user.id); | |
| const skipped = rows.length - ownRows.length; | |
| if (ownRows.length === 0) { | |
| throwNotFound("Question"); | |
| } | |
| const ownIds = ownRows.map((r) => r.id); | |
| await db | |
| .update(question) | |
| .set({ isPublic: true }) | |
| .where(inArray(question.id, ownIds)); | |
| return { success: true, updated: ownRows.length, skipped }; | |
| }), | |
| }); | |