fixes
This commit is contained in:
parent
274198f269
commit
d35bbf5661
2 changed files with 142 additions and 47 deletions
|
|
@ -1,6 +1,6 @@
|
|||
<!-- status: canonical -->
|
||||
<!-- lastReviewed: 2026-04-26 -->
|
||||
<!-- verifiedAgainst: routeHelpers.py (enrichRowsWithFkLabels), routeFeatureTrustee.py (_buildFeatureInternalResolvers) -->
|
||||
<!-- lastReviewed: 2026-06-09 -->
|
||||
<!-- verifiedAgainst: modules/dbHelpers/fkLabelResolver.py, routeFeatureTrustee.py (_buildFeatureInternalResolvers, _paginatedReadEndpoint) -->
|
||||
|
||||
# FK Label Resolution
|
||||
|
||||
|
|
@ -18,16 +18,16 @@ Das Frontend rendert dann `mandateIdLabel` anstelle der ID. Felder, die keinen R
|
|||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Model["Pydantic Model\n(json_schema_extra.fk_target)"] -->|"fk_target.table"| AutoBuild["_buildLabelResolversFromModel()"]
|
||||
Model["Pydantic Model\n(json_schema_extra.fk_target)"] -->|"fk_target.table"| AutoBuild["buildLabelResolversFromModel()"]
|
||||
AutoBuild -->|"resolvers dict"| Enrich["enrichRowsWithFkLabels()"]
|
||||
ExtraRes["extraResolvers\n(feature-intern)"] -->|"merge"| Enrich
|
||||
Enrich -->|"rows + {field}Label"| Response["API Response"]
|
||||
BuiltIn["_BUILTIN_FK_RESOLVERS\n(Mandate, FeatureInstance,\nUserInDB, Role)"] -->|"lookup"| AutoBuild
|
||||
BuiltIn["_BUILTIN_FK_RESOLVERS\n(Mandate, FeatureInstance,\nUserInDB, Role, FileItem)"] -->|"lookup"| AutoBuild
|
||||
```
|
||||
|
||||
### Ablauf
|
||||
|
||||
1. **Modell-Scan**: `_buildLabelResolversFromModel(modelClass)` iteriert ueber alle Felder des Pydantic-Modells und liest `json_schema_extra.fk_target.table`.
|
||||
1. **Modell-Scan**: `buildLabelResolversFromModel(modelClass)` iteriert ueber alle Felder des Pydantic-Modells und liest `json_schema_extra.fk_target.table`.
|
||||
2. **labelField-Gate**: Felder mit `fk_target.labelField = None` werden uebersprungen (Junction-IDs etc. brauchen kein Label).
|
||||
3. **Builtin-Lookup**: Wenn der Tabellenname in `_BUILTIN_FK_RESOLVERS` existiert, wird der zugehoerige Resolver dem Feld zugeordnet.
|
||||
4. **Extra-Resolvers**: Zusaetzliche Resolver (z.B. fuer feature-interne FKs) werden via `extraResolvers` gemerged.
|
||||
|
|
@ -36,16 +36,17 @@ flowchart TD
|
|||
|
||||
## Builtin-Resolvers
|
||||
|
||||
Definiert in `platform-core/modules/routes/routeHelpers.py`:
|
||||
Definiert in `platform-core/modules/dbHelpers/fkLabelResolver.py`:
|
||||
|
||||
| `fk_target.table` | Resolver-Funktion | Datenquelle | Label-Feld |
|
||||
|---|---|---|---|
|
||||
| `Mandate` | `resolveMandateLabels()` | `interfaceDbApp` → `getMandatesByIds()` | `label` oder `name` |
|
||||
| `FeatureInstance` | `resolveInstanceLabels()` | `interfaceFeatures` → `getFeatureInstance()` | `label` |
|
||||
| `UserInDB` | `resolveUserLabels()` | `interfaceDbApp` → `getRecordset(UserInDB)` | `displayName` / `username` / `email` |
|
||||
| `Role` | `resolveRoleLabels()` | `interfaceDbApp` → `getRecordset(Role)` | `roleLabel` |
|
||||
| `Mandate` | `resolveMandateLabels()` | `getRootInterface().db` → `getRecordset(Mandate)` | `label` oder `name` |
|
||||
| `FeatureInstance` | `resolveInstanceLabels()` | `getRootInterface().db` → `getRecordset(FeatureInstance)` | `label` |
|
||||
| `UserInDB` | `resolveUserLabels()` | `getRootInterface().db` → `getRecordset(UserInDB)` | `displayName` / `username` / `email` |
|
||||
| `Role` | `resolveRoleLabels()` | `getRootInterface().db` → `getRecordset(Role)` | `roleLabel` |
|
||||
| `FileItem` | `resolveFileLabels()` | `getRootInterface().db` → `getRecordset(FileItem)` | `fileName` |
|
||||
|
||||
Diese vier Resolver decken alle plattformweiten FK-Beziehungen ab. FK-Felder, die auf andere Tabellen zeigen, werden **nicht** automatisch aufgeloest.
|
||||
Diese fuenf Resolver decken alle plattformweiten FK-Beziehungen ab. FK-Felder, die auf andere Tabellen zeigen, werden **nicht** automatisch aufgeloest.
|
||||
|
||||
## Pydantic-Modell-Annotation
|
||||
|
||||
|
|
@ -84,7 +85,7 @@ userId: str = Field(
|
|||
)
|
||||
```
|
||||
|
||||
`_buildLabelResolversFromModel` erkennt `table: "UserInDB"` und ordnet `resolveUserLabels` zu. Das Ergebnis: jede Row erhaelt `userIdLabel`.
|
||||
`buildLabelResolversFromModel` erkennt `table: "UserInDB"` und ordnet `resolveUserLabels` zu. Das Ergebnis: jede Row erhaelt `userIdLabel`.
|
||||
|
||||
### Feature-internes FK-Ziel (extra Resolver noetig)
|
||||
|
||||
|
|
@ -164,44 +165,131 @@ Dieses Pattern:
|
|||
3. Erstellt einen Resolver, der die Ziel-Tabelle abfragt und ein Label aus den ersten 2 verfuegbaren beschreibenden Feldern baut
|
||||
4. Wird im Route-Handler via `_paginatedReadEndpoint` automatisch aufgerufen
|
||||
|
||||
## WICHTIG: Filter-Dropdown-Enrichment (FormGeneratorTable)
|
||||
---
|
||||
|
||||
> **Regel fuer jeden Route-Handler der eine paginierte Datentabelle (FormGeneratorTable) bedient:**
|
||||
>
|
||||
> Der `mode=filterValues`-Pfad MUSS `enrichRowsWithFkLabels(items, ModelClass)` aufrufen
|
||||
> **bevor** `handleFilterValuesInMemory(items, column, ...)` aufgerufen wird.
|
||||
> Ohne diesen Schritt zeigen Filter-Dropdowns rohe UUIDs statt menschenlesbarer Labels.
|
||||
>
|
||||
> `_extractDistinctValues` erkennt FK-Labels nur, wenn `{field}Label`-Spalten in den Items vorhanden sind.
|
||||
> Diese werden ausschliesslich durch `enrichRowsWithFkLabels` hinzugefuegt.
|
||||
## KRITISCH: Enrichment in ALLEN Pfaden (FormGeneratorTable)
|
||||
|
||||
**Korrektes Pattern (In-Memory-Route):**
|
||||
> **Jeder Route-Handler der eine `FormGeneratorTable` bedient MUSS `enrichRowsWithFkLabels` in ALLEN Datenpfaden aufrufen — nicht nur im Filter-Pfad!**
|
||||
>
|
||||
> **Haeufigster Fehler**: Enrichment nur im `mode=filterValues`-Pfad, aber NICHT im Standard-Paginated-Pfad. Ergebnis: Filter-Dropdowns zeigen Labels, aber die Tabellenzellen zeigen rohe UUIDs.
|
||||
|
||||
### Vollstaendiges korrektes Pattern (Referenz)
|
||||
|
||||
```python
|
||||
if mode == "filterValues":
|
||||
if not column:
|
||||
raise HTTPException(status_code=400, detail="column parameter required")
|
||||
items = _buildItems()
|
||||
enrichRowsWithFkLabels(items, MyModel)
|
||||
return handleFilterValuesInMemory(items, column, pagination)
|
||||
```
|
||||
@router.get("/{instanceId}/entities")
|
||||
def get_entities(
|
||||
request: Request,
|
||||
instanceId: str = Path(...),
|
||||
pagination: Optional[str] = Query(None),
|
||||
mode: Optional[str] = Query(None),
|
||||
column: Optional[str] = Query(None),
|
||||
context: RequestContext = Depends(getRequestContext),
|
||||
):
|
||||
from modules.dbHelpers.fkLabelResolver import enrichRowsWithFkLabels
|
||||
|
||||
**Korrektes Pattern (DB-Paginated mit Fallback):**
|
||||
mandateId = _validateInstanceAccess(instanceId, context)
|
||||
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
|
||||
|
||||
```python
|
||||
if mode == "filterValues":
|
||||
try:
|
||||
values = db.getDistinctColumnValues(MyModel, column, crossPagination, recordFilter)
|
||||
return JSONResponse(content=sorted(values, ...))
|
||||
except Exception:
|
||||
items = [r.model_dump() for r in db.getRecordset(MyModel, ...)]
|
||||
enrichRowsWithFkLabels(items, MyModel)
|
||||
# --- Mode: filterValues ---
|
||||
if mode == "filterValues":
|
||||
if not column:
|
||||
raise HTTPException(status_code=400, detail="column parameter required")
|
||||
items = _loadAllItems(interface)
|
||||
enrichRowsWithFkLabels(items, MyModel, db=getRootInterface().db) # ← PFLICHT
|
||||
return handleFilterValuesInMemory(items, column, pagination)
|
||||
|
||||
# --- Mode: ids ---
|
||||
if mode == "ids":
|
||||
items = _loadAllItems(interface)
|
||||
return handleIdsInMemory(items, pagination)
|
||||
|
||||
# --- Standard-Pfad: Paginierte Tabellendaten ---
|
||||
paginationParams = _parsePagination(pagination)
|
||||
result = interface.getAllEntities(paginationParams)
|
||||
|
||||
def _toDicts(items):
|
||||
return [r.model_dump() if hasattr(r, "model_dump") else r for r in items]
|
||||
|
||||
if paginationParams and hasattr(result, "items"):
|
||||
enriched = enrichRowsWithFkLabels( # ← PFLICHT
|
||||
_toDicts(result.items), MyModel, db=getRootInterface().db
|
||||
)
|
||||
return {
|
||||
"items": enriched,
|
||||
"pagination": PaginationMetadata(...).model_dump(),
|
||||
}
|
||||
|
||||
items = result if isinstance(result, list) else result.items
|
||||
enriched = enrichRowsWithFkLabels( # ← PFLICHT
|
||||
_toDicts(items), MyModel, db=getRootInterface().db
|
||||
)
|
||||
return {"items": enriched, "pagination": None}
|
||||
```
|
||||
|
||||
### Die drei Pflicht-Enrichment-Stellen
|
||||
|
||||
| Pfad | Enrichment noetig? | Warum |
|
||||
|---|---|---|
|
||||
| **Standard (paginated)** | **JA** | Tabellenzellen muessen Labels zeigen |
|
||||
| **`mode=filterValues`** | **JA** | Filter-Dropdowns muessen Labels zeigen |
|
||||
| **`mode=groupSummary`** | **JA** | Gruppierungs-Headers muessen Labels zeigen |
|
||||
| `mode=ids` | Nein | Gibt nur IDs zurueck, keine Labels noetig |
|
||||
|
||||
### Haeufige Fehler (NICHT nachmachen)
|
||||
|
||||
```python
|
||||
# FALSCH: Nur im filterValues-Pfad enrichen
|
||||
if mode == "filterValues":
|
||||
enrichRowsWithFkLabels(items, MyModel, db=...)
|
||||
return handleFilterValuesInMemory(...)
|
||||
|
||||
# Standard-Pfad OHNE Enrichment → UUIDs im UI!
|
||||
return {"items": _toDicts(result.items), "pagination": ...}
|
||||
```
|
||||
|
||||
```python
|
||||
# FALSCH: enrichRowsWithFkLabels aufrufen aber falschen db-Connector uebergeben
|
||||
# Die Mandate/FeatureInstance/User-Tabellen liegen in poweron_app,
|
||||
# nicht in der Feature-DB!
|
||||
enrichRowsWithFkLabels(items, MyModel, db=featureInterface.db) # ← FALSCH
|
||||
enrichRowsWithFkLabels(items, MyModel, db=getRootInterface().db) # ← RICHTIG
|
||||
```
|
||||
|
||||
### Bestes Pattern: Enrichment VOR dem Branching
|
||||
|
||||
Wenn alle Modi dieselben Items laden, ist das sauberste Pattern:
|
||||
|
||||
```python
|
||||
items = _loadAllItems(interface)
|
||||
enrichRowsWithFkLabels(items, MyModel, db=getRootInterface().db)
|
||||
|
||||
if mode == "filterValues":
|
||||
return handleFilterValuesInMemory(items, column, pagination)
|
||||
if mode == "ids":
|
||||
return handleIdsInMemory(items, pagination)
|
||||
# ... Standard-Pagination ...
|
||||
```
|
||||
|
||||
So kann kein Pfad das Enrichment vergessen. Siehe `routeAdminFeatures.py` als Referenz-Implementierung.
|
||||
|
||||
### Feature-interne FKs im Standard-Pfad
|
||||
|
||||
Wenn das Modell FK-Felder hat die auf andere Feature-Tabellen zeigen (z.B. `TrusteePosition.documentId` → `TrusteeDocument`), muessen diese ueber `extraResolvers` aufgeloest werden:
|
||||
|
||||
```python
|
||||
featureResolvers = _buildFeatureInternalResolvers(MyModel, interface.db)
|
||||
enrichRowsWithFkLabels(
|
||||
items, MyModel,
|
||||
db=getRootInterface().db,
|
||||
extraResolvers=featureResolvers or None,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checkliste: Neues FK-Feld hinzufuegen
|
||||
|
||||
### Fall 1: FK auf Mandate / FeatureInstance / UserInDB / Role
|
||||
### Fall 1: FK auf Mandate / FeatureInstance / UserInDB / Role / FileItem
|
||||
|
||||
1. `json_schema_extra` mit `fk_target` annotieren (`db`, `table`, `labelField` — alle drei Pflicht)
|
||||
2. Fertig — der Builtin-Resolver wird automatisch erkannt
|
||||
|
|
@ -217,25 +305,30 @@ if mode == "filterValues":
|
|||
|
||||
1. Einen dedizierten Resolver schreiben (Signatur: `(ids: List[str]) -> Dict[str, Optional[str]]`)
|
||||
2. Im Route-Handler als `extraResolvers` uebergeben
|
||||
3. Fuer haeufig verwendete Ziele: Resolver in `_BUILTIN_FK_RESOLVERS` in `routeHelpers.py` aufnehmen
|
||||
3. Fuer haeufig verwendete Ziele: Resolver in `_BUILTIN_FK_RESOLVERS` in `fkLabelResolver.py` aufnehmen
|
||||
|
||||
### Checkliste fuer neue Datentabellen-Route (FormGeneratorTable)
|
||||
|
||||
1. Tabellen-Pfad: `enrichRowsWithFkLabels(rows, ModelClass)` vor Response
|
||||
2. **Filter-Pfad (`mode=filterValues`)**: `enrichRowsWithFkLabels(items, ModelClass)` vor `handleFilterValuesInMemory`
|
||||
3. IDs-Pfad (`mode=ids`): kein Enrichment noetig
|
||||
4. `fk_target` auf dem Modell mit `db`, `table`, `labelField` (Pflicht, validiert beim Start)
|
||||
- [ ] **Hauptpfad (paginated)**: `enrichRowsWithFkLabels(rows, ModelClass, db=getRootInterface().db)` vor Response
|
||||
- [ ] **Filter-Pfad (`mode=filterValues`)**: `enrichRowsWithFkLabels(items, ModelClass, db=getRootInterface().db)` vor `handleFilterValuesInMemory`
|
||||
- [ ] **GroupSummary-Pfad** (falls vorhanden): Enrichment vor `build_group_summary_groups`
|
||||
- [ ] IDs-Pfad (`mode=ids`): kein Enrichment noetig
|
||||
- [ ] `db=` Parameter: Immer `getRootInterface().db` fuer Builtin-FK-Resolver (Mandate, User, etc.), NICHT die Feature-DB
|
||||
- [ ] `extraResolvers`: Falls das Modell feature-interne FKs hat, `_buildFeatureInternalResolvers(ModelClass, interface.db)` uebergeben
|
||||
- [ ] `fk_target` auf dem Modell mit `db`, `table`, `labelField` (Pflicht, validiert beim Start)
|
||||
- [ ] Testen: Tabelle im UI laden → keine UUIDs sichtbar in FK-Spalten
|
||||
|
||||
## Kern-Dateien
|
||||
|
||||
| Datei | Zweck |
|
||||
|---|---|
|
||||
| `platform-core/modules/routes/routeHelpers.py` | `_BUILTIN_FK_RESOLVERS`, `_buildLabelResolversFromModel`, `enrichRowsWithFkLabels` |
|
||||
| `platform-core/modules/dbHelpers/fkLabelResolver.py` | `_BUILTIN_FK_RESOLVERS`, `buildLabelResolversFromModel`, `enrichRowsWithFkLabels`, alle Resolver-Funktionen |
|
||||
| `platform-core/modules/shared/fkRegistry.py` | `validateFkTargets` (Startup-Validierung), FK-Discovery |
|
||||
| `platform-core/modules/features/trustee/routeFeatureTrustee.py` | `_buildFeatureInternalResolvers` (Referenz-Implementierung) |
|
||||
| `platform-core/modules/features/trustee/routeFeatureTrustee.py` | `_buildFeatureInternalResolvers` (Referenz), `_paginatedReadEndpoint` (generischer Handler) |
|
||||
| `platform-core/modules/features/trustee/datamodelFeatureTrustee.py` | Beispiel-Annotationen (`fk_target` auf allen Modellen) |
|
||||
| `platform-core/modules/routes/routeAdminFeatures.py` | Bestes Pattern: Enrichment VOR Mode-Branching |
|
||||
|
||||
## Siehe auch
|
||||
|
||||
- [FormGenerator Referenz](../ui-nyla/formgenerator.md) — Frontend-Darstellung der aufgeloesten Labels
|
||||
- [Gateway Architektur](architecture.md) — Modulstruktur und routeHelpers
|
||||
- [Gateway Architektur](architecture.md) — Modulstruktur und Resolver-Einbindung
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ Skip: reine Refactors, Formatting, Lint, Dep-Bumps, Test-only, Wiki-Tippfehler.
|
|||
|
||||
## 2026-06-09
|
||||
|
||||
- 2026-06-09 | fix | platform-core | **FK-Label-Resolution im Hauptpfad ergaenzt**: `enrichRowsWithFkLabels` fehlte im Standard-Paginated-Pfad bei Trustee (documents, positions), RealEstate (projects, parcels), Subscriptions und RBAC-Roles. Nur `mode=filterValues` war enriched — Tabellenzellen zeigten rohe UUIDs. Zusaetzlich `FileItem` in `_BUILTIN_FK_RESOLVERS` registriert.
|
||||
- 2026-06-09 | docs | wiki | **fk-label-resolution.md komplett ueberarbeitet**: Kritisches Pattern "Enrichment in ALLEN Pfaden" dokumentiert, haeufige Fehler, vollstaendige Checkliste fuer neue FormGeneratorTable-Routes, aktualisierte Kern-Dateien.
|
||||
- 2026-06-09 | refactor | platform-core | **Intra-Modul-Zyklen komplett bereinigt (B1)**: 9 bidirektionale Import-Zyklen innerhalb von Modul-Ordnern aufgeloest. **interfaces/**: `interfaceDbApp <-> interfaceBootstrap` (copySystemRolesToMandate nach interfaceRbac), `interfaceDbApp <-> interfaceDbBilling` (Billing-Cascade via onMandateDelete Lifecycle-Hook). **serviceCenter/**: `serviceAgent <-> serviceKnowledge` (flagResolution nach core/, FeatureDataProviderProtocol), `serviceExtraction <-> serviceGeneration` (RendererProtocol in core/types). **workflowAutomation/**: `graphUtils <-> pickNotPushMigration` (Migration-Call zum Caller), `conditionOperators <-> upstreamPathsService` (_valueKindResolver.py extrahiert), `executionEngine <-> mainScheduler` (_runNotifications.py), `executionEngine <-> emailPoller` (Pause-Status statt direkter Poller-Start). **features/redmine/**: `serviceRedmine <-> serviceRedmineSync` (getProjectMeta nach interfaceFeatureRedmine). Neue Dateien: `serviceCenter/core/flagResolution.py`, `serviceCenter/core/types.py`, `workflowAutomation/editor/_valueKindResolver.py`, `workflowAutomation/engine/_runNotifications.py`. Ergebnis: **0 bidirektionale Intra-Modul-Zyklen**.
|
||||
|
||||
## 2026-06-08
|
||||
|
|
|
|||
Loading…
Reference in a new issue