mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-11 17:37:59 +00:00
* fix(jobnet-search): preserve full occupation identifiers in tables The documented ESCO concept URI is 72 characters long, but the table renderer cut every identifier to 70, losing the last two UUID characters. Size the identifier column from its contents, retaining the existing minimum width and readable label alignment. Exercise the actual documented occupations CLI with the README API response served over loopback. Table output must retain the same complete URI and UUID as JSON and plain output; no live portal requests. * test(jobnet-search): cover Windows occupation fixture newlines
88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
import { defineCommand, option } from "@bunli/core"
|
|
import { z } from "zod"
|
|
import { apiFetch, writeError } from "../helpers.js"
|
|
|
|
interface OccupationAlias {
|
|
aliasIdentifier: string
|
|
conceptUriDa: string
|
|
alternativeLabelDa: string
|
|
}
|
|
|
|
interface Occupation {
|
|
conceptUriDa: string
|
|
preferredLabelDa: string
|
|
aliases: OccupationAlias[]
|
|
}
|
|
|
|
export const occupations = defineCommand({
|
|
name: "occupations",
|
|
description: "Search occupation types (for building filters)",
|
|
options: {
|
|
"search-string": option(z.string().optional(), {
|
|
description: "Search term for occupation, e.g. sygeplejerske",
|
|
}),
|
|
"per-page": option(z.coerce.number().int().min(1).default(10), {
|
|
description: "Max results to return",
|
|
}),
|
|
format: option(z.enum(["json", "table", "plain"]).default("json"), {
|
|
description: "Output format: json, table, plain",
|
|
}),
|
|
},
|
|
handler: async ({ flags, signal }) => {
|
|
if (signal.aborted) return
|
|
|
|
if (!flags["search-string"]) {
|
|
writeError("--search-string is required", "MISSING_REQUIRED")
|
|
process.exit(1)
|
|
}
|
|
|
|
const params: Record<string, string> = {
|
|
searchString: flags["search-string"],
|
|
pageSize: String(flags["per-page"]),
|
|
}
|
|
|
|
try {
|
|
const rawData = await apiFetch<Occupation[]>("/OccupationSearch", params)
|
|
|
|
if (signal.aborted) return
|
|
|
|
// Client-side filter — API does not enforce pageSize reliably
|
|
const data = rawData.slice(0, flags["per-page"])
|
|
|
|
if (flags.format === "json") {
|
|
console.log(JSON.stringify(data, null, 2))
|
|
} else if (flags.format === "table") {
|
|
outputTable(data)
|
|
} else {
|
|
outputPlain(data)
|
|
}
|
|
} catch (err) {
|
|
writeError(err instanceof Error ? err.message : String(err), "API_ERROR")
|
|
process.exit(1)
|
|
}
|
|
},
|
|
})
|
|
|
|
function outputTable(data: Occupation[]): void {
|
|
// The full URI is an identifier: truncating it cuts the UUID used to build
|
|
// search filters. Widen the column instead of abbreviating its value.
|
|
const uriWidth = data.reduce((width, o) => Math.max(width, o.conceptUriDa.length), 70)
|
|
console.log(`${"conceptUriDa".padEnd(uriWidth)} preferredLabelDa`)
|
|
for (const o of data) {
|
|
const uri = o.conceptUriDa.padEnd(uriWidth)
|
|
const label = o.preferredLabelDa
|
|
console.log(`${uri} ${label}`)
|
|
}
|
|
}
|
|
|
|
function outputPlain(data: Occupation[]): void {
|
|
for (const o of data) {
|
|
console.log(`label: ${o.preferredLabelDa}`)
|
|
console.log(`uri: ${o.conceptUriDa}`)
|
|
if (o.aliases.length > 0) {
|
|
console.log(`aliases: ${o.aliases.map((a) => a.alternativeLabelDa).join(", ")}`)
|
|
}
|
|
console.log("")
|
|
}
|
|
}
|