-- ============================================================================= -- 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_UTILS_PKG.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_UTILS_PKG.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; ]' ); -- =========================================================================== -- TEMPLATE 13: catalogs/ – POST (Katalog anlegen) -- Ergaenzt das bestehende catalogs/-Template um eine Schreib-Operation. -- =========================================================================== -- 13. POST /api/dc/catalogs/ -- Body: {"name":"ADSp 2017","description":"...","document_type_id":1} -- Legt den Katalog mit generation_status=GENERATING an (async Flow). ORDS.DEFINE_HANDLER( p_module_name => 'frigosped.dc', p_pattern => 'catalogs/', p_method => 'POST', p_source_type => ORDS.SOURCE_TYPE_PLSQL, p_comments => '201 Created mit neuer catalog_id; 400 bei fehlendem name/document_type_id', p_source => q'[ DECLARE v_body CLOB; v_name VARCHAR2(100); v_desc CLOB; v_dt_id NUMBER; v_new_id NUMBER; BEGIN v_body := :body_text; v_name := JSON_VALUE(v_body, '$.name'); v_desc := JSON_VALUE(v_body, '$.description' RETURNING CLOB); v_dt_id := TO_NUMBER(JSON_VALUE(v_body, '$.document_type_id')); IF v_name IS NULL OR v_dt_id IS NULL THEN :status_code := 400; HTP.P('{"error":"name und document_type_id sind Pflichtfelder"}'); RETURN; END IF; INSERT INTO dc_question_catalogs (name, description, document_type_id, generation_status) VALUES (v_name, v_desc, v_dt_id, 'GENERATING') RETURNING id INTO v_new_id; :status_code := 201; HTP.P('{"id":' || v_new_id || ',"name":"' || REPLACE(v_name, '"', '''') || '"' || ',"document_type_id":' || v_dt_id || ',"generation_status":"GENERATING"}'); EXCEPTION WHEN OTHERS THEN ROLLBACK; :status_code := 500; HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}'); END; ]' ); -- =========================================================================== -- TEMPLATE 16: catalogs/:catalog_id/generation-status (GET + PUT) -- =========================================================================== ORDS.DEFINE_TEMPLATE( p_module_name => 'frigosped.dc', p_pattern => 'catalogs/:catalog_id/generation-status', p_priority => 0, p_comments => 'Status der asynchronen Katalog-Generierung lesen/schreiben' ); -- 16a. GET /api/dc/catalogs/:catalog_id/generation-status ORDS.DEFINE_HANDLER( p_module_name => 'frigosped.dc', p_pattern => 'catalogs/:catalog_id/generation-status', p_method => 'GET', p_source_type => ORDS.SOURCE_TYPE_COLLECTION_ITEM, p_items_per_page => 1, p_comments => 'Gibt generation_status und generation_error zurueck', p_source => q'[ SELECT c.id AS catalog_id, c.name, c.description, c.generation_status, c.generation_error FROM dc_question_catalogs c WHERE c.id = :catalog_id ]' ); -- 16b. PUT /api/dc/catalogs/:catalog_id/generation-status -- Body: {"generation_status":"READY"} oder {"generation_status":"ERROR","generation_error":"..."} ORDS.DEFINE_HANDLER( p_module_name => 'frigosped.dc', p_pattern => 'catalogs/:catalog_id/generation-status', p_method => 'PUT', p_source_type => ORDS.SOURCE_TYPE_PLSQL, p_comments => 'Setzt generation_status (GENERATING|READY|ERROR) und optionalen Fehlertext', p_source => q'[ DECLARE v_body CLOB; v_status VARCHAR2(20); v_error VARCHAR2(4000); v_rows INTEGER; BEGIN v_body := :body_text; v_status := JSON_VALUE(v_body, '$.generation_status'); v_error := JSON_VALUE(v_body, '$.generation_error'); IF v_status NOT IN ('GENERATING', 'READY', 'ERROR') THEN :status_code := 400; HTP.P('{"error":"generation_status muss GENERATING, READY oder ERROR sein"}'); RETURN; END IF; UPDATE dc_question_catalogs SET generation_status = v_status, generation_error = v_error WHERE id = :catalog_id; v_rows := SQL%ROWCOUNT; IF v_rows = 0 THEN :status_code := 404; HTP.P('{"error":"Katalog nicht gefunden","catalog_id":' || :catalog_id || '}'); RETURN; END IF; :status_code := 200; HTP.P('{"catalog_id":' || :catalog_id || ',"generation_status":"' || v_status || '"' || ',"updated":true}'); EXCEPTION WHEN OTHERS THEN ROLLBACK; :status_code := 500; HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}'); END; ]' ); -- =========================================================================== -- TEMPLATE 14: catalogs/:catalog_id/categories/ (POST) -- =========================================================================== ORDS.DEFINE_TEMPLATE( p_module_name => 'frigosped.dc', p_pattern => 'catalogs/:catalog_id/categories/', p_priority => 0, p_comments => 'Fragenkategorien eines Katalogs anlegen' ); -- 14. POST /api/dc/catalogs/:catalog_id/categories/ -- Body: {"name":"Haftung","description":"...","order_nr":1} ORDS.DEFINE_HANDLER( p_module_name => 'frigosped.dc', p_pattern => 'catalogs/:catalog_id/categories/', p_method => 'POST', p_source_type => ORDS.SOURCE_TYPE_PLSQL, p_comments => '201 Created mit neuer category_id; 400 bei fehlendem name', p_source => q'[ DECLARE v_body CLOB; v_name VARCHAR2(100); v_desc CLOB; v_order NUMBER; v_new_id NUMBER; BEGIN v_body := :body_text; v_name := JSON_VALUE(v_body, '$.name'); v_desc := JSON_VALUE(v_body, '$.description' RETURNING CLOB); v_order := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.order_nr')), 0); IF v_name IS NULL THEN :status_code := 400; HTP.P('{"error":"name ist ein Pflichtfeld"}'); RETURN; END IF; INSERT INTO dc_question_categories (catalog_id, name, description, order_nr) VALUES (:catalog_id, v_name, v_desc, v_order) RETURNING id INTO v_new_id; :status_code := 201; HTP.P('{"id":' || v_new_id || ',"catalog_id":' || :catalog_id || ',"name":"' || REPLACE(v_name, '"', '''') || '"' || ',"order_nr":' || v_order || '}'); EXCEPTION WHEN OTHERS THEN ROLLBACK; :status_code := 500; HTP.P('{"error":"' || REPLACE(SQLERRM, '"', '''') || '"}'); END; ]' ); -- =========================================================================== -- TEMPLATE 15: categories/:category_id/questions/ (POST) -- =========================================================================== ORDS.DEFINE_TEMPLATE( p_module_name => 'frigosped.dc', p_pattern => 'categories/:category_id/questions/', p_priority => 0, p_comments => 'Fragen einer Kategorie anlegen' ); -- 15. POST /api/dc/categories/:category_id/questions/ -- Body: { -- "question_text": "...", -- "evaluation_type": "SCORE", -- "threshold": 70, -- "result_handling": "ABWEICHUNG", -- "example_0_percent": "...", -- "example_100_percent": "...", -- "order_nr": 1 -- } ORDS.DEFINE_HANDLER( p_module_name => 'frigosped.dc', p_pattern => 'categories/:category_id/questions/', p_method => 'POST', p_source_type => ORDS.SOURCE_TYPE_PLSQL, p_comments => '201 Created mit neuer question_id; 400 bei fehlendem question_text', p_source => q'[ DECLARE v_body CLOB; v_text CLOB; v_eval_type VARCHAR2(20); v_threshold NUMBER; v_handling VARCHAR2(20); v_ex0 CLOB; v_ex100 CLOB; v_order NUMBER; v_new_id NUMBER; BEGIN v_body := :body_text; v_text := JSON_VALUE(v_body, '$.question_text' RETURNING CLOB); v_eval_type := NVL(JSON_VALUE(v_body, '$.evaluation_type'), 'SCORE'); v_threshold := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.threshold')), 70); v_handling := NVL(JSON_VALUE(v_body, '$.result_handling'), 'ABWEICHUNG'); v_ex0 := JSON_VALUE(v_body, '$.example_0_percent' RETURNING CLOB); v_ex100 := JSON_VALUE(v_body, '$.example_100_percent' RETURNING CLOB); v_order := NVL(TO_NUMBER(JSON_VALUE(v_body, '$.order_nr')), 0); IF v_text IS NULL THEN :status_code := 400; HTP.P('{"error":"question_text ist ein Pflichtfeld"}'); RETURN; END IF; IF v_eval_type NOT IN ('SCORE', 'BINARY') THEN :status_code := 400; HTP.P('{"error":"evaluation_type muss SCORE oder BINARY sein"}'); RETURN; END IF; IF v_handling NOT IN ('ABWEICHUNG', 'HINWEIS') THEN :status_code := 400; HTP.P('{"error":"result_handling muss ABWEICHUNG oder HINWEIS sein"}'); RETURN; END IF; INSERT INTO dc_questions ( category_id, question_text, evaluation_type, threshold, result_handling, example_0_percent, example_100_percent, order_nr ) VALUES ( :category_id, v_text, v_eval_type, v_threshold, v_handling, v_ex0, v_ex100, v_order ) RETURNING id INTO v_new_id; :status_code := 201; HTP.P('{"id":' || v_new_id || ',"category_id":' || :category_id || ',"evaluation_type":"' || v_eval_type || '"' || ',"threshold":' || v_threshold || ',"order_nr":' || v_order || '}'); 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 und 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 . -- =============================================================================