Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions src/modules/connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,24 @@ export function createConnectorsModule(
},

async getConnection(
integrationType: ConnectorIntegrationType
arg: ConnectorIntegrationType | { connectorId: string }
): Promise<ConnectorConnectionResponse> {
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<ConnectorAccessTokenResponse>(
`/apps/${appId}/external-auth/tokens/${integrationType}`
);
const response = await axios.get<ConnectorAccessTokenResponse>(url);

const data = response as unknown as ConnectorAccessTokenResponse;
return {
Expand Down
29 changes: 29 additions & 0 deletions src/modules/connectors.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,35 @@ export interface ConnectorsModule {
integrationType: ConnectorIntegrationType,
): Promise<ConnectorConnectionResponse>;

/**
* 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<ConnectorConnectionResponse>;

/**
* Retrieves an OAuth access token for an end user's connection to a specific connector.
*
Expand Down
68 changes: 68 additions & 0 deletions tests/unit/connectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof createClient>;
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";
Expand Down
Loading