Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
-- CreateEnum
CREATE TYPE "JobType" AS ENUM ('ONSITE', 'HYBRID', 'REMOTE');

-- CreateEnum
CREATE TYPE "JobSchedule" AS ENUM ('FULL_TIME', 'PART_TIME', 'CONTRACT', 'INTERNSHIP');

-- CreateTable
CREATE TABLE "JobVacancy" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"location" TEXT NOT NULL,
"jobType" "JobType" NOT NULL,
"jobSchedule" "JobSchedule" NOT NULL,
"department" TEXT NOT NULL,
"skills" TEXT[],
"labels" TEXT[],
"salary" TEXT NOT NULL,
"jobDescription" TEXT NOT NULL,
"screeningQuestions" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"externalPortalUrl" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"workspaceId" TEXT NOT NULL,
"isTrashed" BOOLEAN NOT NULL DEFAULT false,

CONSTRAINT "JobVacancy_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "Workspace" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT NOT NULL,

CONSTRAINT "Workspace_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "_WorkspaceMembers" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "_WorkspaceMembers_AB_unique" ON "_WorkspaceMembers"("A", "B");

-- CreateIndex
CREATE INDEX "_WorkspaceMembers_B_index" ON "_WorkspaceMembers"("B");

-- AddForeignKey
ALTER TABLE "JobVacancy" ADD CONSTRAINT "JobVacancy_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "JobVacancy" ADD CONSTRAINT "JobVacancy_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_WorkspaceMembers" ADD CONSTRAINT "_WorkspaceMembers_A_fkey" FOREIGN KEY ("A") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "_WorkspaceMembers" ADD CONSTRAINT "_WorkspaceMembers_B_fkey" FOREIGN KEY ("B") REFERENCES "Workspace"("id") ON DELETE CASCADE ON UPDATE CASCADE;
52 changes: 49 additions & 3 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ datasource db {
}

model User {
id String @id
email String @unique
createdAt DateTime @default(now())
id String @id
email String @unique
createdAt DateTime @default(now())
passwordHash String?
sessions Session[]
jobVacancy JobVacancy[]
workspaces Workspace[] @relation("WorkspaceMembers")
}

model Session {
Expand All @@ -21,3 +23,47 @@ model Session {
expiresAt DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}

enum JobType {
ONSITE
HYBRID
REMOTE
}

enum JobSchedule {
FULL_TIME
PART_TIME
CONTRACT
INTERNSHIP
}

model JobVacancy {
id String @id
title String
location String
jobType JobType
jobSchedule JobSchedule
department String
skills String[]
labels String[]
salary String
jobDescription String @db.Text
screeningQuestions String
user User @relation(fields: [userId], references: [id])
userId String
externalPortalUrl String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
expiresAt DateTime
workspace Workspace @relation(fields: [workspaceId], references: [id])
workspaceId String
isTrashed Boolean @default(false)
}

model Workspace {
id String @id
name String
description String
jobVacancy JobVacancy[]
users User[] @relation("WorkspaceMembers")
}
46 changes: 29 additions & 17 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,12 @@ import {
import { mapPrismaErrorToErrorMessages, mapZodIssuesToErrorMessages } from "./lib/error-handling";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
import fastifySwagger from "@fastify/swagger";
import fastifySwaggerUI from "@fastify/swagger-ui";
import fastifyCors from "@fastify/cors";
import { env } from "./lib/env";
import { ValidationError } from "./shared/errors/validation-error";
import { ServerError } from "./shared/errors/server-error.";
import fastifySwaggerUi from "@fastify/swagger-ui";
import { writeFileSync } from "fs";
import path from "path";
import { openApiPlugin } from "./lib/openapi";
import { protectedRouter } from "./routes/v1/protected-routes/protected-route";

const port = env.PORT;

Expand Down Expand Up @@ -64,27 +62,41 @@ const bootstrap = async () => {
credentials: true,
});

app.register(openApiPlugin);
app.register(fastifySwagger, {
openapi: {
info: {
title: "Work Go API",
description: "",
version: "1.0.0",
},
servers: [],
},
transform: jsonSchemaTransform,
});

app.register(fastifySwaggerUI, {
routePrefix: "/documentation",
uiConfig: {
// Auto-inject the hard-coded token into Authorization header
requestInterceptor: (request) => {
request.headers.authorization =
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uSWQiOiJlN2V2MnFrY28zenZjaWI2In0.XRMnycxdgRjF06u1m7nDr49AXEeSWHM-MTUnwnDjs5c";
return request;
},
},
});

app.get("/", (request, reply) => {
reply.send(`Server running at port ${port}`);
});

app.register(authRouter, { prefix: "/v1/auth" });

app.register(protectedRouter, { prefix: "/v1" });
await app.ready();

if (env.COMPILE_OPENAPI_SPECS === "true") {
console.log("❕ Generating OpenAPI Specs...");
app.generateClientTypes();
console.log(`✅ OpenAPI Specs generated`);

process.exit(0);
} else {
console.log("❕ Starting server, please wait...");
const dest = await app.listen({ host: "0.0.0.0", port });
console.log(`✅ Server listing on ${dest}`);
}
console.log("❕ Starting server, please wait...");
const dest = await app.listen({ host: "0.0.0.0", port });
console.log(`✅ Server listing on ${dest}`);
} catch (error) {
console.log(`❗ Failed to start server: ${error instanceof Error ? error.message : "Uknown error occured"}`);
}
Expand Down
31 changes: 31 additions & 0 deletions src/controllers/job-vacancy-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { z } from "zod";
import { prisma } from "../lib/prisma";
import { generateIdFromEntropySize } from "lucia";
import { JobVacancyRequestSchema } from "../shared/schemas/job-vacancy-schema";

