Skip to content
Merged
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
33 changes: 22 additions & 11 deletions packages/appkit/src/connectors/lakebase/endpoint-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,26 @@ async function compareHosts(
reason,
);
try {
const lookup =
"postgres" in client && client.postgres
? // AppKit's workspace client: the typed Lakebase Postgres client.
client.postgres.getEndpoint(
{ name: endpoint },
{ signal: controller.signal },
)
: "apiClient" in client
? client.apiClient.request(
{
path: `/api/2.0/postgres/${endpoint}`,
method: "GET",
headers: new Headers({ Accept: "application/json" }),
raw: false,
},
contextFromAbortSignal(controller.signal),
)
: Promise.resolve(undefined);
const response = await Promise.race([
client.apiClient.request(
{
path: `/api/2.0/postgres/${endpoint}`,
method: "GET",
headers: new Headers({ Accept: "application/json" }),
raw: false,
},
contextFromAbortSignal(controller.signal),
),
lookup,
new Promise<undefined>((resolve) => {
timer = setTimeout(() => {
controller.abort();
Expand All @@ -97,8 +107,9 @@ async function compareHosts(
if (
typeof error === "object" &&
error !== null &&
"statusCode" in error &&
error.statusCode === 404
// The typed client's sdk-core ApiError reports `httpStatusCode`.
(("statusCode" in error && error.statusCode === 404) ||
("httpStatusCode" in error && error.httpStatusCode === 404))
) {
logger.warn(
"LAKEBASE_ENDPOINT %s was not found. Check the project, branch, and endpoint names; `databricks postgres list-endpoints projects/{project}/branches/{branch}` lists them with their hosts.",
Expand Down
2 changes: 1 addition & 1 deletion packages/appkit/src/connectors/lakebase/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function initializeLakebasePool(
const client = ServiceContext.isInitialized()
? ServiceContext.get().client
: createWorkspaceClient({ clientOptions: getClientOptions() });
resolved.workspaceClient = client.toLegacyWorkspaceClient();
resolved.workspaceClient = client;
}
const [user] = await Promise.all([
getUsernameWithApiLookup(resolved),
Expand Down
105 changes: 105 additions & 0 deletions packages/appkit/src/connectors/lakebase/tests/endpoint-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,3 +316,108 @@ describe("assertEndpointHostMatches", () => {
]);
});
});

// A request-only client (no typed postgres, no legacy apiClient) can't read the
// endpoint, so the check is skipped with a warning instead of blocking startup.
test("skips verification for a request-only client", async () => {
const { endpoint, host } = names();
const request = vi.fn();
await assertEndpointHostMatches({
endpoint,
host,
workspaceClient: { request } as unknown as Client,
});
expect(request).not.toHaveBeenCalled();
expect(warnings()).toContain("Could not verify");
});

// AppKit's workspace client carries the typed Lakebase Postgres client, which
// wins over its raw `request()` seam.
describe("assertEndpointHostMatches with the typed postgres client", () => {
function postgresClient(
impl: (
req: { name: string },
options: { signal: AbortSignal },
) => Promise<unknown>,
) {
const getEndpoint = vi.fn(impl);
const request = vi.fn();
return {
getEndpoint,
request,
client: { postgres: { getEndpoint }, request } as unknown as Client,
};
}

test("rejects a mismatch read through postgres.getEndpoint", async () => {
const { endpoint, host } = names();
const { getEndpoint, request, client } = postgresClient(async () => ({
status: {
hosts: {
host: "ep-expected.database.example.com",
readOnlyHost: "ep-ro.database.example.com",
},
},
}));
vi.spyOn(console, "error").mockImplementation(() => undefined);

await expect(
assertEndpointHostMatches({ endpoint, host, workspaceClient: client }),
).rejects.toThrow("ep-expected.database.example.com");
expect(getEndpoint).toHaveBeenCalledWith(
{ name: endpoint },
{ signal: expect.any(AbortSignal) },
);
expect(request).not.toHaveBeenCalled();
});

test("accepts the camelCase read-only host", async () => {
const { endpoint, host } = names();
const { client } = postgresClient(async () => ({
status: { hosts: { host: "ep-rw.example.com", readOnlyHost: host } },
}));

await assertEndpointHostMatches({
endpoint,
host,
workspaceClient: client,
});
expect(warn).not.toHaveBeenCalled();
});

test("warns when the typed client reports a 404 via httpStatusCode", async () => {
const { endpoint, host } = names();
const { client } = postgresClient(async () => {
throw Object.assign(new Error("not found"), { httpStatusCode: 404 });
});

await assertEndpointHostMatches({
endpoint,
host,
workspaceClient: client,
});
expect(warnings()).toContain("was not found");
});

test("aborts the typed lookup when the deadline expires", async () => {
vi.useFakeTimers();
const { endpoint, host } = names();
let seen: AbortSignal | undefined;
const { client } = postgresClient(
(_req, options) =>
new Promise(() => {
seen = options.signal;
}),
);

const check = assertEndpointHostMatches({
endpoint,
host,
workspaceClient: client,
});
await vi.advanceTimersByTimeAsync(3_000);
await check;
expect(seen?.aborted).toBe(true);
expect(warnings()).toContain("timed out");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ const mocks = vi.hoisted(() => {
request,
client,
createPool: vi.fn(),
createWorkspaceClient: vi.fn(() => ({
toLegacyWorkspaceClient: () => client,
})),
createWorkspaceClient: vi.fn(() => client),
};
});

Expand Down Expand Up @@ -90,7 +88,7 @@ describe("AppKit Lakebase connector initialization", () => {
};
vi.mocked(ServiceContext.isInitialized).mockReturnValue(true);
vi.spyOn(ServiceContext, "get").mockReturnValue({
client: { toLegacyWorkspaceClient: () => legacy },
client: legacy,
} as unknown as ReturnType<typeof ServiceContext.get>);
await initializeLakebasePool();
expect(poolConfig()).toMatchObject({
Expand Down Expand Up @@ -125,7 +123,7 @@ describe("AppKit Lakebase connector initialization", () => {
const requestClient = { currentUser: { me: requestLookup } };
await runInUserContext(
{
client: { toLegacyWorkspaceClient: () => requestClient },
client: requestClient,
userId: "request-user-id",
userEmail: "request-user@example.test",
workspaceId: Promise.resolve("workspace"),
Expand Down
6 changes: 3 additions & 3 deletions packages/appkit/src/plugins/lakebase/lakebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class LakebasePlugin extends Plugin implements ToolProvider {
this.config.pool?.workspaceClient ??
createWorkspaceClient({
clientOptions: getClientOptions(),
}).toLegacyWorkspaceClient(),
}),
};
const [user] = await Promise.all([
getUsernameWithApiLookup(poolConfig),
Expand All @@ -109,7 +109,7 @@ export class LakebasePlugin extends Plugin implements ToolProvider {
const pool = oboManager.getPool(
userKey,
{
workspaceClient: ctx.client.toLegacyWorkspaceClient(),
workspaceClient: ctx.client,
user: userKey,
},
ctx.tokenFingerprint,
Expand Down Expand Up @@ -305,7 +305,7 @@ export class LakebasePlugin extends Plugin implements ToolProvider {
const user = ctx.principal.userEmail ?? ctx.principal.userId;
return {
...this.config.pool,
workspaceClient: ctx.client.toLegacyWorkspaceClient(),
workspaceClient: ctx.client,
user,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ describe("LakebasePlugin - OBO via RoutingPool", () => {
await plugin.setup();

const userCtx = {
client: { toLegacyWorkspaceClient: () => ({}) } as any,
client: {} as any,
userId: "user-123",
userEmail: "alice@example.com",
workspaceId: Promise.resolve("ws-1"),
Expand Down Expand Up @@ -402,7 +402,7 @@ describe("LakebasePlugin - OBO via RoutingPool", () => {
await plugin.setup();

const userCtx = {
client: { toLegacyWorkspaceClient: () => ({}) } as any,
client: {} as any,
userId: "user-123",
userEmail: "alice@example.com",
workspaceId: Promise.resolve("ws-1"),
Expand Down Expand Up @@ -431,7 +431,7 @@ describe("LakebasePlugin - OBO via RoutingPool", () => {
await plugin.setup();

const userCtx = {
client: { toLegacyWorkspaceClient: () => ({}) } as any,
client: {} as any,
userId: "user-123",
workspaceId: Promise.resolve("ws-1"),
isUserContext: true as const,
Expand Down
3 changes: 2 additions & 1 deletion packages/lakebase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"release:sbom": "pnpm exec cdxgen -t js --no-recurse --required-only -o tmp/sbom.cdx.json ."
},
"dependencies": {
"@databricks/sdk-experimental": "0.17.0",
"@databricks/sdk-auth": "0.51.0",
"@databricks/sdk-core": "0.51.0",
"pg": "8.18.0",
"@opentelemetry/api": "1.9.0"
},
Expand Down
Loading
Loading