diff --git a/prisma/migrations/20241114144529_added_job_vacancy_model/migration.sql b/prisma/migrations/20241114144529_added_job_vacancy_model/migration.sql new file mode 100644 index 0000000..ebfc989 --- /dev/null +++ b/prisma/migrations/20241114144529_added_job_vacancy_model/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f50a445..9491bb3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 { @@ -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") +} diff --git a/src/app.ts b/src/app.ts index 0e39e60..662cc1b 100644 --- a/src/app.ts +++ b/src/app.ts @@ -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; @@ -64,7 +62,29 @@ 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}`); @@ -72,19 +92,11 @@ const bootstrap = async () => { 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"}`); } diff --git a/src/controllers/job-vacancy-controller.ts b/src/controllers/job-vacancy-controller.ts new file mode 100644 index 0000000..ed86ab5 --- /dev/null +++ b/src/controllers/job-vacancy-controller.ts @@ -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) { + const jobId = generateIdFromEntropySize(10); + const modifiedJob = { ...job, id: jobId }; + return await prisma.jobVacancy.create({ + data: modifiedJob, + }); + } +} diff --git a/src/routes/v1/protected-routes/job-vacancy-route.ts b/src/routes/v1/protected-routes/job-vacancy-route.ts new file mode 100644 index 0000000..f288ea1 --- /dev/null +++ b/src/routes/v1/protected-routes/job-vacancy-route.ts @@ -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() + .get("/", { schema: { response: { 200: JobVacanciesResponseSchema } } }, async (request) => { + return JobVacanciesResponseSchema.parse(await JobVacancyController.getJobs()); + }); + app.withTypeProvider().post( + "/create", + { + schema: { + body: JobVacancyRequestSchema, + response: { + 200: JobVacancyResponseSchema, + }, + }, + }, + async (request) => { + return JobVacancyResponseSchema.parse(await JobVacancyController.createJob(request.body)); + }, + ); +}; diff --git a/src/routes/v1/protected-routes/protected-route.ts b/src/routes/v1/protected-routes/protected-route.ts new file mode 100644 index 0000000..3a9dc24 --- /dev/null +++ b/src/routes/v1/protected-routes/protected-route.ts @@ -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" }); +}; diff --git a/src/shared/schemas/job-vacancy-schema.ts b/src/shared/schemas/job-vacancy-schema.ts new file mode 100644 index 0000000..f35bab2 --- /dev/null +++ b/src/shared/schemas/job-vacancy-schema.ts @@ -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), +});