From de94958ce1ac011164507b95666bab70526f4284 Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Sun, 30 Oct 2022 19:32:23 +0800 Subject: [PATCH 1/3] [questions][chore] add questions seeding script (#450) --- apps/portal/package.json | 3 +- apps/portal/prisma/seed-questions.ts | 207 +++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 apps/portal/prisma/seed-questions.ts diff --git a/apps/portal/package.json b/apps/portal/package.json index a8a184d8..303c6163 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -9,7 +9,8 @@ "lint": "next lint", "tsc": "tsc", "postinstall": "prisma generate", - "seed": "ts-node prisma/seed.ts" + "seed": "ts-node prisma/seed.ts", + "seed-questions": "ts-node prisma/seed-questions.ts" }, "dependencies": { "@headlessui/react": "^1.7.3", diff --git a/apps/portal/prisma/seed-questions.ts b/apps/portal/prisma/seed-questions.ts new file mode 100644 index 00000000..6f8ebbf4 --- /dev/null +++ b/apps/portal/prisma/seed-questions.ts @@ -0,0 +1,207 @@ +import { PrismaClient } from '@prisma/client'; + +import { JobTitleLabels } from '../src/components/shared/JobTitles'; + +const prisma = new PrismaClient(); + +type QuestionCreateData = Parameters< + typeof prisma.questionsQuestion.create +>[0]['data']; + +function selectRandomRole() { + const roles = Object.keys(JobTitleLabels); + const randomIndex = Math.floor(Math.random() * roles.length); + return roles[randomIndex]; +} + +function generateRandomDate() { + // Return a date between 2020 and 2022. + const start = new Date(2020, 0, 1); + const end = new Date(2022, 0, 1); + return new Date( + start.getTime() + Math.random() * (end.getTime() - start.getTime()), + ); +} + +function generateRandomCodingAnswer() { + return CODING_ANSWER_CONTENT[ + Math.floor(Math.random() * CODING_ANSWER_CONTENT.length) + ]; +} + +function generateRandomBehavioralAnswer() { + return BEHAVIORAL_ANSWER_CONTENT[ + Math.floor(Math.random() * BEHAVIORAL_ANSWER_CONTENT.length) + ]; +} + +const CODING_QUESTION_CONTENT = [ + 'Given a string, find the length of the longest substring without repeating characters.', + 'Given an array of integers, return indices of the two numbers such that they add up to a specific target.', + 'Given a contiguous sequence of numbers in which each number repeats thrice, there is exactly one missing number. Find the missing number.', + 'Find the contiguous subarray within an array (containing at least one number) which has the largest product.', + 'Find a contiguous subarray which has the largest sum.', +]; + +const BEHAVIORAL_QUESTION_CONTENT = [ + 'Tell me about a time you had to work with a difficult person.', + 'Rate your communication skills on a scale of 1 to 10.', + 'Are you a team player?', + 'What is your greatest weakness?', + 'What is your greatest strength?', + 'What is your biggest accomplishment?', + 'What is your biggest failure?', + 'Be honest, how would your friends describe you?', + 'How do you handle stress?', + 'Let’s say you have a deadline to meet. How do you prioritize your work?', +]; + +const CODING_ANSWER_CONTENT = [ + 'This question is easy. Just use a hash map.', + 'This question is hard. I have no idea how to solve it.', + 'This question is medium. I can solve it in 30 minutes.', + 'Can be done with a simple for loop.', + 'Simple recursion can solve this.', + 'Please explain the question again.', + 'Question is not clear.', + 'Brute force solution is the best.', +]; + +const BEHAVIORAL_ANSWER_CONTENT = [ + 'This is a very common question. I have a lot of experience with this.', + "I don't think this is a good question to ask. However, I can answer it.", + 'Most companies ask this question. I think you should ask something else.', + 'I try to take a step back and assess the situation. I figure out what is the most important thing to do and what can wait. I also try to delegate or ask for help when needed.', + 'I try to have a discussion with my manager or the person who I feel is not valuing my work. I try to explain how I feel and what I would like to see change.', + 'I try to have a discussion with the coworker. I try to understand their perspective and see if there is a way to resolve the issue.', +]; + +const CODING_QUESTIONS: Array = CODING_QUESTION_CONTENT.map( + (content) => ({ + content, + questionType: 'CODING', + userId: null, + encounters: { + create: { + location: 'Singapore', + role: selectRandomRole(), + seenAt: generateRandomDate(), + }, + }, + }), +); + +const BEHAVIORAL_QUESTIONS: Array = + BEHAVIORAL_QUESTION_CONTENT.map((content) => ({ + content, + questionType: 'BEHAVIORAL', + userId: null, + encounters: { + create: { + location: 'Singapore', + role: selectRandomRole(), + seenAt: generateRandomDate(), + }, + }, + })); + +const QUESTIONS: Array = [ + ...CODING_QUESTIONS, + ...BEHAVIORAL_QUESTIONS, +]; + +async function main() { + console.log('Performing preliminary checks...'); + + const firstCompany = await prisma.company.findFirst(); + if (!firstCompany) { + throw new Error( + 'No company found. Please seed db with some companies first.', + ); + } + + // Generate random answers to the questions + const users = await prisma.user.findMany(); + if (users.length === 0) { + throw new Error('No users found. Please seed db with some users first.'); + } + + console.log('Seeding started...'); + + console.log('Creating coding and behavioral questions...'); + await Promise.all([ + QUESTIONS.map(async (question) => { + await prisma.questionsQuestion.create({ + data: { + ...question, + encounters: { + create: { + ...question.encounters!.create, + companyId: firstCompany.id, + } as any, + }, + }, + }); + }), + ]); + + console.log('Creating answers to coding questions...'); + const codingQuestions = await prisma.questionsQuestion.findMany({ + where: { + questionType: 'CODING', + }, + }); + await Promise.all( + codingQuestions.map(async (question) => { + const answers = Array.from( + { length: Math.floor(Math.random() * 5) }, + () => ({ + content: generateRandomCodingAnswer(), + userId: users[Math.floor(Math.random() * users.length)].id, + questionId: question.id, + }), + ); + + await prisma.questionsAnswer.createMany({ + data: answers, + }); + }), + ); + + console.log('Creating answers to behavioral questions...'); + const behavioralQuestions = await prisma.questionsQuestion.findMany({ + where: { + questionType: 'BEHAVIORAL', + }, + }); + + await Promise.all( + behavioralQuestions.map(async (question) => { + const answers = Array.from( + { length: Math.floor(Math.random() * 5) }, + + () => ({ + content: generateRandomBehavioralAnswer(), + userId: users[Math.floor(Math.random() * users.length)].id, + questionId: question.id, + }), + ); + + await prisma.questionsAnswer.createMany({ + data: answers, + }); + }), + ); + + console.log('Seeding completed.'); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); From 389862feb35b6c7fd27261d6a8c8bf8a3507702d Mon Sep 17 00:00:00 2001 From: Jeff Sieu Date: Sun, 30 Oct 2022 19:32:33 +0800 Subject: [PATCH 2/3] [questions][ui] update roles typeahead, add sticky search bar (#451) --- .../questions/ContributeQuestionCard.tsx | 88 ++++++++------- .../questions/typeahead/RoleTypeahead.tsx | 10 +- .../[questionId]/[questionSlug]/index.tsx | 16 ++- apps/portal/src/pages/questions/browse.tsx | 101 +++++++++--------- apps/portal/src/pages/questions/lists.tsx | 64 +++++------ .../questions/relabelQuestionAggregates.ts | 26 +++++ 6 files changed, 176 insertions(+), 129 deletions(-) create mode 100644 apps/portal/src/utils/questions/relabelQuestionAggregates.ts diff --git a/apps/portal/src/components/questions/ContributeQuestionCard.tsx b/apps/portal/src/components/questions/ContributeQuestionCard.tsx index fa25d9f1..e9bf5c44 100644 --- a/apps/portal/src/components/questions/ContributeQuestionCard.tsx +++ b/apps/portal/src/components/questions/ContributeQuestionCard.tsx @@ -28,56 +28,54 @@ export default function ContributeQuestionCard({ }; return ( -
- +

+ Contribute +

+
- + ); } diff --git a/apps/portal/src/components/questions/typeahead/RoleTypeahead.tsx b/apps/portal/src/components/questions/typeahead/RoleTypeahead.tsx index d40d6678..60dd3ffa 100644 --- a/apps/portal/src/components/questions/typeahead/RoleTypeahead.tsx +++ b/apps/portal/src/components/questions/typeahead/RoleTypeahead.tsx @@ -1,13 +1,21 @@ -import { ROLES } from '~/utils/questions/constants'; +import { JobTitleLabels } from '~/components/shared/JobTitles'; import type { ExpandedTypeaheadProps } from './ExpandedTypeahead'; import ExpandedTypeahead from './ExpandedTypeahead'; +import type { FilterChoices } from '../filter/FilterSection'; export type RoleTypeaheadProps = Omit< ExpandedTypeaheadProps, 'label' | 'onQueryChange' | 'options' >; +const ROLES: FilterChoices = Object.entries(JobTitleLabels).map( + ([slug, label]) => ({ + id: slug, + label, + value: slug, + }), +); export default function RoleTypeahead(props: RoleTypeaheadProps) { return ( { + if (!aggregatedEncounters) { + return aggregatedEncounters; + } + + return relabelQuestionAggregates(aggregatedEncounters); + }, [aggregatedEncounters]); + const utils = trpc.useContext(); const { data: comments } = trpc.useQuery([ @@ -138,11 +148,11 @@ export default function QuestionPage() {
acc + page.data.length, + (acc, page) => acc + (page.data.length as number), 0, ); }, [questionsQueryData]); @@ -275,7 +277,7 @@ export default function QuestionsBrowsePage() { return selectedRoles.map((role) => ({ checked: true, id: role, - label: role, + label: JobTitleLabels[role as keyof typeof JobTitleLabels], value: role, })); }, [selectedRoles]); @@ -371,7 +373,7 @@ export default function QuestionsBrowsePage() { setSelectedRoles([...selectedRoles, option.value]); } else { setSelectedRoles( - selectedCompanies.filter((role) => role !== option.value), + selectedRoles.filter((role) => role !== option.value), ); } }} @@ -445,20 +447,20 @@ export default function QuestionsBrowsePage() {
-
-
- { - createQuestion({ - companyId: data.company, - content: data.questionContent, - location: data.location, - questionType: data.questionType, - role: data.role, - seenAt: data.date, - }); - }} - /> +
+ { + createQuestion({ + companyId: data.company, + content: data.questionContent, + location: data.location, + questionType: data.questionType, + role: data.role, + seenAt: data.date, + }); + }} + /> +
-
- {(questionsQueryData?.pages ?? []).flatMap( - ({ data: questions }) => - questions.map((question) => ( +
+
+ {(questionsQueryData?.pages ?? []).flatMap( + ({ data: questions }) => + questions.map((question) => { + const { companyCounts, locationCounts, roleCounts } = + relabelQuestionAggregates( + question.aggregatedQuestionEncounters, + ); + + return ( - )), - )} -
+ ); + }), + )} +
diff --git a/apps/portal/src/pages/questions/lists.tsx b/apps/portal/src/pages/questions/lists.tsx index ea4009f8..cbf1b276 100644 --- a/apps/portal/src/pages/questions/lists.tsx +++ b/apps/portal/src/pages/questions/lists.tsx @@ -15,6 +15,7 @@ import DeleteListDialog from '~/components/questions/DeleteListDialog'; import { Button } from '~/../../../packages/ui/dist'; import { APP_TITLE } from '~/utils/questions/constants'; import createSlug from '~/utils/questions/createSlug'; +import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates'; import { trpc } from '~/utils/trpc'; export default function ListPage() { @@ -172,37 +173,38 @@ export default function ListPage() { {lists?.[selectedListIndex] && (
{lists[selectedListIndex].questionEntries.map( - ({ question, id: entryId }) => ( - { - deleteQuestionEntry({ id: entryId }); - }} - /> - ), + ({ question, id: entryId }) => { + const { companyCounts, locationCounts, roleCounts } = + relabelQuestionAggregates( + question.aggregatedQuestionEncounters, + ); + + return ( + { + deleteQuestionEntry({ id: entryId }); + }} + /> + ); + }, )} {lists[selectedListIndex].questionEntries?.length === 0 && (
diff --git a/apps/portal/src/utils/questions/relabelQuestionAggregates.ts b/apps/portal/src/utils/questions/relabelQuestionAggregates.ts new file mode 100644 index 00000000..50a2a5dd --- /dev/null +++ b/apps/portal/src/utils/questions/relabelQuestionAggregates.ts @@ -0,0 +1,26 @@ +import { JobTitleLabels } from '~/components/shared/JobTitles'; + +import type { AggregatedQuestionEncounter } from '~/types/questions'; + +export default function relabelQuestionAggregates({ + locationCounts, + companyCounts, + roleCounts, + latestSeenAt, +}: AggregatedQuestionEncounter) { + const newRoleCounts = Object.fromEntries( + Object.entries(roleCounts).map(([roleId, count]) => [ + JobTitleLabels[roleId as keyof typeof JobTitleLabels], + count, + ]), + ); + + const relabeledAggregate: AggregatedQuestionEncounter = { + companyCounts, + latestSeenAt, + locationCounts, + roleCounts: newRoleCounts, + }; + + return relabeledAggregate; +} 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 3/3] [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; + } });