From b1d41379351aea03366e0d52ec3d46075bc7224d Mon Sep 17 00:00:00 2001 From: Stephan Schellworth Date: Thu, 4 Jun 2026 09:41:43 +0200 Subject: [PATCH] fix(connectors): resolve browse tokens by connection UUID ConnectorResolver now loads tokens with UserConnection.id so connection:authority:username references work in browse. ClickUp routes resolve references via getUserConnectionById; graph node defs use list picker only. Co-authored-by: Cursor --- modules/connectors/connectorResolver.py | 20 +++++++++++++++++-- .../nodeDefinitions/clickup.py | 12 +++++------ modules/routes/routeClickup.py | 5 +++++ .../unit/connectors/test_connectorResolver.py | 17 ++++++++++++++++ 4 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 tests/unit/connectors/test_connectorResolver.py diff --git a/modules/connectors/connectorResolver.py b/modules/connectors/connectorResolver.py index a85002a4..a8b9fd23 100644 --- a/modules/connectors/connectorResolver.py +++ b/modules/connectors/connectorResolver.py @@ -15,6 +15,15 @@ from modules.connectors.connectorProviderBase import ProviderConnector, ServiceA logger = logging.getLogger(__name__) +def _connection_uuid(connection: Any) -> str: + """Resolve UserConnection primary key (tokens are stored by UUID, not reference string).""" + if connection is None: + return "" + if isinstance(connection, dict): + return str(connection.get("id") or "").strip() + return str(getattr(connection, "id", None) or "").strip() + + class ConnectorResolver: """Resolves connectionId → ProviderConnector (with fresh token) → ServiceAdapter.""" @@ -79,9 +88,16 @@ class ConnectorResolver: if not providerClass: raise ValueError(f"No ProviderConnector registered for authority: {authorityStr}") - token = self._security.getFreshToken(connectionId) + resolved_id = _connection_uuid(connection) + if not resolved_id: + raise ValueError(f"Connection {connectionId} has no id") + + token = self._security.getFreshToken(resolved_id) if not token or not token.tokenAccess: - raise ValueError(f"No valid token for connection {connectionId}") + raise ValueError( + f"No valid token for connection {resolved_id}" + + (f" (ref: {connectionId})" if connectionId != resolved_id else "") + ) return providerClass(connection, token.tokenAccess) diff --git a/modules/features/graphicalEditor/nodeDefinitions/clickup.py b/modules/features/graphicalEditor/nodeDefinitions/clickup.py index c1981097..77710a64 100644 --- a/modules/features/graphicalEditor/nodeDefinitions/clickup.py +++ b/modules/features/graphicalEditor/nodeDefinitions/clickup.py @@ -78,8 +78,8 @@ CLICKUP_NODES = [ {"name": "page", "type": "int", "required": False, "frontendType": "number", "description": t("Seite"), "default": 0}, {"name": "listId", "type": "str", "required": False, "frontendType": "clickupList", - "frontendOptions": {"dependsOn": "connectionReference"}, - "description": t("In dieser Liste suchen")}, + "frontendOptions": {"dependsOn": "connectionReference", "patchTeamId": True}, + "description": t("Liste (optional — schränkt die Suche ein)")}, {"name": "includeClosed", "type": "bool", "required": False, "frontendType": "checkbox", "description": t("Erledigte einbeziehen"), "default": False}, {"name": "fullTaskData", "type": "bool", "required": False, "frontendType": "checkbox", @@ -106,7 +106,7 @@ CLICKUP_NODES = [ "description": t("ClickUp-Verbindung")}, {"name": "pathQuery", "type": "str", "required": True, "frontendType": "clickupList", "frontendOptions": {"dependsOn": "connectionReference"}, - "description": t("Pfad zur Liste")}, + "description": t("Liste")}, {"name": "page", "type": "int", "required": False, "frontendType": "number", "description": t("Seite"), "default": 0}, {"name": "includeClosed", "type": "bool", "required": False, "frontendType": "checkbox", @@ -151,11 +151,9 @@ CLICKUP_NODES = [ {"name": "connectionReference", "type": "str", "required": True, "frontendType": "userConnection", "frontendOptions": {"authority": "clickup"}, "description": t("ClickUp-Verbindung")}, - {"name": "pathQuery", "type": "str", "required": False, "frontendType": "clickupList", + {"name": "pathQuery", "type": "str", "required": True, "frontendType": "clickupList", "frontendOptions": {"dependsOn": "connectionReference"}, - "description": t("Pfad zur Liste")}, - {"name": "listId", "type": "str", "required": False, "frontendType": "text", - "description": t("Listen-ID")}, + "description": t("Liste")}, {"name": "name", "type": "str", "required": True, "frontendType": "text", "description": t("Name")}, {"name": "description", "type": "str", "required": False, "frontendType": "textarea", diff --git a/modules/routes/routeClickup.py b/modules/routes/routeClickup.py index c3f4b976..7a869a9f 100644 --- a/modules/routes/routeClickup.py +++ b/modules/routes/routeClickup.py @@ -29,7 +29,12 @@ router = APIRouter( def _getUserConnection(interface, connection_id: str, user_id: str) -> Optional[UserConnection]: + """Resolve by UUID or connection:authority:username (same as browse / workflow).""" try: + if hasattr(interface, "getUserConnectionById"): + conn = interface.getUserConnectionById(connection_id) + if conn is not None: + return conn connections = interface.getUserConnections(user_id) for conn in connections: if conn.id == connection_id: diff --git a/tests/unit/connectors/test_connectorResolver.py b/tests/unit/connectors/test_connectorResolver.py new file mode 100644 index 00000000..0ef82e81 --- /dev/null +++ b/tests/unit/connectors/test_connectorResolver.py @@ -0,0 +1,17 @@ +# Copyright (c) 2025 Patrick Motsch +from types import SimpleNamespace + +from modules.connectors.connectorResolver import _connection_uuid + + +def test_connection_uuid_from_model(): + conn = SimpleNamespace(id="uuid-123") + assert _connection_uuid(conn) == "uuid-123" + + +def test_connection_uuid_from_dict(): + assert _connection_uuid({"id": "uuid-456"}) == "uuid-456" + + +def test_connection_uuid_from_reference_string_returns_empty(): + assert _connection_uuid("connection:clickup:Stephan Schellworth") == ""