Files
hpkoh 397ea3f4aa [questions][feat] add content search (#478)
Co-authored-by: Jeff Sieu <jeffsy00@gmail.com>
2022-10-31 19:24:07 +08:00

465 lines
12 KiB
TypeScript

import { z } from 'zod';
import { QuestionsQuestionType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import { createQuestionWithAggregateData } from '~/utils/questions/server/aggregate-encounters';
import { createRouter } from '../context';
import { SortOrder, SortType } from '~/types/questions.d';
export const questionsQuestionRouter = createRouter()
.query('getQuestionsByFilter', {
input: z.object({
cityIds: z.string().array(),
companyIds: z.string().array(),
content: z.string().optional(),
countryIds: z.string().array(),
cursor: z.string().nullish(),
endDate: z.date().default(new Date()),
limit: z.number().min(1).default(50),
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.string().array(),
sortOrder: z.nativeEnum(SortOrder),
sortType: z.nativeEnum(SortType),
startDate: z.date().optional(),
stateIds: z.string().array(),
}),
async resolve({ ctx, input }) {
const { cursor } = input;
let sortCondition = undefined;
switch (input.sortType) {
case SortType.TOP:
sortCondition = [
{
upvotes: input.sortOrder,
},
{
id: input.sortOrder,
},
]
break;
case SortType.NEW:
sortCondition = [
{
lastSeenAt: input.sortOrder,
},
{
id: input.sortOrder,
},
];
break;
case SortType.ENCOUNTERS:
sortCondition = [
{
numEncounters: input.sortOrder,
},
{
id: input.sortOrder,
},
];
break;
}
const questionsData = await ctx.prisma.questionsQuestion.findMany({
cursor: cursor ? { id: cursor } : undefined,
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
city: true,
company: true,
country: true,
role: true,
seenAt: true,
state: 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.companyIds.length > 0
? {
company: {
id: {
in: input.companyIds,
},
},
}
: {}),
...(input.cityIds.length > 0
? {
city: {
id: {
in: input.cityIds,
},
},
}
: {}),
...(input.countryIds.length > 0
? {
country: {
id: {
in: input.countryIds,
},
},
}
: {}),
...(input.stateIds.length > 0
? {
state: {
id: {
in: input.stateIds,
},
},
}
: {}),
...(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;
nextCursor = nextIdCursor;
}
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: {
city: true,
company: true,
country: true,
role: true,
seenAt: true,
state: 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);
},
})
.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
LIMIT 3;
`;
const relatedQuestionsIdArray = relatedQuestionsId.map(
(current) => current.id,
);
const relatedQuestionsData = await ctx.prisma.questionsQuestion.findMany({
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
city: true,
company: true,
country: true,
role: true,
seenAt: true,
state: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
where: {
id: {
in: relatedQuestionsIdArray,
},
},
});
const processedQuestionsData = relatedQuestionsData.map(
createQuestionWithAggregateData,
);
return processedQuestionsData;
},
})
.query('getQuestionsByFilterAndContent', {
input: z.object({
cityIds: z.string().array(),
companyIds: z.string().array(),
content: z.string(),
countryIds: z.string().array(),
cursor: z.string().nullish(),
endDate: z.date().default(new Date()),
limit: z.number().min(1).default(50),
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.string().array(),
sortOrder: z.nativeEnum(SortOrder),
sortType: z.nativeEnum(SortType),
startDate: z.date().optional(),
stateIds: z.string().array(),
}),
async resolve({ ctx, input }) {
const escapeChars = /[()|&:*!]/g;
const query = input.content
.replace(escapeChars, ' ')
.trim()
.split(/\s+/)
.join(' | ');
let relatedQuestionsId: Array<{ id: string }> = [];
if (input.content !== "") {
relatedQuestionsId = 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
LIMIT 3;
`;
}
const relatedQuestionsIdArray = relatedQuestionsId.map(
(current) => current.id,
);
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 ? { id: cursor } : undefined,
include: {
_count: {
select: {
answers: true,
comments: true,
},
},
encounters: {
select: {
city: true,
company: true,
country: true,
role: true,
seenAt: true,
state: true,
},
},
user: {
select: {
name: true,
},
},
votes: true,
},
orderBy: sortCondition,
take: input.limit + 1,
where: {
id: input.content !== "" ? {
in: relatedQuestionsIdArray,
} : undefined,
...(input.questionTypes.length > 0
? {
questionType: {
in: input.questionTypes,
},
}
: {}),
encounters: {
some: {
seenAt: {
gte: input.startDate,
lte: input.endDate,
},
...(input.companyIds.length > 0
? {
company: {
id: {
in: input.companyIds,
},
},
}
: {}),
...(input.cityIds.length > 0
? {
city: {
id: {
in: input.cityIds,
},
},
}
: {}),
...(input.countryIds.length > 0
? {
country: {
id: {
in: input.countryIds,
},
},
}
: {}),
...(input.stateIds.length > 0
? {
state: {
id: {
in: input.stateIds,
},
},
}
: {}),
...(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;
nextCursor = nextIdCursor;
}
return {
data: processedQuestionsData,
nextCursor,
};
},
});