mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
fix(jobbank-search): emit date key derived from posted in search output (#342)
jobbank search output was missing the shared contract's date field; it now emits date as YYYY-MM-DD derived from posted (kept unchanged), null when the feed item has no pubDate. The inline result mapping is extracted into an exported normalizeSearchItem (behavior-preserving) so the derivation is pinned by tests. Co-authored-by: oscarbol09 <80536682+oscarbol09@users.noreply.github.com>
This commit is contained in:
@@ -247,6 +247,7 @@ bun run src/cli.ts search --education 24 --suitable-for 2 --since 2026-03-01
|
|||||||
"description": "Fuldtidsjob hos Novo Nordisk, Bagsværd (Ansøgningsfrist: 12.04.2026)",
|
"description": "Fuldtidsjob hos Novo Nordisk, Bagsværd (Ansøgningsfrist: 12.04.2026)",
|
||||||
"url": "https://jobbank.dk/job/1234567/novo-nordisk/senior-data-scientist",
|
"url": "https://jobbank.dk/job/1234567/novo-nordisk/senior-data-scientist",
|
||||||
"posted": "2026-03-02T00:00:00+01:00",
|
"posted": "2026-03-02T00:00:00+01:00",
|
||||||
|
"date": "2026-03-02",
|
||||||
"deadline": "2026-04-12"
|
"deadline": "2026-04-12"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -265,6 +266,7 @@ bun run src/cli.ts search --education 24 --suitable-for 2 --since 2026-03-01
|
|||||||
| `description` | string | Raw RSS description field (single-line summary) |
|
| `description` | string | Raw RSS description field (single-line summary) |
|
||||||
| `url` | string | Full URL to job posting |
|
| `url` | string | Full URL to job posting |
|
||||||
| `posted` | string | Publication date in ISO 8601 |
|
| `posted` | string | Publication date in ISO 8601 |
|
||||||
|
| `date` | string \| null | Publication date as `YYYY-MM-DD` (derived from `posted`), or `null` if absent |
|
||||||
| `deadline` | string \| null | Application deadline as `DD.MM.YYYY` string, or `null` if "løbende" / not present |
|
| `deadline` | string \| null | Application deadline as `DD.MM.YYYY` string, or `null` if "løbende" / not present |
|
||||||
|
|
||||||
> `meta.total` is fetched from the HTML page `<title>` in a secondary request (pattern: `"{N} relevante job og karriereopslag"`). If the secondary request fails, `meta.total` is `null`.
|
> `meta.total` is fetched from the HTML page `<title>` in a secondary request (pattern: `"{N} relevante job og karriereopslag"`). If the secondary request fails, `meta.total` is `null`.
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
import { defineCommand, option } from "@bunli/core"
|
import { defineCommand, option } from "@bunli/core"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { rssFetch, fetchWithUA, writeError, parseRssDescription, extractJobIdFromUrl, BASE_URL } from "../helpers.js"
|
import { rssFetch, fetchWithUA, writeError, parseRssDescription, extractJobIdFromUrl, BASE_URL, type RssItem } from "../helpers.js"
|
||||||
|
|
||||||
|
export function normalizeSearchItem(item: RssItem): Record<string, unknown> {
|
||||||
|
const parsed = parseRssDescription(item.description)
|
||||||
|
const id = extractJobIdFromUrl(item.link)
|
||||||
|
const posted = item.pubDate ? new Date(item.pubDate).toISOString() : ""
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title: item.title,
|
||||||
|
company: parsed.company,
|
||||||
|
location: parsed.location,
|
||||||
|
jobType: parsed.jobType,
|
||||||
|
description: item.description,
|
||||||
|
url: item.link,
|
||||||
|
posted,
|
||||||
|
date: posted ? posted.slice(0, 10) : null,
|
||||||
|
deadline: parsed.deadline,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const search = defineCommand({
|
export const search = defineCommand({
|
||||||
name: "search",
|
name: "search",
|
||||||
@@ -134,22 +152,7 @@ export const search = defineCommand({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Normalize items
|
// Normalize items
|
||||||
let results = items.map((item) => {
|
let results = items.map(normalizeSearchItem)
|
||||||
const parsed = parseRssDescription(item.description)
|
|
||||||
const id = extractJobIdFromUrl(item.link)
|
|
||||||
const posted = item.pubDate ? new Date(item.pubDate).toISOString() : ""
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
title: item.title,
|
|
||||||
company: parsed.company,
|
|
||||||
location: parsed.location,
|
|
||||||
jobType: parsed.jobType,
|
|
||||||
description: item.description,
|
|
||||||
url: item.link,
|
|
||||||
posted,
|
|
||||||
deadline: parsed.deadline,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Apply limit
|
// Apply limit
|
||||||
if (flags.limit !== undefined) {
|
if (flags.limit !== undefined) {
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
import { normalizeSearchItem } from "../src/commands/search";
|
||||||
|
import type { RssItem } from "../src/helpers";
|
||||||
|
|
||||||
|
function rssItem(): RssItem {
|
||||||
|
return {
|
||||||
|
title: "Data Scientist",
|
||||||
|
description: "Fuldtidsjob hos Acme A/S, København (Ansøgningsfrist: 31.07.2026)",
|
||||||
|
link: "https://jobbank.dk/job/12345/acme/data-scientist",
|
||||||
|
pubDate: "Fri, 14 Aug 2026 09:30:00 +0200",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Jobbank search normalization", () => {
|
||||||
|
test("derives the /scrape contract date from posted as YYYY-MM-DD", () => {
|
||||||
|
const result = normalizeSearchItem(rssItem());
|
||||||
|
|
||||||
|
expect(result.posted).toBe("2026-08-14T07:30:00.000Z");
|
||||||
|
expect(result.date).toBe((result.posted as string).slice(0, 10));
|
||||||
|
expect(result.date).toBe("2026-08-14");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("emits a null date when pubDate is absent (posted is empty)", () => {
|
||||||
|
const result = normalizeSearchItem({ ...rssItem(), pubDate: "" });
|
||||||
|
|
||||||
|
expect(result.posted).toBe("");
|
||||||
|
expect(result.date).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps the native fields alongside the contract date (additive)", () => {
|
||||||
|
const result = normalizeSearchItem(rssItem());
|
||||||
|
|
||||||
|
expect(result.company).toBe("Acme A/S");
|
||||||
|
expect(result.location).toBe("København");
|
||||||
|
expect(result.url).toBe("https://jobbank.dk/job/12345/acme/data-scientist");
|
||||||
|
expect(result.deadline).toBe("31.07.2026");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user