platform-core/modules/dbHelpers/fkLabelResolver.py
ValueOn AG bc7c6fe27c
Some checks failed
Deploy Plattform-Core (Int) / test (push) Failing after 13s
Deploy Plattform-Core (Int) / deploy (push) Has been skipped
elimination of technical issues (imports)
2026-06-06 00:32:45 +02:00

196 lines
6.9 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
FK label resolution: resolve foreign-key IDs to human-readable labels.
Works with the fk_target annotations on Pydantic models (see fkRegistry.py)
to auto-build label resolvers for paginated record sets.
"""
import logging
from functools import partial
from typing import Any, Callable, Dict, List, Optional
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Individual FK label resolvers (db, ids) -> {id: label}
# ---------------------------------------------------------------------------
def resolveMandateLabels(db, ids: List[str]) -> Dict[str, Optional[str]]:
"""Resolve mandate IDs to labels. Returns None (not the ID!) for
unresolvable entries so the caller can distinguish "resolved" from "missing".
"""
from modules.datamodels.datamodelUam import Mandate
uniqueIds = list(set(ids))
records = db.getRecordset(Mandate, recordFilter={"id": uniqueIds}) or []
found: Dict[str, dict] = {}
for rec in records:
mid = rec.get("id", "")
found[mid] = rec
result: Dict[str, Optional[str]] = {}
for mid in ids:
m = found.get(mid)
label = (m.get("label") or m.get("name")) if m else None
if not label:
logger.debug("resolveMandateLabels: no label for id=%s (found=%s)", mid, m is not None)
result[mid] = label or None
return result
def resolveInstanceLabels(db, ids: List[str]) -> Dict[str, Optional[str]]:
"""Resolve feature-instance IDs to labels. Returns None for unresolvable."""
from modules.datamodels.datamodelFeatures import FeatureInstance
result: Dict[str, Optional[str]] = {}
for iid in ids:
records = db.getRecordset(FeatureInstance, recordFilter={"id": iid})
if records:
label = records[0].get("label") or None
result[iid] = label
else:
logger.debug("resolveInstanceLabels: no label for id=%s", iid)
result[iid] = None
return result
def resolveUserLabels(db, ids: List[str]) -> Dict[str, Optional[str]]:
"""Resolve user IDs to display names. Returns None for unresolvable."""
from modules.datamodels.datamodelUam import UserInDB as _UserInDB
uniqueIds = list(set(ids))
users = db.getRecordset(
_UserInDB,
recordFilter={"id": uniqueIds},
)
result: Dict[str, Optional[str]] = {}
found: Dict[str, dict] = {}
for u in (users or []):
uid = u.get("id", "")
found[uid] = u
for uid in ids:
u = found.get(uid)
if u:
result[uid] = u.get("displayName") or u.get("username") or u.get("email") or None
else:
result[uid] = None
return result
def resolveRoleLabels(db, ids: List[str]) -> Dict[str, Optional[str]]:
"""Resolve Role.id to roleLabel. Returns None for unresolvable."""
if not ids:
return {}
from modules.datamodels.datamodelRbac import Role as _Role
recs = db.getRecordset(
_Role,
recordFilter={"id": list(set(ids))},
) or []
out: Dict[str, Optional[str]] = {i: None for i in ids}
for r in recs:
rid = r.get("id")
if rid:
out[rid] = r.get("roleLabel") or None
for rid in ids:
if out.get(rid) is None:
logger.debug("resolveRoleLabels: no label for id=%s", rid)
return out
# ---------------------------------------------------------------------------
# Resolver registry
# ---------------------------------------------------------------------------
_BUILTIN_FK_RESOLVERS: Dict[str, Callable] = {
"Mandate": resolveMandateLabels,
"FeatureInstance": resolveInstanceLabels,
"UserInDB": resolveUserLabels,
"Role": resolveRoleLabels,
}
def buildLabelResolversFromModel(
modelClass: type,
db=None,
) -> Dict[str, Callable[[List[str]], Dict[str, str]]]:
"""
Auto-build labelResolvers dict from ``json_schema_extra.fk_target`` on a Pydantic model.
Maps field names to resolver functions when the target table has a registered builtin
resolver and ``fk_target.labelField`` is set (non-None).
When ``db`` is provided, the returned resolvers are pre-bound with partial(resolver, db)
so they can be called as resolver(ids).
"""
resolvers: Dict[str, Callable[[List[str]], Dict[str, str]]] = {}
for name, fieldInfo in modelClass.model_fields.items():
extra = fieldInfo.json_schema_extra
if not extra or not isinstance(extra, dict):
continue
tgt = extra.get("fk_target")
if not isinstance(tgt, dict):
continue
if tgt.get("labelField") is None:
continue
fkModel = tgt.get("table")
if fkModel and fkModel in _BUILTIN_FK_RESOLVERS:
fn = _BUILTIN_FK_RESOLVERS[fkModel]
resolvers[name] = partial(fn, db) if db else fn
return resolvers
def enrichRowsWithFkLabels(
rows: List[Dict[str, Any]],
modelClass: type = None,
*,
db=None,
labelResolvers: Optional[Dict[str, Callable[[List[str]], Dict[str, Optional[str]]]]] = None,
extraResolvers: Optional[Dict[str, Callable[[List[str]], Dict[str, Optional[str]]]]] = None,
) -> List[Dict[str, Any]]:
"""Add ``{field}Label`` columns to each row for every FK field that has a
registered resolver.
``modelClass`` — if provided, resolvers are auto-built from ``fk_target``
annotations on the Pydantic model (via ``buildLabelResolversFromModel``).
Requires ``db`` to be passed.
``labelResolvers`` — explicit resolver map that overrides auto-built ones.
Each resolver has signature ``(ids: List[str]) -> Dict[str, Optional[str]]``.
``extraResolvers`` — merged on top of auto-built / explicit resolvers. Use
for ad-hoc fields that are not FK-annotated on the model (e.g.
``createdByUserId`` on billing transactions).
If a label cannot be resolved the ``{field}Label`` value is ``None``
(never the raw ID — that would reintroduce the silent-truncation bug).
"""
resolvers: Dict[str, Callable] = {}
if modelClass is not None and labelResolvers is None:
resolvers = buildLabelResolversFromModel(modelClass, db)
elif labelResolvers is not None:
resolvers = dict(labelResolvers)
if extraResolvers:
resolvers.update(extraResolvers)
if not resolvers or not rows:
return rows
for field, resolver in resolvers.items():
ids = list({str(r.get(field)) for r in rows if r.get(field)})
if not ids:
continue
try:
labelMap = resolver(ids)
except Exception as e:
logger.error("enrichRowsWithFkLabels: resolver for '%s' raised: %s", field, e)
labelMap = {}
labelKey = f"{field}Label"
for r in rows:
fkVal = r.get(field)
if fkVal:
r[labelKey] = labelMap.get(str(fkVal))
else:
r[labelKey] = None
return rows