mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 16:46:24 +00:00
test(jobnet): extract pure helpers and add normalization tests (#133)
* docs: clean all add-template compile artifacts * Add Jobnet CLI normalization tests
This commit is contained in:
@@ -2,7 +2,7 @@ import { defineCommand, option } from "@bunli/core"
|
||||
import { z } from "zod"
|
||||
import { apiFetch, writeError, stripHtml } from "../helpers.js"
|
||||
|
||||
interface DetailApiResponse {
|
||||
export interface DetailApiResponse {
|
||||
id: string
|
||||
title: string
|
||||
body: string
|
||||
@@ -118,15 +118,23 @@ function outputTable(data: DetailApiResponse): void {
|
||||
}
|
||||
|
||||
function outputPlain(data: DetailApiResponse): void {
|
||||
console.log(`Title: ${data.title}`)
|
||||
console.log(`Employer: ${data.employer.name}`)
|
||||
console.log(`Location: ${data.job.address.city ?? "-"}, ${data.job.address.countryName}`)
|
||||
console.log(`Published: ${data.publicationDateTime}`)
|
||||
console.log(`Deadline: ${data.application.deadlineDate ?? "-"}`)
|
||||
console.log(`Positions: ${data.application.availablePositions}`)
|
||||
if (data.application.url) {
|
||||
console.log(`Apply: ${data.application.url}`)
|
||||
}
|
||||
console.log("")
|
||||
console.log(stripHtml(data.body))
|
||||
console.log(formatDetailPlain(data))
|
||||
}
|
||||
|
||||
export function formatDetailPlain(data: DetailApiResponse): string {
|
||||
const lines = [
|
||||
`Title: ${data.title}`,
|
||||
`Employer: ${data.employer.name}`,
|
||||
`Location: ${data.job.address.city ?? "-"}, ${data.job.address.countryName}`,
|
||||
`Published: ${data.publicationDateTime}`,
|
||||
`Deadline: ${data.application.deadlineDate ?? "-"}`,
|
||||
`Positions: ${data.application.availablePositions}`,
|
||||
]
|
||||
|
||||
if (data.application.url) {
|
||||
lines.push(`Apply: ${data.application.url}`)
|
||||
}
|
||||
|
||||
lines.push("", stripHtml(data.body))
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ import { defineCommand, option } from "@bunli/core"
|
||||
import { z } from "zod"
|
||||
import { apiFetch, writeError } from "../helpers.js"
|
||||
|
||||
interface SearchApiResponse {
|
||||
export interface SearchApiResponse {
|
||||
jobAds: JobAdRaw[]
|
||||
searchFacets: SearchFacetsRaw
|
||||
totalJobAdCount: number
|
||||
searchString: string | null
|
||||
}
|
||||
|
||||
interface JobAdRaw {
|
||||
export interface JobAdRaw {
|
||||
jobAdId: string
|
||||
title: string
|
||||
hiringOrgName: string
|
||||
@@ -33,7 +33,7 @@ interface JobAdRaw {
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface SearchFacetsRaw {
|
||||
export interface SearchFacetsRaw {
|
||||
regions: Array<{ type: string; jobAdCount: number }>
|
||||
workHours: Array<{ type: string; jobAdCount: number }>
|
||||
employmentDurations: Array<{ type: string; jobAdCount: number }>
|
||||
@@ -41,6 +41,89 @@ interface SearchFacetsRaw {
|
||||
countries: Array<{ label: string; identifier: string; jobAdCount: number }>
|
||||
}
|
||||
|
||||
export interface SearchFlags {
|
||||
"search-string"?: string
|
||||
page: number
|
||||
"per-page": number
|
||||
order: string
|
||||
region?: string
|
||||
"work-hours"?: string
|
||||
duration?: string
|
||||
"job-type"?: string
|
||||
"postal-code"?: string
|
||||
radius: number
|
||||
"occupation-area"?: string
|
||||
"occupation-group"?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export function buildSearchParams(flags: SearchFlags): Record<string, string> {
|
||||
const params: Record<string, string> = {
|
||||
resultsPerPage: String(flags["per-page"]),
|
||||
pageNumber: String(flags.page),
|
||||
orderType: flags.order,
|
||||
}
|
||||
|
||||
if (flags["search-string"]) params["searchString"] = flags["search-string"]
|
||||
if (flags.region) params["regions"] = flags.region
|
||||
if (flags["work-hours"]) params["workHoursType"] = flags["work-hours"]
|
||||
if (flags.duration) params["employmentDurationType"] = flags.duration
|
||||
if (flags["job-type"]) params["jobAnnouncementType"] = flags["job-type"]
|
||||
if (flags["postal-code"]) {
|
||||
params["postalCode"] = flags["postal-code"]
|
||||
params["kmRadius"] = String(flags.radius)
|
||||
}
|
||||
if (flags["occupation-area"]) params["occupationAreas"] = flags["occupation-area"]
|
||||
if (flags["occupation-group"]) params["occupationGroups"] = flags["occupation-group"]
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export function createSearchOutput(data: SearchApiResponse, flags: SearchFlags) {
|
||||
let results = data.jobAds.map((job) => ({
|
||||
jobAdId: job.jobAdId,
|
||||
title: job.title,
|
||||
hiringOrgName: job.hiringOrgName,
|
||||
occupation: job.occupation ?? null,
|
||||
municipality: job.municipality ?? null,
|
||||
postalCode: job.postalCode ?? null,
|
||||
postalDistrictName: job.postalDistrictName ?? null,
|
||||
country: job.country,
|
||||
publicationDate: job.publicationDate,
|
||||
applicationDeadline: job.applicationDeadline ?? null,
|
||||
applicationDeadlineStatus: job.applicationDeadlineStatus ?? null,
|
||||
workHourPartTime: job.workHourPartTime,
|
||||
isExternal: job.isExternal,
|
||||
hasLogo: job.hasLogo,
|
||||
logoUrl: job.logoUrl ?? null,
|
||||
cvr: job.cvr ?? null,
|
||||
workPlaceAddress: job.workPlaceAddress ?? "",
|
||||
isSeen: job.isSeen,
|
||||
isFavorite: job.isFavorite,
|
||||
}))
|
||||
|
||||
if (flags.limit !== undefined) {
|
||||
results = results.slice(0, flags.limit)
|
||||
}
|
||||
|
||||
const facets = {
|
||||
regions: data.searchFacets.regions ?? [],
|
||||
workHours: data.searchFacets.workHours ?? [],
|
||||
employmentDurations: data.searchFacets.employmentDurations ?? [],
|
||||
occupationAreas: data.searchFacets.occupationAreas ?? [],
|
||||
countries: data.searchFacets.countries ?? [],
|
||||
}
|
||||
|
||||
const meta = {
|
||||
totalJobAdCount: data.totalJobAdCount,
|
||||
pageNumber: flags.page,
|
||||
resultsPerPage: flags["per-page"],
|
||||
searchString: data.searchString ?? null,
|
||||
}
|
||||
|
||||
return { meta, facets, results }
|
||||
}
|
||||
|
||||
export const search = defineCommand({
|
||||
name: "search",
|
||||
description: "Search for job ads with filters",
|
||||
@@ -91,79 +174,21 @@ export const search = defineCommand({
|
||||
handler: async ({ flags, signal }) => {
|
||||
if (signal.aborted) return
|
||||
|
||||
const params: Record<string, string> = {
|
||||
resultsPerPage: String(flags["per-page"]),
|
||||
pageNumber: String(flags.page),
|
||||
orderType: flags.order,
|
||||
}
|
||||
|
||||
if (flags["search-string"]) params["searchString"] = flags["search-string"]
|
||||
if (flags.region) params["regions"] = flags.region
|
||||
if (flags["work-hours"]) params["workHoursType"] = flags["work-hours"]
|
||||
if (flags.duration) params["employmentDurationType"] = flags.duration
|
||||
if (flags["job-type"]) params["jobAnnouncementType"] = flags["job-type"]
|
||||
if (flags["postal-code"]) {
|
||||
params["postalCode"] = flags["postal-code"]
|
||||
params["kmRadius"] = String(flags.radius)
|
||||
}
|
||||
if (flags["occupation-area"]) params["occupationAreas"] = flags["occupation-area"]
|
||||
if (flags["occupation-group"]) params["occupationGroups"] = flags["occupation-group"]
|
||||
const params = buildSearchParams(flags)
|
||||
|
||||
try {
|
||||
const data = await apiFetch<SearchApiResponse>("/FindJob/Search", params)
|
||||
|
||||
if (signal.aborted) return
|
||||
|
||||
// Map raw job ads to documented output shape (omit description)
|
||||
let results = data.jobAds.map((job) => ({
|
||||
jobAdId: job.jobAdId,
|
||||
title: job.title,
|
||||
hiringOrgName: job.hiringOrgName,
|
||||
occupation: job.occupation ?? null,
|
||||
municipality: job.municipality ?? null,
|
||||
postalCode: job.postalCode ?? null,
|
||||
postalDistrictName: job.postalDistrictName ?? null,
|
||||
country: job.country,
|
||||
publicationDate: job.publicationDate,
|
||||
applicationDeadline: job.applicationDeadline ?? null,
|
||||
applicationDeadlineStatus: job.applicationDeadlineStatus ?? null,
|
||||
workHourPartTime: job.workHourPartTime,
|
||||
isExternal: job.isExternal,
|
||||
hasLogo: job.hasLogo,
|
||||
logoUrl: job.logoUrl ?? null,
|
||||
cvr: job.cvr ?? null,
|
||||
workPlaceAddress: job.workPlaceAddress ?? "",
|
||||
isSeen: job.isSeen,
|
||||
isFavorite: job.isFavorite,
|
||||
}))
|
||||
|
||||
if (flags.limit !== undefined) {
|
||||
results = results.slice(0, flags.limit)
|
||||
}
|
||||
|
||||
const facets = {
|
||||
regions: data.searchFacets.regions ?? [],
|
||||
workHours: data.searchFacets.workHours ?? [],
|
||||
employmentDurations: data.searchFacets.employmentDurations ?? [],
|
||||
occupationAreas: data.searchFacets.occupationAreas ?? [],
|
||||
countries: data.searchFacets.countries ?? [],
|
||||
}
|
||||
|
||||
const meta = {
|
||||
totalJobAdCount: data.totalJobAdCount,
|
||||
pageNumber: flags.page,
|
||||
resultsPerPage: flags["per-page"],
|
||||
searchString: data.searchString ?? null,
|
||||
}
|
||||
|
||||
const output = { meta, facets, results }
|
||||
const output = createSearchOutput(data, flags)
|
||||
|
||||
if (flags.format === "json") {
|
||||
console.log(JSON.stringify(output, null, 2))
|
||||
} else if (flags.format === "table") {
|
||||
outputTable(results)
|
||||
outputTable(output.results)
|
||||
} else {
|
||||
outputPlain(results)
|
||||
outputPlain(output.results)
|
||||
}
|
||||
} catch (err) {
|
||||
writeError(err instanceof Error ? err.message : String(err), "API_ERROR")
|
||||
|
||||
Reference in New Issue
Block a user