Files
Dokumenten-Check/Scripts/ORDS REST Services.sql
Wolf G. Beckmann 522bc21439 feat: Add Markdown export service for DOCX and PDF formats
- Implemented MarkdownExportService to convert Markdown text into DOCX and PDF formats.
- Supported Markdown elements include headings, bold, italic, blockquotes, tables, and specific emojis.
- Added methods for generating DOCX and PDF documents with proper formatting and rendering.
- Enhanced application properties to enable logging for production environment.
- Introduced a test-run script to reset project state and trigger processing via API.

Co-authored-by: Copilot <copilot@github.com>
2026-04-28 14:32:45 +02:00

850 lines
30 KiB
MySQL
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.
-- =============================================================================
-- ORDS REST Services: Dokumenten-Check
-- Modul: frigosped.dc
-- Base-Path: /api/dc/
-- =============================================================================
-- Voraussetzungen:
-- - Oracle Database 23c mit installiertem ORDS (>= 23.x)
-- - Script wird als Schema-Owner ausgeführt (hat ORDS_METADATA-Rolle)
-- - Datenbankschema bereits angelegt via "Datenmodell DocumentCheck.sql"
--
-- Deployment:
-- sqlplus user/pass@db @"Scripts/ORDS REST Services.sql"
--
-- Das Skript ist idempotent: bestehende Modul-Definition wird gelöscht und
-- neu erstellt. Bestehende Daten bleiben unberührt.
--
-- Endpunkt-Übersicht:
-- 1. GET /api/dc/catalogs/ Alle Fragenkataloge
-- 2. GET /api/dc/catalogs/:id/questions/ Fragen eines Katalogs (flach, nach order_nr sortiert)
-- 3. GET /api/dc/projects/:id Projektdetail (inkl. Detailstatus + ETA)
-- 4. POST /api/dc/projects/:id/start → IN_PROGRESS (setzt processing_started_at)
-- 5. PUT /api/dc/projects/:id/progress Fortschritt + Phase/Doc/Question/ETA
-- 6. POST /api/dc/projects/:id/complete → COMPLETED (setzt current_phase=COMPLETED)
-- 7. GET /api/dc/projects/:id/documents/ Dokument-Liste (kein BLOB)
-- 8. GET /api/dc/documents/:id/file BLOB-Download (PLSQL-Workaround)
-- 9. GET /api/dc/documents/:id/texts OCR-Text + Übersetzung lesen (für Resume)
-- 9b. PUT /api/dc/documents/:id/texts OCR-Text + Übersetzung schreiben
-- 10. POST /api/dc/projects/:id/results Prüfergebnis einfügen
-- 11. DELETE /api/dc/projects/:id/results Alle Ergebnisse löschen
-- =============================================================================
-- =============================================================================
-- BLOCK 1: Modul, Templates und Handler
-- =============================================================================
BEGIN
-- -------------------------------------------------------------------------
-- Idempotenz: Modul löschen falls vorhanden
-- -------------------------------------------------------------------------
BEGIN
ORDS.DELETE_MODULE(p_module_name => 'frigosped.dc');
EXCEPTION
WHEN OTHERS THEN NULL;
END;
-- -------------------------------------------------------------------------
-- Modul definieren
-- -------------------------------------------------------------------------
ORDS.DEFINE_MODULE(
p_module_name => 'frigosped.dc',
p_base_path => '/api/dc/',
p_items_per_page => 0, -- Pagination deaktiviert (Quarkus steuert selbst)
p_status => 'PUBLISHED',
p_comments => 'Dokumenten-Check Backend-API fuer den Quarkus-Verarbeitungsserver'
);
-- ===========================================================================
-- TEMPLATE 1: catalogs/
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'catalogs/',
p_priority => 0,
p_etag_type => 'HASH',
p_comments => 'Alle Fragenkataloge mit Dokumenttyp-Name'
);
-- 1. GET /api/dc/catalogs/
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'catalogs/',
p_method => 'GET',
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_FEED,
p_items_per_page => 0,
p_comments => 'Gibt alle Kataloge mit Dokumenttyp-Name zurueck',
p_source => q'[
SELECT
c.id,
c.name,
c.description,
c.document_type_id,
dt.name AS document_type_name,
c.row_version,
c.created,
c.updated
FROM dc_question_catalogs c
JOIN dc_document_types dt ON dt.id = c.document_type_id
ORDER BY c.name
]'
);
-- ===========================================================================
-- TEMPLATE 2: catalogs/:catalog_id/questions/
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'catalogs/:catalog_id/questions/',
p_priority => 0,
p_etag_type => 'HASH',
p_comments => 'Alle Fragen eines Katalogs (flach mit Kategorie-Info)'
);
-- 2. GET /api/dc/catalogs/:catalog_id/questions/
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'catalogs/:catalog_id/questions/',
p_method => 'GET',
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_FEED,
p_items_per_page => 0,
p_comments => 'Flacher Join: Kategorie + Frage-Spalten, nach Kategorie/ID sortiert',
p_source => q'[
SELECT
q.id AS question_id,
cat.id AS category_id,
cat.name AS category_name,
cat.description AS category_description,
cat.order_nr AS category_order_nr,
q.question_text,
q.evaluation_type,
q.threshold,
q.result_handling,
q.example_0_percent,
q.example_100_percent,
q.order_nr,
q.row_version
FROM dc_questions q
JOIN dc_question_categories cat ON cat.id = q.category_id
WHERE cat.catalog_id = :catalog_id
ORDER BY cat.order_nr, q.order_nr, q.id
]'
);
-- ===========================================================================
-- TEMPLATE 3: projects/:project_id (kein Trailing-Slash = Einzel-Ressource)
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id',
p_priority => 0,
p_etag_type => 'HASH',
p_comments => 'Einzelnes Projekt'
);
-- 3. GET /api/dc/projects/:project_id
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id',
p_method => 'GET',
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_ITEM,
p_items_per_page => 1,
p_comments => 'Projektdetail mit Status-Details und ETA; 404 wenn nicht gefunden',
p_source => q'[
SELECT
p.id,
p.name,
p.description,
p.catalog_id,
p.created_by_user,
p.status,
p.progress,
p.completed_at,
p.notification_email,
p.row_version,
p.created,
p.updated,
p.processing_started_at,
p.current_phase,
p.current_doc_id,
p.current_question_id,
p.estimated_completion_at
FROM dc_projects p
WHERE p.id = :project_id
]'
);
-- ===========================================================================
-- TEMPLATE 4: projects/:project_id/start
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/start',
p_priority => 0,
p_comments => 'Projekt-Status auf IN_PROGRESS setzen'
);
-- 4. POST /api/dc/projects/:project_id/start
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/start',
p_method => 'POST',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => '200 OK; 409 wenn IN_PROGRESS/COMPLETED; 404 wenn Projekt fehlt',
p_source => q'[
DECLARE
v_status dc_projects.status%TYPE;
BEGIN
SELECT status
INTO v_status
FROM dc_projects
WHERE id = :project_id
FOR UPDATE NOWAIT;
IF v_status IN ('IN_PROGRESS', 'COMPLETED') THEN
:status_code := 409;
HTP.P('{"error":"Projekt ist bereits ' || v_status || '",'
|| '"current_status":"' || v_status || '"}');
RETURN;
END IF;
UPDATE dc_projects
SET status = 'IN_PROGRESS',
progress = 0,
processing_started_at = SYSTIMESTAMP,
current_phase = 'OCR',
current_doc_id = NULL,
current_question_id = NULL,
estimated_completion_at = NULL
WHERE id = :project_id;
:status_code := 200;
HTP.P('{"project_id":' || :project_id
|| ',"status":"IN_PROGRESS"}');
EXCEPTION
WHEN NO_DATA_FOUND THEN
:status_code := 404;
HTP.P('{"error":"Projekt nicht gefunden","project_id":' || :project_id || '}');
WHEN OTHERS THEN
ROLLBACK;
:status_code := 500;
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
END;
]'
);
-- ===========================================================================
-- TEMPLATE 5: projects/:project_id/progress
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/progress',
p_priority => 0,
p_comments => 'Verarbeitungsfortschritt aktualisieren'
);
-- 5. PUT /api/dc/projects/:project_id/progress
-- Body: {"progress":45,"current_phase":"QUESTIONS","current_doc_id":2,
-- "current_question_id":15,"estimated_completion_at":"2026-04-27T16:30:00"}
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/progress',
p_method => 'PUT',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => 'Setzt progress (0-100) + Phase/Dokument/Frage/ETA; 400 bei ungueltigem Wert',
p_source => q'[
DECLARE
v_body CLOB;
v_progress NUMBER;
v_phase VARCHAR2(30);
v_doc_id NUMBER;
v_question_id NUMBER;
v_eta_str VARCHAR2(30);
v_eta TIMESTAMP;
BEGIN
v_body := :body_text;
v_progress := TO_NUMBER(JSON_VALUE(v_body, '$.progress'));
v_phase := JSON_VALUE(v_body, '$.current_phase');
v_doc_id := TO_NUMBER(JSON_VALUE(v_body, '$.current_doc_id'));
v_question_id := TO_NUMBER(JSON_VALUE(v_body, '$.current_question_id'));
v_eta_str := JSON_VALUE(v_body, '$.estimated_completion_at');
IF v_progress IS NULL OR v_progress < 0 OR v_progress > 100 THEN
:status_code := 400;
HTP.P('{"error":"progress muss eine Zahl zwischen 0 und 100 sein"}');
RETURN;
END IF;
IF v_eta_str IS NOT NULL THEN
BEGIN
v_eta := TO_TIMESTAMP(v_eta_str, 'YYYY-MM-DD"T"HH24:MI:SS');
EXCEPTION WHEN OTHERS THEN
v_eta := NULL;
END;
END IF;
UPDATE dc_projects
SET progress = v_progress,
current_phase = NVL(v_phase, current_phase),
current_doc_id = v_doc_id,
current_question_id = v_question_id,
estimated_completion_at = NVL(v_eta, estimated_completion_at)
WHERE id = :project_id;
IF SQL%ROWCOUNT = 0 THEN
:status_code := 404;
HTP.P('{"error":"Projekt nicht gefunden","project_id":' || :project_id || '}');
RETURN;
END IF;
:status_code := 200;
HTP.P('{"project_id":' || :project_id
|| ',"progress":' || v_progress || '}');
EXCEPTION
WHEN VALUE_ERROR THEN
:status_code := 400;
HTP.P('{"error":"progress muss eine gueltige Zahl sein"}');
WHEN OTHERS THEN
ROLLBACK;
:status_code := 500;
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
END;
]'
);
-- ===========================================================================
-- TEMPLATE 6: projects/:project_id/complete
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/complete',
p_priority => 0,
p_comments => 'Projekt auf COMPLETED setzen'
);
-- 6. POST /api/dc/projects/:project_id/complete
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/complete',
p_method => 'POST',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => 'Setzt status=COMPLETED und erstellt Markdown-Bericht',
p_source => q'[
DECLARE
v_status dc_projects.status%TYPE;
v_now DATE := SYSDATE;
BEGIN
SELECT status
INTO v_status
FROM dc_projects
WHERE id = :project_id
FOR UPDATE NOWAIT;
IF v_status = 'COMPLETED' THEN
:status_code := 409;
HTP.P('{"error":"Projekt ist bereits COMPLETED","project_id":' || :project_id || '}');
RETURN;
END IF;
UPDATE dc_projects
SET status = 'COMPLETED',
completed_at = v_now,
progress = 100,
current_phase = 'COMPLETED',
current_doc_id = NULL,
current_question_id = NULL,
estimated_completion_at = NULL
WHERE id = :project_id;
-- Markdown-Bericht generieren (Fehler ignoriert, Projekt bleibt COMPLETED)
BEGIN
dc_generate_report(:project_id);
EXCEPTION WHEN OTHERS THEN NULL;
END;
:status_code := 200;
HTP.P('{"project_id":' || :project_id
|| ',"status":"COMPLETED"'
|| ',"completed_at":"' || TO_CHAR(v_now, 'YYYY-MM-DD"T"HH24:MI:SS') || '"'
|| '}');
EXCEPTION
WHEN NO_DATA_FOUND THEN
:status_code := 404;
HTP.P('{"error":"Projekt nicht gefunden","project_id":' || :project_id || '}');
WHEN OTHERS THEN
ROLLBACK;
:status_code := 500;
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
END;
]'
);
-- ===========================================================================
-- TEMPLATE 12: projects/:project_id/report → Bericht generieren / abrufen
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/report',
p_priority => 0,
p_comments => 'Markdown-Bericht eines Projekts'
);
-- 12. POST /api/dc/projects/:project_id/report (neu) generieren
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/report',
p_method => 'POST',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => 'Generiert report_markdown neu (z.B. nach Konfigurationsänderung)',
p_source => q'[
BEGIN
dc_generate_report(:project_id);
:status_code := 200;
HTP.P('{"project_id":' || :project_id || ',"report_generated":true}');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
:status_code := 500;
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
END;
]'
);
-- ===========================================================================
-- TEMPLATE 7: projects/:project_id/documents/
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/documents/',
p_priority => 0,
p_etag_type => 'HASH',
p_comments => 'Dokument-Liste eines Projekts (ohne BLOB-Payload)'
);
-- 7. GET /api/dc/projects/:project_id/documents/
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/documents/',
p_method => 'GET',
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_FEED,
p_items_per_page => 0,
p_comments => 'Metadaten aller Dokumente; has_original_text/has_translated_text als 0/1',
p_source => q'[
SELECT
d.id,
d.filename,
d.mime_type,
d.document_type_id,
d.catalog_id,
d.uploaded_at,
CASE WHEN d.original_text IS NOT NULL THEN 1 ELSE 0 END AS has_original_text,
CASE WHEN d.translated_text IS NOT NULL THEN 1 ELSE 0 END AS has_translated_text,
d.row_version,
d.created,
d.updated
FROM dc_project_documents d
WHERE d.project_id = :project_id
ORDER BY d.uploaded_at
]'
);
-- ===========================================================================
-- TEMPLATE 8: documents/:doc_id/file
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'documents/:doc_id/file',
p_priority => 0,
p_comments => 'BLOB-Download: original_file mit korrektem Content-Type'
);
-- 8. GET /api/dc/documents/:doc_id/file
-- PLSQL-Workaround: SOURCE_TYPE_MEDIA ruft getString() auf BLOB auf → ORA-17004 in dieser ORDS-Version
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'documents/:doc_id/file',
p_method => 'GET',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => 'Streamt original_file BLOB mit korrektem Content-Type (PLSQL-Workaround fuer ORDS-Bug)',
p_source => q'[
DECLARE
v_blob BLOB;
v_mime VARCHAR2(200);
v_filename VARCHAR2(500);
BEGIN
SELECT original_file, mime_type, filename
INTO v_blob, v_mime, v_filename
FROM dc_project_documents
WHERE id = :doc_id;
IF v_blob IS NULL THEN
:status_code := 404;
HTP.P('{"error":"Datei nicht vorhanden","doc_id":' || :doc_id || '}');
RETURN;
END IF;
OWA_UTIL.MIME_HEADER(NVL(v_mime, 'application/octet-stream'), FALSE);
HTP.P('Content-Disposition: attachment; filename="' || v_filename || '"');
OWA_UTIL.HTTP_HEADER_CLOSE;
WPG_DOCLOAD.DOWNLOAD_FILE(v_blob);
:status_code := 200;
EXCEPTION
WHEN NO_DATA_FOUND THEN
:status_code := 404;
HTP.P('{"error":"Dokument nicht gefunden","doc_id":' || :doc_id || '}');
WHEN OTHERS THEN
:status_code := 500;
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
END;
]'
);
-- ===========================================================================
-- TEMPLATE 9: documents/:doc_id/texts (GET + PUT)
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'documents/:doc_id/texts',
p_priority => 0,
p_comments => 'OCR-Ergebnis und Uebersetzung lesen/schreiben'
);
-- 9a. GET /api/dc/documents/:doc_id/texts (fuer Resume nach Phase A)
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'documents/:doc_id/texts',
p_method => 'GET',
p_source_type => ORDS.SOURCE_TYPE_COLLECTION_ITEM,
p_items_per_page => 1,
p_comments => 'Gibt original_text und translated_text zurueck',
p_source => q'[
SELECT
d.id,
d.original_text,
d.translated_text,
CASE WHEN d.original_text IS NOT NULL THEN 1 ELSE 0 END AS has_original_text,
CASE WHEN d.translated_text IS NOT NULL THEN 1 ELSE 0 END AS has_translated_text
FROM dc_project_documents d
WHERE d.id = :doc_id
]'
);
-- 9b. PUT /api/dc/documents/:doc_id/texts
-- Body: {"original_text": "## ...", "translated_text": "## ..."}
-- Beide Felder sind optional: NULL-Wert ueberschreibt nicht (nur wenn im Body vorhanden)
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'documents/:doc_id/texts',
p_method => 'PUT',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => 'Schreibt original_text und/oder translated_text als CLOB',
p_source => q'[
DECLARE
v_body CLOB;
v_original_text CLOB;
v_translated CLOB;
v_rows INTEGER;
BEGIN
v_body := :body_text;
-- RETURNING CLOB erlaubt Werte groesser als 32767 Bytes (wichtig fuer OCR-Output)
v_original_text := JSON_VALUE(v_body, '$.original_text' RETURNING CLOB);
v_translated := JSON_VALUE(v_body, '$.translated_text' RETURNING CLOB);
UPDATE dc_project_documents
SET original_text = CASE
WHEN JSON_EXISTS(v_body, '$.original_text')
THEN v_original_text
ELSE original_text
END,
translated_text = CASE
WHEN JSON_EXISTS(v_body, '$.translated_text')
THEN v_translated
ELSE translated_text
END
WHERE id = :doc_id;
v_rows := SQL%ROWCOUNT;
IF v_rows = 0 THEN
:status_code := 404;
HTP.P('{"error":"Dokument nicht gefunden","doc_id":' || :doc_id || '}');
RETURN;
END IF;
:status_code := 200;
HTP.P('{"doc_id":' || :doc_id || ',"updated":true}');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
:status_code := 500;
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
END;
]'
);
-- ===========================================================================
-- TEMPLATE 10+11: projects/:project_id/results
-- (POST = Einfuegen, DELETE = Alle loeschen)
-- ===========================================================================
ORDS.DEFINE_TEMPLATE(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/results',
p_priority => 0,
p_comments => 'Prüfergebnisse: POST einfuegen, DELETE alle loeschen'
);
-- 10. POST /api/dc/projects/:project_id/results
-- Body: {
-- "question_id": 5,
-- "doc_id": 101,
-- "answer": "Ja, §305 BGB ist erfuellt.",
-- "score": 85,
-- "result_type": "OK",
-- "deviation": 0,
-- "warning": 0,
-- "the_comment": "Klare Einbeziehungsklausel vorhanden"
-- }
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/results',
p_method => 'POST',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => '201 Created mit Location-Header; 400 bei fehlendem question_id/doc_id/result_type',
p_source => q'[
DECLARE
v_body CLOB;
v_question_id NUMBER;
v_doc_id NUMBER;
v_answer CLOB;
v_score NUMBER;
v_result_type VARCHAR2(20);
v_deviation NUMBER;
v_warning NUMBER;
v_comment CLOB;
v_new_id NUMBER;
BEGIN
v_body := :body_text;
v_question_id := TO_NUMBER(JSON_VALUE(v_body, '$.question_id'));
v_doc_id := TO_NUMBER(JSON_VALUE(v_body, '$.doc_id'));
v_answer := JSON_VALUE(v_body, '$.answer' RETURNING CLOB);
v_score := TO_NUMBER(JSON_VALUE(v_body, '$.score'));
v_result_type := JSON_VALUE(v_body, '$.result_type');
v_deviation := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.deviation')), 0);
v_warning := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.warning')), 0);
v_comment := JSON_VALUE(v_body, '$.the_comment' RETURNING CLOB);
-- Pflichtfelder prüfen
IF v_question_id IS NULL OR v_doc_id IS NULL THEN
:status_code := 400;
HTP.P('{"error":"question_id und doc_id sind Pflichtfelder"}');
RETURN;
END IF;
IF v_result_type NOT IN ('OK', 'UNKLAR', 'NOK') THEN
:status_code := 400;
HTP.P('{"error":"result_type muss OK, UNKLAR oder NOK sein",'
|| '"received":"' || NVL(v_result_type, 'NULL') || '"}');
RETURN;
END IF;
INSERT INTO dc_results (
project_id,
question_id,
doc_id,
answer,
score,
result_type,
deviation,
warning,
the_comment,
evaluated_at
) VALUES (
:project_id,
v_question_id,
v_doc_id,
v_answer,
v_score,
v_result_type,
v_deviation,
v_warning,
v_comment,
SYSDATE
)
RETURNING id INTO v_new_id;
:status_code := 201;
HTP.P('{"id":' || v_new_id
|| ',"project_id":' || :project_id
|| ',"question_id":' || v_question_id
|| ',"doc_id":' || v_doc_id
|| ',"result_type":"' || v_result_type || '"'
|| ',"score":' || NVL(TO_CHAR(v_score), 'null')
|| '}');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
:status_code := 500;
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
END;
]'
);
-- 11. DELETE /api/dc/projects/:project_id/results
ORDS.DEFINE_HANDLER(
p_module_name => 'frigosped.dc',
p_pattern => 'projects/:project_id/results',
p_method => 'DELETE',
p_source_type => ORDS.SOURCE_TYPE_PLSQL,
p_comments => 'Loescht alle Ergebnisse eines Projekts (fuer Re-Processing)',
p_source => q'[
DECLARE
v_deleted INTEGER;
BEGIN
DELETE FROM dc_results
WHERE project_id = :project_id;
v_deleted := SQL%ROWCOUNT;
:status_code := 200;
HTP.P('{"project_id":' || :project_id
|| ',"deleted_count":' || v_deleted || '}');
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
:status_code := 500;
HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}');
END;
]'
);
COMMIT;
END;
/
-- =============================================================================
-- BLOCK 2: Auth / Privilege (fuer Produktion)
-- =============================================================================
-- In der Entwicklung kann dieser Block auskommentiert bleiben das Modul
-- ist dann ohne Auth-Pruefung erreichbar (STATUS = PUBLISHED genuegt).
--
-- Fuer Produktion einkommentieren und danach den OAuth2-Client anlegen:
--
-- SELECT client_id, client_secret
-- FROM user_ords_clients
-- WHERE name = 'quarkus_dc_backend';
--
-- Den client_id/client_secret in die Quarkus application.properties eintragen.
-- =============================================================================
/*
BEGIN
-- Rolle anlegen (falls noch nicht vorhanden)
BEGIN
ORDS.CREATE_ROLE(p_role_name => 'dc_api_user');
EXCEPTION
WHEN OTHERS THEN NULL;
END;
-- Privilege loeschen falls vorhanden
BEGIN
ORDS.DROP_PRIVILEGE(p_name => 'dc.api.privilege');
EXCEPTION
WHEN OTHERS THEN NULL;
END;
-- Privilege definieren: mappt Rolle auf alle /api/dc/-Endpunkte
ORDS.DEFINE_PRIVILEGE(
p_privilege_name => 'dc.api.privilege',
p_roles => ORDS_TYPES.T_ORDS_VARCHARS('dc_api_user'),
p_patterns => ORDS_TYPES.T_ORDS_VARCHARS('/api/dc/*'),
p_module_name => 'frigosped.dc',
p_label => 'Dokumenten-Check API',
p_description => 'Zugriff auf alle /api/dc/-Endpunkte',
p_comments => 'Wird dem OAuth2-Client quarkus_dc_backend zugewiesen'
);
COMMIT;
END;
/
-- OAuth2-Client fuer den Quarkus-Server (einmalig ausfuehren)
BEGIN
OAUTH.CREATE_CLIENT(
p_name => 'quarkus_dc_backend',
p_grant_type => 'CLIENT_CREDENTIALS',
p_owner => 'Frigosped',
p_description => 'Quarkus-Verarbeitungsserver Service-Account',
p_redirect_uri => NULL,
p_support_email => 'admin@frigosped.de',
p_privilege_names => 'dc.api.privilege'
);
COMMIT;
END;
/
*/
-- =============================================================================
-- Test-Aufrufe (curl)
-- =============================================================================
-- Ersetze <BASE> und <SCHEMA> mit deinen ORDS-Werten, z.B.:
-- BASE="https://apex.example.com/ords/FRIGOSPED_APP"
--
-- Ohne Auth (Entwicklung):
--
-- # 1. Alle Kataloge
-- curl -sS "$BASE/api/dc/catalogs/" | jq .
--
-- # 2. Fragen fuer Katalog 1
-- curl -sS "$BASE/api/dc/catalogs/1/questions/" | jq .
--
-- # 3. Projektdetail
-- curl -sS "$BASE/api/dc/projects/42" | jq .
--
-- # 4. Projekt starten
-- curl -sS -X POST "$BASE/api/dc/projects/42/start" | jq .
--
-- # 5. Fortschritt setzen
-- curl -sS -X PUT -H "Content-Type: application/json" \
-- -d '{"progress":35}' "$BASE/api/dc/projects/42/progress" | jq .
--
-- # 6. Projekt abschliessen
-- curl -sS -X POST "$BASE/api/dc/projects/42/complete" | jq .
--
-- # 7. Dokumente auflisten
-- curl -sS "$BASE/api/dc/projects/42/documents/" | jq .
--
-- # 8. Datei herunterladen
-- curl -sS -o original.pdf "$BASE/api/dc/documents/101/file"
--
-- # 9. OCR-Text + Uebersetzung zurueckschreiben
-- curl -sS -X PUT -H "Content-Type: application/json" \
-- -d '{"original_text":"## Vertrag\n\nAbsatz 1...",
-- "translated_text":"## Vertrag (DE)\n\nAbsatz 1..."}' \
-- "$BASE/api/dc/documents/101/texts" | jq .
--
-- # 10. Ergebnis einfuegen
-- curl -sS -X POST -H "Content-Type: application/json" \
-- -d '{"question_id":5,"doc_id":101,"answer":"Ja, §305 erfuellt.",
-- "score":85,"result_type":"OK","deviation":0,"warning":0,
-- "the_comment":"Klare Einbeziehungsklausel vorhanden"}' \
-- "$BASE/api/dc/projects/42/results" | jq .
--
-- # 11. Alle Ergebnisse loeschen (Re-Processing)
-- curl -sS -X DELETE "$BASE/api/dc/projects/42/results" | jq .
-- =============================================================================