Carry firma_slug on job offers, supplied by the ingesting client
The blacklist company slug now lives on the offers themselves. POST /joboffers accepts firma_slug and stores it as supplied (deriving it from firma only when omitted); it is persisted on jobangebote (schema + ALTER/backfill + index) and returned in the Joboffer response. Blacklist matching prefers the client-supplied slug over one derived from firma, so the indexing client controls the identity used to block an offer. OpenAPI request/response schemas updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+10
-4
@@ -633,7 +633,12 @@ function createExternalApi(deps) {
|
|||||||
const urlNorm = blacklist.normalizeUrl(b.quelle_url || '') || null;
|
const urlNorm = blacklist.normalizeUrl(b.quelle_url || '') || null;
|
||||||
const anzeigeDatum = sanitizeInput(b.anzeige_datum || '');
|
const anzeigeDatum = sanitizeInput(b.anzeige_datum || '');
|
||||||
const kontaktEmail = sanitizeInput(b.kontakt_email || '');
|
const kontaktEmail = sanitizeInput(b.kontakt_email || '');
|
||||||
// Column order matches the UPDATE/INSERT statements below (url_norm last).
|
// Company slug: stored as supplied by the client; derived from firma only
|
||||||
|
// when omitted. This is the key the blacklist matches on (see below).
|
||||||
|
const firmaSlug = b.firma_slug != null && String(b.firma_slug).trim() !== ''
|
||||||
|
? sanitizeInput(String(b.firma_slug))
|
||||||
|
: (blacklist.firmaSlug(b.firma) || null);
|
||||||
|
// Column order matches the UPDATE/INSERT statements below (firma_slug last).
|
||||||
const fields = [
|
const fields = [
|
||||||
sanitizeInput(b.firma),
|
sanitizeInput(b.firma),
|
||||||
sanitizeInput(b.stelle),
|
sanitizeInput(b.stelle),
|
||||||
@@ -649,6 +654,7 @@ function createExternalApi(deps) {
|
|||||||
sanitizeInput(b.status || 'offen'),
|
sanitizeInput(b.status || 'offen'),
|
||||||
serializeLabels(b.labels),
|
serializeLabels(b.labels),
|
||||||
urlNorm,
|
urlNorm,
|
||||||
|
firmaSlug,
|
||||||
];
|
];
|
||||||
|
|
||||||
// De-dup: prefer a (quelle, external_id) match, else the same normalized
|
// De-dup: prefer a (quelle, external_id) match, else the same normalized
|
||||||
@@ -671,7 +677,7 @@ function createExternalApi(deps) {
|
|||||||
await dbRun(
|
await dbRun(
|
||||||
`UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, adresse = ?, ansprechpartner = ?, gehalt = ?,
|
`UPDATE jobangebote SET firma = ?, stelle = ?, ort = ?, adresse = ?, ansprechpartner = ?, gehalt = ?,
|
||||||
beschreibung = ?, quelle_url = ?, art = ?, anzeige_datum = ?, kontakt_email = ?, status = ?,
|
beschreibung = ?, quelle_url = ?, art = ?, anzeige_datum = ?, kontakt_email = ?, status = ?,
|
||||||
labels = ?, url_norm = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
labels = ?, url_norm = ?, firma_slug = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`,
|
||||||
[...fields, existing.id]
|
[...fields, existing.id]
|
||||||
);
|
);
|
||||||
const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [existing.id]);
|
const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [existing.id]);
|
||||||
@@ -680,8 +686,8 @@ function createExternalApi(deps) {
|
|||||||
|
|
||||||
const result = await dbRun(
|
const result = await dbRun(
|
||||||
`INSERT INTO jobangebote
|
`INSERT INTO jobangebote
|
||||||
(external_id, quelle, firma, stelle, ort, adresse, ansprechpartner, gehalt, beschreibung, quelle_url, art, anzeige_datum, kontakt_email, status, labels, url_norm)
|
(external_id, quelle, firma, stelle, ort, adresse, ansprechpartner, gehalt, beschreibung, quelle_url, art, anzeige_datum, kontakt_email, status, labels, url_norm, firma_slug)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[externalId, quelle, ...fields]
|
[externalId, quelle, ...fields]
|
||||||
);
|
);
|
||||||
const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [result.lastID]);
|
const row = await dbGet('SELECT * FROM jobangebote WHERE id = ?', [result.lastID]);
|
||||||
|
|||||||
+3
-1
@@ -130,7 +130,9 @@ function offerSignature(offer) {
|
|||||||
external_id: o.external_id != null && String(o.external_id).trim() !== ''
|
external_id: o.external_id != null && String(o.external_id).trim() !== ''
|
||||||
? String(o.external_id).trim() : null,
|
? String(o.external_id).trim() : null,
|
||||||
firma_norm: normText(o.firma),
|
firma_norm: normText(o.firma),
|
||||||
firma_slug: firmaSlug(o.firma),
|
// Prefer a client-supplied slug (offers carry firma_slug); else derive it.
|
||||||
|
firma_slug: o.firma_slug != null && String(o.firma_slug).trim() !== ''
|
||||||
|
? String(o.firma_slug).trim() : firmaSlug(o.firma),
|
||||||
stelle_norm: normText(o.stelle),
|
stelle_norm: normText(o.stelle),
|
||||||
ort_norm: normText(o.ort),
|
ort_norm: normText(o.ort),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -887,6 +887,7 @@ function buildOpenApiSpec(baseUrl = '') {
|
|||||||
labels: { ...labelsSchema, nullable: true },
|
labels: { ...labelsSchema, nullable: true },
|
||||||
verknuepfte_bewerbung_id: { type: 'integer', nullable: true },
|
verknuepfte_bewerbung_id: { type: 'integer', nullable: true },
|
||||||
url_norm: { type: 'string', nullable: true, description: 'Normalisierte URL für die De-Duplizierung (serverseitig gesetzt)' },
|
url_norm: { type: 'string', nullable: true, description: 'Normalisierte URL für die De-Duplizierung (serverseitig gesetzt)' },
|
||||||
|
firma_slug: { type: 'string', nullable: true, description: 'Firmen-Slug für Blacklist-Abgleich; wie übergeben gespeichert, bei Fehlen aus firma abgeleitet.' },
|
||||||
created_at: { type: 'string', format: 'date-time' },
|
created_at: { type: 'string', format: 'date-time' },
|
||||||
updated_at: { type: 'string', format: 'date-time' },
|
updated_at: { type: 'string', format: 'date-time' },
|
||||||
},
|
},
|
||||||
@@ -905,6 +906,7 @@ function buildOpenApiSpec(baseUrl = '') {
|
|||||||
gehalt: { type: 'string' },
|
gehalt: { type: 'string' },
|
||||||
beschreibung: { type: 'string', description: 'Stellenbeschreibungstext' },
|
beschreibung: { type: 'string', description: 'Stellenbeschreibungstext' },
|
||||||
quelle_url: { type: 'string' },
|
quelle_url: { type: 'string' },
|
||||||
|
firma_slug: { type: 'string', description: 'Optionaler Firmen-Slug für den Blacklist-Abgleich (z. B. "bosch"). Wird wie übergeben gespeichert; fehlt er, leitet der Server ihn aus firma ab.' },
|
||||||
art: { type: 'string', enum: ART_OPTIONS },
|
art: { type: 'string', enum: ART_OPTIONS },
|
||||||
anzeige_datum: { type: 'string', format: 'date', description: 'Datum der eigentlichen Stellenanzeige (ISO YYYY-MM-DD)' },
|
anzeige_datum: { type: 'string', format: 'date', description: 'Datum der eigentlichen Stellenanzeige (ISO YYYY-MM-DD)' },
|
||||||
kontakt_email: { type: 'string', format: 'email', description: 'Kontakt-E-Mail-Adresse der Stelle' },
|
kontakt_email: { type: 'string', format: 'email', description: 'Kontakt-E-Mail-Adresse der Stelle' },
|
||||||
|
|||||||
@@ -803,6 +803,19 @@ function initializeDatabase() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
db.run('CREATE INDEX IF NOT EXISTS idx_jobangebote_url_norm ON jobangebote(url_norm)', () => {});
|
db.run('CREATE INDEX IF NOT EXISTS idx_jobangebote_url_norm ON jobangebote(url_norm)', () => {});
|
||||||
|
// Company slug carried on the offer itself (supplied by the indexing
|
||||||
|
// client, else derived from firma). Used for blacklist matching and
|
||||||
|
// "same company" grouping. Backfill derives it from firma.
|
||||||
|
db.run('ALTER TABLE jobangebote ADD COLUMN firma_slug TEXT', () => {
|
||||||
|
db.all('SELECT id, firma FROM jobangebote WHERE firma_slug IS NULL AND firma IS NOT NULL AND firma != ""', (err, rows) => {
|
||||||
|
if (err || !rows) return;
|
||||||
|
rows.forEach((r) => {
|
||||||
|
const slug = blacklist.firmaSlug(r.firma);
|
||||||
|
if (slug) db.run('UPDATE jobangebote SET firma_slug = ? WHERE id = ?', [slug, r.id], () => {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
db.run('CREATE INDEX IF NOT EXISTS idx_jobangebote_firma_slug ON jobangebote(firma_slug)', () => {});
|
||||||
// Blacklist of job offers that must never (re)appear in the list. A
|
// Blacklist of job offers that must never (re)appear in the list. A
|
||||||
// deleted offer is auto-blacklisted; manual entries can block a URL,
|
// deleted offer is auto-blacklisted; manual entries can block a URL,
|
||||||
// a whole domain, a company, or a specific company+title posting.
|
// a whole domain, a company, or a specific company+title posting.
|
||||||
|
|||||||
Reference in New Issue
Block a user