[offers][feat] Add Offers Schema, View Offer Profiles API, and Create Offer Profile API (#353)

* [offers][fix] fix merge conflicts

* [offers][chore] Create prisma schema

* [offers][feat] add create endpoint for profiles

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

* [offers][fix] fix bugs create profile bugs

* [offers][fix] fix create profile bugs

* [offers][feat] Add sorting functionality to list offers

Co-authored-by: Stuart Long Chay Boon <chayboon@gmail.com>
pull/354/head
Bryann Yeap Kok Keong 2 years ago committed by GitHub
parent f8031caa2f
commit 356eeb6954
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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;

@ -58,6 +58,7 @@ model User {
questionsAnswerVotes QuestionsAnswerVote[] questionsAnswerVotes QuestionsAnswerVote[]
questionsAnswerComments QuestionsAnswerComment[] questionsAnswerComments QuestionsAnswerComment[]
questionsAnswerCommentVotes QuestionsAnswerCommentVote[] questionsAnswerCommentVotes QuestionsAnswerCommentVote[]
OffersProfile OffersProfile[]
} }
enum Vote { enum Vote {
@ -89,13 +90,15 @@ enum TodoStatus {
} }
model Company { model Company {
id String @id @default(cuid()) id String @id @default(cuid())
name String @db.Text name String @db.Text
slug String @unique slug String @unique
description String? @db.Text description String? @db.Text
logoUrl String? logoUrl String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
OffersExperience OffersExperience[]
OffersOffer OffersOffer[]
} }
// Start of Resumes project models. // Start of Resumes project models.
@ -170,6 +173,179 @@ model ResumesCommentVote {
// Add Offers project models here, prefix all models with "Offer", // Add Offers project models here, prefix all models with "Offer",
// use camelCase for field names, and try to name them consistently // use camelCase for field names, and try to name them consistently
// across all models in this file. // across all models in this file.
model OffersProfile {
id String @id @default(cuid())
profileName String @unique
createdAt DateTime @default(now())
background OffersBackground?
editToken String
discussion OffersReply[]
offers OffersOffer[]
user User? @relation(fields: [userId], references: [id])
userId String?
}
model OffersBackground {
id String @id @default(cuid())
totalYoe Int?
specificYoes OffersSpecificYoe[]
experiences OffersExperience[] // For extensibility in the future
educations OffersEducation[] // For extensibility in the future
profile OffersProfile @relation(fields: [offersProfileId], references: [id])
offersProfileId String @unique
}
model OffersSpecificYoe {
id String @id @default(cuid())
yoe Int
domain String
background OffersBackground @relation(fields: [backgroundId], references: [id])
backgroundId String
}
model OffersExperience {
id String @id @default(cuid())
company Company? @relation(fields: [companyId], references: [id])
companyId String?
jobType JobType?
title String?
// Add more fields
durationInMonths Int?
specialization String?
// FULLTIME fields
level String?
totalCompensation OffersCurrency? @relation("ExperienceTotalCompensation", fields: [totalCompensationId], references: [id])
totalCompensationId String? @unique
// INTERN fields
monthlySalary OffersCurrency? @relation("ExperienceMonthlySalary", fields: [monthlySalaryId], references: [id])
monthlySalaryId String? @unique
background OffersBackground @relation(fields: [backgroundId], references: [id])
backgroundId String
}
model OffersCurrency {
id String @id @default(cuid())
value Int
currency String
// Experience
OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation")
OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary")
// Full Time
OffersTotalCompensation OffersFullTime? @relation("OfferTotalCompensation")
OffersBaseSalary OffersFullTime? @relation("OfferBaseSalary")
OffersBonus OffersFullTime? @relation("OfferBonus")
OffersStocks OffersFullTime? @relation("OfferStocks")
// Intern
OffersMonthlySalary OffersIntern?
}
enum JobType {
INTERN
FULLTIME
}
model OffersEducation {
id String @id @default(cuid())
type String?
field String?
// Add more fields
school String?
startDate DateTime?
endDate DateTime?
background OffersBackground @relation(fields: [backgroundId], references: [id])
backgroundId String
}
model OffersReply {
id String @id @default(cuid())
creator String
createdAt DateTime @default(now())
message String
replyingToId String?
replyingTo OffersReply? @relation("ReplyThread", fields: [replyingToId], references: [id])
replies OffersReply[] @relation("ReplyThread")
profile OffersProfile @relation(fields: [profileId], references: [id])
profileId String
}
model OffersOffer {
id String @id @default(cuid())
profile OffersProfile @relation(fields: [profileId], references: [id])
profileId String
company Company @relation(fields: [companyId], references: [id])
companyId String
monthYearReceived DateTime
location String
negotiationStrategy String?
comments String?
jobType JobType
OffersIntern OffersIntern? @relation(fields: [offersInternId], references: [id])
offersInternId String? @unique
OffersFullTime OffersFullTime? @relation(fields: [offersFullTimeId], references: [id])
offersFullTimeId String? @unique
}
model OffersIntern {
id String @id @default(cuid())
title String
specialization String
internshipCycle String
startYear Int
monthlySalary OffersCurrency @relation(fields: [monthlySalaryId], references: [id])
monthlySalaryId String @unique
OffersOffer OffersOffer?
}
model OffersFullTime {
id String @id @default(cuid())
title String
specialization String
level String
totalCompensation OffersCurrency @relation("OfferTotalCompensation", fields: [totalCompensationId], references: [id])
totalCompensationId String @unique
baseSalary OffersCurrency @relation("OfferBaseSalary", fields: [baseSalaryId], references: [id])
baseSalaryId String @unique
bonus OffersCurrency @relation("OfferBonus", fields: [bonusId], references: [id])
bonusId String @unique
stocks OffersCurrency @relation("OfferStocks", fields: [stocksId], references: [id])
stocksId String @unique
OffersOffer OffersOffer?
}
// End of Offers project models. // End of Offers project models.
// Start of Questions project models. // Start of Questions project models.

@ -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() { async function main() {
console.log('Seeding started...'); console.log('Seeding started...');
await Promise.all( await Promise.all([
COMPANIES.map(async (company) => { COMPANIES.map(async (company) => {
await prisma.company.upsert({ await prisma.company.upsert({
where: { slug: company.slug }, where: { slug: company.slug },
@ -45,7 +73,14 @@ async function main() {
create: company, create: company,
}); });
}), }),
); OFFER_PROFILES.map(async (offerProfile) => {
await prisma.offersProfile.upsert({
where: { profileName: offerProfile.profileName },
update: offerProfile,
create: offerProfile,
});
}),
]);
console.log('Seeding completed.'); console.log('Seeding completed.');
} }

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

@ -0,0 +1,144 @@
import React, { useState } from 'react';
import { trpc } from '~/utils/trpc';
function Test() {
// F const data = trpc.useQuery([
// 'offers.profile.',
// {
// limit: 3,
// location: 'Singapore, Singapore',
// offset: 0,
// yoeCategory: 0,
// },
// ]);
const [createdData, setCreatedData] = useState("")
const createMutation = trpc.useMutation(['offers.profile.create'], {
onError(error: any) {
alert(error)
},
onSuccess(data) {
setCreatedData(JSON.stringify(data))
},
});
const handleClick = () => {
createMutation.mutate({
"background": {
"educations": [
{
"endDate": new Date("2018-09-30T07:58:54.000Z"),
"field": "Computer Science",
"school": "National University of Singapore",
"startDate": new Date("2014-09-30T07:58:54.000Z"),
"type": "Bachelors"
}
],
"experiences": [
{
"companyId": "cl92ly8xm0000w3mwh5ymyqmx",
"durationInMonths": 24,
"jobType": "FULLTIME",
"level": "Junior",
// "monthlySalary": undefined,
"specialization": "Front End",
"title": "Software Engineer",
"totalCompensation": {
"currency": "SGD",
"value": 104100
}
}
],
"specificYoes": [
{
"domain": "Front End",
"yoe": 2
},
{
"domain": "Full Stack",
"yoe": 2
}
],
"totalYoe": 4
},
"offers": [
{
"comments": "",
"companyId": "cl92ly8xm0000w3mwh5ymyqmx",
"job": {
"base": {
"currency": "SGD",
"value": 84000
},
"bonus": {
"currency": "SGD",
"value": 20000
},
"level": "Junior",
"specialization": "Front End",
"stocks": {
"currency": "SGD",
"value": 100
},
"title": "Software Engineer",
"totalCompensation": {
"currency": "SGD",
"value": 104100
}
},
"jobType": "FULLTIME",
"location": "Singapore, Singapore",
"monthYearReceived": new Date("2022-09-30T07:58:54.000Z"),
"negotiationStrategy": "Leveraged having multiple offers"
},
{
"comments": "",
"companyId": "cl92ly8xm0000w3mwh5ymyqmx",
"job": {
"base": {
"currency": "SGD",
"value": 84000
},
"bonus": {
"currency": "SGD",
"value": 20000
},
"level": "Junior",
"specialization": "Front End",
"stocks": {
"currency": "SGD",
"value": 100
},
"title": "Software Engineer",
"totalCompensation": {
"currency": "SGD",
"value": 104100
}
},
"jobType": "FULLTIME",
"location": "Singapore, Singapore",
"monthYearReceived": new Date("2022-09-30T07:58:54.000Z"),
"negotiationStrategy": "Leveraged having multiple offers"
}
]
});
};
return (
// <ul>
// {createdData.map((x) => {
// return <li key={x.id}>{JSON.stringify(x)}</li>;
// })}
// </ul>
<>
<div>
{createdData}
</div>
<button type="button" onClick={handleClick}>Click me</button>
</>
);
}
export default Test;

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

@ -0,0 +1,269 @@
import crypto, { randomUUID } from "crypto";
import { z } from "zod";
import { Prisma } from "@prisma/client";
import { createProtectedRouter } from "./context";
const valuation = z.object({
currency: z.string(),
value: z.number(),
})
// TODO: handle both full time and intern
const offer = z.object({
comments: z.string(),
companyId: z.string(),
job: z.object({
base: valuation.optional(), // Full time
bonus: valuation.optional(), // Full time
internshipCycle: z.string().optional(), // Intern
level: z.string().optional(), // Full time
monthlySalary: valuation.optional(), // Intern
specialization: z.string(),
startYear: z.number().optional(), // Intern
stocks: valuation.optional(), // Full time
title: z.string(),
totalCompensation: valuation.optional(), // Full time
}),
jobType: z.string(),
location: z.string(),
monthYearReceived: z.date(),
negotiationStrategy: z.string(),
})
const experience = z.object({
companyId: z.string().optional(),
durationInMonths: z.number().optional(),
jobType: z.string().optional(),
level: z.string().optional(),
monthlySalary: valuation.optional(),
specialization: z.string().optional(),
title: z.string().optional(),
totalCompensation: valuation.optional(),
})
const education = z.object({
endDate: z.date().optional(),
field: z.string().optional(),
school: z.string().optional(),
startDate: z.date().optional(),
type: z.string().optional(),
})
export const offersProfileRouter = createProtectedRouter().mutation(
'create',
{
input: z.object({
background: z.object({
educations: z.array(education),
experiences: z.array(experience),
specificYoes: z.array(z.object({
domain: z.string(),
yoe: z.number()
})),
totalYoe: z.number().optional(),
}),
offers: z.array(offer)
}),
async resolve({ ctx, input }) {
// TODO: add more
const token = crypto
.createHash("sha256")
.update(Date.now().toString())
.digest("hex");
const profile = await ctx.prisma.offersProfile.create({
data: {
background: {
create: {
educations: {
create:
input.background.educations.map((x) => ({
endDate: x.endDate,
field: x.field,
school: x.school,
startDate: x.startDate,
type: x.type
}))
},
experiences: {
create:
input.background.experiences.map((x) => {
if (x.jobType === "FULLTIME" && x.totalCompensation?.currency !== undefined && x.totalCompensation.value !== undefined) {
return {
company: {
connect: {
id: x.companyId
}
},
durationInMonths: x.durationInMonths,
jobType: x.jobType,
level: x.level,
specialization: x.specialization,
title: x.title,
totalCompensation: {
create: {
currency: x.totalCompensation?.currency,
value: x.totalCompensation?.value,
}
},
}
}
if (x.jobType === "INTERN" && x.monthlySalary?.currency !== undefined && x.monthlySalary.value !== undefined) {
return {
company: {
connect: {
id: x.companyId
}
},
durationInMonths: x.durationInMonths,
jobType: x.jobType,
monthlySalary: {
create: {
currency: x.monthlySalary?.currency,
value: x.monthlySalary?.value
}
},
specialization: x.specialization,
title: x.title,
}
}
throw Prisma.PrismaClientKnownRequestError
})
},
specificYoes: {
create:
input.background.specificYoes.map((x) => ({
domain: x.domain,
yoe: x.yoe
}))
},
totalYoe: input.background.totalYoe,
}
},
editToken: token,
offers: {
create:
input.offers.map((x) => {
if (x.jobType === "INTERN" && x.job.internshipCycle !== undefined && x.job.monthlySalary?.currency !== undefined && x.job.monthlySalary.value !== undefined && x.job.startYear !== undefined) {
return {
OffersIntern: {
create: {
internshipCycle: x.job.internshipCycle,
monthlySalary: {
create: {
currency: x.job.monthlySalary?.currency,
value: x.job.monthlySalary?.value
}
},
specialization: x.job.specialization,
startYear: x.job.startYear,
title: x.job.title,
}
},
comments: x.comments,
company: {
connect: {
id: x.companyId
}
},
jobType: x.jobType,
location: x.location,
monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy
}
}
if (x.jobType === "FULLTIME" && x.job.base?.currency !== undefined && x.job.base?.value !== undefined && x.job.bonus?.currency !== undefined && x.job.bonus?.value !== undefined && x.job.stocks?.currency !== undefined && x.job.stocks?.value !== undefined && x.job.totalCompensation?.currency !== undefined && x.job.totalCompensation?.value !== undefined && x.job.level !== undefined) {
return {
OffersFullTime: {
create: {
baseSalary: {
create: {
currency: x.job.base?.currency,
value: x.job.base?.value
}
},
bonus: {
create: {
currency: x.job.bonus?.currency,
value: x.job.bonus?.value
}
},
level: x.job.level,
specialization: x.job.specialization,
stocks: {
create: {
currency: x.job.stocks?.currency,
value: x.job.stocks?.value,
}
},
title: x.job.title,
totalCompensation: {
create: {
currency: x.job.totalCompensation?.currency,
value: x.job.totalCompensation?.value,
}
},
}
},
comments: x.comments,
company: {
connect: {
id: x.companyId
}
},
jobType: x.jobType,
location: x.location,
monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy
}
}
// Throw error
throw Prisma.PrismaClientKnownRequestError
})
},
profileName: randomUUID(),
},
include: {
background: {
include: {
educations: true,
experiences: {
include: {
company: true,
monthlySalary: true,
totalCompensation: true
}
},
specificYoes: true
}
},
offers: {
include: {
OffersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true
}
},
OffersIntern: {
include: {
monthlySalary: true
}
}
}
}
},
});
// TODO: add analysis to profile object then return
return profile
}
},
);

@ -0,0 +1,297 @@
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 ascOrder = '+';
const descOrder = '-';
const sortingKeys = ['monthYearReceived', 'totalCompensation', '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;
});
data = data.sort((offer1, offer2) => {
const defaultReturn =
offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime();
if (!input.sortBy) {
return defaultReturn;
}
const order = input.sortBy.charAt(0);
const sortingKey = input.sortBy.substring(1);
if (order === ascOrder) {
return (() => {
if (sortingKey === 'monthYearReceived') {
return (
offer1.monthYearReceived.getTime() -
offer2.monthYearReceived.getTime()
);
}
if (sortingKey === 'totalCompensation') {
const salary1 = offer1.OffersFullTime?.totalCompensation.value
? offer1.OffersFullTime?.totalCompensation.value
: offer1.OffersIntern?.monthlySalary.value;
const salary2 = offer2.OffersFullTime?.totalCompensation.value
? offer2.OffersFullTime?.totalCompensation.value
: offer2.OffersIntern?.monthlySalary.value;
if (salary1 && salary2) {
return salary1 - salary2;
}
}
return defaultReturn;
})();
}
if (order === descOrder) {
return (() => {
if (sortingKey === 'monthYearReceived') {
return (
offer2.monthYearReceived.getTime() -
offer1.monthYearReceived.getTime()
);
}
if (sortingKey === 'totalCompensation') {
const salary1 = offer1.OffersFullTime?.totalCompensation.value
? offer1.OffersFullTime?.totalCompensation.value
: offer1.OffersIntern?.monthlySalary.value;
const salary2 = offer2.OffersFullTime?.totalCompensation.value
? offer2.OffersFullTime?.totalCompensation.value
: offer2.OffersIntern?.monthlySalary.value;
if (salary1 && salary2) {
return salary2 - salary1;
}
}
return defaultReturn;
})();
}
return defaultReturn;
});
return data;
},
});
Loading…
Cancel
Save