@databricks/appkit-ui 0.34.1 → 0.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -0
- package/README.md +3 -3
- package/dist/react/hooks/use-analytics-query.d.ts.map +1 -1
- package/dist/react/hooks/use-analytics-query.js +34 -2
- package/dist/react/hooks/use-analytics-query.js.map +1 -1
- package/docs/api/appkit/Function.createLakebasePoolManager.md +36 -0
- package/docs/api/appkit/Interface.LakebasePool.md +84 -0
- package/docs/api/appkit/Interface.LakebasePoolManager.md +101 -0
- package/docs/api/appkit.md +3 -0
- package/docs/development/llm-guide.md +0 -1
- package/docs/plugins/execution-context.md +6 -0
- package/docs/plugins/lakebase.md +112 -6
- package/llms.txt +3 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -83,6 +83,7 @@ npx @databricks/appkit docs <query>
|
|
|
83
83
|
- [Function: createAgent()](./docs/api/appkit/Function.createAgent.md): Pure factory for agent definitions. Returns the passed-in definition after
|
|
84
84
|
- [Function: createApp()](./docs/api/appkit/Function.createApp.md): Bootstraps AppKit with the provided configuration.
|
|
85
85
|
- [Function: createLakebasePool()](./docs/api/appkit/Function.createLakebasePool.md): Create a Lakebase pool with appkit's logger integration.
|
|
86
|
+
- [Function: createLakebasePoolManager()](./docs/api/appkit/Function.createLakebasePoolManager.md): Create a pool manager that maintains per-key Lakebase connection pools.
|
|
86
87
|
- [Function: defineTool()](./docs/api/appkit/Function.defineTool.md): Defines a single tool entry for a plugin's internal registry.
|
|
87
88
|
- [Function: executeFromRegistry()](./docs/api/appkit/Function.executeFromRegistry.md): Validates tool-call arguments against the entry's schema and invokes its
|
|
88
89
|
- [Function: extractServingEndpoints()](./docs/api/appkit/Function.extractServingEndpoints.md): Extract serving endpoint config from a server file by AST-parsing it.
|
|
@@ -128,7 +129,9 @@ npx @databricks/appkit docs <query>
|
|
|
128
129
|
- [Interface: JobAPI](./docs/api/appkit/Interface.JobAPI.md): User-facing API for a single configured job.
|
|
129
130
|
- [Interface: JobConfig](./docs/api/appkit/Interface.JobConfig.md): Per-job configuration options.
|
|
130
131
|
- [Interface: JobsConnectorConfig](./docs/api/appkit/Interface.JobsConnectorConfig.md): Properties
|
|
132
|
+
- [Interface: LakebasePool](./docs/api/appkit/Interface.LakebasePool.md): Subset of pg.Pool exposed by the Lakebase plugin.
|
|
131
133
|
- [Interface: LakebasePoolConfig](./docs/api/appkit/Interface.LakebasePoolConfig.md): Configuration for creating a Lakebase connection pool
|
|
134
|
+
- [Interface: LakebasePoolManager](./docs/api/appkit/Interface.LakebasePoolManager.md): Manages multiple Lakebase connection pools keyed by an identifier (e.g. userId).
|
|
132
135
|
- [Interface: McpConnectAllResult](./docs/api/appkit/Interface.McpConnectAllResult.md): Per-endpoint outcome of AppKitMcpClient.connectAll. Callers (the
|
|
133
136
|
- [Interface: Message](./docs/api/appkit/Interface.Message.md): Properties
|
|
134
137
|
- [Interface: PluginManifest<TName>](./docs/api/appkit/Interface.PluginManifest.md): Plugin manifest that declares metadata and resource requirements.
|
package/README.md
CHANGED
|
@@ -27,13 +27,13 @@ AppKit's power comes from its plugin system. Each plugin adds a focused capabili
|
|
|
27
27
|
|
|
28
28
|
## Getting started
|
|
29
29
|
|
|
30
|
-
Follow the [Getting Started](https://databricks.
|
|
30
|
+
Follow the [Getting Started](https://www.databricks.com/devhub/docs/appkit/v0/) guide to get started with AppKit.
|
|
31
31
|
|
|
32
|
-
🤖 For AI/code assistants, see the [AI-assisted development](https://databricks.
|
|
32
|
+
🤖 For AI/code assistants, see the [AI-assisted development](https://www.databricks.com/devhub/docs/appkit/v0/development/ai-assisted-development) guide.
|
|
33
33
|
|
|
34
34
|
## Documentation
|
|
35
35
|
|
|
36
|
-
📖 For full AppKit documentation, visit the [AppKit Documentation](https://databricks.
|
|
36
|
+
📖 For full AppKit documentation, visit the [AppKit Documentation](https://www.databricks.com/devhub/docs/appkit/v0/) website.
|
|
37
37
|
|
|
38
38
|
## Contributing
|
|
39
39
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-analytics-query.d.ts","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"use-analytics-query.d.ts","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"mappings":";;;;;AAsGA;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,iBAAA,wBAEJ,QAAA,GAAW,QAAA,YACX,eAAA,gBAAA,CAEV,QAAA,EAAU,CAAA,EACV,UAAA,GAAa,WAAA,CAAY,CAAA,UACzB,OAAA,GAAS,wBAAA,CAAyB,CAAA,IACjC,uBAAA,CAAwB,mBAAA,CAAoB,CAAA,EAAG,CAAA,EAAG,CAAA"}
|
|
@@ -5,6 +5,37 @@ import { useQueryHMR } from "./use-query-hmr.js";
|
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
6
6
|
|
|
7
7
|
//#region src/react/hooks/use-analytics-query.ts
|
|
8
|
+
/**
|
|
9
|
+
* Shallow structural equality for analytics query parameter objects.
|
|
10
|
+
*
|
|
11
|
+
* Analytics query parameters are produced by the `sql.*` builders and are
|
|
12
|
+
* always plain objects keyed to primitive values (string | number | boolean
|
|
13
|
+
* | null | undefined), so shallow equality is sufficient and substantially
|
|
14
|
+
* cheaper than a full deep-equal.
|
|
15
|
+
*/
|
|
16
|
+
function shallowEqualParams(a, b) {
|
|
17
|
+
if (Object.is(a, b)) return true;
|
|
18
|
+
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
|
|
19
|
+
const aKeys = Object.keys(a);
|
|
20
|
+
const bKeys = Object.keys(b);
|
|
21
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
22
|
+
for (const key of aKeys) {
|
|
23
|
+
if (!Object.hasOwn(b, key)) return false;
|
|
24
|
+
if (!Object.is(a[key], b[key])) return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Stabilize a value's identity across renders when it is structurally equal
|
|
30
|
+
* to the previous value. Used to make object-literal parameters safe to pass
|
|
31
|
+
* directly to `useAnalyticsQuery` without forcing every consumer to wrap
|
|
32
|
+
* params in `useMemo`.
|
|
33
|
+
*/
|
|
34
|
+
function useStableParams(value) {
|
|
35
|
+
const ref = useRef(value);
|
|
36
|
+
if (!shallowEqualParams(ref.current, value)) ref.current = value;
|
|
37
|
+
return ref.current;
|
|
38
|
+
}
|
|
8
39
|
function getDevMode() {
|
|
9
40
|
const dev = new URL(window.location.href).searchParams.get("dev");
|
|
10
41
|
return dev ? `?dev=${dev}` : "";
|
|
@@ -52,10 +83,11 @@ function useAnalyticsQuery(queryKey, parameters, options = {}) {
|
|
|
52
83
|
const [error, setError] = useState(null);
|
|
53
84
|
const abortControllerRef = useRef(null);
|
|
54
85
|
if (!queryKey || queryKey.trim().length === 0) throw new Error("useAnalyticsQuery: 'queryKey' must be a non-empty string.");
|
|
86
|
+
const stableParameters = useStableParams(parameters);
|
|
55
87
|
const payload = useMemo(() => {
|
|
56
88
|
try {
|
|
57
89
|
const serialized = JSON.stringify({
|
|
58
|
-
parameters,
|
|
90
|
+
parameters: stableParameters,
|
|
59
91
|
format
|
|
60
92
|
});
|
|
61
93
|
if (new Blob([serialized]).size > maxParametersSize) throw new Error("useAnalyticsQuery: Parameters size exceeds the maximum allowed size");
|
|
@@ -65,7 +97,7 @@ function useAnalyticsQuery(queryKey, parameters, options = {}) {
|
|
|
65
97
|
return null;
|
|
66
98
|
}
|
|
67
99
|
}, [
|
|
68
|
-
|
|
100
|
+
stableParameters,
|
|
69
101
|
format,
|
|
70
102
|
maxParametersSize
|
|
71
103
|
]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-analytics-query.js","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { ArrowClient, connectSSE } from \"@/js\";\nimport type {\n AnalyticsFormat,\n InferParams,\n InferResultByFormat,\n QueryKey,\n UseAnalyticsQueryOptions,\n UseAnalyticsQueryResult,\n} from \"./types\";\nimport { useQueryHMR } from \"./use-query-hmr\";\n\nfunction getDevMode() {\n const url = new URL(window.location.href);\n const searchParams = url.searchParams;\n const dev = searchParams.get(\"dev\");\n\n return dev ? `?dev=${dev}` : \"\";\n}\n\nfunction getArrowStreamUrl(id: string) {\n return `/api/analytics/arrow-result/${id}`;\n}\n\n/**\n * Subscribe to an analytics query over SSE and returns its latest result.\n * Integration hook between client and analytics plugin.\n *\n * The return type is automatically inferred based on the format:\n * - `format: \"JSON_ARRAY\"` (default): Returns typed array from QueryRegistry\n * - `format: \"ARROW_STREAM\"`: Returns TypedArrowTable with row type preserved\n *\n * Note: User context execution is determined by query file naming:\n * - `queryKey.obo.sql`: Executes as user (OBO = on-behalf-of / user delegation)\n * - `queryKey.sql`: Executes as service principal\n *\n * @param queryKey - Analytics query identifier\n * @param parameters - Query parameters (type-safe based on QueryRegistry)\n * @param options - Analytics query settings including format\n * @returns Query result state with format-appropriate data type\n *\n * @example JSON format (default)\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params);\n * // data: Array<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n *\n * @example Arrow format\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params, { format: \"ARROW_STREAM\" });\n * // data: TypedArrowTable<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n */\nexport function useAnalyticsQuery<\n T = unknown,\n K extends QueryKey = QueryKey,\n F extends AnalyticsFormat = \"JSON_ARRAY\",\n>(\n queryKey: K,\n parameters?: InferParams<K> | null,\n options: UseAnalyticsQueryOptions<F> = {} as UseAnalyticsQueryOptions<F>,\n): UseAnalyticsQueryResult<InferResultByFormat<T, K, F>> {\n const format = options?.format ?? \"JSON_ARRAY\";\n const maxParametersSize = options?.maxParametersSize ?? 100 * 1024;\n const autoStart = options?.autoStart ?? true;\n\n const devMode = getDevMode();\n const urlSuffix = `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`;\n\n type ResultType = InferResultByFormat<T, K, F>;\n const [data, setData] = useState<ResultType | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n if (!queryKey || queryKey.trim().length === 0) {\n throw new Error(\n \"useAnalyticsQuery: 'queryKey' must be a non-empty string.\",\n );\n }\n\n const payload = useMemo(() => {\n try {\n const serialized = JSON.stringify({ parameters, format });\n const sizeInBytes = new Blob([serialized]).size;\n if (sizeInBytes > maxParametersSize) {\n throw new Error(\n \"useAnalyticsQuery: Parameters size exceeds the maximum allowed size\",\n );\n }\n\n return serialized;\n } catch (error) {\n console.error(\"useAnalyticsQuery: Failed to serialize parameters\", error);\n return null;\n }\n }, [parameters, format, maxParametersSize]);\n\n const start = useCallback(() => {\n if (payload === null) {\n setError(\"Failed to serialize query parameters\");\n return;\n }\n\n // Abort previous request if exists\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n\n setLoading(true);\n setError(null);\n setData(null);\n\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n connectSSE({\n url: urlSuffix,\n payload: payload,\n signal: abortController.signal,\n onMessage: async (message) => {\n try {\n const parsed = JSON.parse(message.data);\n\n // success - JSON format\n if (parsed.type === \"result\") {\n setLoading(false);\n setData(parsed.data as ResultType);\n return;\n }\n\n // success - Arrow format\n if (parsed.type === \"arrow\") {\n try {\n const arrowData = await ArrowClient.fetchArrow(\n getArrowStreamUrl(parsed.statement_id),\n );\n const table = await ArrowClient.processArrowBuffer(arrowData);\n setLoading(false);\n // Table is cast to TypedArrowTable with row type from QueryRegistry\n setData(table as ResultType);\n return;\n } catch (error) {\n console.error(\n \"[useAnalyticsQuery] Failed to fetch Arrow data\",\n error,\n );\n setLoading(false);\n setError(\"Unable to load data, please try again\");\n return;\n }\n }\n\n // error\n if (parsed.type === \"error\" || parsed.error || parsed.code) {\n const errorMsg =\n parsed.error || parsed.message || \"Unable to execute query\";\n\n setLoading(false);\n setError(errorMsg);\n\n if (parsed.code) {\n console.error(\n `[useAnalyticsQuery] Code: ${parsed.code}, Message: ${errorMsg}`,\n );\n }\n return;\n }\n } catch (error) {\n console.warn(\"[useAnalyticsQuery] Malformed message received\", error);\n }\n },\n onError: (error) => {\n if (abortController.signal.aborted) return;\n setLoading(false);\n\n let userMessage = \"Unable to load data, please try again\";\n\n if (error instanceof Error) {\n if (error.name === \"AbortError\") {\n userMessage = \"Request timed out, please try again\";\n } else if (error.message.includes(\"Failed to fetch\")) {\n userMessage = \"Network error. Please check your connection.\";\n }\n\n console.error(\"[useAnalyticsQuery] Error\", {\n queryKey,\n error: error.message,\n stack: error.stack,\n });\n }\n setError(userMessage);\n },\n });\n }, [queryKey, payload, urlSuffix]);\n\n useEffect(() => {\n if (autoStart) {\n start();\n }\n\n return () => {\n abortControllerRef.current?.abort();\n };\n }, [start, autoStart]);\n\n // Enable HMR for query updates in dev mode\n useQueryHMR(queryKey, start);\n\n return { data, loading, error };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-analytics-query.js","names":[],"sources":["../../../src/react/hooks/use-analytics-query.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport { ArrowClient, connectSSE } from \"@/js\";\nimport type {\n AnalyticsFormat,\n InferParams,\n InferResultByFormat,\n QueryKey,\n UseAnalyticsQueryOptions,\n UseAnalyticsQueryResult,\n} from \"./types\";\nimport { useQueryHMR } from \"./use-query-hmr\";\n\n/**\n * Shallow structural equality for analytics query parameter objects.\n *\n * Analytics query parameters are produced by the `sql.*` builders and are\n * always plain objects keyed to primitive values (string | number | boolean\n * | null | undefined), so shallow equality is sufficient and substantially\n * cheaper than a full deep-equal.\n */\nfunction shallowEqualParams(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (\n a === null ||\n b === null ||\n typeof a !== \"object\" ||\n typeof b !== \"object\"\n ) {\n return false;\n }\n const aKeys = Object.keys(a as Record<string, unknown>);\n const bKeys = Object.keys(b as Record<string, unknown>);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(b, key)) return false;\n if (\n !Object.is(\n (a as Record<string, unknown>)[key],\n (b as Record<string, unknown>)[key],\n )\n ) {\n return false;\n }\n }\n return true;\n}\n\n/**\n * Stabilize a value's identity across renders when it is structurally equal\n * to the previous value. Used to make object-literal parameters safe to pass\n * directly to `useAnalyticsQuery` without forcing every consumer to wrap\n * params in `useMemo`.\n */\nfunction useStableParams<T>(value: T): T {\n const ref = useRef<T>(value);\n if (!shallowEqualParams(ref.current, value)) {\n ref.current = value;\n }\n return ref.current;\n}\n\nfunction getDevMode() {\n const url = new URL(window.location.href);\n const searchParams = url.searchParams;\n const dev = searchParams.get(\"dev\");\n\n return dev ? `?dev=${dev}` : \"\";\n}\n\nfunction getArrowStreamUrl(id: string) {\n return `/api/analytics/arrow-result/${id}`;\n}\n\n/**\n * Subscribe to an analytics query over SSE and returns its latest result.\n * Integration hook between client and analytics plugin.\n *\n * The return type is automatically inferred based on the format:\n * - `format: \"JSON_ARRAY\"` (default): Returns typed array from QueryRegistry\n * - `format: \"ARROW_STREAM\"`: Returns TypedArrowTable with row type preserved\n *\n * Note: User context execution is determined by query file naming:\n * - `queryKey.obo.sql`: Executes as user (OBO = on-behalf-of / user delegation)\n * - `queryKey.sql`: Executes as service principal\n *\n * @param queryKey - Analytics query identifier\n * @param parameters - Query parameters (type-safe based on QueryRegistry)\n * @param options - Analytics query settings including format\n * @returns Query result state with format-appropriate data type\n *\n * @example JSON format (default)\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params);\n * // data: Array<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n *\n * @example Arrow format\n * ```typescript\n * const { data } = useAnalyticsQuery(\"spend_data\", params, { format: \"ARROW_STREAM\" });\n * // data: TypedArrowTable<{ group_key: string; cost_usd: number; ... }> | null\n * ```\n */\nexport function useAnalyticsQuery<\n T = unknown,\n K extends QueryKey = QueryKey,\n F extends AnalyticsFormat = \"JSON_ARRAY\",\n>(\n queryKey: K,\n parameters?: InferParams<K> | null,\n options: UseAnalyticsQueryOptions<F> = {} as UseAnalyticsQueryOptions<F>,\n): UseAnalyticsQueryResult<InferResultByFormat<T, K, F>> {\n const format = options?.format ?? \"JSON_ARRAY\";\n const maxParametersSize = options?.maxParametersSize ?? 100 * 1024;\n const autoStart = options?.autoStart ?? true;\n\n const devMode = getDevMode();\n const urlSuffix = `/api/analytics/query/${encodeURIComponent(queryKey)}${devMode}`;\n\n type ResultType = InferResultByFormat<T, K, F>;\n const [data, setData] = useState<ResultType | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n if (!queryKey || queryKey.trim().length === 0) {\n throw new Error(\n \"useAnalyticsQuery: 'queryKey' must be a non-empty string.\",\n );\n }\n\n // Stabilize the parameters reference across renders. Without this, a fresh\n // object literal at the call site (e.g. `useAnalyticsQuery(\"k\", { limit: 10 })`)\n // would change identity every render, invalidating the `payload` memo and\n // re-running `start` -> infinite refetch loop.\n const stableParameters = useStableParams(parameters);\n\n const payload = useMemo(() => {\n try {\n const serialized = JSON.stringify({\n parameters: stableParameters,\n format,\n });\n const sizeInBytes = new Blob([serialized]).size;\n if (sizeInBytes > maxParametersSize) {\n throw new Error(\n \"useAnalyticsQuery: Parameters size exceeds the maximum allowed size\",\n );\n }\n\n return serialized;\n } catch (error) {\n console.error(\"useAnalyticsQuery: Failed to serialize parameters\", error);\n return null;\n }\n }, [stableParameters, format, maxParametersSize]);\n\n const start = useCallback(() => {\n if (payload === null) {\n setError(\"Failed to serialize query parameters\");\n return;\n }\n\n // Abort previous request if exists\n if (abortControllerRef.current) {\n abortControllerRef.current.abort();\n }\n\n setLoading(true);\n setError(null);\n setData(null);\n\n const abortController = new AbortController();\n abortControllerRef.current = abortController;\n\n connectSSE({\n url: urlSuffix,\n payload: payload,\n signal: abortController.signal,\n onMessage: async (message) => {\n try {\n const parsed = JSON.parse(message.data);\n\n // success - JSON format\n if (parsed.type === \"result\") {\n setLoading(false);\n setData(parsed.data as ResultType);\n return;\n }\n\n // success - Arrow format\n if (parsed.type === \"arrow\") {\n try {\n const arrowData = await ArrowClient.fetchArrow(\n getArrowStreamUrl(parsed.statement_id),\n );\n const table = await ArrowClient.processArrowBuffer(arrowData);\n setLoading(false);\n // Table is cast to TypedArrowTable with row type from QueryRegistry\n setData(table as ResultType);\n return;\n } catch (error) {\n console.error(\n \"[useAnalyticsQuery] Failed to fetch Arrow data\",\n error,\n );\n setLoading(false);\n setError(\"Unable to load data, please try again\");\n return;\n }\n }\n\n // error\n if (parsed.type === \"error\" || parsed.error || parsed.code) {\n const errorMsg =\n parsed.error || parsed.message || \"Unable to execute query\";\n\n setLoading(false);\n setError(errorMsg);\n\n if (parsed.code) {\n console.error(\n `[useAnalyticsQuery] Code: ${parsed.code}, Message: ${errorMsg}`,\n );\n }\n return;\n }\n } catch (error) {\n console.warn(\"[useAnalyticsQuery] Malformed message received\", error);\n }\n },\n onError: (error) => {\n if (abortController.signal.aborted) return;\n setLoading(false);\n\n let userMessage = \"Unable to load data, please try again\";\n\n if (error instanceof Error) {\n if (error.name === \"AbortError\") {\n userMessage = \"Request timed out, please try again\";\n } else if (error.message.includes(\"Failed to fetch\")) {\n userMessage = \"Network error. Please check your connection.\";\n }\n\n console.error(\"[useAnalyticsQuery] Error\", {\n queryKey,\n error: error.message,\n stack: error.stack,\n });\n }\n setError(userMessage);\n },\n });\n }, [queryKey, payload, urlSuffix]);\n\n useEffect(() => {\n if (autoStart) {\n start();\n }\n\n return () => {\n abortControllerRef.current?.abort();\n };\n }, [start, autoStart]);\n\n // Enable HMR for query updates in dev mode\n useQueryHMR(queryKey, start);\n\n return { data, loading, error };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Function: createLakebasePoolManager()
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
function createLakebasePoolManager(baseConfig?: Partial<LakebasePoolConfig>): LakebasePoolManager;
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
Create a pool manager that maintains per-key Lakebase connection pools.
|
|
9
|
+
|
|
10
|
+
Each pool is created via `createLakebasePool` with the base config merged with per-pool overrides (e.g. a user's `workspaceClient` and `user`).
|
|
11
|
+
|
|
12
|
+
A periodic cleanup removes empty Pool objects (where all connections have been closed by pg's built-in `idleTimeoutMillis`) from the internal Map.
|
|
13
|
+
|
|
14
|
+
## Parameters[](#parameters "Direct link to Parameters")
|
|
15
|
+
|
|
16
|
+
| Parameter | Type |
|
|
17
|
+
| ------------- | ------------------------------------------------------------------------------------------ |
|
|
18
|
+
| `baseConfig?` | `Partial`<[`LakebasePoolConfig`](./docs/api/appkit/Interface.LakebasePoolConfig.md)> |
|
|
19
|
+
|
|
20
|
+
## Returns[](#returns "Direct link to Returns")
|
|
21
|
+
|
|
22
|
+
[`LakebasePoolManager`](./docs/api/appkit/Interface.LakebasePoolManager.md)
|
|
23
|
+
|
|
24
|
+
## Example[](#example "Direct link to Example")
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
const poolManager = createLakebasePoolManager();
|
|
28
|
+
|
|
29
|
+
// In a route handler:
|
|
30
|
+
const userPool = poolManager.getPool(userName, {
|
|
31
|
+
workspaceClient: new WorkspaceClient({ token: userToken, host, authType: "pat" }),
|
|
32
|
+
user: userName,
|
|
33
|
+
});
|
|
34
|
+
const result = await userPool.query("SELECT * FROM products");
|
|
35
|
+
|
|
36
|
+
```
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Interface: LakebasePool
|
|
2
|
+
|
|
3
|
+
Subset of `pg.Pool` exposed by the Lakebase plugin.
|
|
4
|
+
|
|
5
|
+
RoutingPool does not extend EventEmitter — event listener methods like `on('error', ...)` are not available. Use `query()`, `connect()`, and `end()` for all pool operations.
|
|
6
|
+
|
|
7
|
+
## Properties[](#properties "Direct link to Properties")
|
|
8
|
+
|
|
9
|
+
### idleCount[](#idlecount "Direct link to idleCount")
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
readonly idleCount: number;
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
***
|
|
17
|
+
|
|
18
|
+
### totalCount[](#totalcount "Direct link to totalCount")
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
readonly totalCount: number;
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
***
|
|
26
|
+
|
|
27
|
+
### waitingCount[](#waitingcount "Direct link to waitingCount")
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
readonly waitingCount: number;
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Methods[](#methods "Direct link to Methods")
|
|
35
|
+
|
|
36
|
+
### connect()[](#connect "Direct link to connect()")
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
connect(): Promise<PoolClient>;
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
#### Returns[](#returns "Direct link to Returns")
|
|
44
|
+
|
|
45
|
+
`Promise`<`PoolClient`>
|
|
46
|
+
|
|
47
|
+
***
|
|
48
|
+
|
|
49
|
+
### end()[](#end "Direct link to end()")
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
end(): Promise<void>;
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### Returns[](#returns-1 "Direct link to Returns")
|
|
57
|
+
|
|
58
|
+
`Promise`<`void`>
|
|
59
|
+
|
|
60
|
+
***
|
|
61
|
+
|
|
62
|
+
### query()[](#query "Direct link to query()")
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
query<T>(text: string, values?: unknown[]): Promise<QueryResult<T>>;
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
#### Type Parameters[](#type-parameters "Direct link to Type Parameters")
|
|
70
|
+
|
|
71
|
+
| Type Parameter | Default type |
|
|
72
|
+
| ------------------------------ | ------------ |
|
|
73
|
+
| `T` *extends* `QueryResultRow` | `any` |
|
|
74
|
+
|
|
75
|
+
#### Parameters[](#parameters "Direct link to Parameters")
|
|
76
|
+
|
|
77
|
+
| Parameter | Type |
|
|
78
|
+
| --------- | ------------ |
|
|
79
|
+
| `text` | `string` |
|
|
80
|
+
| `values?` | `unknown`\[] |
|
|
81
|
+
|
|
82
|
+
#### Returns[](#returns-2 "Direct link to Returns")
|
|
83
|
+
|
|
84
|
+
`Promise`<`QueryResult`<`T`>>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Interface: LakebasePoolManager
|
|
2
|
+
|
|
3
|
+
Manages multiple Lakebase connection pools keyed by an identifier (e.g. userId).
|
|
4
|
+
|
|
5
|
+
Used for On-Behalf-Of (OBO) scenarios where each user needs their own pool with their own OAuth token refresh, enabling features like Row-Level Security.
|
|
6
|
+
|
|
7
|
+
## Properties[](#properties "Direct link to Properties")
|
|
8
|
+
|
|
9
|
+
### size[](#size "Direct link to size")
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
readonly size: number;
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Number of active pools.
|
|
17
|
+
|
|
18
|
+
## Methods[](#methods "Direct link to Methods")
|
|
19
|
+
|
|
20
|
+
### closeAll()[](#closeall "Direct link to closeAll()")
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
closeAll(): Promise<void>;
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Close all managed pools and stop cleanup (for graceful shutdown).
|
|
28
|
+
|
|
29
|
+
#### Returns[](#returns "Direct link to Returns")
|
|
30
|
+
|
|
31
|
+
`Promise`<`void`>
|
|
32
|
+
|
|
33
|
+
***
|
|
34
|
+
|
|
35
|
+
### closePool()[](#closepool "Direct link to closePool()")
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
closePool(key: string): Promise<void>;
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Close and remove a specific pool.
|
|
43
|
+
|
|
44
|
+
#### Parameters[](#parameters "Direct link to Parameters")
|
|
45
|
+
|
|
46
|
+
| Parameter | Type |
|
|
47
|
+
| --------- | -------- |
|
|
48
|
+
| `key` | `string` |
|
|
49
|
+
|
|
50
|
+
#### Returns[](#returns-1 "Direct link to Returns")
|
|
51
|
+
|
|
52
|
+
`Promise`<`void`>
|
|
53
|
+
|
|
54
|
+
***
|
|
55
|
+
|
|
56
|
+
### getPool()[](#getpool "Direct link to getPool()")
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
getPool(
|
|
60
|
+
key: string,
|
|
61
|
+
perPoolConfig: Partial<LakebasePoolConfig>,
|
|
62
|
+
tokenFingerprint?: string): Pool;
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Get an existing pool or create a new one for the given key. When creating, merges `perPoolConfig` with the base config passed to the factory.
|
|
67
|
+
|
|
68
|
+
If `tokenFingerprint` is provided and differs from the cached pool's fingerprint, the stale pool is closed and a fresh one is created with the new config (including the updated `workspaceClient`).
|
|
69
|
+
|
|
70
|
+
#### Parameters[](#parameters-1 "Direct link to Parameters")
|
|
71
|
+
|
|
72
|
+
| Parameter | Type |
|
|
73
|
+
| ------------------- | ------------------------------------------------------------------------------------------ |
|
|
74
|
+
| `key` | `string` |
|
|
75
|
+
| `perPoolConfig` | `Partial`<[`LakebasePoolConfig`](./docs/api/appkit/Interface.LakebasePoolConfig.md)> |
|
|
76
|
+
| `tokenFingerprint?` | `string` |
|
|
77
|
+
|
|
78
|
+
#### Returns[](#returns-2 "Direct link to Returns")
|
|
79
|
+
|
|
80
|
+
`Pool`
|
|
81
|
+
|
|
82
|
+
***
|
|
83
|
+
|
|
84
|
+
### hasPool()[](#haspool "Direct link to hasPool()")
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
hasPool(key: string): boolean;
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Check whether a pool exists for the given key.
|
|
92
|
+
|
|
93
|
+
#### Parameters[](#parameters-2 "Direct link to Parameters")
|
|
94
|
+
|
|
95
|
+
| Parameter | Type |
|
|
96
|
+
| --------- | -------- |
|
|
97
|
+
| `key` | `string` |
|
|
98
|
+
|
|
99
|
+
#### Returns[](#returns-3 "Direct link to Returns")
|
|
100
|
+
|
|
101
|
+
`boolean`
|
package/docs/api/appkit.md
CHANGED
|
@@ -52,7 +52,9 @@ Documentation merge entry for Typedoc — combines the stable `@databricks/appki
|
|
|
52
52
|
| [JobAPI](./docs/api/appkit/Interface.JobAPI.md) | User-facing API for a single configured job. |
|
|
53
53
|
| [JobConfig](./docs/api/appkit/Interface.JobConfig.md) | Per-job configuration options. |
|
|
54
54
|
| [JobsConnectorConfig](./docs/api/appkit/Interface.JobsConnectorConfig.md) | - |
|
|
55
|
+
| [LakebasePool](./docs/api/appkit/Interface.LakebasePool.md) | Subset of `pg.Pool` exposed by the Lakebase plugin. |
|
|
55
56
|
| [LakebasePoolConfig](./docs/api/appkit/Interface.LakebasePoolConfig.md) | Configuration for creating a Lakebase connection pool |
|
|
57
|
+
| [LakebasePoolManager](./docs/api/appkit/Interface.LakebasePoolManager.md) | Manages multiple Lakebase connection pools keyed by an identifier (e.g. userId). |
|
|
56
58
|
| [McpConnectAllResult](./docs/api/appkit/Interface.McpConnectAllResult.md) | Per-endpoint outcome of [AppKitMcpClient.connectAll](./docs/api/appkit/Class.AppKitMcpClient.md#connectall). Callers (the agents plugin in particular) use the split to warn at startup when some MCP servers are unreachable without aborting boot for the rest. |
|
|
57
59
|
| [Message](./docs/api/appkit/Interface.Message.md) | - |
|
|
58
60
|
| [PluginManifest](./docs/api/appkit/Interface.PluginManifest.md) | Plugin manifest that declares metadata and resource requirements. Attached to plugin classes as a static property. Extends the shared PluginManifest with strict resource types. |
|
|
@@ -124,6 +126,7 @@ Documentation merge entry for Typedoc — combines the stable `@databricks/appki
|
|
|
124
126
|
| [createAgent](./docs/api/appkit/Function.createAgent.md) | Pure factory for agent definitions. Returns the passed-in definition after cycle-detecting the sub-agent graph. Accepts the full `AgentDefinition` shape and is safe to call at module top-level. |
|
|
125
127
|
| [createApp](./docs/api/appkit/Function.createApp.md) | Bootstraps AppKit with the provided configuration. |
|
|
126
128
|
| [createLakebasePool](./docs/api/appkit/Function.createLakebasePool.md) | Create a Lakebase pool with appkit's logger integration. Telemetry automatically uses appkit's OpenTelemetry configuration via global registry. |
|
|
129
|
+
| [createLakebasePoolManager](./docs/api/appkit/Function.createLakebasePoolManager.md) | Create a pool manager that maintains per-key Lakebase connection pools. |
|
|
127
130
|
| [defineTool](./docs/api/appkit/Function.defineTool.md) | Defines a single tool entry for a plugin's internal registry. |
|
|
128
131
|
| [executeFromRegistry](./docs/api/appkit/Function.executeFromRegistry.md) | Validates tool-call arguments against the entry's schema and invokes its handler. On validation failure, returns an LLM-friendly error string (matching the behavior of `tool()`) rather than throwing, so the model can self-correct on its next turn. |
|
|
129
132
|
| [extractServingEndpoints](./docs/api/appkit/Function.extractServingEndpoints.md) | Extract serving endpoint config from a server file by AST-parsing it. Looks for `serving({ endpoints: { alias: { env: "..." }, ... } })` calls and extracts the endpoint alias names and their environment variable mappings. |
|
|
@@ -24,7 +24,6 @@ This guide is designed to work even when you *do not* have access to the AppKit
|
|
|
24
24
|
|
|
25
25
|
* **Do not invent APIs**. If unsure, stick to the patterns shown in the documentation and only use documented exports from `@databricks/appkit` and `@databricks/appkit-ui`.
|
|
26
26
|
* **`createApp()` is async**. Prefer **top-level `await createApp(...)`**. If you can't, use `void createApp(...)` and do not ignore promise rejection.
|
|
27
|
-
* **Always memoize query parameters** passed to `useAnalyticsQuery` / charts to avoid refetch loops.
|
|
28
27
|
* **Always handle loading/error/empty states** in UI (use `Skeleton`, error text, empty state).
|
|
29
28
|
* **Always use `sql.*` helpers** for query parameters (do not pass raw strings/numbers unless the query expects none).
|
|
30
29
|
* **Never construct SQL strings dynamically**. Use parameterized queries with `:paramName`.
|
|
@@ -51,6 +51,12 @@ The `plugin.execute` span created by the execution interceptor chain includes th
|
|
|
51
51
|
|
|
52
52
|
These attributes are automatically added when your plugin uses `execute()` or `executeStream()`. All built-in plugins use these methods for their OBO operations. Custom plugins should do the same to get automatic telemetry instrumentation.
|
|
53
53
|
|
|
54
|
+
## Lakebase per-user connections[](#lakebase-per-user-connections "Direct link to Lakebase per-user connections")
|
|
55
|
+
|
|
56
|
+
The Lakebase plugin uses a different mechanism for `asUser(req)`: instead of swapping the `WorkspaceClient` via AsyncLocalStorage, it creates a **separate `pg.Pool` per user**, each with its own OAuth token refresh. This is necessary because PostgreSQL connections are authenticated at connection time — the pool itself is the authentication boundary.
|
|
57
|
+
|
|
58
|
+
See [Lakebase plugin — per-user connections](./docs/plugins/lakebase.md#on-behalf-of-obo--per-user-connections) for details.
|
|
59
|
+
|
|
54
60
|
## Development mode behavior[](#development-mode-behavior "Direct link to Development mode behavior")
|
|
55
61
|
|
|
56
62
|
In local development (`NODE_ENV=development`), if `asUser(req)` is called without a user token, it logs a warning and skips user impersonation — the operation runs with the default credentials configured for the app instead. The telemetry span will show `execution.context: "service"` with `execution.obo_dev_fallback: true` to distinguish these from regular service principal calls.
|
package/docs/plugins/lakebase.md
CHANGED
|
@@ -115,6 +115,95 @@ await createApp({
|
|
|
115
115
|
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
+
## On-Behalf-Of (OBO) — per-user connections[](#on-behalf-of-obo--per-user-connections "Direct link to On-Behalf-Of (OBO) — per-user connections")
|
|
119
|
+
|
|
120
|
+
When your app needs Row-Level Security (RLS) or per-user data isolation, use `asUser(req)` to execute queries using a per-user Lakebase connection pool. Each user's pool is authenticated with their Databricks identity, so PostgreSQL's `current_user` reflects the actual user.
|
|
121
|
+
|
|
122
|
+
### Prerequisites[](#prerequisites-1 "Direct link to Prerequisites")
|
|
123
|
+
|
|
124
|
+
1. **Enable user authorization** in your Databricks App with the **`postgres`** scope. See [User authorization](https://docs.databricks.com/aws/en/dev-tools/databricks-apps/auth#user-authorization) for setup instructions. In your `databricks.yml`:
|
|
125
|
+
|
|
126
|
+
```yaml
|
|
127
|
+
resources:
|
|
128
|
+
apps:
|
|
129
|
+
app:
|
|
130
|
+
user_api_scopes:
|
|
131
|
+
- postgres
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Apps scaffolded with `databricks apps init` and the Lakebase plugin include this automatically.
|
|
136
|
+
|
|
137
|
+
2. Each app user needs a **Postgres role** in Lakebase. Create one with the Databricks CLI:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
databricks postgres create-role "projects/{project_id}/branches/{branch_id}" \
|
|
141
|
+
--json '{"spec": {"identity_type": "USER", "postgres_role": "user@example.com"}}'
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Alternatively, create roles in the Lakebase UI under **Branch Overview** → **Add role**.
|
|
146
|
+
|
|
147
|
+
note
|
|
148
|
+
|
|
149
|
+
Do not grant `databricks_superuser` to OBO users — superusers bypass RLS. Use [fine-grained grants](#fine-grained-permissions) instead.
|
|
150
|
+
|
|
151
|
+
### Usage[](#usage "Direct link to Usage")
|
|
152
|
+
|
|
153
|
+
No configuration needed — just call `asUser(req)`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const AppKit = await createApp({
|
|
157
|
+
plugins: [server(), lakebase()],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Service principal query (default — bypasses RLS as table owner)
|
|
161
|
+
const all = await AppKit.lakebase.query("SELECT * FROM app.orders");
|
|
162
|
+
|
|
163
|
+
// User-scoped query (per-user pool, RLS enforced)
|
|
164
|
+
app.get("/api/my-orders", async (req, res) => {
|
|
165
|
+
const result = await AppKit.lakebase
|
|
166
|
+
.asUser(req)
|
|
167
|
+
.query("SELECT * FROM app.orders ORDER BY created_at DESC");
|
|
168
|
+
res.json(result.rows);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
When `asUser(req)` is called:
|
|
174
|
+
|
|
175
|
+
1. The user's token and identity are extracted from `x-forwarded-access-token` and `x-forwarded-email` headers (set automatically by Databricks Apps).
|
|
176
|
+
2. A per-user `pg.Pool` is created (or reused) with the user's OAuth credentials.
|
|
177
|
+
3. `query()` and `pool` use the user's pool — `current_user` in PostgreSQL reflects the user's identity.
|
|
178
|
+
|
|
179
|
+
### Row-Level Security example[](#row-level-security-example "Direct link to Row-Level Security example")
|
|
180
|
+
|
|
181
|
+
```sql
|
|
182
|
+
-- As the service principal (during app setup):
|
|
183
|
+
ALTER TABLE app.orders ENABLE ROW LEVEL SECURITY;
|
|
184
|
+
|
|
185
|
+
CREATE POLICY user_orders ON app.orders
|
|
186
|
+
FOR ALL TO PUBLIC
|
|
187
|
+
USING (owner = current_user);
|
|
188
|
+
|
|
189
|
+
-- Grant access so OBO users can query
|
|
190
|
+
GRANT USAGE ON SCHEMA app TO PUBLIC;
|
|
191
|
+
GRANT SELECT, INSERT ON ALL TABLES IN SCHEMA app TO PUBLIC;
|
|
192
|
+
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### How it works[](#how-it-works "Direct link to How it works")
|
|
196
|
+
|
|
197
|
+
* The **service principal pool** (`AppKit.lakebase.pool`) is always created and used for DDL operations, seeding, and admin queries.
|
|
198
|
+
* **Per-user pools** are created on the first `asUser(req)` call and cached by user identity. Each pool has its own OAuth token refresh cycle.
|
|
199
|
+
* Idle connections within per-user pools close automatically (30s idle timeout). Empty pool objects are cleaned up periodically.
|
|
200
|
+
* On shutdown, all pools (SP + user) are closed gracefully.
|
|
201
|
+
* In development mode (`NODE_ENV=development`), if no user token is available, `asUser(req)` falls back to the SP pool with a warning.
|
|
202
|
+
|
|
203
|
+
RLS and superusers
|
|
204
|
+
|
|
205
|
+
PostgreSQL superusers bypass Row-Level Security entirely. Users with the `databricks_superuser` role will see all rows regardless of RLS policies. For RLS enforcement, use [fine-grained grants](#fine-grained-permissions) instead of the superuser role.
|
|
206
|
+
|
|
118
207
|
## Database Permissions[](#database-permissions "Direct link to Database Permissions")
|
|
119
208
|
|
|
120
209
|
When you create the app with the Lakebase resource using the [Getting started](#getting-started-with-the-lakebase) guide, the Service Principal is automatically granted `CONNECT_AND_CREATE` permission on the `postgres` resource. This lets the Service Principal connect to the database and create new objects, but **not access any existing schemas or tables.**
|
|
@@ -125,15 +214,30 @@ To develop locally against a deployed Lakebase database:
|
|
|
125
214
|
|
|
126
215
|
1. **Deploy the app first.** The Service Principal creates the database schema and tables on first deploy. Apps generated from `databricks apps init` handle this automatically - they check if tables exist on startup and skip creation if they do.
|
|
127
216
|
|
|
128
|
-
2. **Grant `databricks_superuser
|
|
217
|
+
2. **Grant `databricks_superuser`** (skip if you are the Lakebase project owner — you already have full access):
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
# Create a new role with databricks_superuser
|
|
221
|
+
databricks postgres create-role "projects/{project_id}/branches/{branch_id}" \
|
|
222
|
+
--json '{"spec": {"identity_type": "USER", "postgres_role": "user@example.com", "membership_roles": ["DATABRICKS_SUPERUSER"]}}'
|
|
129
223
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
To grant superuser to an existing role, use [`update-role`](https://docs.databricks.com/aws/en/dev-tools/cli/reference/postgres-commands#databricks-postgres-update-role):
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
databricks postgres update-role \
|
|
230
|
+
"projects/{project_id}/branches/{branch_id}/roles/{role_id}" \
|
|
231
|
+
"spec.membership_roles" \
|
|
232
|
+
--json '{"spec": {"membership_roles": ["DATABRICKS_SUPERUSER"]}}'
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Alternatively, you can manage roles in the Lakebase Autoscaling UI under your project's **Branch Overview** page → **Add role** / **Edit role**.
|
|
133
237
|
|
|
134
238
|
3. **Run locally** - your Databricks user identity (email) is used for OAuth authentication. The `databricks_superuser` role gives full **DML access** (read/write data) but **not DDL** (creating schemas or tables) - that's why deploying first matters (see note below).
|
|
135
239
|
|
|
136
|
-
For other users,
|
|
240
|
+
For other users, repeat step 2 to create an OAuth role with `databricks_superuser` for each user.
|
|
137
241
|
|
|
138
242
|
tip
|
|
139
243
|
|
|
@@ -141,7 +245,9 @@ tip
|
|
|
141
245
|
|
|
142
246
|
Why deploy first?
|
|
143
247
|
|
|
144
|
-
When the app is deployed, the Service Principal creates schemas and tables and becomes their owner.
|
|
248
|
+
When the app is deployed, the Service Principal creates schemas and tables and becomes their owner. `databricks_superuser` gives full DML access (read/write) but not DDL, so local development works only after the schema exists.
|
|
249
|
+
|
|
250
|
+
If you run `npm run dev` first, your credentials own the schema and the deployed app hits `permission denied`. To recover, export any data first (`pg_dump` or a temporary schema copy), then drop the schema and redeploy. After redeploying, the Service Principal recreates the schema on startup. (PostgreSQL schema ownership is tied to the role that created it and cannot be reassigned by regular users.)
|
|
145
251
|
|
|
146
252
|
### Fine-grained permissions[](#fine-grained-permissions "Direct link to Fine-grained permissions")
|
|
147
253
|
|
package/llms.txt
CHANGED
|
@@ -83,6 +83,7 @@ npx @databricks/appkit docs <query>
|
|
|
83
83
|
- [Function: createAgent()](./docs/api/appkit/Function.createAgent.md): Pure factory for agent definitions. Returns the passed-in definition after
|
|
84
84
|
- [Function: createApp()](./docs/api/appkit/Function.createApp.md): Bootstraps AppKit with the provided configuration.
|
|
85
85
|
- [Function: createLakebasePool()](./docs/api/appkit/Function.createLakebasePool.md): Create a Lakebase pool with appkit's logger integration.
|
|
86
|
+
- [Function: createLakebasePoolManager()](./docs/api/appkit/Function.createLakebasePoolManager.md): Create a pool manager that maintains per-key Lakebase connection pools.
|
|
86
87
|
- [Function: defineTool()](./docs/api/appkit/Function.defineTool.md): Defines a single tool entry for a plugin's internal registry.
|
|
87
88
|
- [Function: executeFromRegistry()](./docs/api/appkit/Function.executeFromRegistry.md): Validates tool-call arguments against the entry's schema and invokes its
|
|
88
89
|
- [Function: extractServingEndpoints()](./docs/api/appkit/Function.extractServingEndpoints.md): Extract serving endpoint config from a server file by AST-parsing it.
|
|
@@ -128,7 +129,9 @@ npx @databricks/appkit docs <query>
|
|
|
128
129
|
- [Interface: JobAPI](./docs/api/appkit/Interface.JobAPI.md): User-facing API for a single configured job.
|
|
129
130
|
- [Interface: JobConfig](./docs/api/appkit/Interface.JobConfig.md): Per-job configuration options.
|
|
130
131
|
- [Interface: JobsConnectorConfig](./docs/api/appkit/Interface.JobsConnectorConfig.md): Properties
|
|
132
|
+
- [Interface: LakebasePool](./docs/api/appkit/Interface.LakebasePool.md): Subset of pg.Pool exposed by the Lakebase plugin.
|
|
131
133
|
- [Interface: LakebasePoolConfig](./docs/api/appkit/Interface.LakebasePoolConfig.md): Configuration for creating a Lakebase connection pool
|
|
134
|
+
- [Interface: LakebasePoolManager](./docs/api/appkit/Interface.LakebasePoolManager.md): Manages multiple Lakebase connection pools keyed by an identifier (e.g. userId).
|
|
132
135
|
- [Interface: McpConnectAllResult](./docs/api/appkit/Interface.McpConnectAllResult.md): Per-endpoint outcome of AppKitMcpClient.connectAll. Callers (the
|
|
133
136
|
- [Interface: Message](./docs/api/appkit/Interface.Message.md): Properties
|
|
134
137
|
- [Interface: PluginManifest<TName>](./docs/api/appkit/Interface.PluginManifest.md): Plugin manifest that declares metadata and resource requirements.
|