From 4ff862ba600e5246f0b86d732e81671d78c281d0 Mon Sep 17 00:00:00 2001 From: johnny Date: Sun, 19 Apr 2026 17:21:44 +0300 Subject: [PATCH] feat(connectors): add getConnection({ connectorId }) overload for BYO_SHARED MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend functions can now retrieve the OAuth access token + connection config for a workspace-registered connector (backed by an OrganizationConnector that the app builder consented to) via a new overload on getConnection: await base44.asServiceRole.connectors.getConnection({ connectorId: "..." }); Existing string-arg callers are unchanged — they continue to resolve to the platform-SHARED path. The overload dispatches on argument shape (string vs object) and hits a new server route GET /apps/{app_id}/external-auth/tokens/by-connector/{connector_id} for the workspace-connector lookup. --- src/modules/connectors.ts | 19 ++++++--- src/modules/connectors.types.ts | 29 ++++++++++++++ tests/unit/connectors.test.ts | 68 +++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/src/modules/connectors.ts b/src/modules/connectors.ts index 7755907..37f074d 100644 --- a/src/modules/connectors.ts +++ b/src/modules/connectors.ts @@ -42,15 +42,24 @@ export function createConnectorsModule( }, async getConnection( - integrationType: ConnectorIntegrationType + arg: ConnectorIntegrationType | { connectorId: string } ): Promise { - if (!integrationType || typeof integrationType !== "string") { + let url: string; + if (typeof arg === "string") { + if (!arg) { + throw new Error("Integration type is required and must be a string"); + } + url = `/apps/${appId}/external-auth/tokens/${arg}`; + } else if (arg && typeof arg === "object" && typeof arg.connectorId === "string") { + if (!arg.connectorId) { + throw new Error("Connector ID is required and must be a string"); + } + url = `/apps/${appId}/external-auth/tokens/by-connector/${arg.connectorId}`; + } else { throw new Error("Integration type is required and must be a string"); } - const response = await axios.get( - `/apps/${appId}/external-auth/tokens/${integrationType}` - ); + const response = await axios.get(url); const data = response as unknown as ConnectorAccessTokenResponse; return { diff --git a/src/modules/connectors.types.ts b/src/modules/connectors.types.ts index 9d79edc..ef82950 100644 --- a/src/modules/connectors.types.ts +++ b/src/modules/connectors.types.ts @@ -243,6 +243,35 @@ export interface ConnectorsModule { integrationType: ConnectorIntegrationType, ): Promise; + /** + * Retrieves the OAuth access token and connection configuration for a **workspace-registered** connector + * (a connector backed by an OAuth app registered in the workspace, consented to once by the app builder). + * + * Use this overload when the app's backend function needs to use a connector identified by its + * workspace-connector ID rather than a platform integration type. The token returned represents + * the app builder's consent against the workspace's OAuth app and is shared across all app users + * of the app — identical semantics to the platform-shared `getConnection(integrationType)` form, + * differing only in which OAuth app was used to produce the token. + * + * @param opts - An object with `connectorId` — the ID of the workspace connector (the `OrganizationConnector` database ID) as surfaced in the builder chat context. + * @returns Promise resolving to a {@link ConnectorConnectionResponse} with `accessToken` and `connectionConfig`. + * + * @example + * ```typescript + * // Get the connection for a workspace-registered connector + * const { accessToken, connectionConfig } = await base44.asServiceRole.connectors.getConnection({ + * connectorId: 'abc123def', + * }); + * + * const response = await fetch(`https://${connectionConfig?.subdomain}.snowflakecomputing.com/api/v2/statements`, { + * headers: { Authorization: `Bearer ${accessToken}` }, + * }); + * ``` + */ + getConnection( + opts: { connectorId: string }, + ): Promise; + /** * Retrieves an OAuth access token for an end user's connection to a specific connector. * diff --git a/tests/unit/connectors.test.ts b/tests/unit/connectors.test.ts index 84f2edc..c0ccfe8 100644 --- a/tests/unit/connectors.test.ts +++ b/tests/unit/connectors.test.ts @@ -100,6 +100,74 @@ describe("Connectors module – getConnection", () => { }); }); +describe("Connectors module – getConnection({ connectorId })", () => { + const appId = "test-app-id"; + const serverUrl = "https://base44.app"; + const serviceToken = "service-token-123"; + let base44: ReturnType; + let scope: nock.Scope; + + beforeEach(() => { + base44 = createClient({ + serverUrl, + appId, + serviceToken, + }); + scope = nock(serverUrl); + }); + + afterEach(() => { + nock.cleanAll(); + }); + + test("extracts accessToken and connectionConfig from by-connector endpoint", async () => { + const apiResponse = { + access_token: "builder-oauth-token-xyz789", + integration_type: "snowflake", + connection_config: { subdomain: "xy12345.us-east-1" }, + }; + + scope + .get(`/api/apps/${appId}/external-auth/tokens/by-connector/connector-abc`) + .reply(200, apiResponse); + + const connection = await base44.asServiceRole.connectors.getConnection({ + connectorId: "connector-abc", + }); + + expect(connection.accessToken).toBe("builder-oauth-token-xyz789"); + expect(connection.connectionConfig).toEqual({ + subdomain: "xy12345.us-east-1", + }); + expect(scope.isDone()).toBe(true); + }); + + test("returns connectionConfig as null when API omits connection_config", async () => { + const apiResponse = { + access_token: "token-only", + integration_type: "databricks", + }; + + scope + .get(`/api/apps/${appId}/external-auth/tokens/by-connector/conn-2`) + .reply(200, apiResponse); + + const connection = await base44.asServiceRole.connectors.getConnection({ + connectorId: "conn-2", + }); + + expect(connection.accessToken).toBe("token-only"); + expect(connection.connectionConfig).toBeNull(); + expect(scope.isDone()).toBe(true); + }); + + test("throws when connectorId is empty string", async () => { + await expect( + base44.asServiceRole.connectors.getConnection({ connectorId: "" }) + ).rejects.toThrow("Connector ID is required and must be a string"); + }); +}); + describe("Connectors module – getCurrentAppUserConnection", () => { const appId = "test-app-id"; const serverUrl = "https://base44.app";