From d39990544ac79d4244fdde2383f58d5884c8e8fc Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Sat, 27 Dec 2025 10:46:14 +0100 Subject: [PATCH 1/4] fix(appkit): obo logic and api usage chore: fixup chore: fixup chore: fixup chore: fixup chore: fixup chore: fixup --- .../client/src/routes/analytics.route.tsx | 2 +- .../src/routes/arrow-analytics.route.tsx | 3 - apps/dev-playground/server/index.ts | 46 +++- llms.txt | 49 +++- .../src/react/charts/create-chart.tsx | 3 + packages/appkit-ui/src/react/charts/types.ts | 5 + .../appkit-ui/src/react/charts/wrapper.tsx | 5 + packages/appkit-ui/src/react/hooks/types.ts | 3 + .../src/react/hooks/use-analytics-query.ts | 12 +- .../src/react/hooks/use-chart-data.ts | 11 +- .../src/react/table/table-wrapper.tsx | 3 + packages/appkit-ui/src/react/table/types.ts | 2 + packages/appkit/package.json | 2 + packages/appkit/src/analytics/analytics.ts | 100 ++++--- packages/appkit/src/analytics/query.ts | 5 +- .../src/analytics/tests/analytics.test.ts | 128 ++++----- .../appkit/src/analytics/tests/query.test.ts | 36 +-- .../appkit/src/connectors/lakebase/client.ts | 12 +- .../appkit/src/context/execution-context.ts | 83 ++++++ packages/appkit/src/context/index.ts | 17 ++ .../appkit/src/context/service-context.ts | 253 ++++++++++++++++++ packages/appkit/src/context/user-context.ts | 32 +++ packages/appkit/src/core/appkit.ts | 6 + .../appkit/src/core/tests/databricks.test.ts | 17 +- packages/appkit/src/index.ts | 14 +- .../appkit/src/plugin/interceptors/cache.ts | 4 +- .../appkit/src/plugin/interceptors/retry.ts | 4 +- .../src/plugin/interceptors/telemetry.ts | 4 +- .../appkit/src/plugin/interceptors/timeout.ts | 4 +- .../appkit/src/plugin/interceptors/types.ts | 9 +- packages/appkit/src/plugin/plugin.ts | 145 +++++++++- .../appkit/src/plugin/tests/cache.test.ts | 10 +- .../appkit/src/plugin/tests/plugin.test.ts | 35 +-- .../appkit/src/plugin/tests/retry.test.ts | 6 +- .../appkit/src/plugin/tests/timeout.test.ts | 10 +- packages/appkit/src/server/index.ts | 5 - .../server/tests/server.integration.test.ts | 33 ++- .../appkit/src/server/tests/server.test.ts | 13 +- .../tests/telemetry-interceptor.test.ts | 4 +- .../src/utils/databricks-client-middleware.ts | 212 --------------- packages/appkit/src/utils/index.ts | 1 - pnpm-lock.yaml | 145 +++++----- tools/test-helpers.ts | 153 ++++++++--- 43 files changed, 1078 insertions(+), 568 deletions(-) create mode 100644 packages/appkit/src/context/execution-context.ts create mode 100644 packages/appkit/src/context/index.ts create mode 100644 packages/appkit/src/context/service-context.ts create mode 100644 packages/appkit/src/context/user-context.ts delete mode 100644 packages/appkit/src/utils/databricks-client-middleware.ts diff --git a/apps/dev-playground/client/src/routes/analytics.route.tsx b/apps/dev-playground/client/src/routes/analytics.route.tsx index d183310..a883206 100644 --- a/apps/dev-playground/client/src/routes/analytics.route.tsx +++ b/apps/dev-playground/client/src/routes/analytics.route.tsx @@ -69,7 +69,7 @@ function AnalyticsRoute() { data: untaggedAppsData, loading: untaggedAppsLoading, error: untaggedAppsError, - } = useAnalyticsQuery("untagged_apps", untaggedAppsParams); + } = useAnalyticsQuery("untagged_apps", untaggedAppsParams, { asUser: true }); const metrics = useMemo(() => { if (!summaryDataRaw || summaryDataRaw.length === 0) { diff --git a/apps/dev-playground/client/src/routes/arrow-analytics.route.tsx b/apps/dev-playground/client/src/routes/arrow-analytics.route.tsx index d4dfee7..2018243 100644 --- a/apps/dev-playground/client/src/routes/arrow-analytics.route.tsx +++ b/apps/dev-playground/client/src/routes/arrow-analytics.route.tsx @@ -54,9 +54,6 @@ function ArrowAnalyticsRoute() {
- {/* ============================================================ */} - {/* UNIFIED CHARTS - FORMAT COMPARISON */} - {/* ============================================================ */}

Unified Charts API

diff --git a/apps/dev-playground/server/index.ts b/apps/dev-playground/server/index.ts index 9391c62..ed3fdac 100644 --- a/apps/dev-playground/server/index.ts +++ b/apps/dev-playground/server/index.ts @@ -3,5 +3,49 @@ import { reconnect } from "./reconnect-plugin"; import { telemetryExamples } from "./telemetry-example-plugin"; createApp({ - plugins: [server(), reconnect(), telemetryExamples(), analytics({})], + plugins: [ + server({ autoStart: false }), + reconnect(), + telemetryExamples(), + analytics({}), + ], +}).then((appkit) => { + appkit.server + .extend((app) => { + app.get("/sp", (_req, res) => { + appkit.analytics + .query("SELECT * FROM samples.nyctaxi.trips;") + .then((result) => { + console.log(result[0]); + res.json(result); + }) + .catch((error) => { + console.error("Error:", error); + res.status(500).json({ + error: error.message, + errorCode: error.errorCode, + statusCode: error.statusCode, + }); + }); + }); + + app.get("/obo", (req, res) => { + appkit.analytics + .asUser(req) + .query("SELECT * FROM samples.nyctaxi.trips;") + .then((result) => { + console.log(result[0]); + res.json(result); + }) + .catch((error) => { + console.error("OBO Error:", error); + res.status(500).json({ + error: error.message, + errorCode: error.errorCode, + statusCode: error.statusCode, + }); + }); + }); + }) + .start(); }); diff --git a/llms.txt b/llms.txt index 151859b..b73854a 100644 --- a/llms.txt +++ b/llms.txt @@ -440,23 +440,49 @@ Formats: - `format: "JSON"` (default) returns JSON rows - `format: "ARROW"` returns an Arrow “statement_id” payload over SSE, then the client fetches binary Arrow from `/api/analytics/arrow-result/:jobId` -### Request context (`getRequestContext()`) +### Execution context and `asUser(req)` -If a plugin sets `requiresDatabricksClient = true`, AppKit adds middleware that provides request context. +AppKit manages Databricks authentication via two contexts: -Headers used: +- **ServiceContext** (singleton): Initialized at app startup with service principal credentials +- **ExecutionContext**: Determined at runtime - either service principal or user context + +**Headers used for user context:** - `x-forwarded-user`: required in production; identifies the user -- `x-forwarded-access-token`: optional; enables **user token passthrough** if `DATABRICKS_HOST` is set +- `x-forwarded-access-token`: required for user token passthrough + +**Using `asUser(req)` for user-scoped operations:** + +The `asUser(req)` pattern allows plugins to execute operations using the requesting user's credentials: + +```ts +// In a custom plugin route handler +router.post("/users/me/data", async (req, res) => { + // Execute as the user (uses their Databricks permissions) + const result = await this.asUser(req).query("SELECT ..."); + res.json(result); +}); + +// Service principal execution (default) +router.post("/system/data", async (req, res) => { + const result = await this.query("SELECT ..."); + res.json(result); +}); +``` + +**Context helper functions (exported from `@databricks/appkit`):** + +- `getExecutionContext()`: Returns current context (user or service) +- `getCurrentUserId()`: Returns user ID in user context, service user ID otherwise +- `getWorkspaceClient()`: Returns the appropriate WorkspaceClient for current context +- `getWarehouseId()`: `Promise` (from `DATABRICKS_WAREHOUSE_ID` or auto-selected in dev) +- `getWorkspaceId()`: `Promise` (from `DATABRICKS_WORKSPACE_ID` or fetched) +- `isInUserContext()`: Returns `true` if currently executing in user context -Context fields (real behavior): +**Development mode behavior:** -- `userId`: derived from `x-forwarded-user` (in development it falls back to `serviceUserId`) -- `serviceUserId`: service principal/user ID -- `warehouseId`: `Promise` (from `DATABRICKS_WAREHOUSE_ID`, or auto-selected in development) -- `workspaceId`: `Promise` (from `DATABRICKS_WORKSPACE_ID` or fetched) -- `userDatabricksClient`: present only when passthrough is available (or in dev it equals service client) -- `serviceDatabricksClient`: always present +In local development (`NODE_ENV=development`), if `asUser(req)` is called without a user token, it logs a warning and falls back to the service principal. ### Custom plugins (backend) @@ -469,7 +495,6 @@ import type express from "express"; class MyPlugin extends Plugin { name = "my-plugin"; envVars = []; // list required env vars here - requiresDatabricksClient = false; // set true if you need getRequestContext() injectRoutes(router: express.Router) { this.route(router, { diff --git a/packages/appkit-ui/src/react/charts/create-chart.tsx b/packages/appkit-ui/src/react/charts/create-chart.tsx index bca3319..02233ff 100644 --- a/packages/appkit-ui/src/react/charts/create-chart.tsx +++ b/packages/appkit-ui/src/react/charts/create-chart.tsx @@ -27,6 +27,7 @@ export function createChart( parameters, format, transformer, + asUser, // Data props data, // Common props @@ -41,6 +42,7 @@ export function createChart( parameters?: Record; format?: string; transformer?: unknown; + asUser?: boolean; data?: unknown; height?: number; className?: string; @@ -56,6 +58,7 @@ export function createChart( parameters, format, transformer, + asUser, height, className, ariaLabel, diff --git a/packages/appkit-ui/src/react/charts/types.ts b/packages/appkit-ui/src/react/charts/types.ts index a904a26..41d9762 100644 --- a/packages/appkit-ui/src/react/charts/types.ts +++ b/packages/appkit-ui/src/react/charts/types.ts @@ -85,6 +85,11 @@ export interface QueryProps extends ChartBaseProps { format?: DataFormat; /** Transform raw data before rendering */ transformer?: (data: T) => T; + /** + * Whether to execute the query as the current user + * @default false + */ + asUser?: boolean; // Discriminator: cannot use direct data with query data?: never; diff --git a/packages/appkit-ui/src/react/charts/wrapper.tsx b/packages/appkit-ui/src/react/charts/wrapper.tsx index 2910ff9..de6bb45 100644 --- a/packages/appkit-ui/src/react/charts/wrapper.tsx +++ b/packages/appkit-ui/src/react/charts/wrapper.tsx @@ -12,6 +12,8 @@ import { isArrowTable } from "./types"; // ============================================================================ interface ChartWrapperQueryProps { + /** Whether to execute the query as a user. Default is false. */ + asUser?: boolean; /** Analytics query key */ queryKey: string; /** Query parameters */ @@ -59,6 +61,7 @@ function QueryModeContent({ parameters, format, transformer, + asUser, height, className, ariaLabel, @@ -70,6 +73,7 @@ function QueryModeContent({ parameters, format, transformer, + asUser, }); if (loading) return ; @@ -180,6 +184,7 @@ export function ChartWrapper(props: ChartWrapperProps) { parameters={props.parameters} format={props.format} transformer={props.transformer} + asUser={props.asUser} height={height} className={className} ariaLabel={ariaLabel} diff --git a/packages/appkit-ui/src/react/hooks/types.ts b/packages/appkit-ui/src/react/hooks/types.ts index 5db725f..b9b023f 100644 --- a/packages/appkit-ui/src/react/hooks/types.ts +++ b/packages/appkit-ui/src/react/hooks/types.ts @@ -41,6 +41,9 @@ export interface UseAnalyticsQueryOptions { /** Whether to automatically start the query when the hook is mounted. Default is true. */ autoStart?: boolean; + + /** Whether to execute the query as a user. Default is false. */ + asUser?: boolean; } /** Result state returned by useAnalyticsQuery */ diff --git a/packages/appkit-ui/src/react/hooks/use-analytics-query.ts b/packages/appkit-ui/src/react/hooks/use-analytics-query.ts index 72f09e1..54cc234 100644 --- a/packages/appkit-ui/src/react/hooks/use-analytics-query.ts +++ b/packages/appkit-ui/src/react/hooks/use-analytics-query.ts @@ -59,6 +59,12 @@ export function useAnalyticsQuery< const format = options?.format ?? "JSON"; const maxParametersSize = options?.maxParametersSize ?? 100 * 1024; const autoStart = options?.autoStart ?? true; + const asUser = options?.asUser ?? false; + + const devMode = getDevMode(); + const urlSuffix = asUser + ? `/api/analytics/users/me/query/${encodeURIComponent(queryKey)}${devMode}` + : `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`; type ResultType = InferResultByFormat; const [data, setData] = useState(null); @@ -107,10 +113,8 @@ export function useAnalyticsQuery< const abortController = new AbortController(); abortControllerRef.current = abortController; - const devMode = getDevMode(); - connectSSE({ - url: `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`, + url: urlSuffix, payload: payload, signal: abortController.signal, onMessage: async (message) => { @@ -187,7 +191,7 @@ export function useAnalyticsQuery< setError(userMessage); }, }); - }, [queryKey, payload]); + }, [queryKey, payload, urlSuffix]); useEffect(() => { if (autoStart) { diff --git a/packages/appkit-ui/src/react/hooks/use-chart-data.ts b/packages/appkit-ui/src/react/hooks/use-chart-data.ts index d8d0bd3..acb8f54 100644 --- a/packages/appkit-ui/src/react/hooks/use-chart-data.ts +++ b/packages/appkit-ui/src/react/hooks/use-chart-data.ts @@ -25,6 +25,8 @@ export interface UseChartDataOptions { format?: DataFormat; /** Transform data after fetching */ transformer?: (data: T) => T; + /** Whether to execute the query as the current user. @default false */ + asUser?: boolean; } export interface UseChartDataResult { @@ -102,7 +104,13 @@ function resolveFormat( * ``` */ export function useChartData(options: UseChartDataOptions): UseChartDataResult { - const { queryKey, parameters, format = "auto", transformer } = options; + const { + queryKey, + parameters, + format = "auto", + transformer, + asUser = false, + } = options; // Resolve the format to use const resolvedFormat = useMemo( @@ -120,6 +128,7 @@ export function useChartData(options: UseChartDataOptions): UseChartDataResult { } = useAnalyticsQuery(queryKey, parameters, { autoStart: true, format: resolvedFormat, + asUser, }); // Process and transform data diff --git a/packages/appkit-ui/src/react/table/table-wrapper.tsx b/packages/appkit-ui/src/react/table/table-wrapper.tsx index c4e28ff..bbadd60 100644 --- a/packages/appkit-ui/src/react/table/table-wrapper.tsx +++ b/packages/appkit-ui/src/react/table/table-wrapper.tsx @@ -45,6 +45,7 @@ const CHECKBOX_COLUMN_WIDTH = 40; * @param props.queryKey - The query key to fetch the data * @param props.parameters - The parameters to pass to the query * @param props.transformer - Optional function to transform raw data before creating table + * @param props.asUser - Whether to execute the query as a user. Default is false. * @param props.children - Render function that receives the TanStack Table instance * @param props.className - Optional CSS class name for the wrapper * @param props.ariaLabel - Optional accessibility label @@ -59,6 +60,7 @@ export function TableWrapper( queryKey, parameters, transformer, + asUser = false, children, className, ariaLabel, @@ -76,6 +78,7 @@ export function TableWrapper( const { data, loading, error } = useAnalyticsQuery( queryKey, parameters, + { asUser }, ); useEffect(() => { diff --git a/packages/appkit-ui/src/react/table/types.ts b/packages/appkit-ui/src/react/table/types.ts index 366d29f..d6be90c 100644 --- a/packages/appkit-ui/src/react/table/types.ts +++ b/packages/appkit-ui/src/react/table/types.ts @@ -12,6 +12,8 @@ export interface TableWrapperProps { parameters: Record; /** Optional function to transform raw data before creating table */ transformer?: (data: TRaw[]) => TProcessed[]; + /** Whether to execute the query as a user. Default is false. */ + asUser?: boolean; /** Render function that receives the TanStack Table instance */ children: (data: Table) => React.ReactNode; /** Optional CSS class name for the wrapper */ diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 5024585..bbb2a0b 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -55,9 +55,11 @@ "@opentelemetry/sdk-metrics": "^2.2.0", "@opentelemetry/sdk-node": "^0.208.0", "@opentelemetry/semantic-conventions": "^1.38.0", + "@types/semver": "^7.7.1", "dotenv": "^16.6.1", "express": "^4.22.0", "pg": "^8.16.3", + "semver": "^7.7.3", "shared": "workspace:*", "vite": "npm:rolldown-vite@7.1.14", "ws": "^8.18.3", diff --git a/packages/appkit/src/analytics/analytics.ts b/packages/appkit/src/analytics/analytics.ts index be2247c..5d96068 100644 --- a/packages/appkit/src/analytics/analytics.ts +++ b/packages/appkit/src/analytics/analytics.ts @@ -6,9 +6,13 @@ import type { StreamExecutionSettings, } from "shared"; import { SQLWarehouseConnector } from "../connectors"; +import { + getCurrentUserId, + getWarehouseId, + getWorkspaceClient, +} from "../context"; +import type express from "express"; import { Plugin, toPlugin } from "../plugin"; -import type { Request, Response } from "../utils"; -import { getRequestContext, getWorkspaceClient } from "../utils"; import { queryDefaults } from "./defaults"; import { QueryProcessor } from "./query"; import type { @@ -20,7 +24,6 @@ import type { export class AnalyticsPlugin extends Plugin { name = "analytics"; envVars = []; - requiresDatabricksClient = true; protected static description = "Analytics plugin for data analysis"; protected declare config: IAnalyticsConfig; @@ -41,21 +44,32 @@ export class AnalyticsPlugin extends Plugin { } injectRoutes(router: IAppRouter) { + // Service principal endpoints this.route(router, { name: "arrow", method: "get", path: "/arrow-result/:jobId", - handler: async (req: Request, res: Response) => { + handler: async (req: express.Request, res: express.Response) => { await this._handleArrowRoute(req, res); }, }); + this.route(router, { + name: "query", + method: "post", + path: "/query/:query_key", + handler: async (req: express.Request, res: express.Response) => { + await this._handleQueryRoute(req, res); + }, + }); + + // User context endpoints - use asUser(req) to execute with user's identity this.route(router, { name: "arrowAsUser", method: "get", path: "/users/me/arrow-result/:jobId", - handler: async (req: Request, res: Response) => { - await this._handleArrowRoute(req, res, { asUser: true }); + handler: async (req: express.Request, res: express.Response) => { + await this.asUser(req)._handleArrowRoute(req, res); }, }); @@ -63,30 +77,23 @@ export class AnalyticsPlugin extends Plugin { name: "queryAsUser", method: "post", path: "/users/me/query/:query_key", - handler: async (req: Request, res: Response) => { - await this._handleQueryRoute(req, res, { asUser: true }); - }, - }); - - this.route(router, { - name: "query", - method: "post", - path: "/query/:query_key", - handler: async (req: Request, res: Response) => { - await this._handleQueryRoute(req, res, { asUser: false }); + handler: async (req: express.Request, res: express.Response) => { + await this.asUser(req)._handleQueryRoute(req, res); }, }); } - private async _handleArrowRoute( - req: Request, - res: Response, - { asUser = false }: { asUser?: boolean } = {}, + /** + * Handle Arrow data download requests. + * When called via asUser(req), uses the user's Databricks credentials. + */ + async _handleArrowRoute( + req: express.Request, + res: express.Response, ): Promise { try { const { jobId } = req.params; - - const workspaceClient = getWorkspaceClient(asUser); + const workspaceClient = getWorkspaceClient(); console.log( `Processing Arrow job request: ${jobId} for plugin: ${this.name}`, @@ -111,10 +118,13 @@ export class AnalyticsPlugin extends Plugin { } } - private async _handleQueryRoute( - req: Request, - res: Response, - { asUser = false }: { asUser?: boolean } = {}, + /** + * Handle SQL query execution requests. + * When called via asUser(req), uses the user's Databricks credentials. + */ + async _handleQueryRoute( + req: express.Request, + res: express.Response, ): Promise { const { query_key } = req.params; const { parameters, format = "JSON" } = req.body as IAnalyticsQueryRequest; @@ -131,10 +141,8 @@ export class AnalyticsPlugin extends Plugin { type: "result", }; - const requestContext = getRequestContext(); - const userKey = asUser - ? requestContext.userId - : requestContext.serviceUserId; + // Get user key from current context (automatically includes user ID when in user context) + const userKey = getCurrentUserId(); if (!query_key) { res.status(400).json({ error: "query_key is required" }); @@ -186,9 +194,6 @@ export class AnalyticsPlugin extends Plugin { processedParams, queryParameters.formatParameters, signal, - { - asUser, - }, ); return { type: queryParameters.type, ...result }; @@ -198,15 +203,29 @@ export class AnalyticsPlugin extends Plugin { ); } + /** + * Execute a SQL query using the current execution context. + * + * When called directly: uses service principal credentials. + * When called via asUser(req).query(...): uses user's credentials. + * + * @example + * ```typescript + * // Service principal execution + * const result = await analytics.query("SELECT * FROM table") + * + * // User context execution (in route handler) + * const result = await this.asUser(req).query("SELECT * FROM table") + * ``` + */ async query( query: string, parameters?: Record, formatParameters?: Record, signal?: AbortSignal, - { asUser = false }: { asUser?: boolean } = {}, ): Promise { - const requestContext = getRequestContext(); - const workspaceClient = getWorkspaceClient(asUser); + const workspaceClient = getWorkspaceClient(); + const warehouseId = await getWarehouseId(); const { statement, parameters: sqlParameters } = this.queryProcessor.convertToSQLParameters(query, parameters); @@ -215,7 +234,7 @@ export class AnalyticsPlugin extends Plugin { workspaceClient, { statement, - warehouse_id: await requestContext.warehouseId, + warehouse_id: warehouseId, parameters: sqlParameters, ...formatParameters, }, @@ -225,8 +244,9 @@ export class AnalyticsPlugin extends Plugin { return response.result; } - // If we need arrow stream in more plugins we can define this as a base method in the core plugin class - // and have a generic endpoint for each plugin that consumes this arrow data. + /** + * Get Arrow-formatted data for a completed query job. + */ protected async getArrowData( workspaceClient: WorkspaceClient, jobId: string, diff --git a/packages/appkit/src/analytics/query.ts b/packages/appkit/src/analytics/query.ts index 39c9a2f..c891687 100644 --- a/packages/appkit/src/analytics/query.ts +++ b/packages/appkit/src/analytics/query.ts @@ -1,7 +1,7 @@ import { createHash } from "node:crypto"; import type { sql } from "@databricks/sdk-experimental"; import { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from "shared"; -import { getRequestContext } from "../utils"; +import { getWorkspaceId } from "../context"; type SQLParameterValue = SQLTypeMarker | null | undefined; @@ -18,8 +18,7 @@ export class QueryProcessor { // auto-inject workspaceId if needed and not provided if (queryParams.has("workspaceId") && !processed.workspaceId) { - const requestContext = getRequestContext(); - const workspaceId = await requestContext.workspaceId; + const workspaceId = await getWorkspaceId(); if (workspaceId) { processed.workspaceId = sqlHelpers.string(workspaceId); } diff --git a/packages/appkit/src/analytics/tests/analytics.test.ts b/packages/appkit/src/analytics/tests/analytics.test.ts index 1fd11ab..cfae3f1 100644 --- a/packages/appkit/src/analytics/tests/analytics.test.ts +++ b/packages/appkit/src/analytics/tests/analytics.test.ts @@ -2,13 +2,14 @@ import { createMockRequest, createMockResponse, createMockRouter, - runWithRequestContext, + mockServiceContext, setupDatabricksEnv, } from "@tools/test-helpers"; import { sql } from "shared"; -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { AnalyticsPlugin, analytics } from "../analytics"; import type { IAnalyticsConfig } from "../types"; +import { ServiceContext } from "../../context/service-context"; // Mock CacheManager singleton with actual caching behavior const { mockCacheStore, mockCacheInstance } = vi.hoisted(() => { @@ -52,11 +53,18 @@ vi.mock("../../cache", () => ({ describe("Analytics Plugin", () => { let config: IAnalyticsConfig; + let serviceContextMock: Awaited>; - beforeEach(() => { + beforeEach(async () => { config = { timeout: 5000 }; setupDatabricksEnv(); mockCacheStore.clear(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + }); + + afterEach(() => { + serviceContextMock?.restore(); }); test("Analytics plugin data should have correct name", () => { @@ -101,9 +109,7 @@ describe("Analytics Plugin", () => { }); const mockRes = createMockResponse(); - await runWithRequestContext(async () => { - await handler(mockReq, mockRes); - }); + await handler(mockReq, mockRes); expect(mockRes.status).toHaveBeenCalledWith(400); expect(mockRes.json).toHaveBeenCalledWith({ @@ -140,22 +146,14 @@ describe("Analytics Plugin", () => { }); const mockRes = createMockResponse(); - const mockServiceClient = { service: "client" }; - await runWithRequestContext( - async () => { - await handler(mockReq, mockRes); - }, - { - serviceDatabricksClient: mockServiceClient as any, - }, - ); + await handler(mockReq, mockRes); - // Verify service workspace client is used - expect(capturedWorkspaceClient).toBe(mockServiceClient); + // Verify service workspace client is used (from mocked ServiceContext) + expect(capturedWorkspaceClient).toBeDefined(); - // Verify executeStatement is called with service workspace client + // Verify executeStatement is called expect(executeMock).toHaveBeenCalledWith( - mockServiceClient, + expect.anything(), expect.objectContaining({ statement: "SELECT * FROM test", warehouse_id: "test-warehouse-id", @@ -207,30 +205,25 @@ describe("Analytics Plugin", () => { plugin.injectRoutes(router); const handler = getHandler("POST", "/users/me/query/:query_key"); + // The request needs both x-forwarded-access-token and x-forwarded-user headers const mockReq = createMockRequest({ params: { query_key: "user_profile" }, body: { parameters: { user_id: sql.number(123) } }, - headers: { "x-forwarded-access-token": "user-token-123" }, + headers: { + "x-forwarded-access-token": "user-token-123", + "x-forwarded-user": "user-123", + }, }); const mockRes = createMockResponse(); - const mockUserClient = { user: "client" }; - await runWithRequestContext( - async () => { - await handler(mockReq, mockRes); - }, - { - userDatabricksClient: mockUserClient as any, - userId: "user-token-123", - }, - ); + await handler(mockReq, mockRes); - // Verify user workspace client is used - expect(capturedWorkspaceClient).toBe(mockUserClient); + // Verify a workspace client is used (created via ServiceContext.createUserContext) + expect(capturedWorkspaceClient).toBeDefined(); - // Verify the user workspace client is passed to SQL connector + // Verify the workspace client is passed to SQL connector expect(executeMock).toHaveBeenCalledWith( - mockUserClient, + expect.anything(), expect.objectContaining({ statement: "SELECT * FROM users WHERE id = :user_id", warehouse_id: "test-warehouse-id", @@ -272,18 +265,16 @@ describe("Analytics Plugin", () => { body: { parameters: { foo: sql.string("bar") } }, }); - await runWithRequestContext(async () => { - const mockRes1 = createMockResponse(); - await handler(mockReq, mockRes1); + const mockRes1 = createMockResponse(); + await handler(mockReq, mockRes1); - const mockRes2 = createMockResponse(); - await handler(mockReq, mockRes2); + const mockRes2 = createMockResponse(); + await handler(mockReq, mockRes2); - expect(executeMock).toHaveBeenCalledTimes(1); + expect(executeMock).toHaveBeenCalledTimes(1); - expect(mockRes1.write).toHaveBeenCalledWith("event: result\n"); - expect(mockRes2.write).toHaveBeenCalledWith("event: result\n"); - }); + expect(mockRes1.write).toHaveBeenCalledWith("event: result\n"); + expect(mockRes2.write).toHaveBeenCalledWith("event: result\n"); }); test("should cache user-scoped queries separately per user", async () => { @@ -308,44 +299,41 @@ describe("Analytics Plugin", () => { const handler = getHandler("POST", "/users/me/query/:query_key"); + // User 1's request const mockReq1 = createMockRequest({ params: { query_key: "user_profile" }, body: { parameters: { user_id: sql.number(1) } }, - headers: { "x-forwarded-access-token": "user-token-1" }, + headers: { + "x-forwarded-access-token": "user-token-1", + "x-forwarded-user": "user-1", + }, }); const mockRes1 = createMockResponse(); - await runWithRequestContext( - async () => { - await handler(mockReq1, mockRes1); - }, - { userId: "user-token-1" }, - ); + await handler(mockReq1, mockRes1); + // User 2's request - different user, should not use cache const mockReq2 = createMockRequest({ params: { query_key: "user_profile" }, body: { parameters: { user_id: sql.number(2) } }, - headers: { "x-forwarded-access-token": "user-token-2" }, + headers: { + "x-forwarded-access-token": "user-token-2", + "x-forwarded-user": "user-2", + }, }); const mockRes2 = createMockResponse(); - await runWithRequestContext( - async () => { - await handler(mockReq2, mockRes2); - }, - { userId: "user-token-2" }, - ); + await handler(mockReq2, mockRes2); + // User 1's request again - should use cache const mockReq1Again = createMockRequest({ params: { query_key: "user_profile" }, body: { parameters: { user_id: sql.number(1) } }, - headers: { "x-forwarded-access-token": "user-token-1" }, + headers: { + "x-forwarded-access-token": "user-token-1", + "x-forwarded-user": "user-1", + }, }); const mockRes1Again = createMockResponse(); - await runWithRequestContext( - async () => { - await handler(mockReq1Again, mockRes1Again); - }, - { userId: "user-token-1" }, - ); + await handler(mockReq1Again, mockRes1Again); expect(executeMock).toHaveBeenCalledTimes(2); @@ -389,18 +377,10 @@ describe("Analytics Plugin", () => { }); const mockRes = createMockResponse(); - const mockServiceClient = { service: "client" }; - await runWithRequestContext( - async () => { - await handler(mockReq, mockRes); - }, - { - serviceDatabricksClient: mockServiceClient as any, - }, - ); + await handler(mockReq, mockRes); expect(executeMock).toHaveBeenCalledWith( - mockServiceClient, + expect.anything(), expect.objectContaining({ statement: "SELECT * FROM test", parameters: [], diff --git a/packages/appkit/src/analytics/tests/query.test.ts b/packages/appkit/src/analytics/tests/query.test.ts index 6c1cb84..864f276 100644 --- a/packages/appkit/src/analytics/tests/query.test.ts +++ b/packages/appkit/src/analytics/tests/query.test.ts @@ -1,10 +1,23 @@ -import { runWithRequestContext } from "@tools/test-helpers"; +import { mockServiceContext } from "@tools/test-helpers"; import { sql } from "shared"; -import { describe, expect, test } from "vitest"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { QueryProcessor } from "../query"; +import { ServiceContext } from "../../context/service-context"; describe("QueryProcessor", () => { const processor = new QueryProcessor(); + let serviceContextMock: Awaited>; + + beforeEach(async () => { + ServiceContext.reset(); + serviceContextMock = await mockServiceContext({ + workspaceId: "1234567890", + }); + }); + + afterEach(() => { + serviceContextMock?.restore(); + }); describe("convertToSQLParameters - Parameter Injection Protection", () => { test("should accept valid parameters that exist in query", () => { @@ -143,14 +156,8 @@ describe("QueryProcessor", () => { const query = "SELECT * FROM data WHERE workspace_id = :workspaceId"; const parameters = {}; - const result = await runWithRequestContext( - async () => { - return await processor.processQueryParams(query, parameters); - }, - { - workspaceId: Promise.resolve("1234567890"), - }, - ); + // ServiceContext is already mocked with workspaceId: "1234567890" in beforeEach + const result = await processor.processQueryParams(query, parameters); expect(result.workspaceId).toEqual({ __sql_type: "STRING", @@ -162,14 +169,7 @@ describe("QueryProcessor", () => { const query = "SELECT * FROM data WHERE workspace_id = :workspaceId"; const parameters = { workspaceId: sql.number("9876543210") }; - const result = await runWithRequestContext( - async () => { - return await processor.processQueryParams(query, parameters); - }, - { - workspaceId: Promise.resolve("1234567890"), - }, - ); + const result = await processor.processQueryParams(query, parameters); expect(result.workspaceId).toEqual({ __sql_type: "NUMERIC", diff --git a/packages/appkit/src/connectors/lakebase/client.ts b/packages/appkit/src/connectors/lakebase/client.ts index 053f166..e05d282 100644 --- a/packages/appkit/src/connectors/lakebase/client.ts +++ b/packages/appkit/src/connectors/lakebase/client.ts @@ -268,22 +268,22 @@ export class LakebaseConnector { this.close(); } - /** Get Databricks workspace client - from config or request context */ + /** Get Databricks workspace client - from config or execution context */ private getWorkspaceClient(): WorkspaceClient { if (this.config.workspaceClient) { return this.config.workspaceClient; } try { - const { getRequestContext } = require("../../utils"); - const { serviceDatabricksClient } = getRequestContext(); + const { getWorkspaceClient: getClient } = require("../../context"); + const client = getClient(); // cache it for subsequent calls - this.config.workspaceClient = serviceDatabricksClient; - return serviceDatabricksClient; + this.config.workspaceClient = client; + return client; } catch (_error) { throw new Error( - "Databricks workspace client not available. Either pass it in config or use within AppKit request context.", + "Databricks workspace client not available. Either pass it in config or ensure ServiceContext is initialized.", ); } } diff --git a/packages/appkit/src/context/execution-context.ts b/packages/appkit/src/context/execution-context.ts new file mode 100644 index 0000000..a8fd2d4 --- /dev/null +++ b/packages/appkit/src/context/execution-context.ts @@ -0,0 +1,83 @@ +import { AsyncLocalStorage } from "node:async_hooks"; +import { ServiceContext } from "./service-context"; +import { + isUserContext, + type ExecutionContext, + type UserContext, +} from "./user-context"; + +/** + * AsyncLocalStorage for execution context. + * Used to pass user context through the call stack without explicit parameters. + */ +const executionContextStorage = new AsyncLocalStorage(); + +/** + * Run a function in the context of a user. + * All calls within the function will have access to the user context. + * + * @param userContext - The user context to use + * @param fn - The function to run + * @returns The result of the function + */ +export function runInUserContext(userContext: UserContext, fn: () => T): T { + return executionContextStorage.run(userContext, fn); +} + +/** + * Get the current execution context. + * + * - If running inside a user context (via asUser), returns the user context + * - Otherwise, returns the service context + * + * @throws Error if ServiceContext is not initialized + */ +export function getExecutionContext(): ExecutionContext { + const userContext = executionContextStorage.getStore(); + if (userContext) { + return userContext; + } + return ServiceContext.get(); +} + +/** + * Get the current user ID for cache keying and telemetry. + * + * Returns the user ID if in user context, otherwise the service user ID. + */ +export function getCurrentUserId(): string { + const ctx = getExecutionContext(); + if (isUserContext(ctx)) { + return ctx.userId; + } + return ctx.serviceUserId; +} + +/** + * Get the WorkspaceClient for the current execution context. + */ +export function getWorkspaceClient() { + return getExecutionContext().client; +} + +/** + * Get the warehouse ID promise. + */ +export function getWarehouseId(): Promise { + return getExecutionContext().warehouseId; +} + +/** + * Get the workspace ID promise. + */ +export function getWorkspaceId(): Promise { + return getExecutionContext().workspaceId; +} + +/** + * Check if currently running in a user context. + */ +export function isInUserContext(): boolean { + const ctx = executionContextStorage.getStore(); + return ctx !== undefined; +} diff --git a/packages/appkit/src/context/index.ts b/packages/appkit/src/context/index.ts new file mode 100644 index 0000000..b0145fe --- /dev/null +++ b/packages/appkit/src/context/index.ts @@ -0,0 +1,17 @@ +export { ServiceContext, type ServiceContextState } from "./service-context"; + +export { + isUserContext, + type ExecutionContext, + type UserContext, +} from "./user-context"; + +export { + getExecutionContext, + getCurrentUserId, + getWorkspaceClient, + getWarehouseId, + getWorkspaceId, + isInUserContext, + runInUserContext, +} from "./execution-context"; diff --git a/packages/appkit/src/context/service-context.ts b/packages/appkit/src/context/service-context.ts new file mode 100644 index 0000000..0492f56 --- /dev/null +++ b/packages/appkit/src/context/service-context.ts @@ -0,0 +1,253 @@ +import { + type ClientOptions, + type sql, + WorkspaceClient, +} from "@databricks/sdk-experimental"; +import { coerce } from "semver"; +import { + name as productName, + version as productVersion, +} from "../../package.json"; +import type { UserContext } from "./user-context"; + +/** + * Service context holds the service principal client and shared resources. + * This is initialized once at app startup and shared across all requests. + */ +export interface ServiceContextState { + /** WorkspaceClient authenticated as the service principal */ + client: WorkspaceClient; + /** The service principal's user ID */ + serviceUserId: string; + /** Promise that resolves to the warehouse ID */ + warehouseId: Promise; + /** Promise that resolves to the workspace ID */ + workspaceId: Promise; +} + +function getClientOptions(): ClientOptions { + const isDev = process.env.NODE_ENV === "development"; + const semver = coerce(productVersion); + const normalizedVersion = (semver?.version ?? + productVersion) as ClientOptions["productVersion"]; + + return { + product: productName, + productVersion: normalizedVersion, + ...(isDev && { userAgentExtra: { mode: "dev" } }), + }; +} + +/** + * ServiceContext is a singleton that manages the service principal's + * WorkspaceClient and shared resources like warehouse/workspace IDs. + * + * It's initialized once at app startup and provides the foundation + * for both service principal and user context execution. + */ +export class ServiceContext { + private static instance: ServiceContextState | null = null; + private static initPromise: Promise | null = null; + + /** + * Initialize the service context. Should be called once at app startup. + * Safe to call multiple times - will return the same instance. + */ + static async initialize(): Promise { + if (ServiceContext.instance) { + return ServiceContext.instance; + } + + if (ServiceContext.initPromise) { + return ServiceContext.initPromise; + } + + ServiceContext.initPromise = ServiceContext.createContext(); + ServiceContext.instance = await ServiceContext.initPromise; + return ServiceContext.instance; + } + + /** + * Get the initialized service context. + * @throws Error if not initialized + */ + static get(): ServiceContextState { + if (!ServiceContext.instance) { + throw new Error( + "ServiceContext not initialized. Call ServiceContext.initialize() first.", + ); + } + return ServiceContext.instance; + } + + /** + * Check if the service context has been initialized. + */ + static isInitialized(): boolean { + return ServiceContext.instance !== null; + } + + /** + * Create a user context from request headers. + * + * @param token - The user's access token from x-forwarded-access-token header + * @param userId - The user's ID from x-forwarded-user header + * @param userName - Optional user name + * @throws Error if token is not provided + */ + static createUserContext( + token: string, + userId: string, + userName?: string, + ): UserContext { + if (!token) { + throw new Error("User token is required to create user context"); + } + + const host = process.env.DATABRICKS_HOST; + if (!host) { + throw new Error( + "DATABRICKS_HOST environment variable is required for user context", + ); + } + + const serviceCtx = ServiceContext.get(); + + // Create user client with the OAuth token from Databricks Apps + // Note: We use authType: "pat" because the token is passed as a Bearer token + // just like a PAT, even though it's technically an OAuth token + const userClient = new WorkspaceClient( + { + token, + host, + authType: "pat", + }, + getClientOptions(), + ); + + return { + client: userClient, + userId, + userName, + warehouseId: serviceCtx.warehouseId, + workspaceId: serviceCtx.workspaceId, + isUserContext: true, + }; + } + + /** + * Get the client options for WorkspaceClient. + * Exposed for testing purposes. + */ + static getClientOptions(): ClientOptions { + return getClientOptions(); + } + + private static async createContext(): Promise { + const client = new WorkspaceClient({}, getClientOptions()); + + const warehouseId = ServiceContext.getWarehouseId(client); + const workspaceId = ServiceContext.getWorkspaceId(client); + const currentUser = await client.currentUser.me(); + + if (!currentUser.id) { + throw new Error("Service user ID not found"); + } + + return { + client, + serviceUserId: currentUser.id, + warehouseId, + workspaceId, + }; + } + + private static async getWorkspaceId( + client: WorkspaceClient, + ): Promise { + if (process.env.DATABRICKS_WORKSPACE_ID) { + return process.env.DATABRICKS_WORKSPACE_ID; + } + + const response = (await client.apiClient.request({ + path: "/api/2.0/preview/scim/v2/Me", + method: "GET", + headers: new Headers(), + raw: false, + query: {}, + responseHeaders: ["x-databricks-org-id"], + })) as { "x-databricks-org-id": string }; + + if (!response["x-databricks-org-id"]) { + throw new Error("Workspace ID not found"); + } + + return response["x-databricks-org-id"]; + } + + private static async getWarehouseId( + client: WorkspaceClient, + ): Promise { + if (process.env.DATABRICKS_WAREHOUSE_ID) { + return process.env.DATABRICKS_WAREHOUSE_ID; + } + + if (process.env.NODE_ENV === "development") { + const response = (await client.apiClient.request({ + path: "/api/2.0/sql/warehouses", + method: "GET", + headers: new Headers(), + raw: false, + query: { + skip_cannot_use: "true", + }, + })) as { warehouses: sql.EndpointInfo[] }; + + const priorities: Record = { + RUNNING: 0, + STOPPED: 1, + STARTING: 2, + STOPPING: 3, + DELETED: 99, + DELETING: 99, + }; + + const warehouses = (response.warehouses || []).sort((a, b) => { + return ( + priorities[a.state as sql.State] - priorities[b.state as sql.State] + ); + }); + + if (response.warehouses.length === 0) { + throw new Error( + "Warehouse ID not found. Please configure the DATABRICKS_WAREHOUSE_ID environment variable.", + ); + } + + const firstWarehouse = warehouses[0]; + if ( + firstWarehouse.state === "DELETED" || + firstWarehouse.state === "DELETING" || + !firstWarehouse.id + ) { + throw new Error( + "Warehouse ID not found. Please configure the DATABRICKS_WAREHOUSE_ID environment variable.", + ); + } + + return firstWarehouse.id; + } + + throw new Error( + "Warehouse ID not found. Please configure the DATABRICKS_WAREHOUSE_ID environment variable.", + ); + } + + /** + * Reset the service context. Only for testing purposes. + */ + static reset(): void { + ServiceContext.instance = null; + ServiceContext.initPromise = null; + } +} diff --git a/packages/appkit/src/context/user-context.ts b/packages/appkit/src/context/user-context.ts new file mode 100644 index 0000000..e106510 --- /dev/null +++ b/packages/appkit/src/context/user-context.ts @@ -0,0 +1,32 @@ +import type { ServiceContextState } from "./service-context"; + +/** + * User execution context extends the service context with user-specific data. + * Created on-demand when asUser(req) is called. + */ +export interface UserContext { + /** WorkspaceClient authenticated as the user */ + client: ServiceContextState["client"]; + /** The user's ID (from request headers) */ + userId: string; + /** The user's name (from request headers) */ + userName?: string; + /** Promise that resolves to the warehouse ID (inherited from service context) */ + warehouseId: Promise; + /** Promise that resolves to the workspace ID (inherited from service context) */ + workspaceId: Promise; + /** Flag indicating this is a user context */ + isUserContext: true; +} + +/** + * Execution context can be either service or user context. + */ +export type ExecutionContext = ServiceContextState | UserContext; + +/** + * Check if an execution context is a user context. + */ +export function isUserContext(ctx: ExecutionContext): ctx is UserContext { + return "isUserContext" in ctx && ctx.isUserContext === true; +} diff --git a/packages/appkit/src/core/appkit.ts b/packages/appkit/src/core/appkit.ts index 4e20a63..5fad9d0 100644 --- a/packages/appkit/src/core/appkit.ts +++ b/packages/appkit/src/core/appkit.ts @@ -8,6 +8,7 @@ import type { PluginMap, } from "shared"; import { CacheManager } from "../cache"; +import { ServiceContext } from "../context"; import type { TelemetryConfig } from "../telemetry"; import { TelemetryManager } from "../telemetry"; @@ -91,9 +92,14 @@ export class AppKit { cache?: CacheConfig; } = {}, ): Promise> { + // Initialize core services 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(); + const rawPlugins = config.plugins as T; const preparedPlugins = AppKit.preparePlugins(rawPlugins); const mergedConfig = { diff --git a/packages/appkit/src/core/tests/databricks.test.ts b/packages/appkit/src/core/tests/databricks.test.ts index 6dc6a8a..66613c1 100644 --- a/packages/appkit/src/core/tests/databricks.test.ts +++ b/packages/appkit/src/core/tests/databricks.test.ts @@ -1,7 +1,8 @@ import type { BasePlugin } from "shared"; -import { setupDatabricksEnv } from "@tools/test-helpers"; -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { setupDatabricksEnv, mockServiceContext } from "@tools/test-helpers"; +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { createApp, AppKit } from "../appkit"; +import { ServiceContext } from "../../context/service-context"; // Mock environment validation vi.mock("../utils", () => ({ @@ -175,11 +176,21 @@ class FailingPlugin implements BasePlugin { } describe("AppKit", () => { - beforeEach(() => { + let serviceContextMock: Awaited>; + + beforeEach(async () => { setupDatabricksEnv(); vi.clearAllMocks(); // Reset singleton instance (AppKit as any)._instance = null; + // Reset ServiceContext singleton + ServiceContext.reset(); + // Mock ServiceContext for tests + serviceContextMock = await mockServiceContext(); + }); + + afterEach(() => { + serviceContextMock?.restore(); }); describe("createApp", () => { diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index 519a9cb..0e264d6 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -11,6 +11,19 @@ export { } from "shared"; export { analytics } from "./analytics"; export { CacheManager } from "./cache"; +export { + ServiceContext, + getExecutionContext, + getCurrentUserId, + getWorkspaceClient, + getWarehouseId, + getWorkspaceId, + isInUserContext, + isUserContext, + type ExecutionContext, + type ServiceContextState, + type UserContext, +} from "./context"; export { createApp } from "./core"; export { Plugin, toPlugin } from "./plugin"; export { server } from "./server"; @@ -24,4 +37,3 @@ export { TelemetryConfig, } from "./telemetry"; export { appKitTypesPlugin } from "./type-generator/vite-plugin"; -export { getRequestContext, RequestContext } from "./utils"; diff --git a/packages/appkit/src/plugin/interceptors/cache.ts b/packages/appkit/src/plugin/interceptors/cache.ts index 2ad9f73..7f9f31e 100644 --- a/packages/appkit/src/plugin/interceptors/cache.ts +++ b/packages/appkit/src/plugin/interceptors/cache.ts @@ -1,6 +1,6 @@ import type { CacheManager } from "../../cache"; import type { CacheConfig } from "shared"; -import type { ExecutionContext, ExecutionInterceptor } from "./types"; +import type { InterceptorContext, ExecutionInterceptor } from "./types"; // interceptor to handle caching logic export class CacheInterceptor implements ExecutionInterceptor { @@ -11,7 +11,7 @@ export class CacheInterceptor implements ExecutionInterceptor { async intercept( fn: () => Promise, - context: ExecutionContext, + context: InterceptorContext, ): Promise { // if cache disabled, ignore if (!this.config.enabled || !this.config.cacheKey?.length) { diff --git a/packages/appkit/src/plugin/interceptors/retry.ts b/packages/appkit/src/plugin/interceptors/retry.ts index cc62bb0..274a0d3 100644 --- a/packages/appkit/src/plugin/interceptors/retry.ts +++ b/packages/appkit/src/plugin/interceptors/retry.ts @@ -1,5 +1,5 @@ import type { RetryConfig } from "shared"; -import type { ExecutionContext, ExecutionInterceptor } from "./types"; +import type { InterceptorContext, ExecutionInterceptor } from "./types"; // interceptor to handle retry logic export class RetryInterceptor implements ExecutionInterceptor { @@ -15,7 +15,7 @@ export class RetryInterceptor implements ExecutionInterceptor { async intercept( fn: () => Promise, - context: ExecutionContext, + context: InterceptorContext, ): Promise { let lastError: Error | unknown; diff --git a/packages/appkit/src/plugin/interceptors/telemetry.ts b/packages/appkit/src/plugin/interceptors/telemetry.ts index 8335180..3c0b659 100644 --- a/packages/appkit/src/plugin/interceptors/telemetry.ts +++ b/packages/appkit/src/plugin/interceptors/telemetry.ts @@ -1,7 +1,7 @@ import type { ITelemetry, Span } from "../../telemetry"; import { SpanStatusCode } from "../../telemetry"; import type { TelemetryConfig } from "shared"; -import type { ExecutionContext, ExecutionInterceptor } from "./types"; +import type { InterceptorContext, ExecutionInterceptor } from "./types"; /** * Interceptor to automatically instrument plugin executions with telemetry spans. @@ -15,7 +15,7 @@ export class TelemetryInterceptor implements ExecutionInterceptor { async intercept( fn: () => Promise, - _context: ExecutionContext, + _context: InterceptorContext, ): Promise { const spanName = this.config?.spanName || "plugin.execute"; return this.telemetry.startActiveSpan( diff --git a/packages/appkit/src/plugin/interceptors/timeout.ts b/packages/appkit/src/plugin/interceptors/timeout.ts index e0ee42b..1f5a26a 100644 --- a/packages/appkit/src/plugin/interceptors/timeout.ts +++ b/packages/appkit/src/plugin/interceptors/timeout.ts @@ -1,4 +1,4 @@ -import type { ExecutionContext, ExecutionInterceptor } from "./types"; +import type { InterceptorContext, ExecutionInterceptor } from "./types"; // interceptor to handle timeout logic export class TimeoutInterceptor implements ExecutionInterceptor { @@ -6,7 +6,7 @@ export class TimeoutInterceptor implements ExecutionInterceptor { async intercept( fn: () => Promise, - context: ExecutionContext, + context: InterceptorContext, ): Promise { // create timeout signal const timeoutController = new AbortController(); diff --git a/packages/appkit/src/plugin/interceptors/types.ts b/packages/appkit/src/plugin/interceptors/types.ts index 1e10af3..633e38d 100644 --- a/packages/appkit/src/plugin/interceptors/types.ts +++ b/packages/appkit/src/plugin/interceptors/types.ts @@ -1,10 +1,13 @@ -export interface ExecutionContext { +/** + * Context passed through the interceptor chain. + * Contains signal for cancellation, metadata, and user identification. + */ +export interface InterceptorContext { signal?: AbortSignal; metadata?: Map; userKey: string; - asUser?: boolean; } export interface ExecutionInterceptor { - intercept(fn: () => Promise, context: ExecutionContext): Promise; + intercept(fn: () => Promise, context: InterceptorContext): Promise; } diff --git a/packages/appkit/src/plugin/plugin.ts b/packages/appkit/src/plugin/plugin.ts index 975d894..a890281 100644 --- a/packages/appkit/src/plugin/plugin.ts +++ b/packages/appkit/src/plugin/plugin.ts @@ -13,6 +13,12 @@ import type { } from "shared"; import { AppManager } from "../app"; import { CacheManager } from "../cache"; +import { + ServiceContext, + getCurrentUserId, + runInUserContext, + type UserContext, +} from "../context"; import { StreamManager } from "../stream"; import { type ITelemetry, @@ -26,10 +32,29 @@ import { RetryInterceptor } from "./interceptors/retry"; import { TelemetryInterceptor } from "./interceptors/telemetry"; import { TimeoutInterceptor } from "./interceptors/timeout"; import type { - ExecutionContext, + InterceptorContext, ExecutionInterceptor, } from "./interceptors/types"; +/** + * Methods that should not be proxied by asUser(). + * These are lifecycle/internal methods that don't make sense + * to execute in a user context. + */ +const EXCLUDED_FROM_PROXY = new Set([ + // Lifecycle methods + "setup", + "shutdown", + "validateEnv", + "injectRoutes", + "getEndpoints", + "abortActiveOperations", + // asUser itself - prevent chaining like .asUser().asUser() + "asUser", + // Internal methods + "constructor", +]); + export abstract class Plugin< TConfig extends BasePluginConfig = BasePluginConfig, > implements BasePlugin @@ -42,9 +67,6 @@ export abstract class Plugin< protected telemetry: ITelemetry; protected abstract envVars: string[]; - /** If the plugin requires the Databricks client to be set in the request context */ - requiresDatabricksClient = false; - /** Registered endpoints for this plugin */ private registeredEndpoints: PluginEndpointMap = {}; @@ -80,12 +102,107 @@ export abstract class Plugin< this.streamManager.abortAll(); } + /** + * Execute operations using the user's identity from the request. + * + * Returns a scoped instance of this plugin where all method calls + * will execute with the user's Databricks credentials instead of + * the service principal. + * + * @param req - The Express request containing the user token in headers + * @returns A scoped plugin instance that executes as the user + * @throws Error if user token is not available in request headers + * + * @example + * ```typescript + * // In route handler - execute query as the requesting user + * router.post('/users/me/query/:key', async (req, res) => { + * const result = await this.asUser(req).query(req.params.key) + * res.json(result) + * }) + * + * // Mixed execution in same handler + * router.post('/dashboard', async (req, res) => { + * const [systemData, userData] = await Promise.all([ + * this.getSystemStats(), // Service principal + * this.asUser(req).getUserPreferences(), // User context + * ]) + * res.json({ systemData, userData }) + * }) + * ``` + */ + asUser(req: express.Request): this { + const token = req.headers["x-forwarded-access-token"] as string; + const userId = req.headers["x-forwarded-user"] as string; + const isDev = process.env.NODE_ENV === "development"; + + // In local development, fall back to service principal + // since there's no user token available + if (!token && isDev) { + console.warn( + "[AppKit] asUser() called without user token in development mode. " + + "Using service principal.", + ); + + return this; + } + + if (!token) { + throw new Error( + "User token not available in request headers. " + + "Ensure the request has the x-forwarded-access-token header.", + ); + } + + if (!userId && !isDev) { + throw new Error( + "User ID not available in request headers. " + + "Ensure the request has the x-forwarded-user header.", + ); + } + + const effectiveUserId = userId || "dev-user"; + + const userContext = ServiceContext.createUserContext( + token, + effectiveUserId, + ); + + // Return a proxy that wraps method calls in user context + return this.createUserContextProxy(userContext); + } + + /** + * Creates a proxy that wraps method calls in a user context. + * This allows all plugin methods to automatically use the user's + * Databricks credentials. + */ + private createUserContextProxy(userContext: UserContext): this { + return new Proxy(this, { + get: (target, prop, receiver) => { + const value = Reflect.get(target, prop, receiver); + + if (typeof value !== "function") { + return value; + } + + if (typeof prop === "string" && EXCLUDED_FROM_PROXY.has(prop)) { + return value; + } + + return (...args: unknown[]) => { + return runInUserContext(userContext, () => value.apply(target, args)); + }; + }, + }) as this; + } + // streaming execution with interceptors protected async executeStream( res: IAppResponse, fn: StreamExecuteHandler, options: StreamExecutionSettings, - userKey: string, + userKey?: string, ) { // destructure options const { @@ -100,15 +217,18 @@ export abstract class Plugin< user: userConfig, }); + // Get user key from context if not provided + const effectiveUserKey = userKey ?? getCurrentUserId(); + const self = this; // wrapper function to ensure it returns a generator const asyncWrapperFn = async function* (streamSignal?: AbortSignal) { // build execution context - const context: ExecutionContext = { + const context: InterceptorContext = { signal: streamSignal, metadata: new Map(), - userKey: userKey, + userKey: effectiveUserKey, }; // build interceptors @@ -143,15 +263,18 @@ export abstract class Plugin< protected async execute( fn: (signal?: AbortSignal) => Promise, options: PluginExecutionSettings, - userKey: string, + userKey?: string, ): Promise { const executeConfig = this._buildExecutionConfig(options); const interceptors = this._buildInterceptors(executeConfig); - const context: ExecutionContext = { + // Get user key from context if not provided + const effectiveUserKey = userKey ?? getCurrentUserId(); + + const context: InterceptorContext = { metadata: new Map(), - userKey: userKey, + userKey: effectiveUserKey, }; try { @@ -232,7 +355,7 @@ export abstract class Plugin< private async _executeWithInterceptors( fn: (signal?: AbortSignal) => Promise, interceptors: ExecutionInterceptor[], - context: ExecutionContext, + context: InterceptorContext, ): Promise { // no interceptors, execute directly if (interceptors.length === 0) { diff --git a/packages/appkit/src/plugin/tests/cache.test.ts b/packages/appkit/src/plugin/tests/cache.test.ts index 3acf17f..7e01e77 100644 --- a/packages/appkit/src/plugin/tests/cache.test.ts +++ b/packages/appkit/src/plugin/tests/cache.test.ts @@ -1,7 +1,7 @@ import type { CacheConfig } from "shared"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { CacheInterceptor } from "../interceptors/cache"; -import type { ExecutionContext } from "../interceptors/types"; +import type { InterceptorContext } from "../interceptors/types"; vi.mock("../../telemetry", () => ({ TelemetryManager: { @@ -78,7 +78,7 @@ class MockCacheManager { describe("CacheInterceptor", () => { let cacheManager: MockCacheManager; - let context: ExecutionContext; + let context: InterceptorContext; beforeEach(() => { cacheManager = new MockCacheManager(); @@ -180,7 +180,7 @@ describe("CacheInterceptor", () => { enabled: true, cacheKey: ["query", "sales"], }; - const contextWithToken: ExecutionContext = { + const contextWithToken: InterceptorContext = { metadata: new Map(), userKey: "user1", }; @@ -213,7 +213,7 @@ describe("CacheInterceptor", () => { ); // Service account context - const context1: ExecutionContext = { + const context1: InterceptorContext = { metadata: new Map(), userKey: "service", }; @@ -221,7 +221,7 @@ describe("CacheInterceptor", () => { await interceptor.intercept(fn1, context1); // User context - const context2: ExecutionContext = { + const context2: InterceptorContext = { metadata: new Map(), userKey: "user1", }; diff --git a/packages/appkit/src/plugin/tests/plugin.test.ts b/packages/appkit/src/plugin/tests/plugin.test.ts index 1095992..2fedb65 100644 --- a/packages/appkit/src/plugin/tests/plugin.test.ts +++ b/packages/appkit/src/plugin/tests/plugin.test.ts @@ -8,12 +8,13 @@ import type { PluginExecuteConfig, IAppResponse, } from "shared"; -import { createMockTelemetry } from "@tools/test-helpers"; +import { createMockTelemetry, mockServiceContext } from "@tools/test-helpers"; import { validateEnv } from "../../utils"; import type express from "express"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; -import type { ExecutionContext } from "../interceptors/types"; +import type { InterceptorContext } from "../interceptors/types"; import { Plugin } from "../plugin"; +import { ServiceContext } from "../../context/service-context"; // Mock all dependencies vi.mock("../../app"); @@ -127,10 +128,14 @@ describe("Plugin", () => { let mockApp: AppManager; let mockStreamManager: StreamManager; let config: BasePluginConfig; + let serviceContextMock: Awaited>; - beforeEach(() => { + beforeEach(async () => { vi.useFakeTimers(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + mockTelemetry = createMockTelemetry(); mockCache = { @@ -170,6 +175,7 @@ describe("Plugin", () => { afterEach(() => { vi.useRealTimers(); vi.clearAllMocks(); + serviceContextMock?.restore(); }); describe("constructor", () => { @@ -499,7 +505,7 @@ describe("Plugin", () => { test("should execute function directly when no interceptors", async () => { const plugin = new TestPlugin(config); const mockFn = vi.fn().mockResolvedValue("direct-result"); - const context: ExecutionContext = { + const context: InterceptorContext = { metadata: new Map(), userKey: "test", }; @@ -514,7 +520,7 @@ describe("Plugin", () => { test("should chain interceptors correctly", async () => { const plugin = new TestPlugin(config); const mockFn = vi.fn().mockResolvedValue("chained-result"); - const context: ExecutionContext = { + const context: InterceptorContext = { metadata: new Map(), userKey: "test", }; @@ -541,9 +547,8 @@ describe("Plugin", () => { test("should pass context to interceptors", async () => { const plugin = new TestPlugin(config); const mockFn = vi.fn().mockResolvedValue("context-result"); - const context: ExecutionContext = { + const context: InterceptorContext = { metadata: new Map(), - asUser: true, signal: new AbortController().signal, userKey: "test", }; @@ -571,22 +576,6 @@ describe("Plugin", () => { }); }); - describe("requiresDatabricksClient", () => { - test("should default to false", () => { - const plugin = new TestPlugin(config); - expect(plugin.requiresDatabricksClient).toBe(false); - }); - - test("should allow plugins to override to true", () => { - class PluginWithDatabricksClient extends TestPlugin { - requiresDatabricksClient = true; - } - - const plugin = new PluginWithDatabricksClient(config); - expect(plugin.requiresDatabricksClient).toBe(true); - }); - }); - describe("integration scenarios", () => { test("should handle complex execution flow with all interceptors", async () => { const plugin = new TestPlugin({ diff --git a/packages/appkit/src/plugin/tests/retry.test.ts b/packages/appkit/src/plugin/tests/retry.test.ts index b3b8578..913a826 100644 --- a/packages/appkit/src/plugin/tests/retry.test.ts +++ b/packages/appkit/src/plugin/tests/retry.test.ts @@ -1,10 +1,10 @@ import type { RetryConfig } from "shared"; import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { RetryInterceptor } from "../interceptors/retry"; -import type { ExecutionContext } from "../interceptors/types"; +import type { InterceptorContext } from "../interceptors/types"; describe("RetryInterceptor", () => { - let context: ExecutionContext; + let context: InterceptorContext; beforeEach(() => { context = { @@ -137,7 +137,7 @@ describe("RetryInterceptor", () => { const interceptor = new RetryInterceptor(config); const abortController = new AbortController(); - const contextWithSignal: ExecutionContext = { + const contextWithSignal: InterceptorContext = { metadata: new Map(), signal: abortController.signal, userKey: "test", diff --git a/packages/appkit/src/plugin/tests/timeout.test.ts b/packages/appkit/src/plugin/tests/timeout.test.ts index d0e5f01..b065bcb 100644 --- a/packages/appkit/src/plugin/tests/timeout.test.ts +++ b/packages/appkit/src/plugin/tests/timeout.test.ts @@ -1,9 +1,9 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; import { TimeoutInterceptor } from "../interceptors/timeout"; -import type { ExecutionContext } from "../interceptors/types"; +import type { InterceptorContext } from "../interceptors/types"; describe("TimeoutInterceptor", () => { - let context: ExecutionContext; + let context: InterceptorContext; beforeEach(() => { context = { @@ -61,7 +61,7 @@ describe("TimeoutInterceptor", () => { test("should combine user signal with timeout signal", async () => { const userController = new AbortController(); - const contextWithSignal: ExecutionContext = { + const contextWithSignal: InterceptorContext = { metadata: new Map(), signal: userController.signal, userKey: "test", @@ -82,7 +82,7 @@ describe("TimeoutInterceptor", () => { test("should combine signals when user signal exists", async () => { const userController = new AbortController(); - const contextWithSignal: ExecutionContext = { + const contextWithSignal: InterceptorContext = { metadata: new Map(), signal: userController.signal, userKey: "test", @@ -105,7 +105,7 @@ describe("TimeoutInterceptor", () => { const userController = new AbortController(); userController.abort(new Error("Already aborted")); - const contextWithSignal: ExecutionContext = { + const contextWithSignal: InterceptorContext = { metadata: new Map(), signal: userController.signal, userKey: "test", diff --git a/packages/appkit/src/server/index.ts b/packages/appkit/src/server/index.ts index 454bd70..24810db 100644 --- a/packages/appkit/src/server/index.ts +++ b/packages/appkit/src/server/index.ts @@ -6,7 +6,6 @@ import express from "express"; import type { PluginPhase } from "shared"; import { Plugin, toPlugin } from "../plugin"; import { instrumentations } from "../telemetry"; -import { databricksClientMiddleware } from "../utils"; import { RemoteTunnelController } from "./remote-tunnel/remote-tunnel-controller"; import { StaticServer } from "./static-server"; import type { ServerConfig } from "./types"; @@ -183,10 +182,6 @@ export class ServerPlugin extends Plugin { if (plugin?.injectRoutes && typeof plugin.injectRoutes === "function") { const router = express.Router(); - // add databricks client middleware to the router if the plugin needs the request context - if (plugin.requiresDatabricksClient) - router.use(await databricksClientMiddleware()); - plugin.injectRoutes(router); const basePath = `/api/${plugin.name}`; diff --git a/packages/appkit/src/server/tests/server.integration.test.ts b/packages/appkit/src/server/tests/server.integration.test.ts index 494d20f..b7b8e37 100644 --- a/packages/appkit/src/server/tests/server.integration.test.ts +++ b/packages/appkit/src/server/tests/server.integration.test.ts @@ -1,32 +1,28 @@ -import { afterAll, beforeAll, describe, expect, test, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, test } from "vitest"; import type { Server } from "node:http"; +import { mockServiceContext, setupDatabricksEnv } from "@tools/test-helpers"; // Set required env vars BEFORE imports that use them process.env.DATABRICKS_APP_PORT = "8000"; process.env.FLASK_RUN_HOST = "0.0.0.0"; -// Mock databricks middleware to avoid auth requirements -vi.mock("../../utils", async (importOriginal) => { - const original = await importOriginal(); - return { - ...original, - databricksClientMiddleware: vi - .fn() - .mockResolvedValue((_req: any, _res: any, next: any) => next()), - }; -}); - import { createApp } from "../../core"; import { server as serverPlugin } from "../index"; import { Plugin, toPlugin } from "../../plugin"; +import { ServiceContext } from "../../context/service-context"; // Integration tests - actually start server and make HTTP requests describe("ServerPlugin Integration", () => { let server: Server; let baseUrl: string; + let serviceContextMock: Awaited>; const TEST_PORT = 9876; // Use non-standard port to avoid conflicts beforeAll(async () => { + setupDatabricksEnv(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + const app = await createApp({ plugins: [ serverPlugin({ @@ -47,6 +43,7 @@ describe("ServerPlugin Integration", () => { }); afterAll(async () => { + serviceContextMock?.restore(); if (server) { await new Promise((resolve, reject) => { server.close((err) => { @@ -91,9 +88,14 @@ describe("ServerPlugin Integration", () => { describe("ServerPlugin with custom plugin", () => { let server: Server; let baseUrl: string; + let serviceContextMock: Awaited>; const TEST_PORT = 9877; beforeAll(async () => { + setupDatabricksEnv(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + // Create a simple test plugin class TestPlugin extends Plugin { name = "test-plugin" as const; @@ -134,6 +136,7 @@ describe("ServerPlugin with custom plugin", () => { }); afterAll(async () => { + serviceContextMock?.restore(); if (server) { await new Promise((resolve, reject) => { server.close((err) => { @@ -170,9 +173,14 @@ describe("ServerPlugin with custom plugin", () => { describe("ServerPlugin with extend()", () => { let server: Server; let baseUrl: string; + let serviceContextMock: Awaited>; const TEST_PORT = 9878; beforeAll(async () => { + setupDatabricksEnv(); + ServiceContext.reset(); + serviceContextMock = await mockServiceContext(); + const app = await createApp({ plugins: [ serverPlugin({ @@ -198,6 +206,7 @@ describe("ServerPlugin with extend()", () => { }); afterAll(async () => { + serviceContextMock?.restore(); if (server) { await new Promise((resolve, reject) => { server.close((err) => { diff --git a/packages/appkit/src/server/tests/server.test.ts b/packages/appkit/src/server/tests/server.test.ts index a66d4cc..6898f62 100644 --- a/packages/appkit/src/server/tests/server.test.ts +++ b/packages/appkit/src/server/tests/server.test.ts @@ -91,9 +91,6 @@ vi.mock("../../cache", () => ({ })); vi.mock("../../utils", () => ({ - databricksClientMiddleware: vi - .fn() - .mockResolvedValue((_req: any, _res: any, next: any) => next()), validateEnv: vi.fn(), deepMerge: vi.fn((a, b) => ({ ...a, ...b })), })); @@ -260,14 +257,13 @@ describe("ServerPlugin", () => { ); }); - test("extendRoutes registers databricksClientMiddleware when plugin.requiresDatabricksClient=true", async () => { + test("extendRoutes registers plugin routes correctly", async () => { process.env.NODE_ENV = "production"; const injectRoutes = vi.fn(); const plugins: any = { - "needs-client": { - name: "needs-client", - requiresDatabricksClient: true, + "test-plugin": { + name: "test-plugin", injectRoutes, getEndpoints: vi.fn().mockReturnValue({}), }, @@ -280,10 +276,9 @@ describe("ServerPlugin", () => { expect(routerFn).toHaveBeenCalledTimes(1); const routerInstance = routerFn.mock.results[0].value; - expect(routerInstance.use).toHaveBeenCalledWith(expect.any(Function)); expect(injectRoutes).toHaveBeenCalledWith(routerInstance); expect(mockExpressApp.use).toHaveBeenCalledWith( - "/api/needs-client", + "/api/test-plugin", routerInstance, ); }); diff --git a/packages/appkit/src/telemetry/tests/telemetry-interceptor.test.ts b/packages/appkit/src/telemetry/tests/telemetry-interceptor.test.ts index be6815f..f78358e 100644 --- a/packages/appkit/src/telemetry/tests/telemetry-interceptor.test.ts +++ b/packages/appkit/src/telemetry/tests/telemetry-interceptor.test.ts @@ -2,13 +2,13 @@ import type { TelemetryConfig } from "shared"; import { SpanStatusCode, type Span } from "@opentelemetry/api"; import { beforeEach, describe, expect, test, vi } from "vitest"; import { TelemetryInterceptor } from "../../plugin/interceptors/telemetry"; -import type { ExecutionContext } from "../../plugin/interceptors/types"; +import type { InterceptorContext } from "../../plugin/interceptors/types"; import type { ITelemetry } from "../types"; describe("TelemetryInterceptor", () => { let mockTelemetry: ITelemetry; let mockSpan: Span; - let context: ExecutionContext; + let context: InterceptorContext; beforeEach(() => { mockSpan = { diff --git a/packages/appkit/src/utils/databricks-client-middleware.ts b/packages/appkit/src/utils/databricks-client-middleware.ts deleted file mode 100644 index 0783f1c..0000000 --- a/packages/appkit/src/utils/databricks-client-middleware.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { AsyncLocalStorage } from "node:async_hooks"; -import { - type ClientOptions, - type sql, - WorkspaceClient, -} from "@databricks/sdk-experimental"; -import type express from "express"; -import { - name as productName, - version as productVersion, -} from "../../package.json"; - -export type RequestContext = { - userDatabricksClient?: WorkspaceClient; - serviceDatabricksClient: WorkspaceClient; - userId: string; - userName?: string; - serviceUserId: string; - warehouseId: Promise; - workspaceId: Promise; -}; - -const asyncLocalStorage = new AsyncLocalStorage(); - -function getClientOptions(): ClientOptions { - const isDev = process.env.NODE_ENV === "development"; - const normalizedVersion = productVersion - .split(".") - .slice(0, 3) - .join(".") as ClientOptions["productVersion"]; - - return { - product: productName, - productVersion: normalizedVersion, - ...(isDev && { userAgentExtra: { mode: "dev" } }), - }; -} - -export async function databricksClientMiddleware(): Promise { - const serviceDatabricksClient = new WorkspaceClient({}, getClientOptions()); - const warehouseId = getWarehouseId(serviceDatabricksClient); - const workspaceId = getWorkspaceId(serviceDatabricksClient); - const serviceUserId = (await serviceDatabricksClient.currentUser.me()).id; - - if (!serviceUserId) { - throw new Error("Service user ID not found"); - } - - return async ( - req: express.Request, - res: express.Response, - next: express.NextFunction, - ) => { - const userToken = req.headers["x-forwarded-access-token"] as string; - let userDatabricksClient: WorkspaceClient | undefined; - const host = process.env.DATABRICKS_HOST; - if (userToken && host) { - userDatabricksClient = new WorkspaceClient( - { - token: userToken, - host, - authType: "pat", - }, - getClientOptions(), - ); - } else if (process.env.NODE_ENV === "development") { - // in local development service and no user token are the same - // TODO: use `databricks apps run-local` to fix this - userDatabricksClient = serviceDatabricksClient; - } - - let userName = req.headers["x-forwarded-user"] as string; - if (!userName && process.env.NODE_ENV !== "development") { - res.status(401).json({ error: "Unauthorized" }); - return; - } else { - userName = serviceUserId; - } - - return asyncLocalStorage.run( - { - userDatabricksClient, - serviceDatabricksClient, - warehouseId, - workspaceId, - userId: userName, - serviceUserId, - }, - async () => { - return next(); - }, - ); - }; -} - -/** - * Retrieve the request-scoped context populated by `databricksClientMiddleware`. - * Throws when invoked outside of a request lifecycle. - */ -export function getRequestContext(): RequestContext { - const store = asyncLocalStorage.getStore(); - if (!store) { - throw new Error("Request context not found"); - } - return store; -} - -/** - * Get the appropriate WorkspaceClient based on whether the request - * should be executed as the user or as the service principal. - * - * @param asUser - If true, returns user's WorkspaceClient (requires token passthrough) - * @throws Error if asUser is true but user token passthrough is not enabled - */ -export function getWorkspaceClient(asUser: boolean): WorkspaceClient { - const context = getRequestContext(); - - if (asUser) { - if (!context.userDatabricksClient) { - throw new Error( - `User token passthrough is not enabled for this workspace.`, - ); - } - return context.userDatabricksClient; - } - - return context.serviceDatabricksClient; -} - -async function getWorkspaceId( - workspaceClient: WorkspaceClient, -): Promise { - if (process.env.DATABRICKS_WORKSPACE_ID) { - return process.env.DATABRICKS_WORKSPACE_ID; - } - - const response = (await workspaceClient.apiClient.request({ - path: "/api/2.0/preview/scim/v2/Me", - method: "GET", - headers: new Headers(), - raw: false, - query: {}, - responseHeaders: ["x-databricks-org-id"], - })) as { "x-databricks-org-id": string }; - - if (!response["x-databricks-org-id"]) { - throw new Error("Workspace ID not found"); - } - - return response["x-databricks-org-id"]; -} - -async function getWarehouseId( - workspaceClient: WorkspaceClient, -): Promise { - if (process.env.DATABRICKS_WAREHOUSE_ID) { - return process.env.DATABRICKS_WAREHOUSE_ID; - } - - if (process.env.NODE_ENV === "development") { - const response = (await workspaceClient.apiClient.request({ - path: "/api/2.0/sql/warehouses", - method: "GET", - headers: new Headers(), - raw: false, - query: { - skip_cannot_use: "true", - }, - })) as { warehouses: sql.EndpointInfo[] }; - - const priorities: Record = { - RUNNING: 0, - STOPPED: 1, - STARTING: 2, - STOPPING: 3, - DELETED: 99, - DELETING: 99, - }; - - const warehouses = (response.warehouses || []).sort((a, b) => { - return ( - priorities[a.state as sql.State] - priorities[b.state as sql.State] - ); - }); - - if (response.warehouses.length === 0) { - throw new Error( - "Warehouse ID not found. Please configure the DATABRICKS_WAREHOUSE_ID environment variable.", - ); - } - - const firstWarehouse = warehouses[0]; - if ( - firstWarehouse.state === "DELETED" || - firstWarehouse.state === "DELETING" || - !firstWarehouse.id - ) { - throw new Error( - "Warehouse ID not found. Please configure the DATABRICKS_WAREHOUSE_ID environment variable.", - ); - } - - return firstWarehouse.id; - } - - throw new Error( - "Warehouse ID not found. Please configure the DATABRICKS_WAREHOUSE_ID environment variable.", - ); -} - -export type Request = express.Request; -export type Response = express.Response; diff --git a/packages/appkit/src/utils/index.ts b/packages/appkit/src/utils/index.ts index bd004f4..4f95495 100644 --- a/packages/appkit/src/utils/index.ts +++ b/packages/appkit/src/utils/index.ts @@ -1,4 +1,3 @@ -export * from "./databricks-client-middleware"; export * from "./env-validator"; export * from "./merge"; export * from "./vite-config-merge"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f9b08bc..5fcb992 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -254,6 +254,9 @@ importers: '@opentelemetry/semantic-conventions': specifier: ^1.38.0 version: 1.38.0 + '@types/semver': + specifier: ^7.7.1 + version: 7.7.1 dotenv: specifier: ^16.6.1 version: 16.6.1 @@ -263,6 +266,9 @@ importers: pg: specifier: ^8.16.3 version: 8.16.3 + semver: + specifier: ^7.7.3 + version: 7.7.3 shared: specifier: workspace:* version: link:../shared @@ -2864,8 +2870,8 @@ packages: resolution: {integrity: sha512-Z7x2dZOmznihvdvCvLKMl+nswtOSVxS2H2ocar+U9xx6iMfTp0VGIrX6a4xB1v80IwOPC7dT1LXIJrY70Xu3Jw==} engines: {node: ^20.19.0 || >=22.12.0} - '@oxc-project/types@0.106.0': - resolution: {integrity: sha512-QdsH3rZq480VnOHSHgPYOhjL8O8LBdcnSjM408BpPCCUc0JYYZPG9Gafl9i3OcGk/7137o+gweb4cCv3WAUykg==} + '@oxc-project/types@0.107.0': + resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==} '@oxc-project/types@0.93.0': resolution: {integrity: sha512-yNtwmWZIBtJsMr5TEfoZFDxIWV6OdScOpza/f5YxbqUMJk+j6QX3Cf3jgZShGEFYWQJ5j9mJ6jM0tZHu2J9Yrg==} @@ -3585,8 +3591,8 @@ packages: cpu: [arm64] os: [android] - '@rolldown/binding-android-arm64@1.0.0-beta.58': - resolution: {integrity: sha512-mWj5eE4Qc8TbPdGGaaLvBb9XfDPvE1EmZkJQgiGKwchkWH4oAJcRAKMTw7ZHnb1L+t7Ah41sBkAecaIsuUgsug==} + '@rolldown/binding-android-arm64@1.0.0-beta.59': + resolution: {integrity: sha512-6yLLgyswYwiCfls9+hoNFY9F8TQdwo15hpXDHzlAR0X/GojeKF+AuNcXjYNbOJ4zjl/5D6lliE8CbpB5t1OWIQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] @@ -3597,8 +3603,8 @@ packages: cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-arm64@1.0.0-beta.58': - resolution: {integrity: sha512-wFxUymI/5R8bH8qZFYDfAxAN9CyISEIYke+95oZPiv6EWo88aa5rskjVcCpKA532R+klFmdqjbbaD56GNmTF4Q==} + '@rolldown/binding-darwin-arm64@1.0.0-beta.59': + resolution: {integrity: sha512-hqGXRc162qCCIOAcHN2Cw4eXiVTwYsMFLOhAy1IG2CxY+dwc/l4Ga+dLPkLor3Ikqy5WDn+7kxHbbh6EmshEpQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] @@ -3609,8 +3615,8 @@ packages: cpu: [x64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.58': - resolution: {integrity: sha512-ybp3MkPj23VDV9PhtRwdU5qrGhlViWRV5BjKwO6epaSlUD5lW0WyY+roN3ZAzbma/9RrMTgZ/a/gtQq8YXOcqw==} + '@rolldown/binding-darwin-x64@1.0.0-beta.59': + resolution: {integrity: sha512-ezvvGuhteE15JmMhJW0wS7BaXmhwLy1YHeEwievYaPC1PgGD86wgBKfOpHr9tSKllAXbCe0BeeMvasscWLhKdA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] @@ -3621,8 +3627,8 @@ packages: cpu: [x64] os: [freebsd] - '@rolldown/binding-freebsd-x64@1.0.0-beta.58': - resolution: {integrity: sha512-Evxj3yh7FWvyklUYZa0qTVT9N2zX9TPDqGF056hl8hlCZ9/ndQ2xMv6uw9PD1VlLpukbsqL+/C6M0qwipL0QMg==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.59': + resolution: {integrity: sha512-4fhKVJiEYVd5n6no/mrL3LZ9kByfCGwmONOrdtvx8DJGDQhehH/q3RfhG3V/4jGKhpXgbDjpIjkkFdybCTcgew==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] @@ -3633,8 +3639,8 @@ packages: cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58': - resolution: {integrity: sha512-tYeXprDOrEgVHUbPXH6MPso4cM/c6RTkmJNICMQlYdki4hGMh92aj3yU6CKs+4X5gfG0yj5kVUw/L4M685SYag==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59': + resolution: {integrity: sha512-T3Y52sW6JAhvIqArBw+wtjNU1Ieaz4g0NBxyjSJoW971nZJBZygNlSYx78G4cwkCmo1dYTciTPDOnQygLV23pA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] @@ -3645,8 +3651,8 @@ packages: cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58': - resolution: {integrity: sha512-N78vmZzP6zG967Ohr+MasCjmKtis0geZ1SOVmxrA0/bklTQSzH5kHEjW5Qn+i1taFno6GEre1E40v0wuWsNOQw==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59': + resolution: {integrity: sha512-NIW40jQDSQap2KDdmm9z3B/4OzWJ6trf8dwx3FD74kcQb3v34ThsBFTtzE5KjDuxnxgUlV+DkAu+XgSMKrgufw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -3657,8 +3663,8 @@ packages: cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.58': - resolution: {integrity: sha512-l+p4QVtG72C7wI2SIkNQw/KQtSjuYwS3rV6AKcWrRBF62ClsFUcif5vLaZIEbPrCXu5OFRXigXFJnxYsVVZqdQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': + resolution: {integrity: sha512-CCKEk+H+8c0WGe/8n1E20n85Tq4Pv+HNAbjP1KfUXW+01aCWSMjU56ChNrM2tvHnXicfm7QRNoZyfY8cWh7jLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] @@ -3669,8 +3675,8 @@ packages: cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.58': - resolution: {integrity: sha512-urzJX0HrXxIh0FfxwWRjfPCMeInU9qsImLQxHBgLp5ivji1EEUnOfux8KxPPnRQthJyneBrN2LeqUix9DYrNaQ==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': + resolution: {integrity: sha512-VlfwJ/HCskPmQi8R0JuAFndySKVFX7yPhE658o27cjSDWWbXVtGkSbwaxstii7Q+3Rz87ZXN+HLnb1kd4R9Img==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -3681,8 +3687,8 @@ packages: cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.58': - resolution: {integrity: sha512-7ijfVK3GISnXIwq/1FZo+KyAUJjL3kWPJ7rViAL6MWeEBhEgRzJ0yEd9I8N9aut8Y8ab+EKFJyRNMWZuUBwQ0A==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': + resolution: {integrity: sha512-kuO92hTRyGy0Ts3Nsqll0rfO8eFsEJe9dGQGktkQnZ2hrJrDVN0y419dMgKy/gB2S2o7F2dpWhpfQOBehZPwVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] @@ -3693,8 +3699,8 @@ packages: cpu: [arm64] os: [openharmony] - '@rolldown/binding-openharmony-arm64@1.0.0-beta.58': - resolution: {integrity: sha512-/m7sKZCS+cUULbzyJTIlv8JbjNohxbpAOA6cM+lgWgqVzPee3U6jpwydrib328JFN/gF9A99IZEnuGYqEDJdww==} + '@rolldown/binding-openharmony-arm64@1.0.0-beta.59': + resolution: {integrity: sha512-PXAebvNL4sYfCqi8LdY4qyFRacrRoiPZLo3NoUmiTxm7MPtYYR8CNtBGNokqDmMuZIQIecRaD/jbmFAIDz7DxQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] @@ -3704,8 +3710,8 @@ packages: engines: {node: '>=14.0.0'} cpu: [wasm32] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.58': - resolution: {integrity: sha512-6SZk7zMgv+y3wFFQ9qE5P9NnRHcRsptL1ypmudD26PDY+PvFCvfHRkJNfclWnvacVGxjowr7JOL3a9fd1wWhUw==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.59': + resolution: {integrity: sha512-yJoklQg7XIZq8nAg0bbkEXcDK6sfpjxQGxpg2Nd6ERNtvg+eOaEBRgPww0BVTrYFQzje1pB5qPwC2VnJHT3koQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] @@ -3715,8 +3721,8 @@ packages: cpu: [arm64] os: [win32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58': - resolution: {integrity: sha512-sFqfYPnBZ6xBhMkadB7UD0yjEDRvs7ipR3nCggblN+N4ODCXY6qhg/bKL39+W+dgQybL7ErD4EGERVbW9DAWvg==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59': + resolution: {integrity: sha512-ljZ4+McmCbIuZwEBaoGtiG8Rq2nJjaXEnLEIx+usWetXn1ECjXY0LAhkELxOV6ytv4ensEmoJJ8nXg47hRMjlw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] @@ -3733,8 +3739,8 @@ packages: cpu: [x64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.58': - resolution: {integrity: sha512-AnFWJdAqB8+IDPcGrATYs67Kik/6tnndNJV2jGRmwlbeNiQQ8GhRJU8ETRlINfII0pqi9k4WWLnb00p1QCxw/Q==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.59': + resolution: {integrity: sha512-bMY4tTIwbdZljW+xe/ln1hvs0SRitahQSXfWtvgAtIzgSX9Ar7KqJzU7lRm33YTRFIHLULRi53yNjw9nJGd6uQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -3748,8 +3754,8 @@ packages: '@rolldown/pluginutils@1.0.0-beta.47': resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==} - '@rolldown/pluginutils@1.0.0-beta.58': - resolution: {integrity: sha512-qWhDs6yFGR5xDfdrwiSa3CWGIHxD597uGE/A9xGqytBjANvh4rLCTTkq7szhMV4+Ygh+PMS90KVJ8xWG/TkX4w==} + '@rolldown/pluginutils@1.0.0-beta.59': + resolution: {integrity: sha512-aoh6LAJRyhtazs98ydgpNOYstxUlsOV1KJXcpf/0c0vFcUA8uyd/hwKRhqE/AAPNqAho9RliGsvitCoOzREoVA==} '@rollup/rollup-android-arm-eabi@4.52.4': resolution: {integrity: sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==} @@ -4302,6 +4308,9 @@ packages: '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + '@types/semver@7.7.1': + resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/send@0.17.5': resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} @@ -9146,8 +9155,8 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rolldown@1.0.0-beta.58: - resolution: {integrity: sha512-v1FCjMZCan7f+xGAHBi+mqiE4MlH7I+SXEHSQSJoMOGNNB2UYtvMiejsq9YuUOiZjNeUeV/a21nSFbrUR+4ZCQ==} + rolldown@1.0.0-beta.59: + resolution: {integrity: sha512-Slm000Gd8/AO9z4Kxl4r8mp/iakrbAuJ1L+7ddpkNxgQ+Vf37WPvY63l3oeyZcfuPD1DRrUYBsRPIXSOhvOsmw==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -13928,7 +13937,7 @@ snapshots: '@oxc-project/runtime@0.92.0': {} - '@oxc-project/types@0.106.0': {} + '@oxc-project/types@0.107.0': {} '@oxc-project/types@0.93.0': {} @@ -14672,61 +14681,61 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-beta.41': optional: true - '@rolldown/binding-android-arm64@1.0.0-beta.58': + '@rolldown/binding-android-arm64@1.0.0-beta.59': optional: true '@rolldown/binding-darwin-arm64@1.0.0-beta.41': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-beta.58': + '@rolldown/binding-darwin-arm64@1.0.0-beta.59': optional: true '@rolldown/binding-darwin-x64@1.0.0-beta.41': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.58': + '@rolldown/binding-darwin-x64@1.0.0-beta.59': optional: true '@rolldown/binding-freebsd-x64@1.0.0-beta.41': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.58': + '@rolldown/binding-freebsd-x64@1.0.0-beta.59': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.58': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.59': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.58': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.59': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.58': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.59': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.58': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.59': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-beta.41': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.58': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.59': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-beta.41': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-beta.58': + '@rolldown/binding-openharmony-arm64@1.0.0-beta.59': optional: true '@rolldown/binding-wasm32-wasi@1.0.0-beta.41': @@ -14734,7 +14743,7 @@ snapshots: '@napi-rs/wasm-runtime': 1.0.7 optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.58': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.59': dependencies: '@napi-rs/wasm-runtime': 1.1.1 optional: true @@ -14742,7 +14751,7 @@ snapshots: '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.41': optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.58': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.59': optional: true '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.41': @@ -14751,7 +14760,7 @@ snapshots: '@rolldown/binding-win32-x64-msvc@1.0.0-beta.41': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.58': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.59': optional: true '@rolldown/pluginutils@1.0.0-beta.38': {} @@ -14760,7 +14769,7 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.47': {} - '@rolldown/pluginutils@1.0.0-beta.58': {} + '@rolldown/pluginutils@1.0.0-beta.59': {} '@rollup/rollup-android-arm-eabi@4.52.4': optional: true @@ -15329,6 +15338,8 @@ snapshots: dependencies: '@types/node': 24.10.1 + '@types/semver@7.7.1': {} + '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 @@ -20955,7 +20966,7 @@ snapshots: dependencies: glob: 10.5.0 - rolldown-plugin-dts@0.16.11(rolldown@1.0.0-beta.58)(typescript@5.9.3): + rolldown-plugin-dts@0.16.11(rolldown@1.0.0-beta.59)(typescript@5.9.3): dependencies: '@babel/generator': 7.28.3 '@babel/parser': 7.28.5 @@ -20966,7 +20977,7 @@ snapshots: dts-resolver: 2.1.2 get-tsconfig: 4.12.0 magic-string: 0.30.19 - rolldown: 1.0.0-beta.58 + rolldown: 1.0.0-beta.59 optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -21030,24 +21041,24 @@ snapshots: '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.41 '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.41 - rolldown@1.0.0-beta.58: + rolldown@1.0.0-beta.59: dependencies: - '@oxc-project/types': 0.106.0 - '@rolldown/pluginutils': 1.0.0-beta.58 + '@oxc-project/types': 0.107.0 + '@rolldown/pluginutils': 1.0.0-beta.59 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-beta.58 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.58 - '@rolldown/binding-darwin-x64': 1.0.0-beta.58 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.58 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.58 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.58 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.58 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.58 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.58 - '@rolldown/binding-openharmony-arm64': 1.0.0-beta.58 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.58 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.58 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.58 + '@rolldown/binding-android-arm64': 1.0.0-beta.59 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.59 + '@rolldown/binding-darwin-x64': 1.0.0-beta.59 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.59 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.59 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.59 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.59 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.59 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.59 + '@rolldown/binding-openharmony-arm64': 1.0.0-beta.59 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.59 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.59 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.59 rollup@4.52.4: dependencies: @@ -21676,8 +21687,8 @@ snapshots: diff: 8.0.2 empathic: 2.0.0 hookable: 5.5.3 - rolldown: 1.0.0-beta.58 - rolldown-plugin-dts: 0.16.11(rolldown@1.0.0-beta.58)(typescript@5.9.3) + rolldown: 1.0.0-beta.59 + rolldown-plugin-dts: 0.16.11(rolldown@1.0.0-beta.59)(typescript@5.9.3) semver: 7.7.3 tinyexec: 1.0.1 tinyglobby: 0.2.15 diff --git a/tools/test-helpers.ts b/tools/test-helpers.ts index dd6fa4c..d80072d 100644 --- a/tools/test-helpers.ts +++ b/tools/test-helpers.ts @@ -3,7 +3,8 @@ import type { InstrumentConfig, ITelemetry, } from "../packages/appkit/src/telemetry/types"; -import type { RequestContext } from "../packages/appkit/src/utils/databricks-client-middleware"; +import type { ServiceContextState } from "../packages/appkit/src/context/service-context"; +import type { UserContext } from "../packages/appkit/src/context/user-context"; import { vi } from "vitest"; import type { SpanOptions, Span } from "@opentelemetry/api"; @@ -175,13 +176,28 @@ export function setupDatabricksEnv(overrides: Record = {}) { } /** - * Runs a test function within a request context + * Context options for running tests with mocked service/user context */ -export async function runWithRequestContext( - fn: () => T | Promise, - context?: Partial, -): Promise { - const mockWorkspaceClient = { +export interface TestContextOptions { + /** Mock WorkspaceClient for service principal operations */ + serviceDatabricksClient?: any; + /** Mock WorkspaceClient for user operations */ + userDatabricksClient?: any; + /** User ID for user context */ + userId?: string; + /** Service user ID */ + serviceUserId?: string; + /** Warehouse ID */ + warehouseId?: string; + /** Workspace ID */ + workspaceId?: string; +} + +/** + * Creates a default mock WorkspaceClient for testing + */ +export function createMockWorkspaceClient() { + return { statementExecution: { executeStatement: vi.fn().mockResolvedValue({ status: { state: "SUCCEEDED" }, @@ -189,43 +205,110 @@ export async function runWithRequestContext( }), }, }; +} - const defaultContext: RequestContext = { - userDatabricksClient: mockWorkspaceClient as any, - serviceDatabricksClient: mockWorkspaceClient as any, - userId: "test-user", - serviceUserId: "test-service-user", - warehouseId: Promise.resolve("test-warehouse-id"), - workspaceId: Promise.resolve("test-workspace-id"), - ...context, +/** + * Creates a mock ServiceContext for testing. + * Call this in beforeEach to set up the ServiceContext mock. + */ +export function createMockServiceContext(options: TestContextOptions = {}) { + const mockWorkspaceClient = createMockWorkspaceClient(); + + const serviceContext: ServiceContextState = { + client: (options.serviceDatabricksClient || mockWorkspaceClient) as any, + serviceUserId: options.serviceUserId || "test-service-user", + warehouseId: Promise.resolve(options.warehouseId || "test-warehouse-id"), + workspaceId: Promise.resolve(options.workspaceId || "test-workspace-id"), }; - // Use vi.spyOn to mock getRequestContext and getWorkspaceClient - const utilsModule = await import( - "../packages/appkit/src/utils/databricks-client-middleware" + return serviceContext; +} + +/** + * Creates a mock UserContext for testing. + */ +export function createMockUserContext( + options: TestContextOptions = {}, +): UserContext { + const mockWorkspaceClient = createMockWorkspaceClient(); + + return { + client: (options.userDatabricksClient || mockWorkspaceClient) as any, + userId: options.userId || "test-user", + warehouseId: Promise.resolve(options.warehouseId || "test-warehouse-id"), + workspaceId: Promise.resolve(options.workspaceId || "test-workspace-id"), + isUserContext: true, + }; +} + +/** + * Mocks the ServiceContext singleton for testing. + * Should be called in beforeEach. + * + * @returns Object with spies that can be used to restore the mocks + */ +export async function mockServiceContext(options: TestContextOptions = {}) { + const serviceContext = createMockServiceContext(options); + + const contextModule = await import( + "../packages/appkit/src/context/service-context" ); - const contextSpy = vi - .spyOn(utilsModule, "getRequestContext") - .mockReturnValue(defaultContext); - - // Also mock getWorkspaceClient to return the appropriate client based on asUser - const workspaceClientSpy = vi - .spyOn(utilsModule, "getWorkspaceClient") - .mockImplementation((asUser: boolean) => { - if (asUser) { - if (!defaultContext.userDatabricksClient) { - throw new Error("User token passthrough is not enabled"); - } - return defaultContext.userDatabricksClient; - } - return defaultContext.serviceDatabricksClient; + const getSpy = vi + .spyOn(contextModule.ServiceContext, "get") + .mockReturnValue(serviceContext); + + const initSpy = vi + .spyOn(contextModule.ServiceContext, "initialize") + .mockResolvedValue(serviceContext); + + const isInitializedSpy = vi + .spyOn(contextModule.ServiceContext, "isInitialized") + .mockReturnValue(true); + + // Mock createUserContext to return a test user context + const createUserContextSpy = vi + .spyOn(contextModule.ServiceContext, "createUserContext") + .mockImplementation((_token: string, userId: string, userName?: string) => { + const mockWorkspaceClient = createMockWorkspaceClient(); + return { + client: (options.userDatabricksClient || mockWorkspaceClient) as any, + userId, + userName, + warehouseId: serviceContext.warehouseId, + workspaceId: serviceContext.workspaceId, + isUserContext: true, + }; }); + return { + serviceContext, + getSpy, + initSpy, + isInitializedSpy, + createUserContextSpy, + restore: () => { + getSpy.mockRestore(); + initSpy.mockRestore(); + isInitializedSpy.mockRestore(); + createUserContextSpy.mockRestore(); + }, + }; +} + +/** + * Runs a test function within a mocked service context. + * This sets up the ServiceContext mock, runs the function, and restores the mock. + */ +export async function runWithRequestContext( + fn: () => T | Promise, + context?: TestContextOptions, +): Promise { + const mocks = await mockServiceContext(context); + try { return await fn(); } finally { - contextSpy.mockRestore(); - workspaceClientSpy.mockRestore(); + mocks.restore(); } } From 8656334edfc6b65f9e8504af348ad23cb07f4bed Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Thu, 8 Jan 2026 16:09:41 +0100 Subject: [PATCH 2/4] chore: fixup --- docs/docs/api/appkit/Class.Plugin.md | 113 +++++++++----- docs/docs/api/appkit/Class.ServiceContext.md | 138 ++++++++++++++++++ docs/docs/api/appkit/Function.createApp.md | 2 +- .../api/appkit/Function.getCurrentUserId.md | 15 ++ .../appkit/Function.getExecutionContext.md | 20 +++ .../api/appkit/Function.getRequestContext.md | 14 -- .../api/appkit/Function.getWarehouseId.md | 13 ++ .../api/appkit/Function.getWorkspaceClient.md | 13 ++ .../api/appkit/Function.getWorkspaceId.md | 13 ++ .../api/appkit/Function.isInUserContext.md | 13 ++ .../docs/api/appkit/Function.isUserContext.md | 19 +++ .../appkit/Interface.ServiceContextState.md | 54 +++++++ docs/docs/api/appkit/Interface.UserContext.md | 78 ++++++++++ .../api/appkit/TypeAlias.ExecutionContext.md | 11 ++ .../api/appkit/TypeAlias.RequestContext.md | 85 ----------- docs/docs/api/appkit/index.md | 13 +- docs/docs/api/appkit/typedoc-sidebar.ts | 57 +++++++- packages/appkit/src/index.ts | 7 +- 18 files changed, 528 insertions(+), 150 deletions(-) create mode 100644 docs/docs/api/appkit/Class.ServiceContext.md create mode 100644 docs/docs/api/appkit/Function.getCurrentUserId.md create mode 100644 docs/docs/api/appkit/Function.getExecutionContext.md delete mode 100644 docs/docs/api/appkit/Function.getRequestContext.md create mode 100644 docs/docs/api/appkit/Function.getWarehouseId.md create mode 100644 docs/docs/api/appkit/Function.getWorkspaceClient.md create mode 100644 docs/docs/api/appkit/Function.getWorkspaceId.md create mode 100644 docs/docs/api/appkit/Function.isInUserContext.md create mode 100644 docs/docs/api/appkit/Function.isUserContext.md create mode 100644 docs/docs/api/appkit/Interface.ServiceContextState.md create mode 100644 docs/docs/api/appkit/Interface.UserContext.md create mode 100644 docs/docs/api/appkit/TypeAlias.ExecutionContext.md delete mode 100644 docs/docs/api/appkit/TypeAlias.RequestContext.md diff --git a/docs/docs/api/appkit/Class.Plugin.md b/docs/docs/api/appkit/Class.Plugin.md index 29ef6f1..5e0c447 100644 --- a/docs/docs/api/appkit/Class.Plugin.md +++ b/docs/docs/api/appkit/Class.Plugin.md @@ -1,6 +1,6 @@ # Abstract Class: Plugin\ -Defined in: [appkit/src/plugin/plugin.ts:33](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L33) +Defined in: [appkit/src/plugin/plugin.ts:58](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L58) ## Type Parameters @@ -20,7 +20,7 @@ Defined in: [appkit/src/plugin/plugin.ts:33](https://github.com/databricks/appki new Plugin(config: TConfig): Plugin; ``` -Defined in: [appkit/src/plugin/plugin.ts:54](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L54) +Defined in: [appkit/src/plugin/plugin.ts:76](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L76) #### Parameters @@ -40,7 +40,7 @@ Defined in: [appkit/src/plugin/plugin.ts:54](https://github.com/databricks/appki protected app: AppManager; ``` -Defined in: [appkit/src/plugin/plugin.ts:39](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L39) +Defined in: [appkit/src/plugin/plugin.ts:64](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L64) *** @@ -50,7 +50,7 @@ Defined in: [appkit/src/plugin/plugin.ts:39](https://github.com/databricks/appki protected cache: CacheManager; ``` -Defined in: [appkit/src/plugin/plugin.ts:38](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L38) +Defined in: [appkit/src/plugin/plugin.ts:63](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L63) *** @@ -60,7 +60,7 @@ Defined in: [appkit/src/plugin/plugin.ts:38](https://github.com/databricks/appki protected config: TConfig; ``` -Defined in: [appkit/src/plugin/plugin.ts:54](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L54) +Defined in: [appkit/src/plugin/plugin.ts:76](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L76) *** @@ -70,7 +70,7 @@ Defined in: [appkit/src/plugin/plugin.ts:54](https://github.com/databricks/appki protected devFileReader: DevFileReader; ``` -Defined in: [appkit/src/plugin/plugin.ts:40](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L40) +Defined in: [appkit/src/plugin/plugin.ts:65](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L65) *** @@ -80,7 +80,7 @@ Defined in: [appkit/src/plugin/plugin.ts:40](https://github.com/databricks/appki abstract protected envVars: string[]; ``` -Defined in: [appkit/src/plugin/plugin.ts:43](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L43) +Defined in: [appkit/src/plugin/plugin.ts:68](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L68) *** @@ -90,7 +90,7 @@ Defined in: [appkit/src/plugin/plugin.ts:43](https://github.com/databricks/appki protected isReady: boolean = false; ``` -Defined in: [appkit/src/plugin/plugin.ts:37](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L37) +Defined in: [appkit/src/plugin/plugin.ts:62](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L62) *** @@ -100,7 +100,7 @@ Defined in: [appkit/src/plugin/plugin.ts:37](https://github.com/databricks/appki name: string; ``` -Defined in: [appkit/src/plugin/plugin.ts:52](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L52) +Defined in: [appkit/src/plugin/plugin.ts:74](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L74) #### Implementation of @@ -110,25 +110,13 @@ BasePlugin.name *** -### requiresDatabricksClient - -```ts -requiresDatabricksClient: boolean = false; -``` - -Defined in: [appkit/src/plugin/plugin.ts:46](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L46) - -If the plugin requires the Databricks client to be set in the request context - -*** - ### streamManager ```ts protected streamManager: StreamManager; ``` -Defined in: [appkit/src/plugin/plugin.ts:41](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L41) +Defined in: [appkit/src/plugin/plugin.ts:66](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L66) *** @@ -138,7 +126,7 @@ Defined in: [appkit/src/plugin/plugin.ts:41](https://github.com/databricks/appki protected telemetry: ITelemetry; ``` -Defined in: [appkit/src/plugin/plugin.ts:42](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L42) +Defined in: [appkit/src/plugin/plugin.ts:67](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L67) *** @@ -148,7 +136,7 @@ Defined in: [appkit/src/plugin/plugin.ts:42](https://github.com/databricks/appki static phase: PluginPhase = "normal"; ``` -Defined in: [appkit/src/plugin/plugin.ts:51](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L51) +Defined in: [appkit/src/plugin/plugin.ts:73](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L73) ## Methods @@ -158,7 +146,7 @@ Defined in: [appkit/src/plugin/plugin.ts:51](https://github.com/databricks/appki abortActiveOperations(): void; ``` -Defined in: [appkit/src/plugin/plugin.ts:79](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L79) +Defined in: [appkit/src/plugin/plugin.ts:101](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L101) #### Returns @@ -172,16 +160,67 @@ BasePlugin.abortActiveOperations *** +### asUser() + +```ts +asUser(req: Request): this; +``` + +Defined in: [appkit/src/plugin/plugin.ts:134](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L134) + +Execute operations using the user's identity from the request. + +Returns a scoped instance of this plugin where all method calls +will execute with the user's Databricks credentials instead of +the service principal. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `req` | `Request` | The Express request containing the user token in headers | + +#### Returns + +`this` + +A scoped plugin instance that executes as the user + +#### Throws + +Error if user token is not available in request headers + +#### Example + +```typescript +// In route handler - execute query as the requesting user +router.post('/users/me/query/:key', async (req, res) => { + const result = await this.asUser(req).query(req.params.key) + res.json(result) +}) + +// Mixed execution in same handler +router.post('/dashboard', async (req, res) => { + const [systemData, userData] = await Promise.all([ + this.getSystemStats(), // Service principal + this.asUser(req).getUserPreferences(), // User context + ]) + res.json({ systemData, userData }) +}) +``` + +*** + ### execute() ```ts protected execute( fn: (signal?: AbortSignal) => Promise, options: PluginExecutionSettings, -userKey: string): Promise; +userKey?: string): Promise; ``` -Defined in: [appkit/src/plugin/plugin.ts:143](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L143) +Defined in: [appkit/src/plugin/plugin.ts:263](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L263) #### Type Parameters @@ -195,7 +234,7 @@ Defined in: [appkit/src/plugin/plugin.ts:143](https://github.com/databricks/appk | ------ | ------ | | `fn` | (`signal?`: `AbortSignal`) => `Promise`\<`T`\> | | `options` | `PluginExecutionSettings` | -| `userKey` | `string` | +| `userKey?` | `string` | #### Returns @@ -210,10 +249,10 @@ protected executeStream( res: IAppResponse, fn: StreamExecuteHandler, options: StreamExecutionSettings, -userKey: string): Promise; +userKey?: string): Promise; ``` -Defined in: [appkit/src/plugin/plugin.ts:84](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L84) +Defined in: [appkit/src/plugin/plugin.ts:201](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L201) #### Type Parameters @@ -228,7 +267,7 @@ Defined in: [appkit/src/plugin/plugin.ts:84](https://github.com/databricks/appki | `res` | `IAppResponse` | | `fn` | `StreamExecuteHandler`\<`T`\> | | `options` | [`StreamExecutionSettings`](Interface.StreamExecutionSettings.md) | -| `userKey` | `string` | +| `userKey?` | `string` | #### Returns @@ -242,7 +281,7 @@ Defined in: [appkit/src/plugin/plugin.ts:84](https://github.com/databricks/appki getEndpoints(): PluginEndpointMap; ``` -Defined in: [appkit/src/plugin/plugin.ts:75](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L75) +Defined in: [appkit/src/plugin/plugin.ts:97](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L97) #### Returns @@ -262,7 +301,7 @@ BasePlugin.getEndpoints injectRoutes(_: Router): void; ``` -Defined in: [appkit/src/plugin/plugin.ts:69](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L69) +Defined in: [appkit/src/plugin/plugin.ts:91](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L91) #### Parameters @@ -288,7 +327,7 @@ BasePlugin.injectRoutes protected registerEndpoint(name: string, path: string): void; ``` -Defined in: [appkit/src/plugin/plugin.ts:165](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L165) +Defined in: [appkit/src/plugin/plugin.ts:288](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L288) #### Parameters @@ -309,7 +348,7 @@ Defined in: [appkit/src/plugin/plugin.ts:165](https://github.com/databricks/appk protected route<_TResponse>(router: Router, config: RouteConfig): void; ``` -Defined in: [appkit/src/plugin/plugin.ts:169](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L169) +Defined in: [appkit/src/plugin/plugin.ts:292](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L292) #### Type Parameters @@ -336,7 +375,7 @@ Defined in: [appkit/src/plugin/plugin.ts:169](https://github.com/databricks/appk setup(): Promise; ``` -Defined in: [appkit/src/plugin/plugin.ts:73](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L73) +Defined in: [appkit/src/plugin/plugin.ts:95](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L95) #### Returns @@ -356,7 +395,7 @@ BasePlugin.setup validateEnv(): void; ``` -Defined in: [appkit/src/plugin/plugin.ts:65](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L65) +Defined in: [appkit/src/plugin/plugin.ts:87](https://github.com/databricks/appkit/blob/main/packages/appkit/src/plugin/plugin.ts#L87) #### Returns diff --git a/docs/docs/api/appkit/Class.ServiceContext.md b/docs/docs/api/appkit/Class.ServiceContext.md new file mode 100644 index 0000000..669d256 --- /dev/null +++ b/docs/docs/api/appkit/Class.ServiceContext.md @@ -0,0 +1,138 @@ +# Class: ServiceContext + +Defined in: [appkit/src/context/service-context.ts:48](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L48) + +ServiceContext is a singleton that manages the service principal's +WorkspaceClient and shared resources like warehouse/workspace IDs. + +It's initialized once at app startup and provides the foundation +for both service principal and user context execution. + +## Constructors + +### Constructor + +```ts +new ServiceContext(): ServiceContext; +``` + +#### Returns + +`ServiceContext` + +## Methods + +### createUserContext() + +```ts +static createUserContext( + token: string, + userId: string, + userName?: string): UserContext; +``` + +Defined in: [appkit/src/context/service-context.ts:98](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L98) + +Create a user context from request headers. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `token` | `string` | The user's access token from x-forwarded-access-token header | +| `userId` | `string` | The user's ID from x-forwarded-user header | +| `userName?` | `string` | Optional user name | + +#### Returns + +[`UserContext`](Interface.UserContext.md) + +#### Throws + +Error if token is not provided + +*** + +### get() + +```ts +static get(): ServiceContextState; +``` + +Defined in: [appkit/src/context/service-context.ts:74](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L74) + +Get the initialized service context. + +#### Returns + +[`ServiceContextState`](Interface.ServiceContextState.md) + +#### Throws + +Error if not initialized + +*** + +### getClientOptions() + +```ts +static getClientOptions(): ClientOptions; +``` + +Defined in: [appkit/src/context/service-context.ts:142](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L142) + +Get the client options for WorkspaceClient. +Exposed for testing purposes. + +#### Returns + +`ClientOptions` + +*** + +### initialize() + +```ts +static initialize(): Promise; +``` + +Defined in: [appkit/src/context/service-context.ts:56](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L56) + +Initialize the service context. Should be called once at app startup. +Safe to call multiple times - will return the same instance. + +#### Returns + +`Promise`\<[`ServiceContextState`](Interface.ServiceContextState.md)\> + +*** + +### isInitialized() + +```ts +static isInitialized(): boolean; +``` + +Defined in: [appkit/src/context/service-context.ts:86](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L86) + +Check if the service context has been initialized. + +#### Returns + +`boolean` + +*** + +### reset() + +```ts +static reset(): void; +``` + +Defined in: [appkit/src/context/service-context.ts:249](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L249) + +Reset the service context. Only for testing purposes. + +#### Returns + +`void` diff --git a/docs/docs/api/appkit/Function.createApp.md b/docs/docs/api/appkit/Function.createApp.md index 8d42369..63c630b 100644 --- a/docs/docs/api/appkit/Function.createApp.md +++ b/docs/docs/api/appkit/Function.createApp.md @@ -8,7 +8,7 @@ function createApp(config: { }): Promise>; ``` -Defined in: [appkit/src/core/appkit.ts:127](https://github.com/databricks/appkit/blob/main/packages/appkit/src/core/appkit.ts#L127) +Defined in: [appkit/src/core/appkit.ts:133](https://github.com/databricks/appkit/blob/main/packages/appkit/src/core/appkit.ts#L133) Bootstraps AppKit with the provided configuration. diff --git a/docs/docs/api/appkit/Function.getCurrentUserId.md b/docs/docs/api/appkit/Function.getCurrentUserId.md new file mode 100644 index 0000000..3c173e4 --- /dev/null +++ b/docs/docs/api/appkit/Function.getCurrentUserId.md @@ -0,0 +1,15 @@ +# Function: getCurrentUserId() + +```ts +function getCurrentUserId(): string; +``` + +Defined in: [appkit/src/context/execution-context.ts:48](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L48) + +Get the current user ID for cache keying and telemetry. + +Returns the user ID if in user context, otherwise the service user ID. + +## Returns + +`string` diff --git a/docs/docs/api/appkit/Function.getExecutionContext.md b/docs/docs/api/appkit/Function.getExecutionContext.md new file mode 100644 index 0000000..0239a34 --- /dev/null +++ b/docs/docs/api/appkit/Function.getExecutionContext.md @@ -0,0 +1,20 @@ +# Function: getExecutionContext() + +```ts +function getExecutionContext(): ExecutionContext; +``` + +Defined in: [appkit/src/context/execution-context.ts:35](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L35) + +Get the current execution context. + +- If running inside a user context (via asUser), returns the user context +- Otherwise, returns the service context + +## Returns + +[`ExecutionContext`](TypeAlias.ExecutionContext.md) + +## Throws + +Error if ServiceContext is not initialized diff --git a/docs/docs/api/appkit/Function.getRequestContext.md b/docs/docs/api/appkit/Function.getRequestContext.md deleted file mode 100644 index 2878075..0000000 --- a/docs/docs/api/appkit/Function.getRequestContext.md +++ /dev/null @@ -1,14 +0,0 @@ -# Function: getRequestContext() - -```ts -function getRequestContext(): RequestContext; -``` - -Defined in: [appkit/src/utils/databricks-client-middleware.ts:100](https://github.com/databricks/appkit/blob/main/packages/appkit/src/utils/databricks-client-middleware.ts#L100) - -Retrieve the request-scoped context populated by `databricksClientMiddleware`. -Throws when invoked outside of a request lifecycle. - -## Returns - -[`RequestContext`](TypeAlias.RequestContext.md) diff --git a/docs/docs/api/appkit/Function.getWarehouseId.md b/docs/docs/api/appkit/Function.getWarehouseId.md new file mode 100644 index 0000000..41c276e --- /dev/null +++ b/docs/docs/api/appkit/Function.getWarehouseId.md @@ -0,0 +1,13 @@ +# Function: getWarehouseId() + +```ts +function getWarehouseId(): Promise; +``` + +Defined in: [appkit/src/context/execution-context.ts:66](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L66) + +Get the warehouse ID promise. + +## Returns + +`Promise`\<`string`\> diff --git a/docs/docs/api/appkit/Function.getWorkspaceClient.md b/docs/docs/api/appkit/Function.getWorkspaceClient.md new file mode 100644 index 0000000..ea01543 --- /dev/null +++ b/docs/docs/api/appkit/Function.getWorkspaceClient.md @@ -0,0 +1,13 @@ +# Function: getWorkspaceClient() + +```ts +function getWorkspaceClient(): WorkspaceClient; +``` + +Defined in: [appkit/src/context/execution-context.ts:59](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L59) + +Get the WorkspaceClient for the current execution context. + +## Returns + +`WorkspaceClient` diff --git a/docs/docs/api/appkit/Function.getWorkspaceId.md b/docs/docs/api/appkit/Function.getWorkspaceId.md new file mode 100644 index 0000000..1f8fa8b --- /dev/null +++ b/docs/docs/api/appkit/Function.getWorkspaceId.md @@ -0,0 +1,13 @@ +# Function: getWorkspaceId() + +```ts +function getWorkspaceId(): Promise; +``` + +Defined in: [appkit/src/context/execution-context.ts:73](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L73) + +Get the workspace ID promise. + +## Returns + +`Promise`\<`string`\> diff --git a/docs/docs/api/appkit/Function.isInUserContext.md b/docs/docs/api/appkit/Function.isInUserContext.md new file mode 100644 index 0000000..88e0708 --- /dev/null +++ b/docs/docs/api/appkit/Function.isInUserContext.md @@ -0,0 +1,13 @@ +# Function: isInUserContext() + +```ts +function isInUserContext(): boolean; +``` + +Defined in: [appkit/src/context/execution-context.ts:80](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L80) + +Check if currently running in a user context. + +## Returns + +`boolean` diff --git a/docs/docs/api/appkit/Function.isUserContext.md b/docs/docs/api/appkit/Function.isUserContext.md new file mode 100644 index 0000000..581b604 --- /dev/null +++ b/docs/docs/api/appkit/Function.isUserContext.md @@ -0,0 +1,19 @@ +# Function: isUserContext() + +```ts +function isUserContext(ctx: ExecutionContext): ctx is UserContext; +``` + +Defined in: [appkit/src/context/user-context.ts:30](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L30) + +Check if an execution context is a user context. + +## Parameters + +| Parameter | Type | +| ------ | ------ | +| `ctx` | [`ExecutionContext`](TypeAlias.ExecutionContext.md) | + +## Returns + +`ctx is UserContext` diff --git a/docs/docs/api/appkit/Interface.ServiceContextState.md b/docs/docs/api/appkit/Interface.ServiceContextState.md new file mode 100644 index 0000000..af69fe5 --- /dev/null +++ b/docs/docs/api/appkit/Interface.ServiceContextState.md @@ -0,0 +1,54 @@ +# Interface: ServiceContextState + +Defined in: [appkit/src/context/service-context.ts:17](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L17) + +Service context holds the service principal client and shared resources. +This is initialized once at app startup and shared across all requests. + +## Properties + +### client + +```ts +client: WorkspaceClient; +``` + +Defined in: [appkit/src/context/service-context.ts:19](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L19) + +WorkspaceClient authenticated as the service principal + +*** + +### serviceUserId + +```ts +serviceUserId: string; +``` + +Defined in: [appkit/src/context/service-context.ts:21](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L21) + +The service principal's user ID + +*** + +### warehouseId + +```ts +warehouseId: Promise; +``` + +Defined in: [appkit/src/context/service-context.ts:23](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L23) + +Promise that resolves to the warehouse ID + +*** + +### workspaceId + +```ts +workspaceId: Promise; +``` + +Defined in: [appkit/src/context/service-context.ts:25](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L25) + +Promise that resolves to the workspace ID diff --git a/docs/docs/api/appkit/Interface.UserContext.md b/docs/docs/api/appkit/Interface.UserContext.md new file mode 100644 index 0000000..457e96a --- /dev/null +++ b/docs/docs/api/appkit/Interface.UserContext.md @@ -0,0 +1,78 @@ +# Interface: UserContext + +Defined in: [appkit/src/context/user-context.ts:7](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L7) + +User execution context extends the service context with user-specific data. +Created on-demand when asUser(req) is called. + +## Properties + +### client + +```ts +client: WorkspaceClient; +``` + +Defined in: [appkit/src/context/user-context.ts:9](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L9) + +WorkspaceClient authenticated as the user + +*** + +### isUserContext + +```ts +isUserContext: true; +``` + +Defined in: [appkit/src/context/user-context.ts:19](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L19) + +Flag indicating this is a user context + +*** + +### userId + +```ts +userId: string; +``` + +Defined in: [appkit/src/context/user-context.ts:11](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L11) + +The user's ID (from request headers) + +*** + +### userName? + +```ts +optional userName: string; +``` + +Defined in: [appkit/src/context/user-context.ts:13](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L13) + +The user's name (from request headers) + +*** + +### warehouseId + +```ts +warehouseId: Promise; +``` + +Defined in: [appkit/src/context/user-context.ts:15](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L15) + +Promise that resolves to the warehouse ID (inherited from service context) + +*** + +### workspaceId + +```ts +workspaceId: Promise; +``` + +Defined in: [appkit/src/context/user-context.ts:17](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L17) + +Promise that resolves to the workspace ID (inherited from service context) diff --git a/docs/docs/api/appkit/TypeAlias.ExecutionContext.md b/docs/docs/api/appkit/TypeAlias.ExecutionContext.md new file mode 100644 index 0000000..1c38b18 --- /dev/null +++ b/docs/docs/api/appkit/TypeAlias.ExecutionContext.md @@ -0,0 +1,11 @@ +# Type Alias: ExecutionContext + +```ts +type ExecutionContext = + | ServiceContextState + | UserContext; +``` + +Defined in: [appkit/src/context/user-context.ts:25](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L25) + +Execution context can be either service or user context. diff --git a/docs/docs/api/appkit/TypeAlias.RequestContext.md b/docs/docs/api/appkit/TypeAlias.RequestContext.md deleted file mode 100644 index 3fd9750..0000000 --- a/docs/docs/api/appkit/TypeAlias.RequestContext.md +++ /dev/null @@ -1,85 +0,0 @@ -# Type Alias: RequestContext - -```ts -type RequestContext = { - serviceDatabricksClient: WorkspaceClient; - serviceUserId: string; - userDatabricksClient?: WorkspaceClient; - userId: string; - userName?: string; - warehouseId: Promise; - workspaceId: Promise; -}; -``` - -Defined in: [appkit/src/utils/databricks-client-middleware.ts:13](https://github.com/databricks/appkit/blob/main/packages/appkit/src/utils/databricks-client-middleware.ts#L13) - -## Properties - -### serviceDatabricksClient - -```ts -serviceDatabricksClient: WorkspaceClient; -``` - -Defined in: [appkit/src/utils/databricks-client-middleware.ts:15](https://github.com/databricks/appkit/blob/main/packages/appkit/src/utils/databricks-client-middleware.ts#L15) - -*** - -### serviceUserId - -```ts -serviceUserId: string; -``` - -Defined in: [appkit/src/utils/databricks-client-middleware.ts:18](https://github.com/databricks/appkit/blob/main/packages/appkit/src/utils/databricks-client-middleware.ts#L18) - -*** - -### userDatabricksClient? - -```ts -optional userDatabricksClient: WorkspaceClient; -``` - -Defined in: [appkit/src/utils/databricks-client-middleware.ts:14](https://github.com/databricks/appkit/blob/main/packages/appkit/src/utils/databricks-client-middleware.ts#L14) - -*** - -### userId - -```ts -userId: string; -``` - -Defined in: [appkit/src/utils/databricks-client-middleware.ts:16](https://github.com/databricks/appkit/blob/main/packages/appkit/src/utils/databricks-client-middleware.ts#L16) - -*** - -### userName? - -```ts -optional userName: string; -``` - -Defined in: [appkit/src/utils/databricks-client-middleware.ts:17](https://github.com/databricks/appkit/blob/main/packages/appkit/src/utils/databricks-client-middleware.ts#L17) - -*** - -### warehouseId - -```ts -warehouseId: Promise; -``` - -Defined in: [appkit/src/utils/databricks-client-middleware.ts:19](https://github.com/databricks/appkit/blob/main/packages/appkit/src/utils/databricks-client-middleware.ts#L19) - -*** - -### workspaceId - -```ts -workspaceId: Promise; -``` - -Defined in: [appkit/src/utils/databricks-client-middleware.ts:20](https://github.com/databricks/appkit/blob/main/packages/appkit/src/utils/databricks-client-middleware.ts#L20) diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index 141def8..ec64b33 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -5,6 +5,7 @@ | Class | Description | | ------ | ------ | | [Plugin](Class.Plugin.md) | - | +| [ServiceContext](Class.ServiceContext.md) | ServiceContext is a singleton that manages the service principal's WorkspaceClient and shared resources like warehouse/workspace IDs. | ## Interfaces @@ -13,15 +14,17 @@ | [BasePluginConfig](Interface.BasePluginConfig.md) | - | | [CacheConfig](Interface.CacheConfig.md) | Configuration for caching | | [ITelemetry](Interface.ITelemetry.md) | Plugin-facing interface for OpenTelemetry instrumentation. Provides a thin abstraction over OpenTelemetry APIs for plugins. | +| [ServiceContextState](Interface.ServiceContextState.md) | Service context holds the service principal client and shared resources. This is initialized once at app startup and shared across all requests. | | [StreamExecutionSettings](Interface.StreamExecutionSettings.md) | - | | [TelemetryConfig](Interface.TelemetryConfig.md) | - | +| [UserContext](Interface.UserContext.md) | User execution context extends the service context with user-specific data. Created on-demand when asUser(req) is called. | ## Type Aliases | Type Alias | Description | | ------ | ------ | +| [ExecutionContext](TypeAlias.ExecutionContext.md) | Execution context can be either service or user context. | | [IAppRouter](TypeAlias.IAppRouter.md) | - | -| [RequestContext](TypeAlias.RequestContext.md) | - | | [SQLTypeMarker](TypeAlias.SQLTypeMarker.md) | Object that identifies a typed SQL parameter. Created using sql.date(), sql.string(), sql.number(), sql.boolean(), sql.timestamp(), sql.binary(), or sql.interval(). | ## Variables @@ -36,5 +39,11 @@ | ------ | ------ | | [appKitTypesPlugin](Function.appKitTypesPlugin.md) | Vite plugin to generate types for AppKit queries. Calls generateFromEntryPoint under the hood. | | [createApp](Function.createApp.md) | Bootstraps AppKit with the provided configuration. | -| [getRequestContext](Function.getRequestContext.md) | Retrieve the request-scoped context populated by `databricksClientMiddleware`. Throws when invoked outside of a request lifecycle. | +| [getCurrentUserId](Function.getCurrentUserId.md) | Get the current user ID for cache keying and telemetry. | +| [getExecutionContext](Function.getExecutionContext.md) | Get the current execution context. | +| [getWarehouseId](Function.getWarehouseId.md) | Get the warehouse ID promise. | +| [getWorkspaceClient](Function.getWorkspaceClient.md) | Get the WorkspaceClient for the current execution context. | +| [getWorkspaceId](Function.getWorkspaceId.md) | Get the workspace ID promise. | +| [isInUserContext](Function.isInUserContext.md) | Check if currently running in a user context. | | [isSQLTypeMarker](Function.isSQLTypeMarker.md) | Type guard to check if a value is a SQL type marker | +| [isUserContext](Function.isUserContext.md) | Check if an execution context is a user context. | diff --git a/docs/docs/api/appkit/typedoc-sidebar.ts b/docs/docs/api/appkit/typedoc-sidebar.ts index eefc780..6171796 100644 --- a/docs/docs/api/appkit/typedoc-sidebar.ts +++ b/docs/docs/api/appkit/typedoc-sidebar.ts @@ -9,6 +9,11 @@ const typedocSidebar: SidebarsConfig = { type: "doc", id: "api/appkit/Class.Plugin", label: "Plugin" + }, + { + type: "doc", + id: "api/appkit/Class.ServiceContext", + label: "ServiceContext" } ] }, @@ -31,6 +36,11 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Interface.ITelemetry", label: "ITelemetry" }, + { + type: "doc", + id: "api/appkit/Interface.ServiceContextState", + label: "ServiceContextState" + }, { type: "doc", id: "api/appkit/Interface.StreamExecutionSettings", @@ -40,6 +50,11 @@ const typedocSidebar: SidebarsConfig = { type: "doc", id: "api/appkit/Interface.TelemetryConfig", label: "TelemetryConfig" + }, + { + type: "doc", + id: "api/appkit/Interface.UserContext", + label: "UserContext" } ] }, @@ -49,13 +64,13 @@ const typedocSidebar: SidebarsConfig = { items: [ { type: "doc", - id: "api/appkit/TypeAlias.IAppRouter", - label: "IAppRouter" + id: "api/appkit/TypeAlias.ExecutionContext", + label: "ExecutionContext" }, { type: "doc", - id: "api/appkit/TypeAlias.RequestContext", - label: "RequestContext" + id: "api/appkit/TypeAlias.IAppRouter", + label: "IAppRouter" }, { type: "doc", @@ -91,13 +106,43 @@ const typedocSidebar: SidebarsConfig = { }, { type: "doc", - id: "api/appkit/Function.getRequestContext", - label: "getRequestContext" + id: "api/appkit/Function.getCurrentUserId", + label: "getCurrentUserId" + }, + { + type: "doc", + id: "api/appkit/Function.getExecutionContext", + label: "getExecutionContext" + }, + { + type: "doc", + id: "api/appkit/Function.getWarehouseId", + label: "getWarehouseId" + }, + { + type: "doc", + id: "api/appkit/Function.getWorkspaceClient", + label: "getWorkspaceClient" + }, + { + type: "doc", + id: "api/appkit/Function.getWorkspaceId", + label: "getWorkspaceId" + }, + { + type: "doc", + id: "api/appkit/Function.isInUserContext", + label: "isInUserContext" }, { type: "doc", id: "api/appkit/Function.isSQLTypeMarker", label: "isSQLTypeMarker" + }, + { + type: "doc", + id: "api/appkit/Function.isUserContext", + label: "isUserContext" } ] } diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index 0e264d6..eaca5ea 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -5,10 +5,7 @@ export type { SQLTypeMarker, StreamExecutionSettings, } from "shared"; -export { - isSQLTypeMarker, - sql, -} from "shared"; +export { isSQLTypeMarker, sql } from "shared"; export { analytics } from "./analytics"; export { CacheManager } from "./cache"; export { @@ -34,6 +31,6 @@ export { SeverityNumber, type Span, SpanStatusCode, - TelemetryConfig, + type TelemetryConfig, } from "./telemetry"; export { appKitTypesPlugin } from "./type-generator/vite-plugin"; From c1838a173e9458180da4c9b08fd24df8c699fad6 Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Thu, 8 Jan 2026 16:17:18 +0100 Subject: [PATCH 3/4] chore: fixup --- docs/docs/api/appkit/Class.ServiceContext.md | 138 --------------- docs/docs/api/appkit/Function.createApp.md | 6 +- .../api/appkit/Function.getCurrentUserId.md | 15 -- .../appkit/Function.getExecutionContext.md | 20 --- .../api/appkit/Function.getWarehouseId.md | 13 -- .../api/appkit/Function.getWorkspaceClient.md | 13 -- .../api/appkit/Function.getWorkspaceId.md | 13 -- .../api/appkit/Function.isInUserContext.md | 13 -- .../docs/api/appkit/Function.isUserContext.md | 19 --- docs/docs/api/appkit/Interface.CacheConfig.md | 143 ---------------- docs/docs/api/appkit/Interface.ITelemetry.md | 157 ------------------ .../appkit/Interface.ServiceContextState.md | 54 ------ .../api/appkit/Interface.TelemetryConfig.md | 53 ------ docs/docs/api/appkit/Interface.UserContext.md | 78 --------- .../api/appkit/TypeAlias.ExecutionContext.md | 11 -- .../api/appkit/TypeAlias.SQLTypeMarker.md | 16 -- docs/docs/api/appkit/index.md | 15 -- docs/docs/api/appkit/typedoc-sidebar.ts | 75 --------- packages/appkit/src/index.ts | 36 ++-- 19 files changed, 17 insertions(+), 871 deletions(-) delete mode 100644 docs/docs/api/appkit/Class.ServiceContext.md delete mode 100644 docs/docs/api/appkit/Function.getCurrentUserId.md delete mode 100644 docs/docs/api/appkit/Function.getExecutionContext.md delete mode 100644 docs/docs/api/appkit/Function.getWarehouseId.md delete mode 100644 docs/docs/api/appkit/Function.getWorkspaceClient.md delete mode 100644 docs/docs/api/appkit/Function.getWorkspaceId.md delete mode 100644 docs/docs/api/appkit/Function.isInUserContext.md delete mode 100644 docs/docs/api/appkit/Function.isUserContext.md delete mode 100644 docs/docs/api/appkit/Interface.CacheConfig.md delete mode 100644 docs/docs/api/appkit/Interface.ITelemetry.md delete mode 100644 docs/docs/api/appkit/Interface.ServiceContextState.md delete mode 100644 docs/docs/api/appkit/Interface.TelemetryConfig.md delete mode 100644 docs/docs/api/appkit/Interface.UserContext.md delete mode 100644 docs/docs/api/appkit/TypeAlias.ExecutionContext.md delete mode 100644 docs/docs/api/appkit/TypeAlias.SQLTypeMarker.md diff --git a/docs/docs/api/appkit/Class.ServiceContext.md b/docs/docs/api/appkit/Class.ServiceContext.md deleted file mode 100644 index 669d256..0000000 --- a/docs/docs/api/appkit/Class.ServiceContext.md +++ /dev/null @@ -1,138 +0,0 @@ -# Class: ServiceContext - -Defined in: [appkit/src/context/service-context.ts:48](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L48) - -ServiceContext is a singleton that manages the service principal's -WorkspaceClient and shared resources like warehouse/workspace IDs. - -It's initialized once at app startup and provides the foundation -for both service principal and user context execution. - -## Constructors - -### Constructor - -```ts -new ServiceContext(): ServiceContext; -``` - -#### Returns - -`ServiceContext` - -## Methods - -### createUserContext() - -```ts -static createUserContext( - token: string, - userId: string, - userName?: string): UserContext; -``` - -Defined in: [appkit/src/context/service-context.ts:98](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L98) - -Create a user context from request headers. - -#### Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `token` | `string` | The user's access token from x-forwarded-access-token header | -| `userId` | `string` | The user's ID from x-forwarded-user header | -| `userName?` | `string` | Optional user name | - -#### Returns - -[`UserContext`](Interface.UserContext.md) - -#### Throws - -Error if token is not provided - -*** - -### get() - -```ts -static get(): ServiceContextState; -``` - -Defined in: [appkit/src/context/service-context.ts:74](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L74) - -Get the initialized service context. - -#### Returns - -[`ServiceContextState`](Interface.ServiceContextState.md) - -#### Throws - -Error if not initialized - -*** - -### getClientOptions() - -```ts -static getClientOptions(): ClientOptions; -``` - -Defined in: [appkit/src/context/service-context.ts:142](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L142) - -Get the client options for WorkspaceClient. -Exposed for testing purposes. - -#### Returns - -`ClientOptions` - -*** - -### initialize() - -```ts -static initialize(): Promise; -``` - -Defined in: [appkit/src/context/service-context.ts:56](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L56) - -Initialize the service context. Should be called once at app startup. -Safe to call multiple times - will return the same instance. - -#### Returns - -`Promise`\<[`ServiceContextState`](Interface.ServiceContextState.md)\> - -*** - -### isInitialized() - -```ts -static isInitialized(): boolean; -``` - -Defined in: [appkit/src/context/service-context.ts:86](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L86) - -Check if the service context has been initialized. - -#### Returns - -`boolean` - -*** - -### reset() - -```ts -static reset(): void; -``` - -Defined in: [appkit/src/context/service-context.ts:249](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L249) - -Reset the service context. Only for testing purposes. - -#### Returns - -`void` diff --git a/docs/docs/api/appkit/Function.createApp.md b/docs/docs/api/appkit/Function.createApp.md index 63c630b..54d534a 100644 --- a/docs/docs/api/appkit/Function.createApp.md +++ b/docs/docs/api/appkit/Function.createApp.md @@ -22,10 +22,10 @@ Bootstraps AppKit with the provided configuration. | Parameter | Type | | ------ | ------ | -| `config` | \{ `cache?`: [`CacheConfig`](Interface.CacheConfig.md); `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](Interface.TelemetryConfig.md); \} | -| `config.cache?` | [`CacheConfig`](Interface.CacheConfig.md) | +| `config` | \{ `cache?`: `CacheConfig`; `plugins?`: `T`; `telemetry?`: `TelemetryConfig`; \} | +| `config.cache?` | `CacheConfig` | | `config.plugins?` | `T` | -| `config.telemetry?` | [`TelemetryConfig`](Interface.TelemetryConfig.md) | +| `config.telemetry?` | `TelemetryConfig` | ## Returns diff --git a/docs/docs/api/appkit/Function.getCurrentUserId.md b/docs/docs/api/appkit/Function.getCurrentUserId.md deleted file mode 100644 index 3c173e4..0000000 --- a/docs/docs/api/appkit/Function.getCurrentUserId.md +++ /dev/null @@ -1,15 +0,0 @@ -# Function: getCurrentUserId() - -```ts -function getCurrentUserId(): string; -``` - -Defined in: [appkit/src/context/execution-context.ts:48](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L48) - -Get the current user ID for cache keying and telemetry. - -Returns the user ID if in user context, otherwise the service user ID. - -## Returns - -`string` diff --git a/docs/docs/api/appkit/Function.getExecutionContext.md b/docs/docs/api/appkit/Function.getExecutionContext.md deleted file mode 100644 index 0239a34..0000000 --- a/docs/docs/api/appkit/Function.getExecutionContext.md +++ /dev/null @@ -1,20 +0,0 @@ -# Function: getExecutionContext() - -```ts -function getExecutionContext(): ExecutionContext; -``` - -Defined in: [appkit/src/context/execution-context.ts:35](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L35) - -Get the current execution context. - -- If running inside a user context (via asUser), returns the user context -- Otherwise, returns the service context - -## Returns - -[`ExecutionContext`](TypeAlias.ExecutionContext.md) - -## Throws - -Error if ServiceContext is not initialized diff --git a/docs/docs/api/appkit/Function.getWarehouseId.md b/docs/docs/api/appkit/Function.getWarehouseId.md deleted file mode 100644 index 41c276e..0000000 --- a/docs/docs/api/appkit/Function.getWarehouseId.md +++ /dev/null @@ -1,13 +0,0 @@ -# Function: getWarehouseId() - -```ts -function getWarehouseId(): Promise; -``` - -Defined in: [appkit/src/context/execution-context.ts:66](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L66) - -Get the warehouse ID promise. - -## Returns - -`Promise`\<`string`\> diff --git a/docs/docs/api/appkit/Function.getWorkspaceClient.md b/docs/docs/api/appkit/Function.getWorkspaceClient.md deleted file mode 100644 index ea01543..0000000 --- a/docs/docs/api/appkit/Function.getWorkspaceClient.md +++ /dev/null @@ -1,13 +0,0 @@ -# Function: getWorkspaceClient() - -```ts -function getWorkspaceClient(): WorkspaceClient; -``` - -Defined in: [appkit/src/context/execution-context.ts:59](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L59) - -Get the WorkspaceClient for the current execution context. - -## Returns - -`WorkspaceClient` diff --git a/docs/docs/api/appkit/Function.getWorkspaceId.md b/docs/docs/api/appkit/Function.getWorkspaceId.md deleted file mode 100644 index 1f8fa8b..0000000 --- a/docs/docs/api/appkit/Function.getWorkspaceId.md +++ /dev/null @@ -1,13 +0,0 @@ -# Function: getWorkspaceId() - -```ts -function getWorkspaceId(): Promise; -``` - -Defined in: [appkit/src/context/execution-context.ts:73](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L73) - -Get the workspace ID promise. - -## Returns - -`Promise`\<`string`\> diff --git a/docs/docs/api/appkit/Function.isInUserContext.md b/docs/docs/api/appkit/Function.isInUserContext.md deleted file mode 100644 index 88e0708..0000000 --- a/docs/docs/api/appkit/Function.isInUserContext.md +++ /dev/null @@ -1,13 +0,0 @@ -# Function: isInUserContext() - -```ts -function isInUserContext(): boolean; -``` - -Defined in: [appkit/src/context/execution-context.ts:80](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/execution-context.ts#L80) - -Check if currently running in a user context. - -## Returns - -`boolean` diff --git a/docs/docs/api/appkit/Function.isUserContext.md b/docs/docs/api/appkit/Function.isUserContext.md deleted file mode 100644 index 581b604..0000000 --- a/docs/docs/api/appkit/Function.isUserContext.md +++ /dev/null @@ -1,19 +0,0 @@ -# Function: isUserContext() - -```ts -function isUserContext(ctx: ExecutionContext): ctx is UserContext; -``` - -Defined in: [appkit/src/context/user-context.ts:30](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L30) - -Check if an execution context is a user context. - -## Parameters - -| Parameter | Type | -| ------ | ------ | -| `ctx` | [`ExecutionContext`](TypeAlias.ExecutionContext.md) | - -## Returns - -`ctx is UserContext` diff --git a/docs/docs/api/appkit/Interface.CacheConfig.md b/docs/docs/api/appkit/Interface.CacheConfig.md deleted file mode 100644 index d1d26dd..0000000 --- a/docs/docs/api/appkit/Interface.CacheConfig.md +++ /dev/null @@ -1,143 +0,0 @@ -# Interface: CacheConfig - -Defined in: [shared/src/cache.ts:36](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L36) - -Configuration for caching - -## Indexable - -```ts -[key: string]: unknown -``` - -## Properties - -### cacheKey? - -```ts -optional cacheKey: (string | number | object)[]; -``` - -Defined in: [shared/src/cache.ts:46](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L46) - -Cache key - -*** - -### cleanupProbability? - -```ts -optional cleanupProbability: number; -``` - -Defined in: [shared/src/cache.ts:55](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L55) - -Probability (0-1) of triggering cleanup on each get operation - -*** - -### enabled? - -```ts -optional enabled: boolean; -``` - -Defined in: [shared/src/cache.ts:38](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L38) - -Whether caching is enabled - -*** - -### evictionCheckProbability? - -```ts -optional evictionCheckProbability: number; -``` - -Defined in: [shared/src/cache.ts:58](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L58) - -Probability (0-1) of checking total bytes on each write operation - -*** - -### maxBytes? - -```ts -optional maxBytes: number; -``` - -Defined in: [shared/src/cache.ts:42](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L42) - -Maximum number of bytes in the cache - -*** - -### maxEntryBytes? - -```ts -optional maxEntryBytes: number; -``` - -Defined in: [shared/src/cache.ts:61](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L61) - -Maximum number of bytes per entry in the cache - -*** - -### maxSize? - -```ts -optional maxSize: number; -``` - -Defined in: [shared/src/cache.ts:44](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L44) - -Maximum number of entries in the cache - -*** - -### storage? - -```ts -optional storage: CacheStorage; -``` - -Defined in: [shared/src/cache.ts:48](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L48) - -Cache Storage provider instance - -*** - -### strictPersistence? - -```ts -optional strictPersistence: boolean; -``` - -Defined in: [shared/src/cache.ts:50](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L50) - -Whether to enforce strict persistence - -*** - -### telemetry? - -```ts -optional telemetry: TelemetryOptions; -``` - -Defined in: [shared/src/cache.ts:52](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L52) - -Telemetry configuration - -*** - -### ttl? - -```ts -optional ttl: number; -``` - -Defined in: [shared/src/cache.ts:40](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L40) - -Time to live in seconds diff --git a/docs/docs/api/appkit/Interface.ITelemetry.md b/docs/docs/api/appkit/Interface.ITelemetry.md deleted file mode 100644 index 0478778..0000000 --- a/docs/docs/api/appkit/Interface.ITelemetry.md +++ /dev/null @@ -1,157 +0,0 @@ -# Interface: ITelemetry - -Defined in: [appkit/src/telemetry/types.ts:33](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L33) - -Plugin-facing interface for OpenTelemetry instrumentation. -Provides a thin abstraction over OpenTelemetry APIs for plugins. - -## Methods - -### emit() - -```ts -emit(logRecord: LogRecord): void; -``` - -Defined in: [appkit/src/telemetry/types.ts:57](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L57) - -Emits a log record using the default logger. -Respects the logs enabled/disabled config. - -#### Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `logRecord` | `LogRecord` | The log record to emit | - -#### Returns - -`void` - -*** - -### getLogger() - -```ts -getLogger(options?: InstrumentConfig): Logger; -``` - -Defined in: [appkit/src/telemetry/types.ts:50](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L50) - -Gets a logger for emitting log records. - -#### Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `options?` | `InstrumentConfig` | Instrument customization options. | - -#### Returns - -`Logger` - -*** - -### getMeter() - -```ts -getMeter(options?: InstrumentConfig): Meter; -``` - -Defined in: [appkit/src/telemetry/types.ts:44](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L44) - -Gets a meter for recording metrics. - -#### Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `options?` | `InstrumentConfig` | Instrument customization options. | - -#### Returns - -`Meter` - -*** - -### getTracer() - -```ts -getTracer(options?: InstrumentConfig): Tracer; -``` - -Defined in: [appkit/src/telemetry/types.ts:38](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L38) - -Gets a tracer for creating spans. - -#### Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `options?` | `InstrumentConfig` | Instrument customization options. | - -#### Returns - -`Tracer` - -*** - -### registerInstrumentations() - -```ts -registerInstrumentations(instrumentations: Instrumentation[]): void; -``` - -Defined in: [appkit/src/telemetry/types.ts:81](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L81) - -Register OpenTelemetry instrumentations. -Can be called at any time, but recommended to call in plugin constructor. - -#### Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `instrumentations` | `Instrumentation`\<`InstrumentationConfig`\>[] | Array of OpenTelemetry instrumentations to register | - -#### Returns - -`void` - -*** - -### startActiveSpan() - -```ts -startActiveSpan( - name: string, - options: SpanOptions, - fn: (span: Span) => Promise, -tracerOptions?: InstrumentConfig): Promise; -``` - -Defined in: [appkit/src/telemetry/types.ts:69](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L69) - -Starts an active span and executes a callback function within its context. -Respects the traces enabled/disabled config. -When traces are disabled, executes the callback with a no-op span. - -#### Type Parameters - -| Type Parameter | -| ------ | -| `T` | - -#### Parameters - -| Parameter | Type | Description | -| ------ | ------ | ------ | -| `name` | `string` | The name of the span | -| `options` | `SpanOptions` | Span options including attributes, kind, etc. | -| `fn` | (`span`: `Span`) => `Promise`\<`T`\> | Callback function to execute within the span context | -| `tracerOptions?` | `InstrumentConfig` | Optional tracer configuration (custom name, prefix inclusion) | - -#### Returns - -`Promise`\<`T`\> - -Promise resolving to the callback's return value diff --git a/docs/docs/api/appkit/Interface.ServiceContextState.md b/docs/docs/api/appkit/Interface.ServiceContextState.md deleted file mode 100644 index af69fe5..0000000 --- a/docs/docs/api/appkit/Interface.ServiceContextState.md +++ /dev/null @@ -1,54 +0,0 @@ -# Interface: ServiceContextState - -Defined in: [appkit/src/context/service-context.ts:17](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L17) - -Service context holds the service principal client and shared resources. -This is initialized once at app startup and shared across all requests. - -## Properties - -### client - -```ts -client: WorkspaceClient; -``` - -Defined in: [appkit/src/context/service-context.ts:19](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L19) - -WorkspaceClient authenticated as the service principal - -*** - -### serviceUserId - -```ts -serviceUserId: string; -``` - -Defined in: [appkit/src/context/service-context.ts:21](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L21) - -The service principal's user ID - -*** - -### warehouseId - -```ts -warehouseId: Promise; -``` - -Defined in: [appkit/src/context/service-context.ts:23](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L23) - -Promise that resolves to the warehouse ID - -*** - -### workspaceId - -```ts -workspaceId: Promise; -``` - -Defined in: [appkit/src/context/service-context.ts:25](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/service-context.ts#L25) - -Promise that resolves to the workspace ID diff --git a/docs/docs/api/appkit/Interface.TelemetryConfig.md b/docs/docs/api/appkit/Interface.TelemetryConfig.md deleted file mode 100644 index 89b03fa..0000000 --- a/docs/docs/api/appkit/Interface.TelemetryConfig.md +++ /dev/null @@ -1,53 +0,0 @@ -# Interface: TelemetryConfig - -Defined in: [appkit/src/telemetry/types.ts:5](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L5) - -## Properties - -### exportIntervalMs? - -```ts -optional exportIntervalMs: number; -``` - -Defined in: [appkit/src/telemetry/types.ts:9](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L9) - -*** - -### headers? - -```ts -optional headers: Record; -``` - -Defined in: [appkit/src/telemetry/types.ts:10](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L10) - -*** - -### instrumentations? - -```ts -optional instrumentations: Instrumentation[]; -``` - -Defined in: [appkit/src/telemetry/types.ts:8](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L8) - -*** - -### serviceName? - -```ts -optional serviceName: string; -``` - -Defined in: [appkit/src/telemetry/types.ts:6](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L6) - -*** - -### serviceVersion? - -```ts -optional serviceVersion: string; -``` - -Defined in: [appkit/src/telemetry/types.ts:7](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L7) diff --git a/docs/docs/api/appkit/Interface.UserContext.md b/docs/docs/api/appkit/Interface.UserContext.md deleted file mode 100644 index 457e96a..0000000 --- a/docs/docs/api/appkit/Interface.UserContext.md +++ /dev/null @@ -1,78 +0,0 @@ -# Interface: UserContext - -Defined in: [appkit/src/context/user-context.ts:7](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L7) - -User execution context extends the service context with user-specific data. -Created on-demand when asUser(req) is called. - -## Properties - -### client - -```ts -client: WorkspaceClient; -``` - -Defined in: [appkit/src/context/user-context.ts:9](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L9) - -WorkspaceClient authenticated as the user - -*** - -### isUserContext - -```ts -isUserContext: true; -``` - -Defined in: [appkit/src/context/user-context.ts:19](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L19) - -Flag indicating this is a user context - -*** - -### userId - -```ts -userId: string; -``` - -Defined in: [appkit/src/context/user-context.ts:11](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L11) - -The user's ID (from request headers) - -*** - -### userName? - -```ts -optional userName: string; -``` - -Defined in: [appkit/src/context/user-context.ts:13](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L13) - -The user's name (from request headers) - -*** - -### warehouseId - -```ts -warehouseId: Promise; -``` - -Defined in: [appkit/src/context/user-context.ts:15](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L15) - -Promise that resolves to the warehouse ID (inherited from service context) - -*** - -### workspaceId - -```ts -workspaceId: Promise; -``` - -Defined in: [appkit/src/context/user-context.ts:17](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L17) - -Promise that resolves to the workspace ID (inherited from service context) diff --git a/docs/docs/api/appkit/TypeAlias.ExecutionContext.md b/docs/docs/api/appkit/TypeAlias.ExecutionContext.md deleted file mode 100644 index 1c38b18..0000000 --- a/docs/docs/api/appkit/TypeAlias.ExecutionContext.md +++ /dev/null @@ -1,11 +0,0 @@ -# Type Alias: ExecutionContext - -```ts -type ExecutionContext = - | ServiceContextState - | UserContext; -``` - -Defined in: [appkit/src/context/user-context.ts:25](https://github.com/databricks/appkit/blob/main/packages/appkit/src/context/user-context.ts#L25) - -Execution context can be either service or user context. diff --git a/docs/docs/api/appkit/TypeAlias.SQLTypeMarker.md b/docs/docs/api/appkit/TypeAlias.SQLTypeMarker.md deleted file mode 100644 index 3c049b2..0000000 --- a/docs/docs/api/appkit/TypeAlias.SQLTypeMarker.md +++ /dev/null @@ -1,16 +0,0 @@ -# Type Alias: SQLTypeMarker - -```ts -type SQLTypeMarker = - | SQLStringMarker - | SQLNumberMarker - | SQLBooleanMarker - | SQLBinaryMarker - | SQLDateMarker - | SQLTimestampMarker; -``` - -Defined in: [shared/src/sql/types.ts:36](https://github.com/databricks/appkit/blob/main/packages/shared/src/sql/types.ts#L36) - -Object that identifies a typed SQL parameter. -Created using sql.date(), sql.string(), sql.number(), sql.boolean(), sql.timestamp(), sql.binary(), or sql.interval(). diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index ec64b33..dd6946c 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -5,27 +5,19 @@ | Class | Description | | ------ | ------ | | [Plugin](Class.Plugin.md) | - | -| [ServiceContext](Class.ServiceContext.md) | ServiceContext is a singleton that manages the service principal's WorkspaceClient and shared resources like warehouse/workspace IDs. | ## Interfaces | Interface | Description | | ------ | ------ | | [BasePluginConfig](Interface.BasePluginConfig.md) | - | -| [CacheConfig](Interface.CacheConfig.md) | Configuration for caching | -| [ITelemetry](Interface.ITelemetry.md) | Plugin-facing interface for OpenTelemetry instrumentation. Provides a thin abstraction over OpenTelemetry APIs for plugins. | -| [ServiceContextState](Interface.ServiceContextState.md) | Service context holds the service principal client and shared resources. This is initialized once at app startup and shared across all requests. | | [StreamExecutionSettings](Interface.StreamExecutionSettings.md) | - | -| [TelemetryConfig](Interface.TelemetryConfig.md) | - | -| [UserContext](Interface.UserContext.md) | User execution context extends the service context with user-specific data. Created on-demand when asUser(req) is called. | ## Type Aliases | Type Alias | Description | | ------ | ------ | -| [ExecutionContext](TypeAlias.ExecutionContext.md) | Execution context can be either service or user context. | | [IAppRouter](TypeAlias.IAppRouter.md) | - | -| [SQLTypeMarker](TypeAlias.SQLTypeMarker.md) | Object that identifies a typed SQL parameter. Created using sql.date(), sql.string(), sql.number(), sql.boolean(), sql.timestamp(), sql.binary(), or sql.interval(). | ## Variables @@ -39,11 +31,4 @@ | ------ | ------ | | [appKitTypesPlugin](Function.appKitTypesPlugin.md) | Vite plugin to generate types for AppKit queries. Calls generateFromEntryPoint under the hood. | | [createApp](Function.createApp.md) | Bootstraps AppKit with the provided configuration. | -| [getCurrentUserId](Function.getCurrentUserId.md) | Get the current user ID for cache keying and telemetry. | -| [getExecutionContext](Function.getExecutionContext.md) | Get the current execution context. | -| [getWarehouseId](Function.getWarehouseId.md) | Get the warehouse ID promise. | -| [getWorkspaceClient](Function.getWorkspaceClient.md) | Get the WorkspaceClient for the current execution context. | -| [getWorkspaceId](Function.getWorkspaceId.md) | Get the workspace ID promise. | -| [isInUserContext](Function.isInUserContext.md) | Check if currently running in a user context. | | [isSQLTypeMarker](Function.isSQLTypeMarker.md) | Type guard to check if a value is a SQL type marker | -| [isUserContext](Function.isUserContext.md) | Check if an execution context is a user context. | diff --git a/docs/docs/api/appkit/typedoc-sidebar.ts b/docs/docs/api/appkit/typedoc-sidebar.ts index 6171796..8618ec7 100644 --- a/docs/docs/api/appkit/typedoc-sidebar.ts +++ b/docs/docs/api/appkit/typedoc-sidebar.ts @@ -9,11 +9,6 @@ const typedocSidebar: SidebarsConfig = { type: "doc", id: "api/appkit/Class.Plugin", label: "Plugin" - }, - { - type: "doc", - id: "api/appkit/Class.ServiceContext", - label: "ServiceContext" } ] }, @@ -26,35 +21,10 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Interface.BasePluginConfig", label: "BasePluginConfig" }, - { - type: "doc", - id: "api/appkit/Interface.CacheConfig", - label: "CacheConfig" - }, - { - type: "doc", - id: "api/appkit/Interface.ITelemetry", - label: "ITelemetry" - }, - { - type: "doc", - id: "api/appkit/Interface.ServiceContextState", - label: "ServiceContextState" - }, { type: "doc", id: "api/appkit/Interface.StreamExecutionSettings", label: "StreamExecutionSettings" - }, - { - type: "doc", - id: "api/appkit/Interface.TelemetryConfig", - label: "TelemetryConfig" - }, - { - type: "doc", - id: "api/appkit/Interface.UserContext", - label: "UserContext" } ] }, @@ -62,20 +32,10 @@ const typedocSidebar: SidebarsConfig = { type: "category", label: "Type Aliases", items: [ - { - type: "doc", - id: "api/appkit/TypeAlias.ExecutionContext", - label: "ExecutionContext" - }, { type: "doc", id: "api/appkit/TypeAlias.IAppRouter", label: "IAppRouter" - }, - { - type: "doc", - id: "api/appkit/TypeAlias.SQLTypeMarker", - label: "SQLTypeMarker" } ] }, @@ -104,45 +64,10 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Function.createApp", label: "createApp" }, - { - type: "doc", - id: "api/appkit/Function.getCurrentUserId", - label: "getCurrentUserId" - }, - { - type: "doc", - id: "api/appkit/Function.getExecutionContext", - label: "getExecutionContext" - }, - { - type: "doc", - id: "api/appkit/Function.getWarehouseId", - label: "getWarehouseId" - }, - { - type: "doc", - id: "api/appkit/Function.getWorkspaceClient", - label: "getWorkspaceClient" - }, - { - type: "doc", - id: "api/appkit/Function.getWorkspaceId", - label: "getWorkspaceId" - }, - { - type: "doc", - id: "api/appkit/Function.isInUserContext", - label: "isInUserContext" - }, { type: "doc", id: "api/appkit/Function.isSQLTypeMarker", label: "isSQLTypeMarker" - }, - { - type: "doc", - id: "api/appkit/Function.isUserContext", - label: "isUserContext" } ] } diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index eaca5ea..743ef96 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -1,36 +1,28 @@ +// Types from shared export type { BasePluginConfig, - CacheConfig, IAppRouter, - SQLTypeMarker, StreamExecutionSettings, } from "shared"; export { isSQLTypeMarker, sql } from "shared"; -export { analytics } from "./analytics"; -export { CacheManager } from "./cache"; -export { - ServiceContext, - getExecutionContext, - getCurrentUserId, - getWorkspaceClient, - getWarehouseId, - getWorkspaceId, - isInUserContext, - isUserContext, - type ExecutionContext, - type ServiceContextState, - type UserContext, -} from "./context"; + +// Core export { createApp } from "./core"; -export { Plugin, toPlugin } from "./plugin"; +export { analytics } from "./analytics"; export { server } from "./server"; -export type { ITelemetry } from "./telemetry"; + +// Plugin authoring +export { Plugin, toPlugin } from "./plugin"; +export { CacheManager } from "./cache"; + +// Telemetry (for advanced custom telemetry) export { + SeverityNumber, + SpanStatusCode, type Counter, type Histogram, - SeverityNumber, type Span, - SpanStatusCode, - type TelemetryConfig, } from "./telemetry"; + +// Vite plugin export { appKitTypesPlugin } from "./type-generator/vite-plugin"; From 6dcf31497659017acd8f99e96a2a88b10a82a22c Mon Sep 17 00:00:00 2001 From: MarioCadenas Date: Thu, 8 Jan 2026 17:13:21 +0100 Subject: [PATCH 4/4] chore: fixup --- docs/docs/api/appkit/Function.createApp.md | 6 +- docs/docs/api/appkit/Interface.CacheConfig.md | 143 ++++++++++++++++ docs/docs/api/appkit/Interface.ITelemetry.md | 157 ++++++++++++++++++ .../api/appkit/Interface.TelemetryConfig.md | 53 ++++++ docs/docs/api/appkit/index.md | 3 + docs/docs/api/appkit/typedoc-sidebar.ts | 15 ++ packages/appkit/src/index.ts | 3 + 7 files changed, 377 insertions(+), 3 deletions(-) create mode 100644 docs/docs/api/appkit/Interface.CacheConfig.md create mode 100644 docs/docs/api/appkit/Interface.ITelemetry.md create mode 100644 docs/docs/api/appkit/Interface.TelemetryConfig.md diff --git a/docs/docs/api/appkit/Function.createApp.md b/docs/docs/api/appkit/Function.createApp.md index 54d534a..63c630b 100644 --- a/docs/docs/api/appkit/Function.createApp.md +++ b/docs/docs/api/appkit/Function.createApp.md @@ -22,10 +22,10 @@ Bootstraps AppKit with the provided configuration. | Parameter | Type | | ------ | ------ | -| `config` | \{ `cache?`: `CacheConfig`; `plugins?`: `T`; `telemetry?`: `TelemetryConfig`; \} | -| `config.cache?` | `CacheConfig` | +| `config` | \{ `cache?`: [`CacheConfig`](Interface.CacheConfig.md); `plugins?`: `T`; `telemetry?`: [`TelemetryConfig`](Interface.TelemetryConfig.md); \} | +| `config.cache?` | [`CacheConfig`](Interface.CacheConfig.md) | | `config.plugins?` | `T` | -| `config.telemetry?` | `TelemetryConfig` | +| `config.telemetry?` | [`TelemetryConfig`](Interface.TelemetryConfig.md) | ## Returns diff --git a/docs/docs/api/appkit/Interface.CacheConfig.md b/docs/docs/api/appkit/Interface.CacheConfig.md new file mode 100644 index 0000000..d1d26dd --- /dev/null +++ b/docs/docs/api/appkit/Interface.CacheConfig.md @@ -0,0 +1,143 @@ +# Interface: CacheConfig + +Defined in: [shared/src/cache.ts:36](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L36) + +Configuration for caching + +## Indexable + +```ts +[key: string]: unknown +``` + +## Properties + +### cacheKey? + +```ts +optional cacheKey: (string | number | object)[]; +``` + +Defined in: [shared/src/cache.ts:46](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L46) + +Cache key + +*** + +### cleanupProbability? + +```ts +optional cleanupProbability: number; +``` + +Defined in: [shared/src/cache.ts:55](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L55) + +Probability (0-1) of triggering cleanup on each get operation + +*** + +### enabled? + +```ts +optional enabled: boolean; +``` + +Defined in: [shared/src/cache.ts:38](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L38) + +Whether caching is enabled + +*** + +### evictionCheckProbability? + +```ts +optional evictionCheckProbability: number; +``` + +Defined in: [shared/src/cache.ts:58](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L58) + +Probability (0-1) of checking total bytes on each write operation + +*** + +### maxBytes? + +```ts +optional maxBytes: number; +``` + +Defined in: [shared/src/cache.ts:42](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L42) + +Maximum number of bytes in the cache + +*** + +### maxEntryBytes? + +```ts +optional maxEntryBytes: number; +``` + +Defined in: [shared/src/cache.ts:61](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L61) + +Maximum number of bytes per entry in the cache + +*** + +### maxSize? + +```ts +optional maxSize: number; +``` + +Defined in: [shared/src/cache.ts:44](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L44) + +Maximum number of entries in the cache + +*** + +### storage? + +```ts +optional storage: CacheStorage; +``` + +Defined in: [shared/src/cache.ts:48](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L48) + +Cache Storage provider instance + +*** + +### strictPersistence? + +```ts +optional strictPersistence: boolean; +``` + +Defined in: [shared/src/cache.ts:50](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L50) + +Whether to enforce strict persistence + +*** + +### telemetry? + +```ts +optional telemetry: TelemetryOptions; +``` + +Defined in: [shared/src/cache.ts:52](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L52) + +Telemetry configuration + +*** + +### ttl? + +```ts +optional ttl: number; +``` + +Defined in: [shared/src/cache.ts:40](https://github.com/databricks/appkit/blob/main/packages/shared/src/cache.ts#L40) + +Time to live in seconds diff --git a/docs/docs/api/appkit/Interface.ITelemetry.md b/docs/docs/api/appkit/Interface.ITelemetry.md new file mode 100644 index 0000000..0478778 --- /dev/null +++ b/docs/docs/api/appkit/Interface.ITelemetry.md @@ -0,0 +1,157 @@ +# Interface: ITelemetry + +Defined in: [appkit/src/telemetry/types.ts:33](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L33) + +Plugin-facing interface for OpenTelemetry instrumentation. +Provides a thin abstraction over OpenTelemetry APIs for plugins. + +## Methods + +### emit() + +```ts +emit(logRecord: LogRecord): void; +``` + +Defined in: [appkit/src/telemetry/types.ts:57](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L57) + +Emits a log record using the default logger. +Respects the logs enabled/disabled config. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `logRecord` | `LogRecord` | The log record to emit | + +#### Returns + +`void` + +*** + +### getLogger() + +```ts +getLogger(options?: InstrumentConfig): Logger; +``` + +Defined in: [appkit/src/telemetry/types.ts:50](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L50) + +Gets a logger for emitting log records. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `options?` | `InstrumentConfig` | Instrument customization options. | + +#### Returns + +`Logger` + +*** + +### getMeter() + +```ts +getMeter(options?: InstrumentConfig): Meter; +``` + +Defined in: [appkit/src/telemetry/types.ts:44](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L44) + +Gets a meter for recording metrics. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `options?` | `InstrumentConfig` | Instrument customization options. | + +#### Returns + +`Meter` + +*** + +### getTracer() + +```ts +getTracer(options?: InstrumentConfig): Tracer; +``` + +Defined in: [appkit/src/telemetry/types.ts:38](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L38) + +Gets a tracer for creating spans. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `options?` | `InstrumentConfig` | Instrument customization options. | + +#### Returns + +`Tracer` + +*** + +### registerInstrumentations() + +```ts +registerInstrumentations(instrumentations: Instrumentation[]): void; +``` + +Defined in: [appkit/src/telemetry/types.ts:81](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L81) + +Register OpenTelemetry instrumentations. +Can be called at any time, but recommended to call in plugin constructor. + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `instrumentations` | `Instrumentation`\<`InstrumentationConfig`\>[] | Array of OpenTelemetry instrumentations to register | + +#### Returns + +`void` + +*** + +### startActiveSpan() + +```ts +startActiveSpan( + name: string, + options: SpanOptions, + fn: (span: Span) => Promise, +tracerOptions?: InstrumentConfig): Promise; +``` + +Defined in: [appkit/src/telemetry/types.ts:69](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L69) + +Starts an active span and executes a callback function within its context. +Respects the traces enabled/disabled config. +When traces are disabled, executes the callback with a no-op span. + +#### Type Parameters + +| Type Parameter | +| ------ | +| `T` | + +#### Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `name` | `string` | The name of the span | +| `options` | `SpanOptions` | Span options including attributes, kind, etc. | +| `fn` | (`span`: `Span`) => `Promise`\<`T`\> | Callback function to execute within the span context | +| `tracerOptions?` | `InstrumentConfig` | Optional tracer configuration (custom name, prefix inclusion) | + +#### Returns + +`Promise`\<`T`\> + +Promise resolving to the callback's return value diff --git a/docs/docs/api/appkit/Interface.TelemetryConfig.md b/docs/docs/api/appkit/Interface.TelemetryConfig.md new file mode 100644 index 0000000..89b03fa --- /dev/null +++ b/docs/docs/api/appkit/Interface.TelemetryConfig.md @@ -0,0 +1,53 @@ +# Interface: TelemetryConfig + +Defined in: [appkit/src/telemetry/types.ts:5](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L5) + +## Properties + +### exportIntervalMs? + +```ts +optional exportIntervalMs: number; +``` + +Defined in: [appkit/src/telemetry/types.ts:9](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L9) + +*** + +### headers? + +```ts +optional headers: Record; +``` + +Defined in: [appkit/src/telemetry/types.ts:10](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L10) + +*** + +### instrumentations? + +```ts +optional instrumentations: Instrumentation[]; +``` + +Defined in: [appkit/src/telemetry/types.ts:8](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L8) + +*** + +### serviceName? + +```ts +optional serviceName: string; +``` + +Defined in: [appkit/src/telemetry/types.ts:6](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L6) + +*** + +### serviceVersion? + +```ts +optional serviceVersion: string; +``` + +Defined in: [appkit/src/telemetry/types.ts:7](https://github.com/databricks/appkit/blob/main/packages/appkit/src/telemetry/types.ts#L7) diff --git a/docs/docs/api/appkit/index.md b/docs/docs/api/appkit/index.md index dd6946c..415735d 100644 --- a/docs/docs/api/appkit/index.md +++ b/docs/docs/api/appkit/index.md @@ -11,7 +11,10 @@ | Interface | Description | | ------ | ------ | | [BasePluginConfig](Interface.BasePluginConfig.md) | - | +| [CacheConfig](Interface.CacheConfig.md) | Configuration for caching | +| [ITelemetry](Interface.ITelemetry.md) | Plugin-facing interface for OpenTelemetry instrumentation. Provides a thin abstraction over OpenTelemetry APIs for plugins. | | [StreamExecutionSettings](Interface.StreamExecutionSettings.md) | - | +| [TelemetryConfig](Interface.TelemetryConfig.md) | - | ## Type Aliases diff --git a/docs/docs/api/appkit/typedoc-sidebar.ts b/docs/docs/api/appkit/typedoc-sidebar.ts index 8618ec7..1b4302f 100644 --- a/docs/docs/api/appkit/typedoc-sidebar.ts +++ b/docs/docs/api/appkit/typedoc-sidebar.ts @@ -21,10 +21,25 @@ const typedocSidebar: SidebarsConfig = { id: "api/appkit/Interface.BasePluginConfig", label: "BasePluginConfig" }, + { + type: "doc", + id: "api/appkit/Interface.CacheConfig", + label: "CacheConfig" + }, + { + type: "doc", + id: "api/appkit/Interface.ITelemetry", + label: "ITelemetry" + }, { type: "doc", id: "api/appkit/Interface.StreamExecutionSettings", label: "StreamExecutionSettings" + }, + { + type: "doc", + id: "api/appkit/Interface.TelemetryConfig", + label: "TelemetryConfig" } ] }, diff --git a/packages/appkit/src/index.ts b/packages/appkit/src/index.ts index 743ef96..f6923fc 100644 --- a/packages/appkit/src/index.ts +++ b/packages/appkit/src/index.ts @@ -1,6 +1,7 @@ // Types from shared export type { BasePluginConfig, + CacheConfig, IAppRouter, StreamExecutionSettings, } from "shared"; @@ -19,9 +20,11 @@ export { CacheManager } from "./cache"; export { SeverityNumber, SpanStatusCode, + type TelemetryConfig, type Counter, type Histogram, type Span, + type ITelemetry, } from "./telemetry"; // Vite plugin