internal/ and libs/ were both shared-code roots with no rule for which
one a package belonged in, so the split carried no information. Adopt a
rule the language answers by itself:
shared Go -> internal/
shared TypeScript -> libs/ (npm workspace packages)
libs/{ports,repo,server} move to internal/, leaving libs/ holding only
the two TypeScript packages (@jobs-scraper/rabbitmq-ts and
@jobs-scraper/browser-automation), which matches npm workspace
convention. internal/ is also Go's marker for code not importable from
outside the repo, which is accurate here since none of it is published.
Import paths are rewritten mechanically (jobs-scraper/libs/ ->
jobs-scraper/internal/) across 15 lines in 10 files. The 6 moved files
are pure renames with no content change. Doing this after the module
collapse in the previous commit meant no go.mod or replace-directive
edits were needed.
gofmt is applied to services/api/pkg/http/job.go, whose import group the
rewrite left out of order. Three files were already unformatted before
this refactor (internal/openai/interface.go, internal/ports/job-queries.go,
internal/repo/job-analysis-result.go) and are deliberately left alone to
keep this diff limited to the move.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
248 lines
8.6 KiB
Go
248 lines
8.6 KiB
Go
package http
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/gorilla/schema"
|
|
"github.com/jobs-scraper/internal/ports"
|
|
"github.com/jobs-scraper/internal/repo"
|
|
"github.com/jobs-scraper/internal/utils"
|
|
)
|
|
|
|
// JobHandler handles HTTP requests related to jobs
|
|
type JobHandler struct {
|
|
jobCommands *ports.JobCommands
|
|
jobAnalysisResultRepo *repo.JobAnalysisResultRepository
|
|
jobQueries *ports.JobQueries
|
|
}
|
|
|
|
// NewJobHandler creates a new instance of JobHandler
|
|
func NewJobHandler(jobCommands *ports.JobCommands, jobAnalysisResultRepo *repo.JobAnalysisResultRepository, jobQueries *ports.JobQueries) *JobHandler {
|
|
return &JobHandler{
|
|
jobCommands: jobCommands,
|
|
jobAnalysisResultRepo: jobAnalysisResultRepo,
|
|
jobQueries: jobQueries,
|
|
}
|
|
}
|
|
|
|
var decoder = schema.NewDecoder()
|
|
|
|
// RegisterRoutes registers all routes to the router
|
|
func (h *JobHandler) RegisterRoutes(router *mux.Router) {
|
|
jobs := router.PathPrefix("/jobs").Subrouter()
|
|
|
|
//jobs.Use(AuthMiddleware)
|
|
|
|
jobs.HandleFunc("", utils.Make(h.CreateJob)).Methods("POST")
|
|
jobs.HandleFunc("/analysis", utils.Make(h.GetAllAnalysisResults)).Methods("GET")
|
|
jobs.HandleFunc("/{id}/analysis", utils.Make(h.GetJobAnalysisResult)).Methods("GET")
|
|
jobs.HandleFunc("/analysis/top-matches", utils.Make(h.GetTopMatches)).Methods("GET")
|
|
jobs.HandleFunc("", utils.Make(h.GetJobs)).Methods("GET")
|
|
jobs.HandleFunc("/apply/{id}", utils.Make(h.ApplyForJob)).Methods("POST")
|
|
}
|
|
|
|
// CreateJob handles the creation of a new job
|
|
// @Summary Create a new job
|
|
// @Description Creates a new job with the provided specifications
|
|
// @Tags jobs
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param job body ports.CreateJobCommand true "Job creation data"
|
|
// @Success 201 {object} map[string]string "Successfully created job"
|
|
// @Failure 400 {object} map[string]string "Invalid request data"
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
// @Router /jobs [post]
|
|
func (h *JobHandler) CreateJob(w http.ResponseWriter, r *http.Request) error {
|
|
var cmd ports.CreateJobCommand
|
|
if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
|
|
slog.Error(err.Error())
|
|
return utils.NewAPIError(http.StatusBadRequest, utils.InvalidJSON())
|
|
}
|
|
|
|
err := h.jobCommands.CreateJob.Handle(cmd)
|
|
if err != nil {
|
|
slog.Error(err.Error())
|
|
return utils.NewAPIError(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusCreated, map[string]string{"message": "Success"})
|
|
return nil
|
|
}
|
|
|
|
// Apply applies for a job by id
|
|
// @Summary applies for a job
|
|
// @Description Applies for a job with the provided id
|
|
// @Tags jobs
|
|
// @Param id query string false "Job ID"
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 201 {object} map[string]string "Successfully applied for job"
|
|
// @Failure 400 {object} map[string]string "Invalid request data"
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
// @Router /jobs/apply/{id} [post]
|
|
func (h *JobHandler) ApplyForJob(w http.ResponseWriter, r *http.Request) error {
|
|
jobId := r.URL.Query().Get("id")
|
|
|
|
if jobId == "" {
|
|
slog.Error("Job ID is required")
|
|
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("job ID is required"))
|
|
}
|
|
|
|
jobIdInt, err := strconv.Atoi(jobId)
|
|
|
|
if err != nil {
|
|
slog.Error("Invalid Job ID", "error", err)
|
|
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid job ID"))
|
|
}
|
|
|
|
err = h.jobCommands.ApplyForJob.Handle(jobIdInt)
|
|
|
|
if err != nil {
|
|
slog.Error(err.Error())
|
|
return utils.NewAPIError(http.StatusInternalServerError, fmt.Errorf("Internal server error"))
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusCreated, map[string]string{"message": "Success"})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetJobs handles the retrieval of all jobs
|
|
// @Summary Gets all jobs
|
|
// @Description Gets all jobs with the provided specifications
|
|
// @Tags jobs
|
|
// @Param location query string false "Location filter"
|
|
// @Param keywords query string false "Keywords filter (searches title and company)"
|
|
// @Param fwt query string false "Full/Part time filter"
|
|
// @Param provider query int false "Job provider (LinkedIn=0, Indeed=1, Glassdoor=2, Bayt=3, TokyoDev=4, JapanDev=5, Google=6, HiringCafe=7)" Enums(0,1,2,3,4,5,6,7)
|
|
// @Param pageNumber query int false "Page number"
|
|
// @Param pageSize query int false "Number of elements"
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Success 200 {object} map[string]any "Successfully retrieved jobs"
|
|
// @Failure 400 {object} map[string]any "Invalid request data"
|
|
// @Failure 500 {object} map[string]any "Internal server error"
|
|
// @Router /jobs [get]
|
|
func (h *JobHandler) GetJobs(w http.ResponseWriter, r *http.Request) error {
|
|
query := ports.GetJobQuery{
|
|
PageNumber: 1,
|
|
PageSize: 10,
|
|
}
|
|
|
|
if pageNumberStr := r.URL.Query().Get("pageNumber"); pageNumberStr != "" {
|
|
if page, err := strconv.Atoi(pageNumberStr); err == nil && page > 0 {
|
|
query.PageNumber = page
|
|
}
|
|
}
|
|
|
|
if pageSizeStr := r.URL.Query().Get("pageSize"); pageSizeStr != "" {
|
|
if pageSize, err := strconv.Atoi(pageSizeStr); err == nil && pageSize > 0 {
|
|
query.PageSize = pageSize
|
|
}
|
|
}
|
|
|
|
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
|
|
slog.Error("Failed to decode query params", "error", err)
|
|
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid query parameters: %w", err))
|
|
}
|
|
|
|
result, totalCount, err := h.jobQueries.GetJobs.Handle(query)
|
|
|
|
if err != nil {
|
|
slog.Error(err.Error())
|
|
return utils.NewAPIError(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
pagination := utils.NewPageViewModel(totalCount, query.PageNumber, query.PageSize)
|
|
|
|
utils.WriteJSON(w, http.StatusOK, map[string]any{
|
|
"jobs": result,
|
|
"pagination": pagination,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetAllAnalysisResults handles retrieving all job analysis results
|
|
// @Summary Get all job analysis results
|
|
// @Description Retrieves all job analysis results from the database
|
|
// @Tags jobs
|
|
// @Produce json
|
|
// @Success 200 {array} domain.JobAnalysisResult "List of job analysis results"
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
// @Router /jobs/analysis [get]
|
|
func (h *JobHandler) GetAllAnalysisResults(w http.ResponseWriter, r *http.Request) error {
|
|
results, err := h.jobAnalysisResultRepo.GetAllAnalysisResults()
|
|
if err != nil {
|
|
slog.Error("Failed to get analysis results", "error", err)
|
|
return utils.NewAPIError(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, results)
|
|
return nil
|
|
}
|
|
|
|
// GetJobAnalysisResult handles retrieving analysis result for a specific job
|
|
// @Summary Get job analysis result by job ID
|
|
// @Description Retrieves the analysis result for a specific job
|
|
// @Tags jobs
|
|
// @Produce json
|
|
// @Param id path int true "Job ID"
|
|
// @Success 200 {object} domain.JobAnalysisResult "Job analysis result"
|
|
// @Failure 400 {object} map[string]string "Invalid job ID"
|
|
// @Failure 404 {object} map[string]string "Analysis result not found"
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
// @Router /jobs/{id}/analysis [get]
|
|
func (h *JobHandler) GetJobAnalysisResult(w http.ResponseWriter, r *http.Request) error {
|
|
vars := mux.Vars(r)
|
|
jobID, err := strconv.ParseInt(vars["id"], 10, 64)
|
|
if err != nil {
|
|
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid job ID"))
|
|
}
|
|
|
|
result, err := h.jobAnalysisResultRepo.GetAnalysisResultByJobID(jobID)
|
|
if err != nil {
|
|
slog.Error("Failed to get analysis result", "jobID", jobID, "error", err)
|
|
return utils.NewAPIError(http.StatusNotFound, fmt.Errorf("analysis result not found"))
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, result)
|
|
return nil
|
|
}
|
|
|
|
// GetTopMatches handles retrieving top matching jobs based on analysis score
|
|
// @Summary Get top matching jobs
|
|
// @Description Retrieves jobs with high analysis match scores
|
|
// @Tags jobs
|
|
// @Produce json
|
|
// @Param min_score query int false "Minimum match score (default: 70)"
|
|
// @Success 200 {array} domain.JobAnalysisResult "List of top matching job analysis results"
|
|
// @Failure 400 {object} map[string]string "Invalid minimum score"
|
|
// @Failure 500 {object} map[string]string "Internal server error"
|
|
// @Router /jobs/analysis/top-matches [get]
|
|
func (h *JobHandler) GetTopMatches(w http.ResponseWriter, r *http.Request) error {
|
|
minScoreStr := r.URL.Query().Get("min_score")
|
|
minScore := 70 // default minimum score
|
|
|
|
if minScoreStr != "" {
|
|
var err error
|
|
minScore, err = strconv.Atoi(minScoreStr)
|
|
if err != nil {
|
|
return utils.NewAPIError(http.StatusBadRequest, fmt.Errorf("invalid minimum score"))
|
|
}
|
|
}
|
|
|
|
results, err := h.jobAnalysisResultRepo.GetAnalysisResultsByMatchScore(minScore)
|
|
if err != nil {
|
|
slog.Error("Failed to get top matches", "minScore", minScore, "error", err)
|
|
return utils.NewAPIError(http.StatusInternalServerError, err)
|
|
}
|
|
|
|
utils.WriteJSON(w, http.StatusOK, results)
|
|
return nil
|
|
}
|