mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
fix(jobdanmark-search): emit the /scrape contract fields in search output (#340)
Adds additive normalization so jobdanmark search output carries the cross-portal contract fields: company from companyName, location as the city after the postal code in companyAddress (null-safe - a missing or null address yields null instead of crashing the search), and date/deadline converted from DD-MM-YYYY to YYYY-MM-DD with safe passthrough on unexpected formats. Native fields unchanged. Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
This commit is contained in:
@@ -148,7 +148,11 @@ bun run src/cli.ts search --text "sygeplejerske" --zip 8000 --limit 10
|
|||||||
"url": "https://jobdanmark.dk/media/idvbnt4y/rah-service-as-billede.png",
|
"url": "https://jobdanmark.dk/media/idvbnt4y/rah-service-as-billede.png",
|
||||||
"focalPoint": { "top": 0.488, "left": 0.499 }
|
"focalPoint": { "top": 0.488, "left": 0.499 }
|
||||||
},
|
},
|
||||||
"silhouetteLogo": false
|
"silhouetteLogo": false,
|
||||||
|
"company": "Rah Service A/S",
|
||||||
|
"location": "Ringkøbing",
|
||||||
|
"date": "2026-03-12",
|
||||||
|
"deadline": "2026-04-10"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -161,6 +165,7 @@ bun run src/cli.ts search --text "sygeplejerske" --zip 8000 --limit 10
|
|||||||
> - `companyLogo` can be `null`.
|
> - `companyLogo` can be `null`.
|
||||||
> - `publishedDate` format: `"DD-MM-YYYY"`.
|
> - `publishedDate` format: `"DD-MM-YYYY"`.
|
||||||
> - `coverImage` can be `null`.
|
> - `coverImage` can be `null`.
|
||||||
|
> - Every result also carries the cross-portal contract fields `company`, `location`, `date` and `deadline`, derived from `companyName`, the city after the postal code in `companyAddress`, and the day-first dates converted to `YYYY-MM-DD` — `/scrape` Step 2 expects search output to include title, company, location, date, and URL. Native fields are preserved unchanged.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { defineCommand, option } from "@bunli/core"
|
|||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { apiPost, writeError, BASE_URL } from "../helpers.js"
|
import { apiPost, writeError, BASE_URL } from "../helpers.js"
|
||||||
|
|
||||||
interface ApiSearchItem {
|
export interface ApiSearchItem {
|
||||||
title: string
|
title: string
|
||||||
companyName: string
|
companyName: string
|
||||||
companyLogo: {
|
companyLogo: {
|
||||||
@@ -12,7 +12,7 @@ interface ApiSearchItem {
|
|||||||
} | null
|
} | null
|
||||||
companyLogoSvgMarkup: string | null
|
companyLogoSvgMarkup: string | null
|
||||||
overlayColor: string | null
|
overlayColor: string | null
|
||||||
companyAddress: string
|
companyAddress: string | null
|
||||||
jobTypes: string[]
|
jobTypes: string[]
|
||||||
boostJob: boolean
|
boostJob: boolean
|
||||||
publishedDate: string
|
publishedDate: string
|
||||||
@@ -34,7 +34,12 @@ interface ApiSearchResponse {
|
|||||||
totalPages: number
|
totalPages: number
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeItem(item: ApiSearchItem): Record<string, unknown> {
|
function toContractDate(value: string | null): string | null {
|
||||||
|
const match = value?.match(/^(\d{2})-(\d{2})-(\d{4})$/)
|
||||||
|
return match ? `${match[3]}-${match[2]}-${match[1]}` : (value ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeItem(item: ApiSearchItem): Record<string, unknown> {
|
||||||
const relativeUrl = item.url
|
const relativeUrl = item.url
|
||||||
const fullUrl = relativeUrl.startsWith("http")
|
const fullUrl = relativeUrl.startsWith("http")
|
||||||
? relativeUrl
|
? relativeUrl
|
||||||
@@ -77,6 +82,10 @@ function normalizeItem(item: ApiSearchItem): Record<string, unknown> {
|
|||||||
slug,
|
slug,
|
||||||
coverImage,
|
coverImage,
|
||||||
silhouetteLogo: item.silhouetteLogo,
|
silhouetteLogo: item.silhouetteLogo,
|
||||||
|
company: item.companyName,
|
||||||
|
location: item.companyAddress?.match(/\d{4}\s+(.+)$/)?.[1] ?? null,
|
||||||
|
date: toContractDate(item.publishedDate),
|
||||||
|
deadline: toContractDate(item.applicationDeadline),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { normalizeItem, type ApiSearchItem } from "../src/commands/search";
|
||||||
|
|
||||||
|
function item(): ApiSearchItem {
|
||||||
|
return {
|
||||||
|
title: "Softwareudvikler",
|
||||||
|
companyName: "Statens It",
|
||||||
|
companyLogo: null,
|
||||||
|
companyLogoSvgMarkup: null,
|
||||||
|
overlayColor: null,
|
||||||
|
companyAddress: "Lautruphøj 2, 2750 Ballerup",
|
||||||
|
jobTypes: ["fuldtid"],
|
||||||
|
boostJob: false,
|
||||||
|
publishedDate: "27-07-2026",
|
||||||
|
applicationDeadline: "17-08-2026",
|
||||||
|
url: "/job/softwareudvikler-til-statens-it",
|
||||||
|
coverImage: null,
|
||||||
|
silhouetteLogo: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Jobdanmark search normalization", () => {
|
||||||
|
test("additively emits the /scrape contract fields (company, location, date, deadline)", () => {
|
||||||
|
const result = normalizeItem(item());
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
company: "Statens It",
|
||||||
|
location: "Ballerup",
|
||||||
|
date: "2026-07-27",
|
||||||
|
deadline: "2026-08-17",
|
||||||
|
url: "https://jobdanmark.dk/job/softwareudvikler-til-statens-it",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("maps a missing address zip and a null deadline to null", () => {
|
||||||
|
const result = normalizeItem({
|
||||||
|
...item(),
|
||||||
|
companyAddress: "Lautruphøj 2",
|
||||||
|
applicationDeadline: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.location).toBeNull();
|
||||||
|
expect(result.deadline).toBeNull();
|
||||||
|
expect(result.company).toBe("Statens It");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("survives a null companyAddress from the API", () => {
|
||||||
|
const result = normalizeItem({
|
||||||
|
...item(),
|
||||||
|
companyAddress: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.location).toBeNull();
|
||||||
|
expect(result.company).toBe("Statens It");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps native fields unchanged (additive contract)", () => {
|
||||||
|
const result = normalizeItem(item());
|
||||||
|
|
||||||
|
expect(result.companyName).toBe("Statens It");
|
||||||
|
expect(result.publishedDate).toBe("27-07-2026");
|
||||||
|
expect(result.applicationDeadline).toBe("17-08-2026");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -46,6 +46,13 @@ per-file diff commands.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **`jobdanmark-search` search output now carries the `/scrape` contract fields** - the CLI
|
||||||
|
exposed the API-native schema (`companyName`, `publishedDate` in `DD-MM-YYYY`, …) with no
|
||||||
|
`company`, `location`, `date` or `deadline`, so every `/scrape` run flagged jobdanmark as
|
||||||
|
degraded and the `seen_jobs.json` dedupe lost the company. Search results now additively emit
|
||||||
|
`company`, `location` (city after the postal code in `companyAddress`), and `date`/`deadline`
|
||||||
|
in the `YYYY-MM-DD` convention, with null-safe handling of a missing address.
|
||||||
|
|
||||||
- **`jobnet-search` search output now carries the `/scrape` contract fields** - the CLI emitted
|
- **`jobnet-search` search output now carries the `/scrape` contract fields** - the CLI emitted
|
||||||
the raw Jobnet API schema (`jobAdId`, `hiringOrgName`, `publicationDate`, …) with no
|
the raw Jobnet API schema (`jobAdId`, `hiringOrgName`, `publicationDate`, …) with no
|
||||||
`company`, `location`, `date` or `url`, so every `/scrape` run flagged jobnet as degraded
|
`company`, `location`, `date` or `url`, so every `/scrape` run flagged jobnet as degraded
|
||||||
|
|||||||
Reference in New Issue
Block a user