feat: Add catalog generation functionality and related models

- Implemented CatalogResource for handling catalog generation requests and status checks.
- Created CatalogGenerationService to manage the asynchronous generation process.
- Added models for catalog generation responses, requests, and structured catalog data.
- Introduced utility scripts for database interaction and Kubernetes pod retrieval.
- Enhanced error handling and logging throughout the catalog generation workflow.
This commit is contained in:
Wolf G. Beckmann
2026-05-04 19:03:54 +02:00
parent 522bc21439
commit ac9ab7ab15
21 changed files with 1853 additions and 202 deletions

View File

@@ -365,7 +365,7 @@ BEGIN
-- Markdown-Bericht generieren (Fehler ignoriert, Projekt bleibt COMPLETED)
BEGIN
dc_generate_report(:project_id);
DC_UTILS_PKG.generate_report(:project_id);
EXCEPTION WHEN OTHERS THEN NULL;
END;
@@ -406,7 +406,7 @@ BEGIN
p_comments => 'Generiert report_markdown neu (z.B. nach Konfigurationsänderung)',
p_source => q'[
BEGIN
dc_generate_report(:project_id);
DC_UTILS_PKG.generate_report(:project_id);
:status_code := 200;
HTP.P('{"project_id":' || :project_id || ',"report_generated":true}');
EXCEPTION
@@ -731,6 +731,301 @@ BEGIN
]'
);
-- ===========================================================================
-- 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;
/