2026-01-21 16:31:25 +00:00

127 lines
4.6 KiB
TypeScript

import { Router, Request, Response } from 'express';
import { randomUUID } from 'crypto';
import { z } from 'zod';
import * as jobsRepo from '../../repositories/jobs.js';
import { inferManualJobDetails } from '../../services/manualJob.js';
import { scoreJobSuitability } from '../../services/scorer.js';
import { getProfile } from '../../services/profile.js';
import type { ApiResponse, ManualJobInferenceResponse } from '../../../shared/types.js';
export const manualJobsRouter = Router();
const manualJobInferenceSchema = z.object({
jobDescription: z.string().trim().min(1).max(40000),
});
const manualJobImportSchema = z.object({
job: z.object({
title: z.string().trim().min(1).max(500),
employer: z.string().trim().min(1).max(500),
jobUrl: z.string().trim().url().max(2000).optional(),
applicationLink: z.string().trim().url().max(2000).optional(),
location: z.string().trim().max(200).optional(),
salary: z.string().trim().max(200).optional(),
deadline: z.string().trim().max(100).optional(),
jobDescription: z.string().trim().min(1).max(40000),
jobType: z.string().trim().max(200).optional(),
jobLevel: z.string().trim().max(200).optional(),
jobFunction: z.string().trim().max(200).optional(),
disciplines: z.string().trim().max(200).optional(),
degreeRequired: z.string().trim().max(200).optional(),
starting: z.string().trim().max(200).optional(),
}),
});
const cleanOptional = (value?: string | null) => {
if (!value) return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
};
/**
* POST /api/manual-jobs/infer - Infer job details from a pasted description
*/
manualJobsRouter.post('/infer', async (req: Request, res: Response) => {
try {
const input = manualJobInferenceSchema.parse(req.body ?? {});
const result = await inferManualJobDetails(input.jobDescription);
const response: ApiResponse<ManualJobInferenceResponse> = {
success: true,
data: {
job: result.job,
warning: result.warning ?? null,
},
};
res.json(response);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ success: false, error: error.message });
}
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ success: false, error: message });
}
});
/**
* POST /api/manual-jobs/import - Import a manually curated job into the DB
*/
manualJobsRouter.post('/import', async (req: Request, res: Response) => {
try {
const input = manualJobImportSchema.parse(req.body ?? {});
const job = input.job;
const jobUrl =
cleanOptional(job.jobUrl) ||
cleanOptional(job.applicationLink) ||
`manual://${randomUUID()}`;
const createdJob = await jobsRepo.createJob({
source: 'manual',
title: job.title.trim(),
employer: job.employer.trim(),
jobUrl,
applicationLink: cleanOptional(job.applicationLink) ?? undefined,
location: cleanOptional(job.location) ?? undefined,
salary: cleanOptional(job.salary) ?? undefined,
deadline: cleanOptional(job.deadline) ?? undefined,
jobDescription: job.jobDescription.trim(),
jobType: cleanOptional(job.jobType) ?? undefined,
jobLevel: cleanOptional(job.jobLevel) ?? undefined,
jobFunction: cleanOptional(job.jobFunction) ?? undefined,
disciplines: cleanOptional(job.disciplines) ?? undefined,
degreeRequired: cleanOptional(job.degreeRequired) ?? undefined,
starting: cleanOptional(job.starting) ?? undefined,
});
// Score asynchronously so the import returns immediately.
(async () => {
try {
const rawProfile = await getProfile();
if (!rawProfile || typeof rawProfile !== 'object' || Array.isArray(rawProfile)) {
throw new Error('Invalid resume profile format');
}
const profile = rawProfile as Record<string, unknown>;
const { score, reason } = await scoreJobSuitability(createdJob, profile);
await jobsRepo.updateJob(createdJob.id, {
suitabilityScore: score,
suitabilityReason: reason,
});
} catch (error) {
console.warn('Manual job scoring failed:', error);
}
})().catch((error) => {
console.warn('Manual job scoring task failed to start:', error);
});
res.json({ success: true, data: createdJob });
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ success: false, error: error.message });
}
const message = error instanceof Error ? error.message : 'Unknown error';
res.status(500).json({ success: false, error: message });
}
});