diff --git a/apps/portal/prisma/migrations/20221014105030_add_question_content_search/migration.sql b/apps/portal/prisma/migrations/20221014105030_add_question_content_search/migration.sql new file mode 100644 index 00000000..2aed7085 --- /dev/null +++ b/apps/portal/prisma/migrations/20221014105030_add_question_content_search/migration.sql @@ -0,0 +1,8 @@ +-- AlterTable +ALTER TABLE "QuestionsQuestion" ADD COLUMN "contentSearch" TSVECTOR + GENERATED ALWAYS AS + to_tsvector('english', coalesce(content, '')) + STORED; + +-- CreateIndex +CREATE INDEX "QuestionsQuestion_contentSearch_idx" ON "QuestionsQuestion" USING GIN("textSearch"); \ No newline at end of file diff --git a/apps/portal/prisma/migrations/20221023102619_add_list_schema/migration.sql b/apps/portal/prisma/migrations/20221023102619_add_list_schema/migration.sql new file mode 100644 index 00000000..ccc20bf1 --- /dev/null +++ b/apps/portal/prisma/migrations/20221023102619_add_list_schema/migration.sql @@ -0,0 +1,36 @@ +-- CreateTable +CREATE TABLE "QuestionsList" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "name" VARCHAR(256) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsList_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionsListQuestionEntry" ( + "id" TEXT NOT NULL, + "listId" TEXT NOT NULL, + "questionId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "QuestionsListQuestionEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "QuestionsList_userId_name_key" ON "QuestionsList"("userId", "name"); + +-- CreateIndex +CREATE UNIQUE INDEX "QuestionsListQuestionEntry_listId_questionId_key" ON "QuestionsListQuestionEntry"("listId", "questionId"); + +-- AddForeignKey +ALTER TABLE "QuestionsList" ADD CONSTRAINT "QuestionsList_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsListQuestionEntry" ADD CONSTRAINT "QuestionsListQuestionEntry_listId_fkey" FOREIGN KEY ("listId") REFERENCES "QuestionsList"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionsListQuestionEntry" ADD CONSTRAINT "QuestionsListQuestionEntry_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "QuestionsQuestion"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index c2c6938b..f6bde90e 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -60,6 +60,7 @@ model User { questionsAnswerCommentVotes QuestionsAnswerCommentVote[] OffersProfile OffersProfile[] offersDiscussion OffersReply[] + questionsLists QuestionsList[] } enum Vote { @@ -406,12 +407,16 @@ model QuestionsQuestion { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - user User? @relation(fields: [userId], references: [id], onDelete: SetNull) - encounters QuestionsQuestionEncounter[] - votes QuestionsQuestionVote[] - comments QuestionsQuestionComment[] - answers QuestionsAnswer[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + encounters QuestionsQuestionEncounter[] + votes QuestionsQuestionVote[] + comments QuestionsQuestionComment[] + answers QuestionsAnswer[] + QuestionsListQuestionEntry QuestionsListQuestionEntry[] + contentSearch Unsupported("TSVECTOR")? + + @@index([contentSearch]) @@index([lastSeenAt, id]) @@index([upvotes, id]) } @@ -532,4 +537,30 @@ model QuestionsAnswerCommentVote { @@unique([answerCommentId, userId]) } +model QuestionsList { + id String @id @default(cuid()) + userId String + name String @db.VarChar(256) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + questionEntries QuestionsListQuestionEntry[] + + @@unique([userId, name]) +} + +model QuestionsListQuestionEntry { + id String @id @default(cuid()) + listId String + questionId String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + list QuestionsList @relation(fields: [listId], references: [id], onDelete: Cascade) + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + + @@unique([listId, questionId]) +} + // End of Questions project models. diff --git a/apps/portal/src/server/router/questions-list-crud.ts b/apps/portal/src/server/router/questions-list-crud.ts new file mode 100644 index 00000000..1f375497 --- /dev/null +++ b/apps/portal/src/server/router/questions-list-crud.ts @@ -0,0 +1,199 @@ +import { z } from 'zod'; +import { TRPCError } from '@trpc/server'; + +import { createProtectedRouter } from './context'; + +export const questionListRouter = createProtectedRouter() + .query('getListsByUser', { + async resolve({ ctx }) { + const userId = ctx.session?.user?.id; + + return await ctx.prisma.questionsList.findMany({ + include: { + questionEntries: { + include: { + question: true, + }, + } + }, + orderBy: { + createdAt: 'asc', + }, + where: { + id: userId, + }, + }); + } + }) + .query('getListById', { + input: z.object({ + listId: z.string(), + }), + async resolve({ ctx }) { + const userId = ctx.session?.user?.id; + + return await ctx.prisma.questionsList.findMany({ + include: { + questionEntries: { + include: { + question: true, + }, + } + }, + orderBy: { + createdAt: 'asc', + }, + where: { + id: userId, + }, + }); + } + }) + .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?.id !== 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?.id !== 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?.id !== userId) { + throw new TRPCError({ + code: 'UNAUTHORIZED', + message: 'User have no authorization to record.', + }); + } + + + const listToAugment = await ctx.prisma.questionsList.findUnique({ + where: { + id: entryToDelete.listId, + }, + }); + + if (listToAugment?.id !== 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 index 9cb2f707..c74e8782 100644 --- a/apps/portal/src/server/router/questions-question-router.ts +++ b/apps/portal/src/server/router/questions-question-router.ts @@ -316,6 +316,31 @@ export const questionsQuestionRouter = createProtectedRouter() return question; }, }) + .query('getRelatedQuestionsByContent', { + input: z.object({ + content: z.string(), + }), + async resolve({ ctx, input }) { + const escapeChars = /[()|&:*!]/g; + + const query = + input.content + .replace(escapeChars, " ") + .trim() + .split(/\s+/) + .join(" | "); + + const relatedQuestions = await ctx.prisma.$queryRaw` + SELECT * FROM "QuestionsQuestion" + WHERE + "contentSearch" @@ to_tsquery('english', ${query}) + ORDER BY ts_rank("textSearch", to_tsquery('english', ${query})) DESC + `; + + return relatedQuestions; + } + + }) .mutation('create', { input: z.object({ companyId: z.string(), @@ -537,7 +562,7 @@ export const questionsQuestionRouter = createProtectedRouter() const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1; - const [questionVote] = await ctx.prisma.$transaction([ + const [ questionVote ] = await ctx.prisma.$transaction([ ctx.prisma.questionsQuestionVote.delete({ where: { id: input.id,