"""Entity Field System — Lesen/Schreiben dynamischer Feldwerte — AP1.10b/10c.""" from __future__ import annotations from typing import Any, Optional from psycopg2.extras import Json, RealDictCursor from db import get_connection from entity_fields.validation import normalize_field_value, validate_required_fields def _serialize_definition(row: dict[str, Any]) -> dict[str, Any]: result = dict(row) if result.get("id"): result["id"] = str(result["id"]) validation = result.get("validation_json") if validation is None: result["validation_json"] = {} return result def _api_value_from_row(row: dict[str, Any], field_type: str) -> Any: if field_type == "number": if row.get("value_number") is not None: return float(row["value_number"]) return None if field_type == "boolean": if row.get("value_boolean") is not None: return bool(row["value_boolean"]) return False if field_type == "date": value_date = row.get("value_date") if value_date is not None: return value_date.isoformat() return None text = row.get("value_text") or "" return text def list_field_definitions( *, archetype_key: str, ) -> list[dict[str, Any]]: conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT id, archetype_key, field_key, field_type, label, required, searchable, sort_order, validation_json FROM field_definitions WHERE archetype_key = %s ORDER BY sort_order ASC, field_key ASC """, (archetype_key,), ) return [_serialize_definition(dict(row)) for row in cur.fetchall()] finally: conn.close() def get_entity_field_values( *, tenant_id: str, entity_type: str, entity_id: str, definitions: list[dict[str, Any]], ) -> dict[str, Any]: if not definitions: return {} field_keys = [item["field_key"] for item in definitions] type_by_key = {item["field_key"]: item["field_type"] for item in definitions} conn = get_connection() try: with conn.cursor(cursor_factory=RealDictCursor) as cur: cur.execute( """ SELECT field_key, value_text, value_number, value_boolean, value_date FROM field_values WHERE tenant_id = %s AND entity_type = %s AND entity_id = %s AND field_key = ANY(%s) """, (tenant_id, entity_type, entity_id, field_keys), ) rows = {row["field_key"]: dict(row) for row in cur.fetchall()} finally: conn.close() result: dict[str, Any] = {} for field_key in field_keys: row = rows.get(field_key) if row is None: field_type = type_by_key[field_key] if field_type == "boolean": result[field_key] = False else: result[field_key] = None if field_type in {"number", "date"} else "" else: result[field_key] = _api_value_from_row(row, type_by_key[field_key]) return result def patch_entity_field_values( *, tenant_id: str, entity_type: str, entity_id: str, definitions: list[dict[str, Any]], values: dict[str, Any], ) -> dict[str, Any]: if not values: return get_entity_field_values( tenant_id=tenant_id, entity_type=entity_type, entity_id=entity_id, definitions=definitions, ) allowed_keys = {item["field_key"] for item in definitions} unknown = sorted(set(values.keys()) - allowed_keys) if unknown: raise ValueError(f"Unbekannte Felder: {', '.join(unknown)}") definition_by_key = {item["field_key"]: item for item in definitions} merged = get_entity_field_values( tenant_id=tenant_id, entity_type=entity_type, entity_id=entity_id, definitions=definitions, ) merged.update(values) validate_required_fields(definitions=definitions, values=merged) conn = get_connection() try: with conn.cursor() as cur: for field_key, raw_value in values.items(): definition = definition_by_key[field_key] normalized = normalize_field_value( field_type=definition["field_type"], raw_value=raw_value, validation_json=definition.get("validation_json"), ) cur.execute( """ INSERT INTO field_values ( tenant_id, entity_type, entity_id, field_key, value_json, value_text, value_number, value_boolean, value_date ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) ON CONFLICT (tenant_id, entity_type, entity_id, field_key) DO UPDATE SET value_json = EXCLUDED.value_json, value_text = EXCLUDED.value_text, value_number = EXCLUDED.value_number, value_boolean = EXCLUDED.value_boolean, value_date = EXCLUDED.value_date, updated_at = NOW() """, ( tenant_id, entity_type, entity_id, field_key, Json(normalized["value_json"]) if normalized["value_json"] is not None else None, normalized["value_text"], normalized["value_number"], normalized["value_boolean"], normalized["value_date"], ), ) conn.commit() except Exception: conn.rollback() raise finally: conn.close() return get_entity_field_values( tenant_id=tenant_id, entity_type=entity_type, entity_id=entity_id, definitions=definitions, ) def get_initiative_archetype_key(*, tenant_id: str, initiative_id: str) -> Optional[str]: conn = get_connection() try: with conn.cursor() as cur: cur.execute( """ SELECT archetype_key FROM initiatives WHERE tenant_id = %s AND id = %s """, (tenant_id, initiative_id), ) row = cur.fetchone() return row[0] if row else None finally: conn.close()