Files
jobbi-bewerbung/lib/openapi.js
T
thomasandClaude 5d6fed7041 Show job offer posting date (anzeige_datum) alongside ingestion date
- Add anzeige_datum DATE column to jobangebote (with ALTER TABLE migration
  for pre-existing tables) — the original posting date supplied by the
  third party
- Accept anzeige_datum in POST /api/v1/joboffers (create + upsert) and
  document it in the OpenAPI spec
- Render both on /jobangebote: "Anzeige: <anzeige_datum>" and
  "Einspielung: <created_at>"

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-03 21:46:12 +02:00

825 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// OpenAPI 3.0 specification for the third-party REST API mounted under /api/v1.
// Kept as a plain factory so the serving route can inject the real `servers`
// (request host) before returning the document to /swagger.json.
const ART_OPTIONS = [
'E-Mail', 'Online-Portal', 'Indeed', 'StepStone',
'Firmenwebsite', 'Post', 'Initiativbewerbung',
'Arbeitsagentur', 'Sonstiges',
];
const STATUS_OPTIONS = [
'Entwurf', 'Gesendet', 'Eingangsbestätigung', 'Vorstellungsgespräch',
'Absage', 'Einstellung', 'Keine Rückmeldung',
];
// Shared reusable schemas -----------------------------------------------
const errorResponse = {
description: 'Fehlerantwort',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Error' },
},
},
};
function buildOpenApiSpec(baseUrl = '') {
return {
openapi: '3.0.3',
info: {
title: 'Bewerbungs-Tracker REST-API',
version: '1.0.0',
description:
'REST-API für Drittanbietersoftware zum Lesen und Verwalten von ' +
'Bewerbungen, Statusverlauf, generierten Unterlagen (PDFs), ' +
'E-Mail-Korrespondenz, Einstellungen und Statistiken.\n\n' +
'Alle Endpunkte sind unter `/api/v1` gemountet und erfordern ' +
'Authentifizierung über einen API-Key im Header `X-API-Key` ' +
'(konfiguriert via Umgebungsvariable `API_TOKEN`).',
},
servers: baseUrl ? [{ url: `${baseUrl}/api/v1` }] : [{ url: '/api/v1' }],
tags: [
{ name: 'Applications', description: 'Bewerbungen (CRUD + Statusverlauf)' },
{ name: 'Attachments', description: 'Generierte Bewerbungsunterlagen (PDFs)' },
{ name: 'Emails', description: 'E-Mail-Korrespondenz zu einer Bewerbung' },
{ name: 'Generation', description: 'KI-Generierung der Unterlagen anstoßen/abfragen' },
{ name: 'Settings', description: 'Benutzer- / Jobcenter-Einstellungen' },
{ name: 'Statistics', description: 'Statistiken und Export' },
{ name: 'Templates', description: 'Basis-Unterlagen (Vorlagen)' },
{ name: 'JobOffers', description: 'Jobangebote (Einspielung durch Drittanbietersoftware)' },
{ name: 'System', description: 'System-Endpunkte' },
],
paths: {
'/health': {
get: {
tags: ['System'],
summary: 'Verfügbarkeit prüfen',
description: 'Liefert den Status der API. Ohne Authentifizierung aufrufbar.',
security: [],
responses: {
'200': {
description: 'API erreichbar',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/Health' },
},
},
},
},
},
},
'/applications': {
get: {
tags: ['Applications'],
summary: 'Bewerbungen auflisten',
description: 'Liefert alle Bewerbungen, optional gefiltert nach Datum, Art, Status oder Freitext.',
parameters: [
{ name: 'month', in: 'query', schema: { type: 'string', pattern: '^[0-9]{1,2}$' }, description: 'Monat (0112)' },
{ name: 'year', in: 'query', schema: { type: 'string', pattern: '^[0-9]{4}$' }, description: 'Jahr (z. B. 2026)' },
{ name: 'status', in: 'query', schema: { type: 'string', enum: STATUS_OPTIONS }, description: 'Filter nach Status' },
{ name: 'art', in: 'query', schema: { type: 'string', enum: ART_OPTIONS }, description: 'Filter nach Bewerbungsart' },
{ name: 'search', in: 'query', schema: { type: 'string' }, description: 'Freitextsuche in Firma und Stelle' },
{ name: 'limit', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 500, default: 100 }, description: 'Max. Anzahl Ergebnisse' },
{ name: 'offset', in: 'query', schema: { type: 'integer', minimum: 0, default: 0 }, description: 'Ergebnis-Offset (Paging)' },
],
responses: {
'200': {
description: 'Liste der Bewerbungen (mit Statusverlauf)',
content: {
'application/json': {
schema: {
type: 'array',
items: { $ref: '#/components/schemas/Application' },
},
},
},
},
'401': errorResponse,
'500': errorResponse,
},
},
post: {
tags: ['Applications'],
summary: 'Neue Bewerbung anlegen',
description:
'Legt eine neue Bewerbung an. Ein Duplicate-Guard warnt (HTTP 409), ' +
'falls bereits eine Bewerbung für dieselbe Firma + Stelle existiert; ' +
'mit `force=true` wird sie trotzdem angelegt.',
requestBody: {
required: true,
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ApplicationCreate' },
},
},
},
responses: {
'200': {
description: 'Bewerbung angelegt',
content: {
'application/json': {
schema: { $ref: '#/components/schemas/ApplicationCreated' },
},
},
},
'400': errorResponse,
'401': errorResponse,
'409': {
description: 'Mögliche Dublette erkannt',
content: {
'application/json': { schema: { $ref: '#/components/schemas/DuplicateConflict' } },
},
},
'500': errorResponse,
},
},
},
'/applications/{id}': {
get: {
tags: ['Applications'],
summary: 'Einzelne Bewerbung abrufen',
parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
responses: {
'200': {
description: 'Bewerbung',
content: { 'application/json': { schema: { $ref: '#/components/schemas/Application' } } },
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
put: {
tags: ['Applications'],
summary: 'Bewerbung aktualisieren',
parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
requestBody: {
required: true,
content: { 'application/json': { schema: { $ref: '#/components/schemas/ApplicationUpdate' } } },
},
responses: {
'200': {
description: 'Aktualisierte Bewerbung',
content: { 'application/json': { schema: { $ref: '#/components/schemas/ApplicationCreated' } } },
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
delete: {
tags: ['Applications'],
summary: 'Bewerbung löschen',
description: 'Löscht die Bewerbung inkl. Statusverlauf und verknüpfter Anhänge (kaskadierend).',
parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
responses: {
'200': { description: 'Gelöscht', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ok' } } } },
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
'/applications/{id}/timeline': {
get: {
tags: ['Applications'],
summary: 'Statusverlauf abrufen',
parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
responses: {
'200': {
description: 'Statusverlauf (chronologisch)',
content: {
'application/json': {
schema: { type: 'array', items: { $ref: '#/components/schemas/TimelineEntry' } },
},
},
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
post: {
tags: ['Applications'],
summary: 'Statusverlauf-Eintrag hinzufügen',
description: 'Fügt einen neuen Status-Verlaufseintrag hinzu und aktualisiert den aktuellen Status der Bewerbung.',
parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
requestBody: {
required: true,
content: { 'application/json': { schema: { $ref: '#/components/schemas/TimelineEntryCreate' } } },
},
responses: {
'200': {
description: 'Angelegter Eintrag',
content: { 'application/json': { schema: { $ref: '#/components/schemas/TimelineEntry' } } },
},
'400': errorResponse,
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
'/applications/{id}/timeline/{eintragId}': {
delete: {
tags: ['Applications'],
summary: 'Statusverlauf-Eintrag löschen',
parameters: [
{ $ref: '#/components/parameters/ApplicationId' },
{ name: 'eintragId', in: 'path', required: true, schema: { type: 'integer' } },
],
responses: {
'200': { description: 'Gelöscht', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ok' } } } },
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
'/applications/{id}/attachments': {
get: {
tags: ['Attachments'],
summary: 'Generierte Anhänge auflisten',
parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
responses: {
'200': {
description: 'Anhänge (Metadaten, ohne Dateiinhalt)',
content: {
'application/json': {
schema: { type: 'array', items: { $ref: '#/components/schemas/Attachment' } },
},
},
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
'/applications/{id}/attachments/{attachmentId}': {
get: {
tags: ['Attachments'],
summary: 'Anhang herunterladen',
description: 'Liefert die Binärdatei (i. d. R. ein generiertes PDF) als Download.',
parameters: [
{ $ref: '#/components/parameters/ApplicationId' },
{ name: 'attachmentId', in: 'path', required: true, schema: { type: 'integer' } },
],
responses: {
'200': {
description: 'Binärdatei',
content: {
'application/octet-stream': { schema: { type: 'string', format: 'binary' } },
},
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
'/applications/{id}/emails': {
get: {
tags: ['Emails'],
summary: 'E-Mail-Korrespondenz abrufen',
description: 'Gesendete und empfangene E-Mails zur Bewerbung (chronologisch), inkl. Anhang-Metadaten.',
parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
responses: {
'200': {
description: 'E-Mails',
content: {
'application/json': {
schema: { type: 'array', items: { $ref: '#/components/schemas/Email' } },
},
},
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
'/emails/{emailId}/attachments/{attachmentId}': {
get: {
tags: ['Emails'],
summary: 'E-Mail-Anhang herunterladen',
parameters: [
{ name: 'emailId', in: 'path', required: true, schema: { type: 'integer' } },
{ name: 'attachmentId', in: 'path', required: true, schema: { type: 'integer' } },
],
responses: {
'200': {
description: 'Binärdatei',
content: {
'application/octet-stream': { schema: { type: 'string', format: 'binary' } },
},
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
'/applications/{id}/generate': {
post: {
tags: ['Generation'],
summary: 'Unterlagen-Generierung anstoßen',
description:
'Startet die asynchrone KI-Generierung der Bewerbungsunterlagen (Anschreiben + Lebenslauf als PDF). ' +
'Bestehende Anhänge werden vorher gelöscht. Den Fortschritt via ' +
'`GET /applications/{id}/generation-status` abfragen.',
parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
requestBody: {
required: false,
content: {
'application/json': {
schema: {
type: 'object',
properties: {
llm_notizen: { type: 'string', description: 'Freitext-Kontext für die KI (optional)' },
},
},
},
},
},
responses: {
'202': {
description: 'Generierung gestartet',
content: { 'application/json': { schema: { $ref: '#/components/schemas/Ok' } } },
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
'/applications/{id}/generation-status': {
get: {
tags: ['Generation'],
summary: 'Generierungsstatus abfragen',
parameters: [{ $ref: '#/components/parameters/ApplicationId' }],
responses: {
'200': {
description: 'Status + aktuelle Anhänge',
content: { 'application/json': { schema: { $ref: '#/components/schemas/GenerationStatus' } } },
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
'/settings': {
get: {
tags: ['Settings'],
summary: 'Einstellungen abrufen',
responses: {
'200': { description: 'Einstellungen', content: { 'application/json': { schema: { $ref: '#/components/schemas/Settings' } } } },
'401': errorResponse,
'500': errorResponse,
},
},
put: {
tags: ['Settings'],
summary: 'Einstellungen speichern',
requestBody: {
required: true,
content: { 'application/json': { schema: { $ref: '#/components/schemas/Settings' } } },
},
responses: {
'200': { description: 'Gespeichert', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ok' } } } },
'401': errorResponse,
'500': errorResponse,
},
},
},
'/statistics': {
get: {
tags: ['Statistics'],
summary: 'Statistiken abrufen',
description: 'Gesamtzahl sowie Aufschlüsselung nach Art und Status.',
responses: {
'200': {
description: 'Statistiken',
content: { 'application/json': { schema: { $ref: '#/components/schemas/Statistics' } } },
},
'401': errorResponse,
'500': errorResponse,
},
},
},
'/export': {
get: {
tags: ['Statistics'],
summary: 'Bewerbungen exportieren',
description: 'Wie `GET /applications`, aber ohne interne Notizen geeignet für PDF-/Jobcenter-Export.',
parameters: [
{ name: 'month', in: 'query', schema: { type: 'string', pattern: '^[0-9]{1,2}$' } },
{ name: 'year', in: 'query', schema: { type: 'string', pattern: '^[0-9]{4}$' } },
],
responses: {
'200': {
description: 'Bewerbungen (ohne interne_notizen)',
content: {
'application/json': {
schema: { type: 'array', items: { $ref: '#/components/schemas/Application' } },
},
},
},
'401': errorResponse,
'500': errorResponse,
},
},
},
'/templates': {
get: {
tags: ['Templates'],
summary: 'Basis-Unterlagen (Vorlagen) auflisten',
responses: {
'200': {
description: 'Vorlagen',
content: {
'application/json': {
schema: { type: 'array', items: { $ref: '#/components/schemas/Template' } },
},
},
},
'401': errorResponse,
'500': errorResponse,
},
},
},
'/joboffers': {
get: {
tags: ['JobOffers'],
summary: 'Jobangebote auflisten',
responses: {
'200': {
description: 'Jobangebote (neueste zuerst)',
content: {
'application/json': {
schema: { type: 'array', items: { $ref: '#/components/schemas/JobOffer' } },
},
},
},
'401': errorResponse,
'500': errorResponse,
},
},
post: {
tags: ['JobOffers'],
summary: 'Jobangebot einspielen (Upsert)',
description:
'Spielt ein Jobangebot ein. Wird von Drittanbietersoftware genutzt, um ' +
'Stellen in den Tracker zu übernehmen. Upsert über (quelle, external_id): ' +
'wiederholtes Senden desselben Angebots aktualisiert es statt ein Duplikat ' +
'anzulegen. Wird 201 (created) oder 200 (updated) zurückgegeben.',
requestBody: {
required: true,
content: { 'application/json': { schema: { $ref: '#/components/schemas/JobOfferCreate' } } },
},
responses: {
'200': {
description: 'Angebot aktualisiert',
content: { 'application/json': { schema: { $ref: '#/components/schemas/JobOfferResult' } } },
},
'201': {
description: 'Angebot angelegt',
content: { 'application/json': { schema: { $ref: '#/components/schemas/JobOfferResult' } } },
},
'400': errorResponse,
'401': errorResponse,
'500': errorResponse,
},
},
},
'/joboffers/{id}': {
get: {
tags: ['JobOffers'],
summary: 'Einzelnes Jobangebot abrufen',
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
responses: {
'200': {
description: 'Jobangebot',
content: { 'application/json': { schema: { $ref: '#/components/schemas/JobOffer' } } },
},
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
delete: {
tags: ['JobOffers'],
summary: 'Jobangebot löschen',
parameters: [{ name: 'id', in: 'path', required: true, schema: { type: 'integer' } }],
responses: {
'200': { description: 'Gelöscht', content: { 'application/json': { schema: { $ref: '#/components/schemas/Ok' } } } },
'401': errorResponse,
'404': errorResponse,
'500': errorResponse,
},
},
},
},
components: {
parameters: {
ApplicationId: {
name: 'id',
in: 'path',
required: true,
schema: { type: 'integer' },
description: 'Bewerbungs-ID',
},
},
securitySchemes: {
ApiKeyAuth: {
type: 'apiKey',
in: 'header',
name: 'X-API-Key',
description: 'API-Token aus der Umgebungsvariable API_TOKEN.',
},
},
schemas: {
Health: {
type: 'object',
properties: {
status: { type: 'string', example: 'ok' },
api: { type: 'string', example: 'bewerbungs-tracker/v1' },
},
},
Ok: {
type: 'object',
properties: { success: { type: 'boolean', example: true } },
},
Error: {
type: 'object',
properties: { error: { type: 'string' } },
},
Application: {
type: 'object',
properties: {
id: { type: 'integer' },
datum: { type: 'string', format: 'date' },
firma: { type: 'string' },
stelle: { type: 'string' },
art: { type: 'string', enum: ART_OPTIONS, nullable: true },
status: { type: 'string', enum: STATUS_OPTIONS, nullable: true },
notizen: { type: 'string', nullable: true },
interne_notizen: { type: 'string', nullable: true },
ort: { type: 'string', nullable: true },
stellenbeschreibung: { type: 'string', nullable: true },
quelle_url: { type: 'string', nullable: true },
email_empfaenger: { type: 'string', nullable: true },
email_betreff: { type: 'string', nullable: true },
email_anschreiben: { type: 'string', nullable: true },
llm_notizen: { type: 'string', nullable: true },
generierung_status: { type: 'string', nullable: true },
generierung_fehler: { type: 'string', nullable: true },
verlauf: {
type: 'array',
nullable: true,
items: { $ref: '#/components/schemas/TimelineEntry' },
},
created_at: { type: 'string', format: 'date-time' },
updated_at: { type: 'string', format: 'date-time' },
},
},
ApplicationCreate: {
type: 'object',
required: ['datum', 'firma', 'stelle'],
properties: {
datum: { type: 'string', format: 'date' },
firma: { type: 'string' },
stelle: { type: 'string' },
art: { type: 'string', enum: ART_OPTIONS },
status: { type: 'string', enum: STATUS_OPTIONS },
notizen: { type: 'string' },
interne_notizen: { type: 'string' },
ort: { type: 'string' },
stellenbeschreibung: { type: 'string' },
quelle_url: { type: 'string' },
llm_notizen: { type: 'string' },
kommentar: { type: 'string', description: 'Kommentar zum initialen Status-Eintrag' },
force: { type: 'boolean', description: 'Dubletten-Prüfung überspringen' },
},
},
ApplicationUpdate: {
type: 'object',
required: ['datum', 'firma', 'stelle'],
properties: {
datum: { type: 'string', format: 'date' },
firma: { type: 'string' },
stelle: { type: 'string' },
art: { type: 'string', enum: ART_OPTIONS },
status: { type: 'string', enum: STATUS_OPTIONS },
notizen: { type: 'string' },
interne_notizen: { type: 'string' },
ort: { type: 'string' },
stellenbeschreibung: { type: 'string' },
quelle_url: { type: 'string' },
llm_notizen: { type: 'string' },
},
},
ApplicationCreated: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
application: { $ref: '#/components/schemas/Application' },
},
},
DuplicateConflict: {
type: 'object',
properties: {
duplicate: { type: 'boolean', example: true },
error: { type: 'string' },
matches: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
datum: { type: 'string', format: 'date' },
firma: { type: 'string' },
stelle: { type: 'string' },
ort: { type: 'string', nullable: true },
status: { type: 'string', nullable: true },
reason: { type: 'string' },
},
},
},
},
},
TimelineEntry: {
type: 'object',
properties: {
id: { type: 'integer' },
bewerbung_id: { type: 'integer' },
datum: { type: 'string', format: 'date' },
status: { type: 'string', enum: STATUS_OPTIONS },
kommentar: { type: 'string', nullable: true },
created_at: { type: 'string', format: 'date-time' },
},
},
TimelineEntryCreate: {
type: 'object',
required: ['status'],
properties: {
datum: { type: 'string', format: 'date', description: 'Standard: heute' },
status: { type: 'string', enum: STATUS_OPTIONS },
kommentar: { type: 'string' },
},
},
Attachment: {
type: 'object',
properties: {
id: { type: 'integer' },
bewerbung_id: { type: 'integer' },
name: { type: 'string', nullable: true },
dateiname: { type: 'string' },
mime: { type: 'string', nullable: true },
created_at: { type: 'string', format: 'date-time' },
},
},
Email: {
type: 'object',
properties: {
id: { type: 'integer' },
bewerbung_id: { type: 'integer', nullable: true },
direction: { type: 'string', enum: ['in', 'out'] },
from_addr: { type: 'string', nullable: true },
to_addr: { type: 'string', nullable: true },
subject: { type: 'string', nullable: true },
body_text: { type: 'string', nullable: true },
email_date: { type: 'string', format: 'date-time', nullable: true },
anhaenge: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'integer' },
name: { type: 'string', nullable: true },
mime: { type: 'string', nullable: true },
},
},
},
created_at: { type: 'string', format: 'date-time' },
},
},
GenerationStatus: {
type: 'object',
properties: {
status: { type: 'string', enum: ['nicht_gestartet', 'ausstehend', 'fertig', 'fehler'], nullable: true },
fehler: { type: 'string', nullable: true },
anhaenge: {
type: 'array',
items: { $ref: '#/components/schemas/Attachment' },
},
},
},
Settings: {
type: 'object',
properties: {
name: { type: 'string' },
adresse: { type: 'string' },
kundennummer: { type: 'string' },
},
},
Statistics: {
type: 'object',
properties: {
total: { type: 'integer' },
byArt: {
type: 'array',
items: {
type: 'object',
properties: { art: { type: 'string' }, count: { type: 'integer' } },
},
},
byStatus: {
type: 'array',
items: {
type: 'object',
properties: { status: { type: 'string' }, count: { type: 'integer' } },
},
},
},
},
Template: {
type: 'object',
properties: {
id: { type: 'integer' },
typ: { type: 'string' },
name: { type: 'string', nullable: true },
inhalt: { type: 'string' },
created_at: { type: 'string', format: 'date-time' },
},
},
JobOffer: {
type: 'object',
properties: {
id: { type: 'integer' },
external_id: { type: 'string', nullable: true, description: 'ID im Drittanbietersystem (Upsert-Schlüssel)' },
quelle: { type: 'string', example: 'drittanbieter' },
firma: { type: 'string' },
stelle: { type: 'string' },
ort: { type: 'string', nullable: true },
gehalt: { type: 'string', nullable: true },
beschreibung: { type: 'string', nullable: true },
quelle_url: { type: 'string', nullable: true },
art: { type: 'string', nullable: true },
anzeige_datum: { type: 'string', format: 'date', nullable: true, description: 'Datum der eigentlichen Stellenanzeige (vom Drittanbieter übergeben)' },
status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'] },
verknuepfte_bewerbung_id: { type: 'integer', nullable: true },
created_at: { type: 'string', format: 'date-time' },
updated_at: { type: 'string', format: 'date-time' },
},
},
JobOfferCreate: {
type: 'object',
required: ['firma', 'stelle'],
properties: {
external_id: { type: 'string', description: 'ID im Drittanbietersystem; bei Wiederholung wird das Angebot aktualisiert' },
quelle: { type: 'string', example: 'drittanbieter', description: 'Name der Drittanbietersoftware (Standard: drittanbieter)' },
firma: { type: 'string' },
stelle: { type: 'string' },
ort: { type: 'string' },
gehalt: { type: 'string' },
beschreibung: { type: 'string', description: 'Stellenbeschreibungstext' },
quelle_url: { type: 'string' },
art: { type: 'string', enum: ART_OPTIONS },
anzeige_datum: { type: 'string', format: 'date', description: 'Datum der eigentlichen Stellenanzeige (ISO YYYY-MM-DD)' },
status: { type: 'string', enum: ['offen', 'uebernommen', 'abgelehnt'], default: 'offen' },
},
},
JobOfferResult: {
type: 'object',
properties: {
success: { type: 'boolean', example: true },
action: { type: 'string', enum: ['created', 'updated'] },
joboffer: { $ref: '#/components/schemas/JobOffer' },
},
},
},
},
security: [{ ApiKeyAuth: [] }],
};
}
module.exports = { buildOpenApiSpec };