Skip to content
10 changes: 9 additions & 1 deletion packages/appkit/src/context/execution-context.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { ConfigurationError } from "../errors";
import { ServiceContext } from "./service-context";
import {
type ExecutionContext,
Expand Down Expand Up @@ -64,7 +65,14 @@ export function getWorkspaceClient() {
* Get the warehouse ID promise.
*/
export function getWarehouseId(): Promise<string> {
return getExecutionContext().warehouseId;
const ctx = getExecutionContext();
if (!ctx.warehouseId) {
throw ConfigurationError.resourceNotFound(
"Warehouse ID",
"No plugin requires a SQL Warehouse. Add a sql_warehouse resource to your plugin manifest, or set DATABRICKS_WAREHOUSE_ID",
);
}
return ctx.warehouseId;
}

/**
Expand Down
14 changes: 10 additions & 4 deletions packages/appkit/src/context/service-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ export interface ServiceContextState {
client: WorkspaceClient;
/** The service principal's user ID */
serviceUserId: string;
/** Promise that resolves to the warehouse ID */
warehouseId: Promise<string>;
/** Promise that resolves to the warehouse ID (only present when a plugin requires `SQL_WAREHOUSE` resource) */
warehouseId?: Promise<string>;
/** Promise that resolves to the workspace ID */
workspaceId: Promise<string>;
}
Expand Down Expand Up @@ -58,10 +58,12 @@ export class ServiceContext {
* Initialize the service context. Should be called once at app startup.
* Safe to call multiple times - will return the same instance.
*
* @param options - Which shared resources to resolve (derived from plugin manifests).
* @param client - Optional pre-configured WorkspaceClient to use instead
* of creating one from environment credentials.
*/
static async initialize(
options?: { warehouseId?: boolean },
client?: WorkspaceClient,
): Promise<ServiceContextState> {
if (ServiceContext.instance) {
Expand All @@ -72,7 +74,7 @@ export class ServiceContext {
return ServiceContext.initPromise;
}

ServiceContext.initPromise = ServiceContext.createContext(client);
ServiceContext.initPromise = ServiceContext.createContext(options, client);
ServiceContext.instance = await ServiceContext.initPromise;
return ServiceContext.instance;
}
Expand Down Expand Up @@ -153,11 +155,15 @@ export class ServiceContext {
}

private static async createContext(
options?: { warehouseId?: boolean },
client?: WorkspaceClient,
): Promise<ServiceContextState> {
const wsClient = client ?? new WorkspaceClient({}, getClientOptions());

const warehouseId = ServiceContext.getWarehouseId(wsClient);
const warehouseId = options?.warehouseId
? ServiceContext.getWarehouseId(wsClient)
: undefined;

const workspaceId = ServiceContext.getWorkspaceId(wsClient);
const currentUser = await wsClient.currentUser.me();

Expand Down
4 changes: 2 additions & 2 deletions packages/appkit/src/context/user-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ export interface UserContext {
userId: string;
/** The user's name (from request headers) */
userName?: string;
/** Promise that resolves to the warehouse ID (inherited from service context) */
warehouseId: Promise<string>;
/** Promise that resolves to the warehouse ID (inherited from service context, only present when a plugin requires `SQL_WAREHOUSE` resource) */
warehouseId?: Promise<string>;
/** Promise that resolves to the workspace ID (inherited from service context) */
workspaceId: Promise<string>;
/** Flag indicating this is a user context */
Expand Down
18 changes: 13 additions & 5 deletions packages/appkit/src/core/appkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
} from "shared";
import { CacheManager } from "../cache";
import { ServiceContext } from "../context";
import { ResourceRegistry } from "../registry";
import { ResourceRegistry, ResourceType } from "../registry";
import type { TelemetryConfig } from "../telemetry";
import { TelemetryManager } from "../telemetry";

Expand Down Expand Up @@ -148,14 +148,22 @@ export class AppKit<TPlugins extends InputPluginMap> {
TelemetryManager.initialize(config?.telemetry);
await CacheManager.getInstance(config?.cache);

// Initialize ServiceContext for Databricks client management
// This provides the service principal client and shared resources
await ServiceContext.initialize(config?.client);

const rawPlugins = config.plugins as T;

// Collect manifest resources via registry
const registry = new ResourceRegistry();
registry.collectResources(rawPlugins);

// Derive ServiceContext needs from what manifests declared
const needsWarehouse = registry
.getRequired()
.some((r) => r.type === ResourceType.SQL_WAREHOUSE);
await ServiceContext.initialize(
{ warehouseId: needsWarehouse },
config?.client,
);

// Validate env vars
registry.enforceValidation();

const preparedPlugins = AppKit.preparePlugins(rawPlugins);
Expand Down
56 changes: 56 additions & 0 deletions packages/appkit/src/core/tests/databricks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,62 @@ describe("AppKit", () => {
});
});

describe("createApp derives ServiceContext options from registry", () => {
test("should call ServiceContext.initialize with warehouseId: false when no plugin requires sql_warehouse", async () => {
const contextModule = await import("../../context/service-context");
const initSpy = vi.spyOn(contextModule.ServiceContext, "initialize");
const pluginData = [
{ plugin: CoreTestPlugin, config: {}, name: "coreTest" },
];
await createApp({ plugins: pluginData });
expect(initSpy).toHaveBeenCalledWith({ warehouseId: false }, undefined);
initSpy.mockRestore();
});

test("should call ServiceContext.initialize with warehouseId: true when a plugin requires sql_warehouse", async () => {
const PluginWithRequiredResource = class extends CoreTestPlugin {
static manifest: PluginManifest = {
name: "withResource",
displayName: "With Resource",
description: "Plugin with required warehouse",
resources: {
required: [
{
type: ResourceType.SQL_WAREHOUSE,
alias: "wh",
resourceKey: "warehouse",
description: "Warehouse",
permission: "CAN_USE",
fields: { id: { env: "DATABRICKS_WAREHOUSE_ID" } },
},
],
optional: [],
},
};
};
const prevWh = process.env.DATABRICKS_WAREHOUSE_ID;
process.env.DATABRICKS_WAREHOUSE_ID = "wh-123";
try {
const contextModule = await import("../../context/service-context");
const initSpy = vi.spyOn(contextModule.ServiceContext, "initialize");
await createApp({
plugins: [
{
plugin: PluginWithRequiredResource,
config: {},
name: "withResource",
},
],
});
expect(initSpy).toHaveBeenCalledWith({ warehouseId: true }, undefined);
initSpy.mockRestore();
} finally {
if (prevWh !== undefined) process.env.DATABRICKS_WAREHOUSE_ID = prevWh;
else delete process.env.DATABRICKS_WAREHOUSE_ID;
}
});
});

describe("SDK context binding", () => {
test("should bind SDK methods to plugin instance", async () => {
class ContextTestPlugin implements BasePlugin {
Expand Down