Files
nocobase-mcp/nocobase_crm_mcp/crm_core.py
T

75 lines
3.0 KiB
Python

from __future__ import annotations
from .crm_mixin import CrmMixin
from .crm_shared import (
PROJECTION_FALLBACK_WARNING,
Any,
CrmConfig,
LabelSpec,
NbClient,
NbCommandError,
_fields_with_projections,
_unique_strings,
)
class CrmCoreMixin(CrmMixin):
def __init__(self, config: CrmConfig, client: NbClient) -> None:
self.config = config
self.client = client
def _customer_spec(self) -> LabelSpec:
return LabelSpec(self.config.customer.id_field, tuple(self.config.customer.name_fields or ()))
def _contact_spec(self) -> LabelSpec:
return LabelSpec(self.config.contact.id_field, tuple(self.config.contact.name_fields or ()))
def _user_spec(self) -> LabelSpec:
return LabelSpec(self.config.user.id_field, tuple(self.config.user.label_fields or ()))
def _project_spec(self) -> LabelSpec:
return LabelSpec(self.config.project.id_field, tuple(self.config.project.name_fields or ()))
def _team_fields(self) -> list[str]:
# Only the related user ids are needed: _team_users re-queries the users
# collection for their details, and _compact_team just counts members.
team = self.config.team
id_only = LabelSpec(self.config.user.id_field, ())
return _fields_with_projections(
[team.id_field, team.name_field],
[
*self._projection(team.manager_field, id_only, None),
*self._projection(team.member_field, id_only, None),
],
)
def _projection(self, relation: str | None, spec: LabelSpec, configured_fields: list[str] | None) -> list[str]:
"""Build dotted field paths that fetch only a relation's id and name.
Without this NocoBase expands every related row in full -- junction table
rows, audit user objects, long free text -- none of which is rendered.
The configured-fields whitelist still applies, just to the relation root
rather than to each dotted path.
"""
if not relation:
return []
if configured_fields and relation not in configured_fields:
return []
return [f"{relation}.{name}" for name in spec.projection_fields()]
def _list_with_projection_fallback(self, resource: str, *, fields: list[str], **kwargs: Any) -> dict[str, Any]:
"""Run a projected list query, degrading to relation roots if it is rejected.
A misconfigured ``nameFields`` entry would otherwise turn every read into
a hard failure; falling back keeps the tool usable and says why.
"""
try:
return self._list_records_window(resource, fields=fields, **kwargs)
except NbCommandError:
flat = _unique_strings([path.split(".", 1)[0] for path in fields])
if flat == _unique_strings(fields):
raise
result = self._list_records_window(resource, fields=flat, **kwargs)
result.setdefault("data_quality_warnings", []).append(PROJECTION_FALLBACK_WARNING)
return result