From 59b1dc68f34cfb643db08dcb1e2caff41f46c72a Mon Sep 17 00:00:00 2001 From: hpkoh Date: Fri, 4 Nov 2022 18:15:50 +0800 Subject: [PATCH] [questions][feat] add tags --- .../20221104091537_add_tags/migration.sql | 28 +++++++ .../migration.sql | 25 ++++++ apps/portal/prisma/schema.prisma | 30 +++++-- .../questions/questions-question-router.ts | 53 +++++++++++- .../questions/questions-tag-user-router.ts | 81 +++++++++++++++++++ apps/portal/src/types/questions.d.ts | 1 + .../questions/server/aggregate-encounters.ts | 5 ++ 7 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 apps/portal/prisma/migrations/20221104091537_add_tags/migration.sql create mode 100644 apps/portal/prisma/migrations/20221104093450_update_tag_table_name/migration.sql create mode 100644 apps/portal/src/server/router/questions/questions-tag-user-router.ts diff --git a/apps/portal/prisma/migrations/20221104091537_add_tags/migration.sql b/apps/portal/prisma/migrations/20221104091537_add_tags/migration.sql new file mode 100644 index 00000000..71352a89 --- /dev/null +++ b/apps/portal/prisma/migrations/20221104091537_add_tags/migration.sql @@ -0,0 +1,28 @@ +-- CreateTable +CREATE TABLE "QuestionTags" ( + "id" TEXT NOT NULL, + "tag" TEXT NOT NULL, + + CONSTRAINT "QuestionTags_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "QuestionQuestionTagEntry" ( + "id" TEXT NOT NULL, + "questionId" TEXT NOT NULL, + "tagId" TEXT NOT NULL, + + CONSTRAINT "QuestionQuestionTagEntry_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "QuestionTags_tag_key" ON "QuestionTags"("tag"); + +-- CreateIndex +CREATE UNIQUE INDEX "QuestionQuestionTagEntry_questionId_tagId_key" ON "QuestionQuestionTagEntry"("questionId", "tagId"); + +-- AddForeignKey +ALTER TABLE "QuestionQuestionTagEntry" ADD CONSTRAINT "QuestionQuestionTagEntry_questionId_fkey" FOREIGN KEY ("questionId") REFERENCES "QuestionsQuestion"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "QuestionQuestionTagEntry" ADD CONSTRAINT "QuestionQuestionTagEntry_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "QuestionTags"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/portal/prisma/migrations/20221104093450_update_tag_table_name/migration.sql b/apps/portal/prisma/migrations/20221104093450_update_tag_table_name/migration.sql new file mode 100644 index 00000000..e02291d0 --- /dev/null +++ b/apps/portal/prisma/migrations/20221104093450_update_tag_table_name/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the `QuestionTags` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "QuestionQuestionTagEntry" DROP CONSTRAINT "QuestionQuestionTagEntry_tagId_fkey"; + +-- DropTable +DROP TABLE "QuestionTags"; + +-- CreateTable +CREATE TABLE "QuestionTag" ( + "id" TEXT NOT NULL, + "tag" TEXT NOT NULL, + + CONSTRAINT "QuestionTag_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "QuestionTag_tag_key" ON "QuestionTag"("tag"); + +-- AddForeignKey +ALTER TABLE "QuestionQuestionTagEntry" ADD CONSTRAINT "QuestionQuestionTagEntry_tagId_fkey" FOREIGN KEY ("tagId") REFERENCES "QuestionTag"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma index cbf65808..a8035e90 100644 --- a/apps/portal/prisma/schema.prisma +++ b/apps/portal/prisma/schema.prisma @@ -455,18 +455,36 @@ 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[] - questionsListQuestionEntries QuestionsListQuestionEntry[] + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + encounters QuestionsQuestionEncounter[] + votes QuestionsQuestionVote[] + comments QuestionsQuestionComment[] + answers QuestionsAnswer[] + listQuestionEntries QuestionsListQuestionEntry[] + questionTagEntries QuestionQuestionTagEntry[] @@index([lastSeenAt, id]) @@index([numEncounters, id]) @@index([upvotes, id]) } +model QuestionTag { + id String @id @default(cuid()) + tag String @unique + questionTagEntries QuestionQuestionTagEntry[] +} + +model QuestionQuestionTagEntry { + id String @id @default(cuid()) + questionId String + tagId String + + question QuestionsQuestion @relation(fields: [questionId], references: [id], onDelete: Cascade) + tag QuestionTag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@unique([questionId, tagId]) +} + model QuestionsQuestionEncounter { id String @id @default(cuid()) questionId String 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 4d506b94..79ddc5fa 100644 --- a/apps/portal/src/server/router/questions/questions-question-router.ts +++ b/apps/portal/src/server/router/questions/questions-question-router.ts @@ -24,6 +24,7 @@ export const questionsQuestionRouter = createRouter() sortType: z.nativeEnum(SortType), startDate: z.date().optional(), stateIds: z.string().array(), + tagIds: z.string().array(), }), async resolve({ ctx, input }) { const { cursor } = input; @@ -82,6 +83,11 @@ export const questionsQuestionRouter = createRouter() state: true, }, }, + questionTagEntries: { + select: { + tag: true, + }, + }, user: { select: { name: true, @@ -99,6 +105,19 @@ export const questionsQuestionRouter = createRouter() }, } : {}), + ...(input.tagIds.length > 0 + ? { + questionTagEntries: { + some : { + tag: { + id: { + in: input.tagIds, + }, + }, + } + }, + } + : {}), encounters: { some: { seenAt: { @@ -150,6 +169,7 @@ export const questionsQuestionRouter = createRouter() : {}), }, }, + }, }); @@ -197,6 +217,11 @@ export const questionsQuestionRouter = createRouter() state: true, }, }, + questionTagEntries: { + select: { + tag: true, + }, + }, user: { select: { name: true, @@ -262,6 +287,11 @@ export const questionsQuestionRouter = createRouter() state: true, }, }, + questionTagEntries: { + select: { + tag: true, + }, + }, user: { select: { name: true, @@ -298,6 +328,7 @@ export const questionsQuestionRouter = createRouter() sortType: z.nativeEnum(SortType), startDate: z.date().optional(), stateIds: z.string().array(), + tagIds: z.string().array(), }), async resolve({ ctx, input }) { const escapeChars = /[()|&:*!]/g; @@ -367,6 +398,11 @@ export const questionsQuestionRouter = createRouter() state: true, }, }, + questionTagEntries: { + select: { + tag: true, + }, + }, user: { select: { name: true, @@ -377,9 +413,6 @@ export const questionsQuestionRouter = createRouter() orderBy: sortCondition, take: input.limit + 1, where: { - id: input.content !== "" ? { - in: relatedQuestionsIdArray, - } : undefined, ...(input.questionTypes.length > 0 ? { questionType: { @@ -387,6 +420,19 @@ export const questionsQuestionRouter = createRouter() }, } : {}), + ...(input.tagIds.length > 0 + ? { + questionTagEntries: { + some : { + tag: { + id: { + in: input.tagIds, + }, + }, + } + }, + } + : {}), encounters: { some: { seenAt: { @@ -438,6 +484,7 @@ export const questionsQuestionRouter = createRouter() : {}), }, }, + }, }); diff --git a/apps/portal/src/server/router/questions/questions-tag-user-router.ts b/apps/portal/src/server/router/questions/questions-tag-user-router.ts new file mode 100644 index 00000000..75b0be09 --- /dev/null +++ b/apps/portal/src/server/router/questions/questions-tag-user-router.ts @@ -0,0 +1,81 @@ +import { z } from 'zod'; + +import { createProtectedRouter } from '../context'; + +export const questionsTagUserRouter = createProtectedRouter() + .mutation('create', { + input: z.object({ + tag: z.string(), + }), + async resolve({ ctx, input }) { + return await ctx.prisma.questionTag.upsert({ + where: { + tag : input.tag, + }, + update: {}, + create : { + tag : input.tag, + } + }); + }, + }) + .mutation('addTagToQuestion', { + input: z.object({ + questionId: z.string(), + tagId: z.string(), + }), + async resolve({ ctx, input }) { + return await ctx.prisma.questionQuestionTagEntry.create({ + data: { + question:{ + connect: { + id: input.questionId, + }, + }, + tag:{ + connect: { + id: input.tagId, + }, + }, + }, + }); + }, + }) + .mutation('removeTagFromQuestion', { + input: z.object({ + id: z.string(), + }), + async resolve({ ctx, input }) { + return await ctx.prisma.questionQuestionTagEntry.delete({ + where: { + id: input.id, + }, + }); + } + }) + .mutation('combineTags', { + input: z.object({ + tagToCombineId: z.string(), + tagToCombineToId: z.string(), + }), + async resolve({ ctx, input }) { + return await ctx.prisma.$transaction(async (tx) => { + const questionTagsUpdated = await tx.questionQuestionTagEntry.updateMany({ + where: { + tagId: input.tagToCombineId, + }, + data: { + tagId: input.tagToCombineId, + }, + }); + + tx.questionTag.delete({ + where: { + id: input.tagToCombineId, + }, + }); + + return questionTagsUpdated; + }); + } + }); diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts index 0a4a9339..08af458b 100644 --- a/apps/portal/src/types/questions.d.ts +++ b/apps/portal/src/types/questions.d.ts @@ -5,6 +5,7 @@ export type Question = { content: string; id: string; numAnswers: number; + tags: Array; numComments: number; numVotes: number; receivedCount: number; diff --git a/apps/portal/src/utils/questions/server/aggregate-encounters.ts b/apps/portal/src/utils/questions/server/aggregate-encounters.ts index 189bcd1a..e999a0a2 100644 --- a/apps/portal/src/utils/questions/server/aggregate-encounters.ts +++ b/apps/portal/src/utils/questions/server/aggregate-encounters.ts @@ -4,6 +4,7 @@ import type { Country, QuestionsQuestion, QuestionsQuestionVote, + QuestionTag, State, } from '@prisma/client'; import { Vote } from '@prisma/client'; @@ -14,6 +15,8 @@ import type { Question, } from '~/types/questions'; +type QuestionTagEntry = { tag: QuestionTag; } + type AggregatableEncounters = Array<{ city: City | null; company: Company | null; @@ -28,6 +31,7 @@ type QuestionWithAggregatableData = QuestionsQuestion & { answers: number; comments: number; }; + questionTagEntries: Array; encounters: AggregatableEncounters; user: { name: string | null; @@ -66,6 +70,7 @@ export function createQuestionWithAggregateData( numVotes: votes, receivedCount: data.encounters.length, seenAt: data.encounters[0].seenAt, + tags: data.tags, type: data.questionType, updatedAt: data.updatedAt, user: data.user?.name ?? '',