From 42e990f180569baca0f6ed7f4358923330f9a5ab Mon Sep 17 00:00:00 2001 From: hpkoh <53825802+hpkoh@users.noreply.github.com> Date: Sun, 30 Oct 2022 19:51:49 +0800 Subject: [PATCH] [questions][feat] add text search (#456) --- .../server/router/questions-list-router.ts | 275 ----------- .../router/questions-question-router.ts | 437 ------------------ .../questions/questions-question-router.ts | 60 +++ 3 files changed, 60 insertions(+), 712 deletions(-) delete mode 100644 apps/portal/src/server/router/questions-list-router.ts delete mode 100644 apps/portal/src/server/router/questions-question-router.ts diff --git a/apps/portal/src/server/router/questions-list-router.ts b/apps/portal/src/server/router/questions-list-router.ts deleted file mode 100644 index 3187c914..00000000 --- a/apps/portal/src/server/router/questions-list-router.ts +++ /dev/null @@ -1,275 +0,0 @@ -import { z } from 'zod'; -import { TRPCError } from '@trpc/server'; - -import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters'; - -import { createProtectedRouter } from './context'; - -export const questionsListRouter = createProtectedRouter() - .query('getListsByUser', { - async resolve({ ctx }) { - const userId = ctx.session?.user?.id; - - // TODO: Optimize by not returning question entries - const questionsLists = await ctx.prisma.questionsList.findMany({ - include: { - questionEntries: { - include: { - question: { - include: { - _count: { - select: { - answers: true, - comments: true, - }, - }, - encounters: { - select: { - company: true, - location: true, - role: true, - seenAt: true, - }, - }, - user: { - select: { - name: true, - }, - }, - votes: true, - }, - }, - }, - }, - }, - orderBy: { - createdAt: 'asc', - }, - where: { - userId, - }, - }); - - const lists = questionsLists.map((list) => ({ - ...list, - questionEntries: list.questionEntries.map((entry) => ({ - ...entry, - question: createQuestionWithAggregateData(entry.question), - })), - })); - - return lists; - }, - }) - .query('getListById', { - input: z.object({ - listId: z.string(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - const { listId } = input; - - const questionList = await ctx.prisma.questionsList.findFirst({ - include: { - questionEntries: { - include: { - question: { - include: { - _count: { - select: { - answers: true, - comments: true, - }, - }, - encounters: { - select: { - company: true, - location: true, - role: true, - seenAt: true, - }, - }, - user: { - select: { - name: true, - }, - }, - votes: true, - }, - }, - }, - }, - }, - orderBy: { - createdAt: 'asc', - }, - where: { - id: listId, - userId, - }, - }); - - if (!questionList) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Question list not found', - }); - } - - return { - ...questionList, - questionEntries: questionList.questionEntries.map((questionEntry) => ({ - ...questionEntry, - question: createQuestionWithAggregateData(questionEntry.question), - })), - }; - }, - }) - .mutation('create', { - input: z.object({ - name: z.string(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - - const { name } = input; - - return await ctx.prisma.questionsList.create({ - data: { - name, - userId, - }, - }); - }, - }) - .mutation('update', { - input: z.object({ - id: z.string(), - name: z.string().optional(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - const { name, id } = input; - - const listToUpdate = await ctx.prisma.questionsList.findUnique({ - where: { - id: input.id, - }, - }); - - if (listToUpdate?.id !== userId) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'User have no authorization to record.', - }); - } - - return await ctx.prisma.questionsList.update({ - data: { - name, - }, - where: { - id, - }, - }); - }, - }) - .mutation('delete', { - input: z.object({ - id: z.string(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - - const listToDelete = await ctx.prisma.questionsList.findUnique({ - where: { - id: input.id, - }, - }); - - if (listToDelete?.userId !== userId) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'User have no authorization to record.', - }); - } - - return await ctx.prisma.questionsList.delete({ - where: { - id: input.id, - }, - }); - }, - }) - .mutation('createQuestionEntry', { - input: z.object({ - listId: z.string(), - questionId: z.string(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - - const listToAugment = await ctx.prisma.questionsList.findUnique({ - where: { - id: input.listId, - }, - }); - - if (listToAugment?.userId !== userId) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'User have no authorization to record.', - }); - } - - const { questionId, listId } = input; - - return await ctx.prisma.questionsListQuestionEntry.create({ - data: { - listId, - questionId, - }, - }); - }, - }) - .mutation('deleteQuestionEntry', { - input: z.object({ - id: z.string(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - - const entryToDelete = - await ctx.prisma.questionsListQuestionEntry.findUnique({ - where: { - id: input.id, - }, - }); - - if (entryToDelete === null) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Entry not found.', - }); - } - - const listToAugment = await ctx.prisma.questionsList.findUnique({ - where: { - id: entryToDelete.listId, - }, - }); - - if (listToAugment?.userId !== userId) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'User have no authorization to record.', - }); - } - - return await ctx.prisma.questionsListQuestionEntry.delete({ - where: { - id: input.id, - }, - }); - }, - }); diff --git a/apps/portal/src/server/router/questions-question-router.ts b/apps/portal/src/server/router/questions-question-router.ts deleted file mode 100644 index b0c02981..00000000 --- a/apps/portal/src/server/router/questions-question-router.ts +++ /dev/null @@ -1,437 +0,0 @@ -import { z } from 'zod'; -import { QuestionsQuestionType, Vote } from '@prisma/client'; -import { TRPCError } from '@trpc/server'; - -import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters'; - -import { createProtectedRouter } from './context'; - -import { SortOrder, SortType } from '~/types/questions.d'; - -export const questionsQuestionRouter = createProtectedRouter() - .query('getQuestionsByFilter', { - input: z.object({ - companyNames: z.string().array(), - cursor: z - .object({ - idCursor: z.string().optional(), - lastSeenCursor: z.date().nullish().optional(), - upvoteCursor: z.number().optional(), - }) - .nullish(), - endDate: z.date().default(new Date()), - limit: z.number().min(1).default(50), - locations: z.string().array(), - questionTypes: z.nativeEnum(QuestionsQuestionType).array(), - roles: z.string().array(), - sortOrder: z.nativeEnum(SortOrder), - sortType: z.nativeEnum(SortType), - startDate: z.date().optional(), - }), - async resolve({ ctx, input }) { - const { cursor } = input; - - const sortCondition = - input.sortType === SortType.TOP - ? [ - { - upvotes: input.sortOrder, - }, - { - id: input.sortOrder, - }, - ] - : [ - { - lastSeenAt: input.sortOrder, - }, - { - id: input.sortOrder, - }, - ]; - - const questionsData = await ctx.prisma.questionsQuestion.findMany({ - cursor: - cursor !== undefined - ? { - id: cursor ? cursor!.idCursor : undefined, - } - : undefined, - include: { - _count: { - select: { - answers: true, - comments: true, - }, - }, - encounters: { - select: { - company: true, - location: true, - role: true, - seenAt: true, - }, - }, - user: { - select: { - name: true, - }, - }, - votes: true, - }, - orderBy: sortCondition, - take: input.limit + 1, - where: { - ...(input.questionTypes.length > 0 - ? { - questionType: { - in: input.questionTypes, - }, - } - : {}), - encounters: { - some: { - seenAt: { - gte: input.startDate, - lte: input.endDate, - }, - ...(input.companyNames.length > 0 - ? { - company: { - name: { - in: input.companyNames, - }, - }, - } - : {}), - ...(input.locations.length > 0 - ? { - location: { - in: input.locations, - }, - } - : {}), - ...(input.roles.length > 0 - ? { - role: { - in: input.roles, - }, - } - : {}), - }, - }, - }, - }); - - const processedQuestionsData = questionsData.map( - createQuestionWithAggregateData, - ); - - let nextCursor: typeof cursor | undefined = undefined; - - if (questionsData.length > input.limit) { - const nextItem = questionsData.pop()!; - processedQuestionsData.pop(); - - const nextIdCursor: string | undefined = nextItem.id; - const nextLastSeenCursor = - input.sortType === SortType.NEW ? nextItem.lastSeenAt : undefined; - const nextUpvoteCursor = - input.sortType === SortType.TOP ? nextItem.upvotes : undefined; - - nextCursor = { - idCursor: nextIdCursor, - lastSeenCursor: nextLastSeenCursor, - upvoteCursor: nextUpvoteCursor, - }; - } - - return { - data: processedQuestionsData, - nextCursor, - }; - }, - }) - .query('getQuestionById', { - input: z.object({ - id: z.string(), - }), - async resolve({ ctx, input }) { - const questionData = await ctx.prisma.questionsQuestion.findUnique({ - include: { - _count: { - select: { - answers: true, - comments: true, - }, - }, - encounters: { - select: { - company: true, - location: true, - role: true, - seenAt: true, - }, - }, - user: { - select: { - name: true, - }, - }, - votes: true, - }, - where: { - id: input.id, - }, - }); - if (!questionData) { - throw new TRPCError({ - code: 'NOT_FOUND', - message: 'Question not found', - }); - } - - return createQuestionWithAggregateData(questionData); - }, - }) - .mutation('create', { - input: z.object({ - companyId: z.string(), - content: z.string(), - location: z.string(), - questionType: z.nativeEnum(QuestionsQuestionType), - role: z.string(), - seenAt: z.date(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - - return await ctx.prisma.questionsQuestion.create({ - data: { - content: input.content, - encounters: { - create: { - company: { - connect: { - id: input.companyId, - }, - }, - location: input.location, - role: input.role, - seenAt: input.seenAt, - user: { - connect: { - id: userId, - }, - }, - }, - }, - lastSeenAt: input.seenAt, - questionType: input.questionType, - userId, - }, - }); - }, - }) - .mutation('update', { - input: z.object({ - content: z.string().optional(), - id: z.string(), - questionType: z.nativeEnum(QuestionsQuestionType).optional(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - - const questionToUpdate = await ctx.prisma.questionsQuestion.findUnique({ - where: { - id: input.id, - }, - }); - - if (questionToUpdate?.id !== userId) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'User have no authorization to record.', - // Optional: pass the original error to retain stack trace - }); - } - - const { content, questionType } = input; - - return await ctx.prisma.questionsQuestion.update({ - data: { - content, - questionType, - }, - where: { - id: input.id, - }, - }); - }, - }) - .mutation('delete', { - input: z.object({ - id: z.string(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - - const questionToDelete = await ctx.prisma.questionsQuestion.findUnique({ - where: { - id: input.id, - }, - }); - - if (questionToDelete?.id !== userId) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'User have no authorization to record.', - // Optional: pass the original error to retain stack trace - }); - } - - return await ctx.prisma.questionsQuestion.delete({ - where: { - id: input.id, - }, - }); - }, - }) - .query('getVote', { - input: z.object({ - questionId: z.string(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - const { questionId } = input; - - return await ctx.prisma.questionsQuestionVote.findUnique({ - where: { - questionId_userId: { questionId, userId }, - }, - }); - }, - }) - .mutation('createVote', { - input: z.object({ - questionId: z.string(), - vote: z.nativeEnum(Vote), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - const { questionId, vote } = input; - - const incrementValue = vote === Vote.UPVOTE ? 1 : -1; - - const [questionVote] = await ctx.prisma.$transaction([ - ctx.prisma.questionsQuestionVote.create({ - data: { - questionId, - userId, - vote, - }, - }), - ctx.prisma.questionsQuestion.update({ - data: { - upvotes: { - increment: incrementValue, - }, - }, - where: { - id: questionId, - }, - }), - ]); - return questionVote; - }, - }) - .mutation('updateVote', { - input: z.object({ - id: z.string(), - vote: z.nativeEnum(Vote), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - const { id, vote } = input; - - const voteToUpdate = await ctx.prisma.questionsQuestionVote.findUnique({ - where: { - id: input.id, - }, - }); - - if (voteToUpdate?.userId !== userId) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'User have no authorization to record.', - }); - } - - const incrementValue = vote === Vote.UPVOTE ? 2 : -2; - - const [questionVote] = await ctx.prisma.$transaction([ - ctx.prisma.questionsQuestionVote.update({ - data: { - vote, - }, - where: { - id, - }, - }), - ctx.prisma.questionsQuestion.update({ - data: { - upvotes: { - increment: incrementValue, - }, - }, - where: { - id: voteToUpdate.questionId, - }, - }), - ]); - - return questionVote; - }, - }) - .mutation('deleteVote', { - input: z.object({ - id: z.string(), - }), - async resolve({ ctx, input }) { - const userId = ctx.session?.user?.id; - - const voteToDelete = await ctx.prisma.questionsQuestionVote.findUnique({ - where: { - id: input.id, - }, - }); - - if (voteToDelete?.userId !== userId) { - throw new TRPCError({ - code: 'UNAUTHORIZED', - message: 'User have no authorization to record.', - }); - } - - const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1; - - const [questionVote] = await ctx.prisma.$transaction([ - ctx.prisma.questionsQuestionVote.delete({ - where: { - id: input.id, - }, - }), - ctx.prisma.questionsQuestion.update({ - data: { - upvotes: { - increment: incrementValue, - }, - }, - where: { - id: voteToDelete.questionId, - }, - }), - ]); - return questionVote; - }, - }); diff --git a/apps/portal/src/server/router/questions/questions-question-router.ts b/apps/portal/src/server/router/questions/questions-question-router.ts index f3b48b9b..6ac47532 100644 --- a/apps/portal/src/server/router/questions/questions-question-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-router.ts @@ -193,4 +193,64 @@ export const questionsQuestionRouter = createRouter() return createQuestionWithAggregateData(questionData); }, + }) + .query('getRelatedQuestions', { + input: z.object({ + content: z.string(), + }), + async resolve({ ctx, input }) { + const escapeChars = /[()|&:*!]/g; + + const query = + input.content + .replace(escapeChars, " ") + .trim() + .split(/\s+/) + .join(" | "); + + const relatedQuestionsId : Array<{id:string}> = await ctx.prisma.$queryRaw` + SELECT id FROM "QuestionsQuestion" + WHERE + to_tsvector("content") @@ to_tsquery('english', ${query}) + ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC; + `; + + const relatedQuestionsIdArray = relatedQuestionsId.map(current => current.id); + + const relatedQuestionsData = await ctx.prisma.questionsQuestion.findMany({ + include: { + _count: { + select: { + answers: true, + comments: true, + }, + }, + encounters: { + select: { + company: true, + location: true, + role: true, + seenAt: true, + }, + }, + user: { + select: { + name: true, + }, + }, + votes: true, + }, + where: { + id : { + in : relatedQuestionsIdArray, + } + }, + }); + + const processedQuestionsData = relatedQuestionsData.map( + createQuestionWithAggregateData, + ); + + return processedQuestionsData; + } });