[offers][feature] Create list offers API with filter functionality

pull/353/head
Stuart Long Chay Boon 3 years ago
parent 738d18df76
commit 5e1be8b296

@ -0,0 +1,204 @@
-- CreateEnum
CREATE TYPE "JobType" AS ENUM ('INTERN', 'FULLTIME');
-- CreateTable
CREATE TABLE "OffersProfile" (
"id" TEXT NOT NULL,
"profileName" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"editToken" TEXT NOT NULL,
"userId" TEXT,
CONSTRAINT "OffersProfile_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersBackground" (
"id" TEXT NOT NULL,
"totalYoe" INTEGER,
"offersProfileId" TEXT NOT NULL,
CONSTRAINT "OffersBackground_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersSpecificYoe" (
"id" TEXT NOT NULL,
"yoe" INTEGER NOT NULL,
"domain" TEXT NOT NULL,
"backgroundId" TEXT NOT NULL,
CONSTRAINT "OffersSpecificYoe_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersExperience" (
"id" TEXT NOT NULL,
"companyId" TEXT,
"jobType" "JobType",
"title" TEXT,
"durationInMonths" INTEGER,
"specialization" TEXT,
"level" TEXT,
"totalCompensationId" TEXT,
"monthlySalaryId" TEXT,
"backgroundId" TEXT NOT NULL,
CONSTRAINT "OffersExperience_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersCurrency" (
"id" TEXT NOT NULL,
"value" INTEGER NOT NULL,
"currency" TEXT NOT NULL,
CONSTRAINT "OffersCurrency_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersEducation" (
"id" TEXT NOT NULL,
"type" TEXT,
"field" TEXT,
"isAttending" BOOLEAN,
"school" TEXT,
"startDate" TIMESTAMP(3),
"endDate" TIMESTAMP(3),
"backgroundId" TEXT NOT NULL,
CONSTRAINT "OffersEducation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersReply" (
"id" TEXT NOT NULL,
"creator" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"message" TEXT NOT NULL,
"replyingToId" TEXT,
"profileId" TEXT NOT NULL,
CONSTRAINT "OffersReply_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersOffer" (
"id" TEXT NOT NULL,
"profileId" TEXT NOT NULL,
"companyId" TEXT NOT NULL,
"monthYearReceived" TIMESTAMP(3) NOT NULL,
"location" TEXT NOT NULL,
"negotiationStrategy" TEXT,
"comments" TEXT,
"jobType" "JobType" NOT NULL,
CONSTRAINT "OffersOffer_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "OffersIntern" (
"offerId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"specialization" TEXT NOT NULL,
"internshipCycle" TEXT NOT NULL,
"startYear" INTEGER NOT NULL,
"monthlySalaryId" TEXT NOT NULL,
CONSTRAINT "OffersIntern_pkey" PRIMARY KEY ("offerId")
);
-- CreateTable
CREATE TABLE "OffersFullTime" (
"offerId" TEXT NOT NULL,
"title" TEXT NOT NULL,
"specialization" TEXT NOT NULL,
"level" TEXT NOT NULL,
"totalCompensationId" TEXT NOT NULL,
"baseSalaryId" TEXT NOT NULL,
"bonusId" TEXT NOT NULL,
"stocksId" TEXT NOT NULL,
CONSTRAINT "OffersFullTime_pkey" PRIMARY KEY ("offerId")
);
-- CreateIndex
CREATE UNIQUE INDEX "OffersBackground_offersProfileId_key" ON "OffersBackground"("offersProfileId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersExperience_totalCompensationId_key" ON "OffersExperience"("totalCompensationId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersExperience_monthlySalaryId_key" ON "OffersExperience"("monthlySalaryId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersIntern_monthlySalaryId_key" ON "OffersIntern"("monthlySalaryId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersFullTime_totalCompensationId_key" ON "OffersFullTime"("totalCompensationId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersFullTime_baseSalaryId_key" ON "OffersFullTime"("baseSalaryId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersFullTime_bonusId_key" ON "OffersFullTime"("bonusId");
-- CreateIndex
CREATE UNIQUE INDEX "OffersFullTime_stocksId_key" ON "OffersFullTime"("stocksId");
-- AddForeignKey
ALTER TABLE "OffersProfile" ADD CONSTRAINT "OffersProfile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersBackground" ADD CONSTRAINT "OffersBackground_offersProfileId_fkey" FOREIGN KEY ("offersProfileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersSpecificYoe" ADD CONSTRAINT "OffersSpecificYoe_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_totalCompensationId_fkey" FOREIGN KEY ("totalCompensationId") REFERENCES "OffersCurrency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_monthlySalaryId_fkey" FOREIGN KEY ("monthlySalaryId") REFERENCES "OffersCurrency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersExperience" ADD CONSTRAINT "OffersExperience_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersEducation" ADD CONSTRAINT "OffersEducation_backgroundId_fkey" FOREIGN KEY ("backgroundId") REFERENCES "OffersBackground"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersReply" ADD CONSTRAINT "OffersReply_replyingToId_fkey" FOREIGN KEY ("replyingToId") REFERENCES "OffersReply"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersReply" ADD CONSTRAINT "OffersReply_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersOffer" ADD CONSTRAINT "OffersOffer_profileId_fkey" FOREIGN KEY ("profileId") REFERENCES "OffersProfile"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersOffer" ADD CONSTRAINT "OffersOffer_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersIntern" ADD CONSTRAINT "OffersIntern_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersIntern" ADD CONSTRAINT "OffersIntern_monthlySalaryId_fkey" FOREIGN KEY ("monthlySalaryId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "OffersOffer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_totalCompensationId_fkey" FOREIGN KEY ("totalCompensationId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_baseSalaryId_fkey" FOREIGN KEY ("baseSalaryId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_bonusId_fkey" FOREIGN KEY ("bonusId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "OffersFullTime" ADD CONSTRAINT "OffersFullTime_stocksId_fkey" FOREIGN KEY ("stocksId") REFERENCES "OffersCurrency"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

@ -176,7 +176,7 @@ model ResumesCommentVote {
model OffersProfile {
id String @id @default(cuid())
profileName String
profileName String @unique
createdAt DateTime @default(now())
background OffersBackground?
@ -269,7 +269,6 @@ model OffersEducation {
id String @id @default(cuid())
type String?
field String?
isAttending Boolean?
// Add more fields
school String?

@ -35,9 +35,37 @@ const COMPANIES = [
},
];
const OFFER_PROFILES = [
{
id: 'cl91v97ex000109mt7fka5rto',
profileName: 'battery-horse-stable-cow',
editToken: 'cl91ulmhg000009l86o45aspt',
},
{
id: 'cl91v9iw2000209mtautgdnxq',
profileName: 'house-zebra-fast-giraffe',
editToken: 'cl91umigc000109l80f1tcqe8',
},
{
id: 'cl91v9m3y000309mt1ctw55wi',
profileName: 'keyboard-mouse-lazy-cat',
editToken: 'cl91ummoa000209l87q2b8hl7',
},
{
id: 'cl91v9p09000409mt5rvoasf1',
profileName: 'router-hen-bright-pig',
editToken: 'cl91umqa3000309l87jyefe9k',
},
{
id: 'cl91v9uda000509mt5i5fez3v',
profileName: 'screen-ant-dirty-bird',
editToken: 'cl91umuj9000409l87ez85vmg',
},
];
async function main() {
console.log('Seeding started...');
await Promise.all(
await Promise.all([
COMPANIES.map(async (company) => {
await prisma.company.upsert({
where: { slug: company.slug },
@ -45,7 +73,14 @@ async function main() {
create: company,
});
}),
);
OFFER_PROFILES.map(async (offerProfile) => {
await prisma.offersProfile.upsert({
where: { profileName: offerProfile.profileName },
update: offerProfile,
create: offerProfile,
});
}),
]);
console.log('Seeding completed.');
}

@ -0,0 +1,25 @@
import React from 'react';
import { trpc } from '~/utils/trpc';
function test() {
const data = trpc.useQuery([
'offers.list',
{
limit: 3,
location: 'Singapore, Singapore',
offset: 0,
yoeCategory: 0,
},
]);
return (
<ul>
{data.data?.map((x) => {
return <li key={x.id}>{JSON.stringify(x)}</li>;
})}
</ul>
);
}
export default test;

@ -2,6 +2,7 @@ import superjson from 'superjson';
import { companiesRouter } from './companies-router';
import { createRouter } from './context';
import { offersRouter } from './offers';
import { protectedExampleRouter } from './protected-example-router';
import { questionsAnswerCommentRouter } from './questions-answer-comment-router';
import { questionsAnswerRouter } from './questions-answer-router';
@ -32,7 +33,8 @@ export const appRouter = createRouter()
.merge('questions.answers.comments.', questionsAnswerCommentRouter)
.merge('questions.answers.', questionsAnswerRouter)
.merge('questions.questions.comments.', questionsQuestionCommentRouter)
.merge('questions.questions.', questionsQuestionRouter);
.merge('questions.questions.', questionsQuestionRouter)
.merge('offers.', offersRouter);
// Export type definition of API
export type AppRouter = typeof appRouter;

@ -0,0 +1,229 @@
import assert from 'assert';
import { z } from 'zod';
import { createRouter } from './context';
const yoeCategoryMap: Record<number, string> = {
0: 'Internship',
1: 'Fresh Grad',
2: 'Mid',
3: 'Senior',
};
const getYoeRange = (yoeCategory: number) => {
return yoeCategoryMap[yoeCategory] === 'Fresh Grad'
? { maxYoe: 3, minYoe: 0 }
: yoeCategoryMap[yoeCategory] === 'Mid'
? { maxYoe: 7, minYoe: 4 }
: yoeCategoryMap[yoeCategory] === 'Senior'
? { maxYoe: null, minYoe: 8 }
: null;
};
const sortingKeys = ['date', 'tc', 'yoe'];
const createSortByValidationRegex = () => {
const startsWithPlusOrMinusOnly = '^[+-]{1}';
const sortingKeysRegex = sortingKeys.join('|');
return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')');
};
export const offersRouter = createRouter().query('list', {
input: z.object({
company: z.string().nullish(),
dateEnd: z.date().nullish(),
dateStart: z.date().nullish(),
limit: z.number().nonnegative(),
location: z.string(),
offset: z.number().nonnegative(),
salaryMax: z.number().nullish(),
salaryMin: z.number().nonnegative().nullish(),
sortby: z.string().regex(createSortByValidationRegex()).nullish(),
title: z.string().nullish(),
yoeCategory: z.number().min(0).max(3),
}),
async resolve({ ctx, input }) {
const yoeRange = getYoeRange(input.yoeCategory);
let data = !yoeRange
? await ctx.prisma.offersOffer.findMany({
// Internship
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
},
skip: input.limit * input.offset,
take: input.limit,
where: {
AND: [
{
location: input.location,
},
{
OffersIntern: {
isNot: null,
},
},
{
OffersFullTime: {
is: null,
},
},
],
},
})
: yoeRange.maxYoe
? await ctx.prisma.offersOffer.findMany({
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
},
// Junior, Mid
skip: input.limit * input.offset,
take: input.limit,
where: {
AND: [
{
location: input.location,
},
{
OffersIntern: {
is: null,
},
},
{
OffersFullTime: {
isNot: null,
},
},
{
profile: {
background: {
totalYoe: {
gte: yoeRange.minYoe,
},
},
},
},
{
profile: {
background: {
totalYoe: {
gte: yoeRange.maxYoe,
},
},
},
},
],
},
})
: await ctx.prisma.offersOffer.findMany({
// Senior
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
OffersIntern: {
include: {
monthlySalary: true,
},
},
company: true,
},
skip: input.limit * input.offset,
take: input.limit,
where: {
AND: [
{
location: input.location,
},
{
OffersIntern: {
is: null,
},
},
{
OffersFullTime: {
isNot: null,
},
},
{
profile: {
background: {
totalYoe: {
gte: yoeRange.minYoe,
},
},
},
},
],
},
});
data = data.filter((offer) => {
let validRecord = true;
if (input.company) {
validRecord = validRecord && offer.company.name === input.company;
}
if (input.title) {
validRecord =
validRecord &&
(offer.OffersFullTime?.title === input.title ||
offer.OffersIntern?.title === input.title);
}
if (input.dateStart && input.dateEnd) {
validRecord =
validRecord &&
offer.monthYearReceived.getTime() >= input.dateStart.getTime() &&
offer.monthYearReceived.getTime() <= input.dateEnd.getTime();
}
if (input.salaryMin && input.salaryMax) {
const salary = offer.OffersFullTime?.totalCompensation.value
? offer.OffersFullTime?.totalCompensation.value
: offer.OffersIntern?.monthlySalary.value;
assert(salary);
validRecord =
validRecord && salary >= input.salaryMin && salary <= input.salaryMax;
}
return validRecord;
});
return data;
},
});
Loading…
Cancel
Save