platform-core/modules/serviceCenter/services/serviceKnowledge/udbNodes.py
ValueOn AG 4a60086c80
Some checks failed
Deploy Plattform-Core (Int) / test (push) Failing after 15s
Deploy Plattform-Core (Int) / deploy (push) Has been skipped
cp adapted to 2026 poweron
2026-06-09 09:53:31 +02:00

1056 lines
41 KiB
Python

# Copyright (c) 2026 PowerOn AG
# All rights reserved.
"""Polymorphic UdbNode hierarchy for the Unified Data Bar.
Each UDB tree node is represented by an `UdbNode` subclass that encapsulates
its own behavior:
- which flags it supports (`supportsFlag`)
- whether the current user may edit it (`canEdit`)
- how to compute an effective flag value (`getEffectiveFlag`)
- how to persist a flag change (`setFlag`)
- how to enumerate its logical children for aggregation
(`getLogicalChildren`)
- how to render itself to a JSON dict (`toDict`)
Concrete subclasses fall into four families that mirror the UDB domain
model (see wiki/b-reference/platform/unified-data-bar.md):
- SyntheticContainerNode -- structural containers, no DB record
- DataSourceNode (+children)-- user-private DataSource records
- FdsRecordNode (+children)-- feature-owned FeatureDataSource records
- FdsFieldNode -- virtual per-column nodes under fdsTable
The classes use `_inheritFlags.py` as a helper module for the actual
walk/aggregate/cascade arithmetic, so the inheritance semantics live in
one place. The classes themselves only express "what does this node type
DO" -- ownership, RBAC, persistence routing, child enumeration.
"""
from __future__ import annotations
import logging
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional, Tuple
logger = logging.getLogger(__name__)
_KEY_SEP = "|"
def _decode(key: str) -> Tuple[str, List[str]]:
"""Decode a UDB tree key into (kind, [parts...])."""
parts = key.split(_KEY_SEP)
return parts[0], parts[1:]
def _encode(kind: str, *parts: str) -> str:
"""Encode kind + parts into a stable tree key."""
return _KEY_SEP.join((kind, *parts))
# ---------------------------------------------------------------------------
# Abstract base
# ---------------------------------------------------------------------------
class UdbNode(ABC):
"""Polymorphic UDB tree node.
Subclasses MUST implement the abstract methods. Defaults are kept
minimal so each subclass's responsibilities are explicit.
`parentKey` is set by the builder when the node is emitted; it is the
tree key of the directly-rendered parent (which may differ from the
semantic ancestor used for flag inheritance).
"""
kind: str = "abstract"
def __init__(self, key: str, label: str, parentKey: Optional[str] = None) -> None:
self.key = key
self.label = label
self.parentKey = parentKey
# --- domain hooks -------------------------------------------------------
def supportsFlag(self, flag: str) -> bool:
"""Whether this node carries a value for `flag` at all.
Subclasses override to restrict (e.g. fdsField only has neutralize).
Scope was removed from DataSource nodes (privacy, 2026-06) and never
existed on FDS nodes. Only Files (folder-files) retain scope.
"""
return flag in ("neutralize", "ragIndexEnabled")
@abstractmethod
def canEdit(self, context: Any, rootIf: Any) -> bool:
"""Permission check: may the calling user mutate flags on this node?
Polymorph rule:
- DataSource* nodes: owner-of-record check (rec.userId == user).
- FdsRecord* / FdsField nodes: feature-admin check on the FDS's
featureInstanceId.
- Synthetic containers: never editable (defensive).
"""
@abstractmethod
def getEffectiveFlag(self, flag: str, allDs: List[Dict[str, Any]],
allFds: List[Dict[str, Any]], mode: str = "aggregate") -> Any:
"""Compute the effective value of `flag` for this node.
`mode='walk'` returns the concrete inherited value (never 'mixed').
`mode='aggregate'` returns 'mixed' iff the logical subtree disagrees.
Synthetic containers compute aggregate from their logical children.
"""
def setFlag(self, flag: str, value: Any, rootIf: Any) -> List[str]:
"""Persist a new value for `flag`. Default: not supported.
Returns the IDs of descendant records reset to None (cascade).
Subclasses that own a DB record override; synthetic containers and
virtual nodes refuse.
"""
raise NotImplementedError(
f"{type(self).__name__} does not support setFlag({flag!r})"
)
def getLogicalChildren(self, allDs: List[Dict[str, Any]],
allFds: List[Dict[str, Any]],
rootIf: Any, context: Any) -> List["UdbNode"]:
"""Return the children used for aggregate flag computation.
These are NOT the rendered tree children -- they are the logical
descendants whose effective values determine whether this node is
'mixed'. Default: empty (leaf).
"""
return []
@abstractmethod
def toDict(self, allDs: List[Dict[str, Any]],
allFds: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Serialize to the dict shape consumed by the frontend tree.
Implementations include effective flag values for the flags this
node supports, plus rendering hints (icon, hasChildren, etc.).
"""
# ---------------------------------------------------------------------------
# Synthetic containers
# ---------------------------------------------------------------------------
class SyntheticContainerNode(UdbNode):
"""Structural container with no DB record (personalRoot, mandateGroup).
Aggregates the effective flag values from its logical children for
display only; cannot be edited.
"""
kind = "synthRoot"
def __init__(self, key: str, label: str, icon: str,
parentKey: Optional[str] = None,
displayOrder: int = 0,
defaultExpanded: bool = False,
logicalChildren: Optional[List[UdbNode]] = None) -> None:
super().__init__(key, label, parentKey)
self.icon = icon
self.displayOrder = displayOrder
self.defaultExpanded = defaultExpanded
self._logicalChildren: List[UdbNode] = list(logicalChildren or [])
def addLogicalChild(self, child: UdbNode) -> None:
self._logicalChildren.append(child)
def supportsFlag(self, flag: str) -> bool:
return False
def canEdit(self, context: Any, rootIf: Any) -> bool:
return False
def getLogicalChildren(self, allDs, allFds, rootIf, context) -> List[UdbNode]:
return list(self._logicalChildren)
def getEffectiveFlag(self, flag, allDs, allFds, mode="aggregate") -> Any:
return _aggregateFromChildren(self, flag, allDs, allFds, mode)
def toDict(self, allDs, allFds) -> Dict[str, Any]:
return {
"key": self.key,
"kind": "synthRoot" if self.key != _KEY_MANDATE_GROUP_PREFIX else self.kind,
"parentKey": self.parentKey,
"label": self.label,
"icon": self.icon,
"hasChildren": True,
"dataSourceId": None,
"modelType": None,
"effectiveNeutralize": self.getEffectiveFlag("neutralize", allDs, allFds, "aggregate"),
"effectiveScope": "personal",
"effectiveRagIndexEnabled": self.getEffectiveFlag("ragIndexEnabled", allDs, allFds, "aggregate"),
"supportsRag": False,
"canBeAdded": False,
"displayOrder": self.displayOrder,
"defaultExpanded": self.defaultExpanded,
}
_KEY_MANDATE_GROUP_PREFIX = "mgrp"
class MandateGroupNode(SyntheticContainerNode):
"""Synthetic top-level container, one per accessible mandate."""
kind = "mandateGroup"
def __init__(self, mandateId: str, label: str) -> None:
super().__init__(
key=_encode("mgrp", mandateId),
label=label,
icon="mandate",
parentKey=None,
defaultExpanded=True,
)
self.mandateId = mandateId
def toDict(self, allDs, allFds) -> Dict[str, Any]:
out = super().toDict(allDs, allFds)
out["kind"] = "mandateGroup"
out["mandateId"] = self.mandateId
return out
# ---------------------------------------------------------------------------
# DataSource family
# ---------------------------------------------------------------------------
class _DataSourceFamilyNode(UdbNode):
"""Shared behavior for DS-backed nodes (connection, service, folder, file).
A node either has an existing DataSource record (`rec`) or is "virtual"
-- a coordinate (connectionId/sourceType/path) that the user has not
yet pinned. Virtual nodes can still report effective flag values (they
walk the ancestor chain) but cannot be edited until the record exists.
"""
def __init__(self, key: str, label: str, parentKey: Optional[str],
connectionId: str, sourceType: str, path: str,
rec: Optional[Dict[str, Any]]) -> None:
super().__init__(key, label, parentKey)
self.connectionId = connectionId
self.sourceType = sourceType
self.path = path
self.rec = rec
def canEdit(self, context: Any, rootIf: Any) -> bool:
if self.rec:
ownerId = str(self.rec.get("userId") or "")
return ownerId == str(context.user.id)
return _isConnectionOwner(rootIf, str(context.user.id), self.connectionId)
def getEffectiveFlag(self, flag, allDs, allFds, mode="aggregate") -> Any:
if not self.supportsFlag(flag):
return False
from modules.serviceCenter.core.flagResolution import (
resolveEffectiveForPath,
)
out = resolveEffectiveForPath(self.connectionId, self.sourceType, self.path, allDs, mode=mode)
key = "effective" + flag[0].upper() + flag[1:]
return out.get(key, False)
def setFlag(self, flag, value, rootIf) -> List[str]:
from modules.datamodels.datamodelDataSource import DataSource
from modules.serviceCenter.core.flagResolution import (
cascadeResetDescendants,
)
if not self.rec:
self.rec = _findOrCreateDs(
rootIf, self.connectionId, self.sourceType, self.path,
)
sourceId = self.rec.get("id")
resetIds: List[str] = []
if value is not None:
resetIds = cascadeResetDescendants(rootIf, self.rec, flag)
rootIf.db.recordModify(DataSource, sourceId, {flag: value})
return resetIds
class ConnectionNode(_DataSourceFamilyNode):
"""Connection-root DataSource node (path='/', authority sourceType)."""
kind = "connection"
def __init__(self, connectionId: str, authority: str, label: str,
parentKey: str, rec: Optional[Dict[str, Any]]) -> None:
super().__init__(
key=_encode("conn", connectionId),
label=label,
parentKey=parentKey,
connectionId=connectionId,
sourceType=str(authority),
path="/",
rec=rec,
)
self.authority = authority
def toDict(self, allDs, allFds) -> Dict[str, Any]:
return _dsDict(self, allDs)
class ServiceNode(_DataSourceFamilyNode):
"""Service-level node (e.g. sharepoint, drive) under a Connection."""
kind = "service"
def __init__(self, connectionId: str, service: str, sourceType: str,
label: str, parentKey: str, rec: Optional[Dict[str, Any]]) -> None:
super().__init__(
key=_encode("svc", connectionId, service),
label=label,
parentKey=parentKey,
connectionId=connectionId,
sourceType=sourceType,
path="/",
rec=rec,
)
self.service = service
def toDict(self, allDs, allFds) -> Dict[str, Any]:
out = _dsDict(self, allDs)
out["service"] = self.service
return out
class _BrowseNode(_DataSourceFamilyNode):
"""Folder/File node from a connector browse() result."""
def __init__(self, kind: str, connectionId: str, service: str,
sourceType: str, path: str, label: str,
parentKey: str, rec: Optional[Dict[str, Any]],
hasChildren: bool) -> None:
super().__init__(
key=_encode("ds", connectionId, sourceType, path),
label=label,
parentKey=parentKey,
connectionId=connectionId,
sourceType=sourceType,
path=path,
rec=rec,
)
self.kind = kind
self.service = service
self._hasChildren = hasChildren
def toDict(self, allDs, allFds) -> Dict[str, Any]:
out = _dsDict(self, allDs)
out["service"] = self.service
out["hasChildren"] = self._hasChildren
return out
class FolderNode(_BrowseNode):
"""Folder DataSource node from connector browse()."""
def __init__(self, **kwargs) -> None:
super().__init__(kind="folder", **kwargs)
class FileNode(_BrowseNode):
"""File DataSource node from connector browse()."""
def __init__(self, **kwargs) -> None:
super().__init__(kind="file", **kwargs)
def _dsDict(node: _DataSourceFamilyNode, allDs: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Shared serialization for DS-family nodes."""
return {
"key": node.key,
"kind": node.kind,
"parentKey": node.parentKey,
"label": node.label,
"icon": getattr(node, "authority", None) or getattr(node, "service", None) or node.kind,
"hasChildren": True,
"dataSourceId": node.rec.get("id") if node.rec else None,
"modelType": "DataSource" if node.rec else None,
"effectiveNeutralize": node.getEffectiveFlag("neutralize", allDs, [], "aggregate"),
"effectiveScope": "personal",
"effectiveRagIndexEnabled": node.getEffectiveFlag("ragIndexEnabled", allDs, [], "aggregate"),
"supportsRag": True,
"canBeAdded": node.rec is None,
"connectionId": node.connectionId,
"sourceType": node.sourceType,
"path": node.path,
"authority": getattr(node, "authority", None),
}
# ---------------------------------------------------------------------------
# FDS family
# ---------------------------------------------------------------------------
class _FdsFamilyNode(UdbNode):
"""Shared behavior for FDS-backed nodes (featureNode/fdsTable/fdsRecord).
FDS has no `scope` attribute (visibility is feature RBAC). Edit
permission requires the user to hold a feature-admin role on the
FDS's `featureInstanceId`.
"""
def __init__(self, key: str, label: str, parentKey: Optional[str],
featureInstanceId: str, tableName: str,
recordFilter: Optional[Dict[str, str]],
rec: Optional[Dict[str, Any]]) -> None:
super().__init__(key, label, parentKey)
self.featureInstanceId = featureInstanceId
self.tableName = tableName
self.recordFilter = recordFilter
self.rec = rec
def supportsFlag(self, flag: str) -> bool:
return flag in ("neutralize", "ragIndexEnabled")
def canEdit(self, context: Any, rootIf: Any) -> bool:
return isFeatureAdmin(rootIf, str(context.user.id), self.featureInstanceId)
def getEffectiveFlag(self, flag, allDs, allFds, mode="aggregate") -> Any:
if not self.supportsFlag(flag):
return None
from modules.serviceCenter.core.flagResolution import (
resolveEffectiveForFds,
)
out = resolveEffectiveForFds(self.featureInstanceId, self.tableName,
self.recordFilter, allFds, mode=mode)
key = "effective" + flag[0].upper() + flag[1:]
return out.get(key, False)
def setFlag(self, flag, value, rootIf) -> List[str]:
if not self.supportsFlag(flag):
raise ValueError(f"FDS does not support flag {flag!r}")
from modules.datamodels.datamodelFeatures import FeatureDataSource
from modules.serviceCenter.core.flagResolution import (
cascadeResetDescendantsFds,
)
if not self.rec:
raise RuntimeError(
f"Cannot setFlag on virtual FDS node {self.key}: "
"create the FeatureDataSource record first."
)
sourceId = self.rec.get("id")
resetIds: List[str] = []
if value is not None:
resetIds = cascadeResetDescendantsFds(rootIf, self.rec, flag)
modifyFields: Dict[str, Any] = {flag: value}
self._onSetFlag(modifyFields, flag, value, rootIf)
rootIf.db.recordModify(FeatureDataSource, sourceId, modifyFields)
return resetIds
def _onSetFlag(self, modifyFields: Dict[str, Any], flag: str,
value: Any, rootIf: Any) -> None:
"""Subclass hook: extend `modifyFields` or perform side-effects."""
pass
class FdsWorkspaceNode(_FdsFamilyNode):
"""Synthetic feature-wildcard FDS node (tableName='*').
Rendered as 'featureNode' in the tree; one per accessible feature
instance under its mandate group.
"""
kind = "featureNode"
def __init__(self, mandateId: str, featureCode: str, featureInstanceId: str,
label: str, icon: str, parentKey: str,
rec: Optional[Dict[str, Any]]) -> None:
super().__init__(
key=_encode("feat", mandateId, featureCode, featureInstanceId),
label=label,
parentKey=parentKey,
featureInstanceId=featureInstanceId,
tableName="*",
recordFilter=None,
rec=rec,
)
self.mandateId = mandateId
self.featureCode = featureCode
self.icon = icon
def _onSetFlag(self, modifyFields, flag, value, rootIf):
if flag == "neutralize" and value is not None:
self._clearDescendantNeutralizeFields(rootIf)
def _clearDescendantNeutralizeFields(self, rootIf):
"""Wipe `neutralizeFields` on all descendant table FDS records.
When the workspace sets an explicit neutralize, per-column
overrides on descendant tables become obsolete — the workspace
value cascades down via inheritance. Without clearing them the
table aggregate stays 'mixed' because some field children still
read True from the list while others inherit the new value.
"""
from modules.datamodels.datamodelFeatures import FeatureDataSource
allFds = rootIf.db.getRecordset(
FeatureDataSource,
recordFilter={"featureInstanceId": self.featureInstanceId},
) or []
ownId = self.rec.get("id") if self.rec else None
for fds in allFds:
fdsId = fds.get("id")
if fdsId == ownId:
continue
nf = fds.get("neutralizeFields")
if isinstance(nf, list) and len(nf) > 0:
rootIf.db.recordModify(
FeatureDataSource, fdsId, {"neutralizeFields": None},
)
def toDict(self, allDs, allFds) -> Dict[str, Any]:
return {
"key": self.key,
"kind": "featureNode",
"parentKey": self.parentKey,
"label": self.label,
"icon": self.icon,
"hasChildren": True,
"dataSourceId": self.rec.get("id") if self.rec else None,
"modelType": "FeatureDataSource" if self.rec else None,
"effectiveNeutralize": self.getEffectiveFlag("neutralize", allDs, allFds, "aggregate"),
"effectiveScope": "personal",
"effectiveRagIndexEnabled": self.getEffectiveFlag("ragIndexEnabled", allDs, allFds, "aggregate"),
"supportsRag": True,
"canBeAdded": self.rec is None,
"featureInstanceId": self.featureInstanceId,
"featureCode": self.featureCode,
"mandateId": self.mandateId,
"tableName": "*",
}
class FdsTableNode(_FdsFamilyNode):
"""Table-level FDS node (concrete tableName, no recordFilter)."""
kind = "fdsTable"
def __init__(self, featureInstanceId: str, featureCode: str, tableName: str,
objectKey: str, label: str, parentKey: str,
rec: Optional[Dict[str, Any]], hasFields: bool) -> None:
super().__init__(
key=_encode("fdstbl", featureInstanceId, tableName),
label=label,
parentKey=parentKey,
featureInstanceId=featureInstanceId,
tableName=tableName,
recordFilter=None,
rec=rec,
)
self.featureCode = featureCode
self.objectKey = objectKey
self._hasFields = hasFields
def _onSetFlag(self, modifyFields, flag, value, rootIf):
if flag == "neutralize" and value is not None:
modifyFields["neutralizeFields"] = None
def toDict(self, allDs, allFds) -> Dict[str, Any]:
neutralizeFields: List[str] = []
if self.rec and isinstance(self.rec.get("neutralizeFields"), list):
neutralizeFields = [f for f in self.rec["neutralizeFields"] if isinstance(f, str)]
return {
"key": self.key,
"kind": "fdsTable",
"parentKey": self.parentKey,
"label": self.label,
"icon": "table",
"hasChildren": self._hasFields,
"dataSourceId": self.rec.get("id") if self.rec else None,
"modelType": "FeatureDataSource" if self.rec else None,
"effectiveNeutralize": self.getEffectiveFlag("neutralize", allDs, allFds, "aggregate"),
"effectiveScope": "personal",
"effectiveRagIndexEnabled": self.getEffectiveFlag("ragIndexEnabled", allDs, allFds, "aggregate"),
"supportsRag": True,
"canBeAdded": self.rec is None,
"featureInstanceId": self.featureInstanceId,
"featureCode": self.featureCode,
"tableName": self.tableName,
"objectKey": self.objectKey,
"neutralizeFields": neutralizeFields,
}
class FdsRowNode(_FdsFamilyNode):
"""Row-level FDS node (recordFilter pins specific rows)."""
kind = "fdsRecord"
def __init__(self, featureInstanceId: str, tableName: str, recordId: str,
recordFilter: Dict[str, str], label: str, parentKey: str,
rec: Optional[Dict[str, Any]]) -> None:
super().__init__(
key=_encode("fdsrec", featureInstanceId, tableName, recordId),
label=label,
parentKey=parentKey,
featureInstanceId=featureInstanceId,
tableName=tableName,
recordFilter=recordFilter,
rec=rec,
)
def toDict(self, allDs, allFds) -> Dict[str, Any]:
return {
"key": self.key,
"kind": "fdsRecord",
"parentKey": self.parentKey,
"label": self.label,
"icon": "row",
"hasChildren": False,
"dataSourceId": self.rec.get("id") if self.rec else None,
"modelType": "FeatureDataSource" if self.rec else None,
"effectiveNeutralize": self.getEffectiveFlag("neutralize", allDs, allFds, "aggregate"),
"effectiveScope": "personal",
"effectiveRagIndexEnabled": self.getEffectiveFlag("ragIndexEnabled", allDs, allFds, "aggregate"),
"supportsRag": True,
"canBeAdded": self.rec is None,
"featureInstanceId": self.featureInstanceId,
"tableName": self.tableName,
}
class FdsFieldNode(UdbNode):
"""Per-column virtual node under an FdsTableNode.
Has no DB record of its own. The 'neutralize' state is two-source:
1. `field in tableRec.neutralizeFields` -> explicit override -> True.
2. otherwise -> field INHERITS the effective neutralize of its
table (which itself walks the FDS ancestor chain up to the
workspace wildcard).
Setting the flag toggles the field's membership in
`tableRec.neutralizeFields` only; it never writes a dedicated record.
Supports only the `neutralize` flag; scope and rag do not exist
field-level. The per-list mechanic intentionally cannot express
"explicit False"; an explicit table value covers all fields equally
via inheritance (matching the cascade-reset semantics in
`_FdsFamilyNode.setFlag`, which wipes `neutralizeFields` when the
table's neutralize is set explicitly).
"""
kind = "fdsField"
def __init__(self, featureInstanceId: str, tableName: str, fieldName: str,
parentKey: str, tableRec: Optional[Dict[str, Any]],
featureCode: str = "") -> None:
super().__init__(
key=_encode("fdsfld", featureInstanceId, tableName, fieldName),
label=fieldName,
parentKey=parentKey,
)
self.featureInstanceId = featureInstanceId
self.tableName = tableName
self.fieldName = fieldName
self.featureCode = featureCode
self.tableRec = tableRec
def supportsFlag(self, flag: str) -> bool:
return flag == "neutralize"
def canEdit(self, context: Any, rootIf: Any) -> bool:
return isFeatureAdmin(rootIf, str(context.user.id), self.featureInstanceId)
def getEffectiveFlag(self, flag, allDs, allFds, mode="aggregate") -> Any:
if flag != "neutralize":
return False
rec = self.tableRec or _findFdsByCoord(allFds, self.featureInstanceId, self.tableName, None)
fields = rec.get("neutralizeFields") if rec else None
if isinstance(fields, list) and self.fieldName in fields:
return True
# Not explicitly overridden -> inherit from the table's effective
# neutralize. Use walk mode so the inherited value is concrete
# (never 'mixed'); a single field cannot itself be ambiguous.
from modules.serviceCenter.core.flagResolution import (
resolveEffectiveForFds,
)
out = resolveEffectiveForFds(
self.featureInstanceId, self.tableName, None, allFds, mode="walk",
)
value = out.get("effectiveNeutralize", False)
return bool(value) if isinstance(value, bool) else False
def setFlag(self, flag, value, rootIf) -> List[str]:
if flag != "neutralize":
raise ValueError(f"FdsFieldNode does not support flag {flag!r}")
from modules.datamodels.datamodelFeatures import FeatureDataSource
# Resolve or auto-create the underlying table-record FDS so we
# have somewhere to persist the neutralizeFields entry.
rec = self.tableRec
if not rec:
rec = _findOrCreateTableFds(rootIf, self.featureInstanceId, self.tableName,
self.featureCode, self.fieldName)
recId = rec.get("id")
currentFields = rec.get("neutralizeFields") if isinstance(rec.get("neutralizeFields"), list) else []
fields = set(currentFields)
if value:
fields.add(self.fieldName)
else:
fields.discard(self.fieldName)
newList = sorted(fields)
rootIf.db.recordModify(FeatureDataSource, recId, {
"neutralizeFields": newList if newList else None,
})
# Update the in-memory record so subsequent reads see the change.
rec["neutralizeFields"] = newList if newList else None
self.tableRec = rec
return []
def toDict(self, allDs, allFds) -> Dict[str, Any]:
rec = self.tableRec or _findFdsByCoord(allFds, self.featureInstanceId, self.tableName, None)
return {
"key": self.key,
"kind": "fdsField",
"parentKey": self.parentKey,
"label": self.label,
"icon": "field",
"hasChildren": False,
"dataSourceId": rec.get("id") if rec else None,
"modelType": "FeatureDataSource" if rec else None,
"effectiveNeutralize": self.getEffectiveFlag("neutralize", allDs, allFds, "aggregate"),
"effectiveScope": "personal",
"effectiveRagIndexEnabled": False,
"supportsRag": False,
"canBeAdded": rec is None,
"featureInstanceId": self.featureInstanceId,
"tableName": self.tableName,
"fieldName": self.fieldName,
}
# ---------------------------------------------------------------------------
# Cross-cutting helpers
# ---------------------------------------------------------------------------
def _isConnectionOwner(rootIf: Any, userId: str, connectionId: str) -> bool:
"""Return True iff the UserConnection belongs to this user."""
try:
from modules.datamodels.datamodelUam import UserConnection
conn = rootIf.db.getRecord(UserConnection, connectionId)
if not conn:
return False
return str(conn.get("userId") or "") == userId
except Exception:
return False
def _findOrCreateDs(rootIf: Any, connectionId: str, sourceType: str,
path: str) -> Dict[str, Any]:
"""Look up a DataSource by coordinate. Create a stub if missing.
Analogous to `_findOrCreateTableFds` for FDS fields: the user
clicks toggle on a browse-discovered folder that has no DataSource
record yet. Instead of returning 403, we auto-create the record
so the flag can be persisted.
"""
from modules.datamodels.datamodelDataSource import DataSource
from modules.datamodels.datamodelUam import UserConnection
from modules.serviceCenter.core.flagResolution import normalisePath
normPath = normalisePath(path)
existing = rootIf.db.getRecordset(DataSource, recordFilter={
"connectionId": connectionId, "sourceType": sourceType,
}) or []
for rec in existing:
if normalisePath(rec.get("path")) == normPath:
return rec
conn = rootIf.db.getRecord(UserConnection, connectionId)
if not conn:
raise RuntimeError(f"UserConnection {connectionId} not found")
userId = str(conn.get("userId") or "")
mandateId = ""
for rec in existing:
mid = rec.get("mandateId")
if mid:
mandateId = str(mid)
break
pathLabel = normPath.rsplit("/", 1)[-1] or normPath
stub = DataSource(
connectionId=connectionId,
sourceType=sourceType,
path=normPath,
label=pathLabel,
displayPath=normPath,
userId=userId,
mandateId=mandateId,
)
created = rootIf.db.recordCreate(DataSource, stub.model_dump())
if isinstance(created, dict):
return created
return stub.model_dump()
def isFeatureAdmin(rootIf: Any, userId: str, featureInstanceId: str) -> bool:
"""Return True iff the user holds a `*-admin` role on this feature instance.
Convention: feature-specific admin role labels end with `-admin`
(e.g. `workspace-admin`, `automation-admin`). See `.cursor/rules/
rbac-role-separation.mdc`.
"""
try:
access = rootIf.getFeatureAccess(userId, featureInstanceId)
if not access or not getattr(access, "enabled", True):
return False
accessId = getattr(access, "id", None) or (access.get("id") if isinstance(access, dict) else None)
if not accessId:
return False
roleIds = rootIf.getRoleIdsForFeatureAccess(accessId) or []
if not roleIds:
return False
from modules.datamodels.datamodelRbac import Role
for rid in roleIds:
rec = rootIf.db.getRecord(Role, rid)
if not rec:
continue
label = str(rec.get("roleLabel") or "").lower()
if label.endswith("-admin"):
return True
return False
except Exception as exc:
logger.warning("isFeatureAdmin check failed (user=%s feature=%s): %s",
userId, featureInstanceId, exc)
return False
def _findFdsByCoord(allFds: List[Dict[str, Any]], featureInstanceId: str,
tableName: str, recordFilter: Optional[Dict[str, str]]) -> Optional[Dict[str, Any]]:
target = recordFilter or None
for fds in allFds:
if fds.get("featureInstanceId") != featureInstanceId:
continue
if (fds.get("tableName") or "") != tableName:
continue
if (fds.get("recordFilter") or None) == target:
return fds
return None
def _findOrCreateTableFds(rootIf: Any, featureInstanceId: str, tableName: str,
featureCode: str, anyFieldName: str) -> Dict[str, Any]:
"""Look up the table-level FDS record. Create a minimal stub if missing.
Required so an `fdsField` neutralize toggle can persist its state
without forcing the user to first 'add' the table via a separate UI
affordance. The stub carries the same featureInstanceId/tableName
coordinate; its `objectKey`/`label` are filled from the RBAC catalog
so list endpoints still render it correctly.
"""
from modules.datamodels.datamodelFeatures import FeatureDataSource
existing = rootIf.db.getRecordset(FeatureDataSource, recordFilter={
"featureInstanceId": featureInstanceId,
"tableName": tableName,
}) or []
for rec in existing:
if (rec.get("recordFilter") or None) is None:
return rec
# Resolve mandate + objectKey/label via catalog so the stub is well-formed.
fi = rootIf.getFeatureInstance(featureInstanceId)
mandateId = str(fi.mandateId) if fi and getattr(fi, "mandateId", None) else ""
objectKey = ""
label = tableName
try:
from modules.security.rbacCatalog import getCatalogService
catalog = getCatalogService()
for obj in catalog.getDataObjects(featureCode) or []:
meta = obj.get("meta", {}) if isinstance(obj, dict) else {}
if meta.get("table") == tableName:
objectKey = obj.get("objectKey", "")
lbl = obj.get("label")
if lbl:
try:
from modules.shared.i18nRegistry import resolveText
label = resolveText(lbl) or tableName
except Exception:
label = str(lbl) or tableName
break
except Exception:
pass
stub = FeatureDataSource(
featureInstanceId=featureInstanceId,
featureCode=featureCode,
tableName=tableName,
objectKey=objectKey,
label=label,
mandateId=mandateId,
recordFilter=None,
)
created = rootIf.db.recordCreate(FeatureDataSource, stub.model_dump())
if isinstance(created, dict):
return created
return stub.model_dump()
def _aggregateFromChildren(node: UdbNode, flag: str,
allDs: List[Dict[str, Any]],
allFds: List[Dict[str, Any]],
mode: str) -> Any:
"""Aggregate `flag` across a node's logical children.
Returns 'mixed' iff at least two children disagree (in walk-mode),
otherwise the agreed value or the default for the flag. Used by
synthetic containers that have no DB record of their own.
"""
children = node.getLogicalChildren(allDs, allFds, None, None)
if not children:
return False
seen = set()
last: Any = None
for child in children:
if not child.supportsFlag(flag):
continue
val = child.getEffectiveFlag(flag, allDs, allFds, "aggregate")
if val == "mixed":
return "mixed"
norm = int(val) if isinstance(val, bool) else val
seen.add(norm)
last = val
if len(seen) > 1:
return "mixed"
if not seen:
return False
return last
# ---------------------------------------------------------------------------
# Lookup by tree key -> UdbNode
# ---------------------------------------------------------------------------
def buildNodeForKey(key: str, context: Any, rootIf: Any) -> Optional[UdbNode]:
"""Materialize the `UdbNode` instance for a given tree key.
Used by the generic /flag/ endpoint: the caller passes the key it sees
in the tree, and we resolve it back to the polymorphic node so we can
call canEdit / setFlag without the route knowing the node type.
Returns None when the key is unknown (caller should 404).
"""
from modules.interfaces.interfaceDbApp import getRootInterface
if rootIf is None:
rootIf = getRootInterface()
from modules.datamodels.datamodelDataSource import DataSource
from modules.datamodels.datamodelFeatures import FeatureDataSource
kind, parts = _decode(key)
if kind == "conn" and len(parts) == 1:
connId = parts[0]
rec = _findDsByCoord(rootIf, connId, None, "/")
# Authority is whatever sourceType the connection-root record has;
# for setFlag we only need rec. ConnectionNode's label is not used.
authority = rec.get("sourceType") if rec else ""
return ConnectionNode(connId, authority, label="", parentKey="", rec=rec)
if kind == "svc" and len(parts) == 2:
connId, service = parts
sourceType = _SERVICE_TO_SOURCE_TYPE.get(service, service)
rec = _findDsByCoord(rootIf, connId, sourceType, "/")
return ServiceNode(connId, service, sourceType, label="", parentKey="", rec=rec)
if kind == "ds" and len(parts) >= 3:
connId = parts[0]
sourceType = parts[1]
path = _KEY_SEP.join(parts[2:])
rec = _findDsByCoord(rootIf, connId, sourceType, path)
# We do not know if it is folder or file at this point; use FolderNode.
return FolderNode(
connectionId=connId, service=_reverseService(sourceType),
sourceType=sourceType, path=path, label="", parentKey="",
rec=rec, hasChildren=True,
)
if kind == "feat" and len(parts) == 3:
mandateId, featureCode, featureInstanceId = parts
rec = _findFdsByCoord(_loadAllFds(rootIf, featureInstanceId), featureInstanceId, "*", None)
return FdsWorkspaceNode(mandateId, featureCode, featureInstanceId,
label="", icon="", parentKey="", rec=rec)
if kind == "fdstbl" and len(parts) == 2:
featureInstanceId, tableName = parts
rec = _findFdsByCoord(_loadAllFds(rootIf, featureInstanceId), featureInstanceId, tableName, None)
featureCode = ""
if rec:
featureCode = str(rec.get("featureCode") or "")
else:
fi = rootIf.getFeatureInstance(featureInstanceId)
featureCode = str(fi.featureCode) if fi else ""
return FdsTableNode(featureInstanceId, featureCode, tableName,
objectKey="", label="", parentKey="",
rec=rec, hasFields=True)
if kind == "fdsfld" and len(parts) >= 3:
featureInstanceId = parts[0]
tableName = parts[1]
fieldName = _KEY_SEP.join(parts[2:])
allFds = _loadAllFds(rootIf, featureInstanceId)
tableRec = _findFdsByCoord(allFds, featureInstanceId, tableName, None)
featureCode = str(tableRec.get("featureCode") or "") if tableRec else ""
if not featureCode:
fi = rootIf.getFeatureInstance(featureInstanceId)
featureCode = str(fi.featureCode) if fi else ""
return FdsFieldNode(featureInstanceId, tableName, fieldName,
parentKey="", tableRec=tableRec, featureCode=featureCode)
return None
def _findDsByCoord(rootIf: Any, connectionId: str, sourceType: Optional[str],
path: str) -> Optional[Dict[str, Any]]:
from modules.datamodels.datamodelDataSource import DataSource
from modules.serviceCenter.core.flagResolution import normalisePath
rf = {"connectionId": connectionId}
if sourceType is not None:
rf["sourceType"] = sourceType
records = rootIf.db.getRecordset(DataSource, recordFilter=rf) or []
norm = normalisePath(path)
if sourceType is None:
# connection-root: any record with path='/' on this connection
for r in records:
if normalisePath(r.get("path")) == "/":
return r
return None
for r in records:
if normalisePath(r.get("path")) == norm:
return r
return None
def _loadAllFds(rootIf: Any, featureInstanceId: str) -> List[Dict[str, Any]]:
from modules.datamodels.datamodelFeatures import FeatureDataSource
return rootIf.db.getRecordset(
FeatureDataSource, recordFilter={"featureInstanceId": featureInstanceId}
) or []
# Imported from _buildTree for buildNodeForKey resolution; kept here as a
# small private mirror to avoid import cycles.
_SERVICE_TO_SOURCE_TYPE: Dict[str, str] = {
"sharepoint": "sharepointFolder",
"onedrive": "onedriveFolder",
"outlook": "outlookFolder",
"drive": "googleDriveFolder",
"gmail": "gmailFolder",
"files": "ftpFolder",
"clickup": "clickup",
"kdrive": "kdriveFolder",
"mail": "mailFolder",
"calendar": "calendarFolder",
"contact": "contactFolder",
}
def _reverseService(sourceType: str) -> str:
for svc, st in _SERVICE_TO_SOURCE_TYPE.items():
if st == sourceType:
return svc
return sourceType