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