export class JobVacancyController {
public static async getJobs() {
/* await prisma.jobVacancy.create({
data: {
id: "some-job-id",
title: "Software Engineer",
description: "Develop and maintain software solutions",
company: "Tech Innovators Inc.",
location: "Remote",
url: "https://tech-innovators.com/jobs/software-engineer",
postedAt: new Date("2024-10-24"), // Set to the date the job was posted
expiresAt: new Date("2024-12-24"), // Set to the date the job expires
userId: "e7ev2qkco3zvcib6", // The ID of the user who is posting the job
},
}); */
const jobs = await prisma.jobVacancy.findMany();
return jobs;
}
public static async createJob(job: z.infer<typeof JobVacancyRequestSchema>) {
const jobId = generateIdFromEntropySize(10);
const modifiedJob = { ...job, id: jobId };
return await prisma.jobVacancy.create({
data: modifiedJob,
});
}
}
30 changes: 30 additions & 0 deletions src/routes/v1/protected-routes/job-vacancy-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FastifyPluginAsync } from "fastify";
import { ZodTypeProvider } from "fastify-type-provider-zod";
import {
JobVacanciesResponseSchema,
JobVacancyRequestSchema,
JobVacancyResponseSchema,
} from "../../../shared/schemas/job-vacancy-schema";
import { JobVacancyController } from "../../../controllers/job-vacancy-controller";

export const jobVacancyRouter: FastifyPluginAsync = async (app) => {
app
.withTypeProvider<ZodTypeProvider>()
.get("/", { schema: { response: { 200: JobVacanciesResponseSchema } } }, async (request) => {
return JobVacanciesResponseSchema.parse(await JobVacancyController.getJobs());
});
app.withTypeProvider<ZodTypeProvider>().post(
"/create",
{
schema: {
body: JobVacancyRequestSchema,
response: {
200: JobVacancyResponseSchema,
},
},
},
async (request) => {
return JobVacancyResponseSchema.parse(await JobVacancyController.createJob(request.body));
},
);
};
12 changes: 12 additions & 0 deletions src/routes/v1/protected-routes/protected-route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FastifyPluginAsync } from "fastify";
import { RegisterResponseSchema } from "../../../shared/schemas/auth-schema";
import { AuthController } from "../../../controllers/auth-controller";
import { jobVacancyRouter } from "./job-vacancy-route";

export const protectedRouter: FastifyPluginAsync = async (app) => {
app.addHook("onRequest", async (request, reply) => {
const res = RegisterResponseSchema.parse(await AuthController.verifySessionToken(request.headers.authorization));
console.log("Protected route: ", res);
});
app.register(jobVacancyRouter, { prefix: "/job-Vacancies" });
};
50 changes: 50 additions & 0 deletions src/shared/schemas/job-vacancy-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import dayjs from "dayjs";
import { z } from "zod";

export const JobVacancyResponseSchema = z.object({
id: z.string(),
title: z.string(),
location: z.string(),
jobType: z.string(),
jobSchedule: z.string(),
department: z.string(),
skills: z.array(z.string()),
labels: z.array(z.string()),
salary: z.string(),
jobDescription: z.string(),
screeningQuestions: z.string(),
userId: z.string(),
externalPortalUrl: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
expiresAt: z.date(),
workspaceId: z.string(),
isTrashed: z.boolean().default(false),
});

export const JobVacanciesResponseSchema = z.array(JobVacancyResponseSchema);

const JobType = z.enum(["ONSITE", "REMOTE", "HYBRID"]);
const JobSchedule = z.enum(["FULL_TIME", "PART_TIME", "CONTRACT", "INTERNSHIP"]);

export const JobVacancyRequestSchema = z.object({
title: z.string(),
location: z.string(),
jobType: JobType,
jobSchedule: JobSchedule,
department: z.string(),
skills: z.array(z.string()),
labels: z.array(z.string()),
salary: z.string(),
jobDescription: z.string(),
screeningQuestions: z.string(),
userId: z.string(),
externalPortalUrl: z.string(),
expiresAt: z
.string()
.refine((string) => dayjs(string).isValid)
.transform((str) => new Date(str)),

workspaceId: z.string(),
isTrashed: z.boolean().default(false),
});