@databricks/appkit 0.45.0 → 0.47.0
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 +1 -0
- package/dist/agents/databricks.d.ts +25 -1
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +20 -1
- package/dist/agents/databricks.js.map +1 -1
- package/dist/app/index.d.ts +49 -2
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +87 -10
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +2 -2
- package/dist/cli/commands/generate-types.js +9 -4
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js +52 -0
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/types.d.ts +11 -0
- package/dist/core/agent/types.d.ts.map +1 -1
- package/dist/core/agent/types.js.map +1 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +2 -0
- package/dist/core/appkit.js.map +1 -1
- package/dist/core/lifecycle-manager.js +183 -0
- package/dist/core/lifecycle-manager.js.map +1 -0
- package/dist/core/plugin-context.d.ts +19 -1
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +9 -3
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +2 -1
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +3 -3
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts +16 -0
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +162 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/metric.js +7 -0
- package/dist/plugins/analytics/mv/cache.js +51 -0
- package/dist/plugins/analytics/mv/cache.js.map +1 -0
- package/dist/plugins/analytics/mv/constants.js +72 -0
- package/dist/plugins/analytics/mv/constants.js.map +1 -0
- package/dist/plugins/analytics/mv/formatters.js +151 -0
- package/dist/plugins/analytics/mv/formatters.js.map +1 -0
- package/dist/plugins/analytics/mv/index.js +6 -0
- package/dist/plugins/analytics/mv/registry.js +55 -0
- package/dist/plugins/analytics/mv/registry.js.map +1 -0
- package/dist/plugins/analytics/mv/schemas.js +178 -0
- package/dist/plugins/analytics/mv/schemas.js.map +1 -0
- package/dist/plugins/analytics/types.js.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts +10 -2
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +18 -7
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/server/index.d.ts +51 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js +110 -23
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/registry/resource-registry.d.ts +9 -1
- package/dist/registry/resource-registry.d.ts.map +1 -1
- package/dist/registry/resource-registry.js +22 -5
- package/dist/registry/resource-registry.js.map +1 -1
- package/dist/schemas/metric-fqn.js +14 -0
- package/dist/schemas/metric-fqn.js.map +1 -0
- package/dist/shared/src/execute.d.ts +1 -3
- package/dist/shared/src/execute.d.ts.map +1 -1
- package/dist/shared/src/plugin.d.ts +16 -0
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/metric-fqn.js +78 -46
- package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/schemas/metric-source.js +90 -0
- package/dist/shared/src/schemas/metric-source.js.map +1 -0
- package/dist/stream/buffers.js +1 -1
- package/dist/stream/buffers.js.map +1 -1
- package/dist/stream/defaults.js +0 -2
- package/dist/stream/defaults.js.map +1 -1
- package/dist/stream/stream-manager.d.ts +2 -2
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +26 -24
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/stream-registry.js +30 -23
- package/dist/stream/stream-registry.js.map +1 -1
- package/dist/stream/timers.js +17 -0
- package/dist/stream/timers.js.map +1 -0
- package/dist/stream/types.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js +19 -13
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/type-generator/index.js +14 -7
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/mv-registry/config.js +13 -31
- package/dist/type-generator/mv-registry/config.js.map +1 -1
- package/dist/type-generator/mv-registry/describe.js +1 -31
- package/dist/type-generator/mv-registry/describe.js.map +1 -1
- package/dist/type-generator/mv-registry/sync.js +1 -1
- package/dist/type-generator/mv-registry/sync.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts +5 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +21 -4
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/utils/safe-handler.js +28 -0
- package/dist/utils/safe-handler.js.map +1 -0
- package/docs/api/appkit/Class.Plugin.md +2 -0
- package/docs/api/appkit/Class.ResourceRegistry.md +1 -1
- package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
- package/docs/api/appkit/Interface.GenerationParams.md +58 -0
- package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
- package/docs/api/appkit.md +1 -0
- package/docs/development/type-generation.md +3 -3
- package/docs/plugins/analytics.md +172 -23
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { Plugin } from \"vite\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n TypegenFatalError,\n TypegenSyntaxError,\n} from \"./index\";\nimport type { PreflightMode } from \"./preflight\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * How long the DEV background watcher waits for a STARTING warehouse to reach\n * RUNNING before giving up. Short relative to the CLI's preflight budget: this\n * is a best-effort \"regenerate once the warehouse warms up\" convenience, not a\n * gate, so we'd rather stop polling than hold a detached task open for minutes.\n */\nconst DEV_WAREHOUSE_WATCH_MAX_MS = 60_000;\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /**\n * Path to the metric registry d.ts file (relative to client folder).\n * Defaults to a sibling of `outFile`, computed by the generator.\n */\n mvOutFile?: string;\n /** Folders to watch for changes. */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let mvOutFile: string | undefined;\n let watchFolders: string[];\n\n // Single-flight state for runGenerate(). `inFlight` is the promise of the\n // currently-running drain (null when idle); `queued` records that a trigger\n // arrived while a run was active so exactly ONE trailing run fires afterwards\n // (latest-wins — coalesces any number of overlapping triggers into a single\n // rerun). `queued` is read/cleared synchronously inside the drain loop so a\n // trigger landing in any window is caught before the drain exits.\n //\n // `pendingMode` is the mode the next generate should run in (latest-wins, like\n // `queued`): the foreground build runs non-blocking in dev (instant degrade)\n // while the background warehouse watch runs blocking (real DESCRIBEs). A\n // blocking watch trigger that lands while a non-blocking foreground run is in\n // flight therefore still describes when its trailing run fires.\n let inFlight: Promise<void> | null = null;\n let queued = false;\n let pendingMode: PreflightMode = \"non-blocking\";\n\n // The currently-armed DEV background warehouse watch, if any. Aborting it\n // stops a pending waitUntilRunning (server shutdown, or a newer arm replacing\n // an older one).\n let watchController: AbortController | null = null;\n\n /**\n * Generate types once in the given preflight {@link PreflightMode}. Never\n * throws in dev (logs instead); in production it rethrows so the build fails.\n * This is the un-guarded core — callers should go through {@link runGenerate}\n * so concurrent triggers can't race-write the .d.ts.\n *\n * @param mode - preflight policy for this run. The foreground build passes a\n * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it\n * degrades instantly); the background warehouse watch passes \"blocking\" so\n * its regenerate actually DESCRIBEs and lands real (non-degraded) types.\n */\n async function generateOnce(mode: PreflightMode) {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder: watchFolders[0],\n warehouseId,\n noCache: false,\n mode,\n mvOutFile,\n });\n } catch (error) {\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // report in their message. Their stack frames and attached query arrays\n // point into appkit internals and only add noise, so surface just the\n // message — both when failing the prod build and when logging in dev.\n const isTypegenError =\n error instanceof TypegenSyntaxError ||\n error instanceof TypegenFatalError;\n\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n if (isTypegenError) error.stack = error.message;\n throw error;\n }\n\n if (isTypegenError) {\n logger.error(\"%s\", error.message);\n } else {\n logger.error(\"Error generating types: %O\", error);\n }\n }\n }\n\n /**\n * Single-flight wrapper around {@link generateOnce}. The initial build, the\n * .sql watcher, and the DEV warehouse watch all route through here so they can\n * never run typegen concurrently (which would race-write the .d.ts).\n *\n * If a run is already in flight, this does NOT start a second one — it records\n * the requested mode and sets a trailing flag so exactly one more run fires\n * after the current finishes, coalescing any number of overlapping triggers\n * (latest-wins, including the mode: a blocking watch trigger that arrives mid\n * non-blocking foreground run still describes when its trailing run fires).\n *\n * @param mode - preflight policy for this run. Recorded into `pendingMode`,\n * which the drain reads for each generate (latest trigger wins).\n * @returns A promise that resolves when this trigger's work (including any\n * trailing run it scheduled) has completed.\n */\n function runGenerate(mode: PreflightMode): Promise<void> {\n pendingMode = mode;\n\n if (inFlight) {\n // A run is active: remember that another trigger arrived and ride out the\n // current run. One trailing run then covers all coalesced triggers and\n // runs in the latest requested mode (recorded above).\n queued = true;\n return inFlight;\n }\n\n // Drain in a loop rather than recursing after a single queued-check: a\n // trigger can land in the window between generateOnce() resolving and the\n // check, so we re-test `queued` until it's clear. Critically, `inFlight` is\n // cleared synchronously in the SAME tick as the final `queued === false`\n // observation — never deferred to a .finally microtask — so there's no\n // window where a trigger sees `inFlight` set but the drain has already\n // decided to exit. The guard stays held for the whole drain, so concurrent\n // triggers only ever set the flag; they never start a parallel generate.\n const drain = async (): Promise<void> => {\n while (true) {\n queued = false;\n // Snapshot the mode synchronously alongside clearing `queued` so a\n // trigger landing during this generate is observed (via `queued`) on the\n // next loop with its own mode, not silently dropped.\n const runMode = pendingMode;\n await generateOnce(runMode);\n // Synchronous check + clear, atomic w.r.t. other (synchronous) callers.\n if (!queued) {\n inFlight = null;\n return;\n }\n }\n };\n\n inFlight = drain();\n return inFlight;\n }\n\n /**\n * DEV-only: get the warehouse to RUNNING in the background and regenerate with\n * real (non-degraded) types once it is — without blocking dev startup. The\n * foreground build only ever degrades in dev (instant `unknown`/cached types),\n * so this is what lands actual DESCRIBE results in the editor for EVERY\n * reachable warehouse state, not just one that happens to already be warm.\n *\n * Post-probe behaviour by state:\n * - RUNNING → describe right away (the dev foreground degraded, so a running\n * warehouse would otherwise never get real types). `waitUntilRunning`\n * returns immediately for an already-running warehouse, then the blocking\n * regenerate fires.\n * - STARTING → it's already coming up; just wait for RUNNING, then describe.\n * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.\n * - DELETED / DELETING → return (a deleted warehouse can't be started, and\n * blocking typegen would treat it as fatal); leave the degraded types.\n *\n * No-op in production or without a warehouse id. Replaces any previously-armed\n * watch (aborting it first). Fully self-contained: it never throws into the\n * caller and never re-arms itself. The whole lifecycle is abortable via the\n * shared {@link watchController} — its signal is threaded into\n * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the\n * regenerate routes through {@link runGenerate} so it can't race-write the\n * .d.ts with the foreground degrade or a `.sql` re-trigger.\n *\n * The regenerate runs in \"blocking\" mode (not the foreground's non-blocking)\n * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —\n * the whole point of warming the warehouse in the background.\n */\n function armWarehouseWatch(): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n if (!warehouseId) return;\n\n // Supersede any in-flight watch so we never run two concurrently.\n watchController?.abort();\n const controller = new AbortController();\n watchController = controller;\n const { signal } = controller;\n\n void (async () => {\n try {\n const client = new WorkspaceClient({});\n const state = await getWarehouseState(client, warehouseId);\n\n // A deleted/deleting warehouse can't be started and blocking typegen\n // would treat it as fatal — leave the degraded types and stop. Every\n // other state (including RUNNING) proceeds to wait-then-describe so the\n // dev editor gets real types, not just the foreground's degraded ones.\n if (state === \"DELETED\" || state === \"DELETING\") {\n return;\n }\n\n // Stopped/stopping won't reach RUNNING on its own — nudge it. RUNNING and\n // STARTING need no start (RUNNING is already up; STARTING is coming up),\n // so don't issue a redundant one. A failed start is non-fatal: give up\n // silently rather than throw out of the detached task (the developer\n // still has degraded/cached types).\n let startedByUs = false;\n if (state === \"STOPPED\" || state === \"STOPPING\") {\n try {\n logger.debug(\"Warehouse is %s; starting it.\", state);\n await startWarehouse(client, warehouseId);\n startedByUs = true;\n } catch {\n return;\n }\n }\n\n // Wait for RUNNING. For an already-RUNNING warehouse this returns on the\n // first poll; for STARTING/STOPPED it polls (abortably) until the\n // warehouse warms up, a terminal state, or the deadline.\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,\n signal,\n // We just issued the start, so the first poll(s) often still report\n // STOPPED/STOPPING before the start propagates. Poll through those\n // instead of bailing, or the regenerate would never fire. When we\n // didn't start it (RUNNING/STARTING branch), keep the default terminal\n // states.\n treatStoppedAsTransient: startedByUs,\n });\n\n if (final === \"RUNNING\" && !signal.aborted) {\n logger.debug(\"Warehouse is RUNNING; regenerating types.\");\n // Blocking: the warehouse is RUNNING now, so describe it and emit real\n // (non-degraded) types — unlike the foreground dev run, which degraded.\n // Routed through the single-flight guard so it coalesces with the\n // foreground degrade / any `.sql` re-trigger instead of racing them.\n await runGenerate(\"blocking\");\n }\n } catch {\n // Detached background task: any failure (timeout, abort, connectivity,\n // auth) is non-fatal — the developer still has degraded/cached types.\n }\n })();\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n if (!existsSync(path.join(process.cwd(), \"config\", \"queries\"))) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n // The metric out-path resolves against projectRoot only when explicitly\n // provided; an unset option passes through as undefined so the generator\n // computes its sibling-of-outFile default. In the all-defaults case the\n // final path is identical (the default outFile above lives in\n // shared/<TYPES_DIR>/), and a customized outFile now keeps its metric\n // sibling next to it instead of pinning it under shared/.\n mvOutFile =\n options?.mvOutFile !== undefined\n ? path.resolve(projectRoot, options.mvOutFile)\n : undefined;\n watchFolders = options?.watchFolders ?? [\n path.join(process.cwd(), \"config\", \"queries\"),\n ];\n },\n\n buildStart() {\n // Production: block the build on this generate (and surface failures).\n // The watch is a dev-only no-op, so just run typegen.\n if (process.env.NODE_ENV === \"production\") {\n return runGenerate(\"blocking\");\n }\n\n // Dev: don't block startup waiting on typegen. The foreground generate runs\n // non-blocking — it skips the warehouse entirely and writes degraded\n // (cached/`unknown`) types instantly. Then arm the warehouse watch so the\n // warehouse gets a one-shot BLOCKING regenerate (real types) in the\n // background for EVERY reachable state: RUNNING describes right away, while\n // STARTING/STOPPED are waited (and started) until they reach RUNNING.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n if (\n isWatchedFile &&\n (changedFile.endsWith(\".sql\") ||\n // Basename equality, not endsWith: a sibling like\n // \"legacy-metric-views.json\" must not trigger a regenerate —\n // only the real config file does.\n path.basename(changedFile) === \"metric-views.json\")\n ) {\n // Route through the single-flight runner (was fire-and-forget\n // generate(), which could race the initial build / watch). This is a\n // dev-only hook, so degrade instantly (non-blocking), then re-arm the\n // warehouse watch so the edited query or metric-view source is\n // re-described in the background against the running warehouse (or\n // once a still-starting one warms up), landing fresh\n // blocking-described types.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n }\n });\n\n // Tear down any pending warehouse watch when the dev server closes so a\n // long backoff can't keep the process alive after shutdown.\n server.httpServer?.once(\"close\", () => {\n watchController?.abort();\n });\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { Plugin } from \"vite\";\nimport { METRIC_CONFIG_FILE } from \"../../../shared/src/schemas/metric-fqn\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n TypegenFatalError,\n TypegenSyntaxError,\n} from \"./index\";\nimport type { PreflightMode } from \"./preflight\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * How long the DEV background watcher waits for a STARTING warehouse to reach\n * RUNNING before giving up. Short relative to the CLI's preflight budget: this\n * is a best-effort \"regenerate once the warehouse warms up\" convenience, not a\n * gate, so we'd rather stop polling than hold a detached task open for minutes.\n */\nconst DEV_WAREHOUSE_WATCH_MAX_MS = 60_000;\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /**\n * Path to the metric registry d.ts file (relative to client folder).\n * Defaults to a sibling of `outFile`, computed by the generator.\n */\n mvOutFile?: string;\n /**\n * Folders to watch for changes. Defaults to `config/queries` and\n * `config/metric-views`. When overridden, include a `queries` folder and/or a\n * `metric-views` folder — they are resolved by their trailing path segment.\n */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let mvOutFile: string | undefined;\n let watchFolders: string[];\n // The queries + metric-views config folders, resolved in `configResolved`.\n // Passed explicitly into generateFromEntryPoint so neither is inferred from\n // `watchFolders` ordering (which used to assume queries was `watchFolders[0]`).\n let queryFolder: string | undefined;\n let metricViewsFolder: string | undefined;\n\n // Single-flight state for runGenerate(). `inFlight` is the promise of the\n // currently-running drain (null when idle); `queued` records that a trigger\n // arrived while a run was active so exactly ONE trailing run fires afterwards\n // (latest-wins — coalesces any number of overlapping triggers into a single\n // rerun). `queued` is read/cleared synchronously inside the drain loop so a\n // trigger landing in any window is caught before the drain exits.\n //\n // `pendingMode` is the mode the next generate should run in (latest-wins, like\n // `queued`): the foreground build runs non-blocking in dev (instant degrade)\n // while the background warehouse watch runs blocking (real DESCRIBEs). A\n // blocking watch trigger that lands while a non-blocking foreground run is in\n // flight therefore still describes when its trailing run fires.\n let inFlight: Promise<void> | null = null;\n let queued = false;\n let pendingMode: PreflightMode = \"non-blocking\";\n\n // The currently-armed DEV background warehouse watch, if any. Aborting it\n // stops a pending waitUntilRunning (server shutdown, or a newer arm replacing\n // an older one).\n let watchController: AbortController | null = null;\n\n /**\n * Generate types once in the given preflight {@link PreflightMode}. Never\n * throws in dev (logs instead); in production it rethrows so the build fails.\n * This is the un-guarded core — callers should go through {@link runGenerate}\n * so concurrent triggers can't race-write the .d.ts.\n *\n * @param mode - preflight policy for this run. The foreground build passes a\n * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it\n * degrades instantly); the background warehouse watch passes \"blocking\" so\n * its regenerate actually DESCRIBEs and lands real (non-degraded) types.\n */\n async function generateOnce(mode: PreflightMode) {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder,\n metricViewsFolder,\n warehouseId,\n noCache: false,\n mode,\n mvOutFile,\n });\n } catch (error) {\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // report in their message. Their stack frames and attached query arrays\n // point into appkit internals and only add noise, so surface just the\n // message — both when failing the prod build and when logging in dev.\n const isTypegenError =\n error instanceof TypegenSyntaxError ||\n error instanceof TypegenFatalError;\n\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n if (isTypegenError) error.stack = error.message;\n throw error;\n }\n\n if (isTypegenError) {\n logger.error(\"%s\", error.message);\n } else {\n logger.error(\"Error generating types: %O\", error);\n }\n }\n }\n\n /**\n * Single-flight wrapper around {@link generateOnce}. The initial build, the\n * .sql watcher, and the DEV warehouse watch all route through here so they can\n * never run typegen concurrently (which would race-write the .d.ts).\n *\n * If a run is already in flight, this does NOT start a second one — it records\n * the requested mode and sets a trailing flag so exactly one more run fires\n * after the current finishes, coalescing any number of overlapping triggers\n * (latest-wins, including the mode: a blocking watch trigger that arrives mid\n * non-blocking foreground run still describes when its trailing run fires).\n *\n * @param mode - preflight policy for this run. Recorded into `pendingMode`,\n * which the drain reads for each generate (latest trigger wins).\n * @returns A promise that resolves when this trigger's work (including any\n * trailing run it scheduled) has completed.\n */\n function runGenerate(mode: PreflightMode): Promise<void> {\n pendingMode = mode;\n\n if (inFlight) {\n // A run is active: remember that another trigger arrived and ride out the\n // current run. One trailing run then covers all coalesced triggers and\n // runs in the latest requested mode (recorded above).\n queued = true;\n return inFlight;\n }\n\n // Drain in a loop rather than recursing after a single queued-check: a\n // trigger can land in the window between generateOnce() resolving and the\n // check, so we re-test `queued` until it's clear. Critically, `inFlight` is\n // cleared synchronously in the SAME tick as the final `queued === false`\n // observation — never deferred to a .finally microtask — so there's no\n // window where a trigger sees `inFlight` set but the drain has already\n // decided to exit. The guard stays held for the whole drain, so concurrent\n // triggers only ever set the flag; they never start a parallel generate.\n const drain = async (): Promise<void> => {\n while (true) {\n queued = false;\n // Snapshot the mode synchronously alongside clearing `queued` so a\n // trigger landing during this generate is observed (via `queued`) on the\n // next loop with its own mode, not silently dropped.\n const runMode = pendingMode;\n await generateOnce(runMode);\n // Synchronous check + clear, atomic w.r.t. other (synchronous) callers.\n if (!queued) {\n inFlight = null;\n return;\n }\n }\n };\n\n inFlight = drain();\n return inFlight;\n }\n\n /**\n * DEV-only: get the warehouse to RUNNING in the background and regenerate with\n * real (non-degraded) types once it is — without blocking dev startup. The\n * foreground build only ever degrades in dev (instant `unknown`/cached types),\n * so this is what lands actual DESCRIBE results in the editor for EVERY\n * reachable warehouse state, not just one that happens to already be warm.\n *\n * Post-probe behaviour by state:\n * - RUNNING → describe right away (the dev foreground degraded, so a running\n * warehouse would otherwise never get real types). `waitUntilRunning`\n * returns immediately for an already-running warehouse, then the blocking\n * regenerate fires.\n * - STARTING → it's already coming up; just wait for RUNNING, then describe.\n * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.\n * - DELETED / DELETING → return (a deleted warehouse can't be started, and\n * blocking typegen would treat it as fatal); leave the degraded types.\n *\n * No-op in production or without a warehouse id. Replaces any previously-armed\n * watch (aborting it first). Fully self-contained: it never throws into the\n * caller and never re-arms itself. The whole lifecycle is abortable via the\n * shared {@link watchController} — its signal is threaded into\n * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the\n * regenerate routes through {@link runGenerate} so it can't race-write the\n * .d.ts with the foreground degrade or a `.sql` re-trigger.\n *\n * The regenerate runs in \"blocking\" mode (not the foreground's non-blocking)\n * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —\n * the whole point of warming the warehouse in the background.\n */\n function armWarehouseWatch(): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n if (!warehouseId) return;\n\n // Supersede any in-flight watch so we never run two concurrently.\n watchController?.abort();\n const controller = new AbortController();\n watchController = controller;\n const { signal } = controller;\n\n void (async () => {\n try {\n const client = new WorkspaceClient({});\n const state = await getWarehouseState(client, warehouseId);\n\n // A deleted/deleting warehouse can't be started and blocking typegen\n // would treat it as fatal — leave the degraded types and stop. Every\n // other state (including RUNNING) proceeds to wait-then-describe so the\n // dev editor gets real types, not just the foreground's degraded ones.\n if (state === \"DELETED\" || state === \"DELETING\") {\n return;\n }\n\n // Stopped/stopping won't reach RUNNING on its own — nudge it. RUNNING and\n // STARTING need no start (RUNNING is already up; STARTING is coming up),\n // so don't issue a redundant one. A failed start is non-fatal: give up\n // silently rather than throw out of the detached task (the developer\n // still has degraded/cached types).\n let startedByUs = false;\n if (state === \"STOPPED\" || state === \"STOPPING\") {\n try {\n logger.debug(\"Warehouse is %s; starting it.\", state);\n await startWarehouse(client, warehouseId);\n startedByUs = true;\n } catch {\n return;\n }\n }\n\n // Wait for RUNNING. For an already-RUNNING warehouse this returns on the\n // first poll; for STARTING/STOPPED it polls (abortably) until the\n // warehouse warms up, a terminal state, or the deadline.\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,\n signal,\n // We just issued the start, so the first poll(s) often still report\n // STOPPED/STOPPING before the start propagates. Poll through those\n // instead of bailing, or the regenerate would never fire. When we\n // didn't start it (RUNNING/STARTING branch), keep the default terminal\n // states.\n treatStoppedAsTransient: startedByUs,\n });\n\n if (final === \"RUNNING\" && !signal.aborted) {\n logger.debug(\"Warehouse is RUNNING; regenerating types.\");\n // Blocking: the warehouse is RUNNING now, so describe it and emit real\n // (non-degraded) types — unlike the foreground dev run, which degraded.\n // Routed through the single-flight guard so it coalesces with the\n // foreground degrade / any `.sql` re-trigger instead of racing them.\n await runGenerate(\"blocking\");\n }\n } catch {\n // Detached background task: any failure (timeout, abort, connectivity,\n // auth) is non-fatal — the developer still has degraded/cached types.\n }\n })();\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n // Run when either config surface exists. Metric-view types are\n // independent of `.sql` queries, so a metric-only project (a\n // `config/metric-views/` with no `config/queries/`) must still activate\n // the plugin.\n const hasQueries = existsSync(\n path.join(process.cwd(), \"config\", \"queries\"),\n );\n const hasMetricViews = existsSync(\n path.join(process.cwd(), \"config\", \"metric-views\"),\n );\n if (!hasQueries && !hasMetricViews) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n // The metric out-path resolves against projectRoot only when explicitly\n // provided; an unset option passes through as undefined so the generator\n // computes its sibling-of-outFile default. In the all-defaults case the\n // final path is identical (the default outFile above lives in\n // shared/<TYPES_DIR>/), and a customized outFile now keeps its metric\n // sibling next to it instead of pinning it under shared/.\n mvOutFile =\n options?.mvOutFile !== undefined\n ? path.resolve(projectRoot, options.mvOutFile)\n : undefined;\n\n const defaultQueryFolder = path.join(process.cwd(), \"config\", \"queries\");\n const defaultMetricViewsFolder = path.join(\n process.cwd(),\n \"config\",\n \"metric-views\",\n );\n watchFolders = options?.watchFolders ?? [\n defaultQueryFolder,\n defaultMetricViewsFolder,\n ];\n\n // Resolve the two config folders explicitly rather than assuming a\n // position in `watchFolders`. With a custom `watchFolders`, match by the\n // trailing segment; otherwise use the computed defaults.\n if (options?.watchFolders) {\n queryFolder = watchFolders.find((f) => path.basename(f) === \"queries\");\n metricViewsFolder = watchFolders.find(\n (f) => path.basename(f) === \"metric-views\",\n );\n } else {\n queryFolder = defaultQueryFolder;\n metricViewsFolder = defaultMetricViewsFolder;\n }\n },\n\n buildStart() {\n // Production: block the build on this generate (and surface failures).\n // The watch is a dev-only no-op, so just run typegen.\n if (process.env.NODE_ENV === \"production\") {\n return runGenerate(\"blocking\");\n }\n\n // Dev: don't block startup waiting on typegen. The foreground generate runs\n // non-blocking — it skips the warehouse entirely and writes degraded\n // (cached/`unknown`) types instantly. Then arm the warehouse watch so the\n // warehouse gets a one-shot BLOCKING regenerate (real types) in the\n // background for EVERY reachable state: RUNNING describes right away, while\n // STARTING/STOPPED are waited (and started) until they reach RUNNING.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n // The metric config is `definitions.json` — a far more generic name\n // than the old `metric-views.json`. Match it by DIRECTORY, not bare\n // basename: only a `definitions.json` sitting directly in the\n // metric-views folder is the config (a `definitions.json` elsewhere in\n // a watched tree must not trigger a regenerate).\n const isMetricConfig =\n metricViewsFolder !== undefined &&\n path.basename(changedFile) === METRIC_CONFIG_FILE &&\n path.dirname(path.resolve(changedFile)) ===\n path.resolve(metricViewsFolder);\n\n if (isWatchedFile && (changedFile.endsWith(\".sql\") || isMetricConfig)) {\n // Route through the single-flight runner (was fire-and-forget\n // generate(), which could race the initial build / watch). This is a\n // dev-only hook, so degrade instantly (non-blocking), then re-arm the\n // warehouse watch so the edited query or metric-view source is\n // re-described in the background against the running warehouse (or\n // once a still-starting one warms up), landing fresh\n // blocking-described types.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n }\n });\n\n // Tear down any pending warehouse watch when the dev server closes so a\n // long backoff can't keep the process alive after shutdown.\n server.httpServer?.once(\"close\", () => {\n watchController?.abort();\n });\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/utils/safe-handler.ts
|
|
2
|
+
/**
|
|
3
|
+
* Wrap an Express handler so synchronous throws and async rejections are
|
|
4
|
+
* forwarded to the error-handling middleware via `next(err)`.
|
|
5
|
+
*
|
|
6
|
+
* Express 4 does not catch rejected promises from async handlers — a
|
|
7
|
+
* rejection (e.g. `asUser()` throwing `AuthenticationError` before any
|
|
8
|
+
* response is written) escapes as an unhandledRejection, which leaves the
|
|
9
|
+
* request hanging and crashes Node by default.
|
|
10
|
+
*
|
|
11
|
+
* Error-handling middleware (arity 4, `(err, req, res, next)`) is returned
|
|
12
|
+
* unchanged: Express identifies it by function arity, so wrapping it would
|
|
13
|
+
* silently turn it into a regular handler.
|
|
14
|
+
*
|
|
15
|
+
* Note: handlers must RETURN their promise (e.g. `(req, res) =>
|
|
16
|
+
* this._handle(req, res)` or `async (req, res) => { await ... }`) for
|
|
17
|
+
* rejections to be caught — a fire-and-forget body like
|
|
18
|
+
* `(req, res) => { this._handle(req, res); }` hides the promise from the
|
|
19
|
+
* wrapper and rejections escape as unhandledRejection again.
|
|
20
|
+
*/
|
|
21
|
+
function forwardAsyncErrors(handler) {
|
|
22
|
+
if (handler.length > 3) return handler;
|
|
23
|
+
return (req, res, next) => Promise.resolve().then(() => handler(req, res, next)).catch(next);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
//#endregion
|
|
27
|
+
export { forwardAsyncErrors };
|
|
28
|
+
//# sourceMappingURL=safe-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"safe-handler.js","names":[],"sources":["../../src/utils/safe-handler.ts"],"sourcesContent":["import type express from \"express\";\n\n/**\n * Wrap an Express handler so synchronous throws and async rejections are\n * forwarded to the error-handling middleware via `next(err)`.\n *\n * Express 4 does not catch rejected promises from async handlers — a\n * rejection (e.g. `asUser()` throwing `AuthenticationError` before any\n * response is written) escapes as an unhandledRejection, which leaves the\n * request hanging and crashes Node by default.\n *\n * Error-handling middleware (arity 4, `(err, req, res, next)`) is returned\n * unchanged: Express identifies it by function arity, so wrapping it would\n * silently turn it into a regular handler.\n *\n * Note: handlers must RETURN their promise (e.g. `(req, res) =>\n * this._handle(req, res)` or `async (req, res) => { await ... }`) for\n * rejections to be caught — a fire-and-forget body like\n * `(req, res) => { this._handle(req, res); }` hides the promise from the\n * wrapper and rejections escape as unhandledRejection again.\n */\nexport function forwardAsyncErrors(\n handler: (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => unknown,\n): express.RequestHandler {\n if (handler.length > 3) return handler as express.RequestHandler;\n return (req, res, next) =>\n Promise.resolve()\n .then(() => handler(req, res, next))\n .catch(next);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,mBACd,SAKwB;AACxB,KAAI,QAAQ,SAAS,EAAG,QAAO;AAC/B,SAAQ,KAAK,KAAK,SAChB,QAAQ,SAAS,CACd,WAAW,QAAQ,KAAK,KAAK,KAAK,CAAC,CACnC,MAAM,KAAK"}
|
|
@@ -224,6 +224,8 @@ abortActiveOperations(): void;
|
|
|
224
224
|
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
+
Cancel in-flight work (abort signals, SSE streams). Runs in the first phase of graceful shutdown, BEFORE any plugin's `shutdown()` hook — so it must not tear down shared resources (e.g. connection pools) that other plugins' hooks may still need. Put teardown in `shutdown()`.
|
|
228
|
+
|
|
227
229
|
#### Returns[](#returns-1 "Direct link to Returns")
|
|
228
230
|
|
|
229
231
|
`void`
|
|
@@ -249,7 +249,7 @@ const registry = ResourceRegistry.getInstance();
|
|
|
249
249
|
const result = registry.validate();
|
|
250
250
|
|
|
251
251
|
if (!result.valid) {
|
|
252
|
-
console.error(
|
|
252
|
+
console.error(ResourceRegistry.formatMissingResources(result.missing));
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
```
|
|
@@ -35,6 +35,17 @@ When true, the thread used for a chat request against this agent is deleted from
|
|
|
35
35
|
|
|
36
36
|
***
|
|
37
37
|
|
|
38
|
+
### generationParams?[](#generationparams "Direct link to generationParams?")
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
optional generationParams: GenerationParams;
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Optional generation parameters (`temperature`, `top_p`, `stop`, `frequency_penalty`, `presence_penalty`) forwarded to the OpenAI-compatible serving request body. Only set keys are sent. Applied only when AppKit builds the adapter itself (string or omitted `model`); when you pass a pre-built `AgentAdapter`, configure generation params on it directly.
|
|
46
|
+
|
|
47
|
+
***
|
|
48
|
+
|
|
38
49
|
### instructions[](#instructions "Direct link to instructions")
|
|
39
50
|
|
|
40
51
|
```ts
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Interface: GenerationParams
|
|
2
|
+
|
|
3
|
+
Optional generation parameters forwarded to the OpenAI-compatible serving request body. Names match the serving API wire keys. Only keys that are set are sent — undefined values are omitted so the endpoint applies its own defaults. Ranges are not validated here; the serving endpoint validates.
|
|
4
|
+
|
|
5
|
+
## Properties[](#properties "Direct link to Properties")
|
|
6
|
+
|
|
7
|
+
### frequency\_penalty?[](#frequency_penalty "Direct link to frequency_penalty?")
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
optional frequency_penalty: number;
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Penalize tokens by frequency.
|
|
15
|
+
|
|
16
|
+
***
|
|
17
|
+
|
|
18
|
+
### presence\_penalty?[](#presence_penalty "Direct link to presence_penalty?")
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
optional presence_penalty: number;
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Penalize tokens by prior presence.
|
|
26
|
+
|
|
27
|
+
***
|
|
28
|
+
|
|
29
|
+
### stop?[](#stop "Direct link to stop?")
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
optional stop: string | string[];
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Stop sequence(s) that end generation.
|
|
37
|
+
|
|
38
|
+
***
|
|
39
|
+
|
|
40
|
+
### temperature?[](#temperature "Direct link to temperature?")
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
optional temperature: number;
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Sampling temperature.
|
|
48
|
+
|
|
49
|
+
***
|
|
50
|
+
|
|
51
|
+
### top\_p?[](#top_p "Direct link to top_p?")
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
optional top_p: number;
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Nucleus sampling probability mass (`top_p`).
|
|
@@ -31,6 +31,17 @@ Mirrors `AgentDefinition.ephemeral` — skip thread persistence.
|
|
|
31
31
|
|
|
32
32
|
***
|
|
33
33
|
|
|
34
|
+
### generationParams?[](#generationparams "Direct link to generationParams?")
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
optional generationParams: GenerationParams;
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Mirrors `AgentDefinition.generationParams`.
|
|
42
|
+
|
|
43
|
+
***
|
|
44
|
+
|
|
34
45
|
### instructions[](#instructions "Direct link to instructions")
|
|
35
46
|
|
|
36
47
|
```ts
|
package/docs/api/appkit.md
CHANGED
|
@@ -47,6 +47,7 @@ Documentation merge entry for Typedoc — combines the stable `@databricks/appki
|
|
|
47
47
|
| [FileResource](./docs/api/appkit/Interface.FileResource.md) | Describes the file or directory being acted upon. |
|
|
48
48
|
| [FunctionTool](./docs/api/appkit/Interface.FunctionTool.md) | - |
|
|
49
49
|
| [GenerateDatabaseCredentialRequest](./docs/api/appkit/Interface.GenerateDatabaseCredentialRequest.md) | Request parameters for generating database OAuth credentials |
|
|
50
|
+
| [GenerationParams](./docs/api/appkit/Interface.GenerationParams.md) | Optional generation parameters forwarded to the OpenAI-compatible serving request body. Names match the serving API wire keys. Only keys that are set are sent — undefined values are omitted so the endpoint applies its own defaults. Ranges are not validated here; the serving endpoint validates. |
|
|
50
51
|
| [IJobsConfig](./docs/api/appkit/Interface.IJobsConfig.md) | Configuration for the Jobs plugin. |
|
|
51
52
|
| [ITelemetry](./docs/api/appkit/Interface.ITelemetry.md) | Plugin-facing interface for OpenTelemetry instrumentation. Provides a thin abstraction over OpenTelemetry APIs for plugins. |
|
|
52
53
|
| [JobAPI](./docs/api/appkit/Interface.JobAPI.md) | User-facing API for a single configured job. |
|
|
@@ -88,13 +88,13 @@ In blocking mode the generator starts a stopped warehouse, waits (bounded) for i
|
|
|
88
88
|
|
|
89
89
|
## Metric-view types[](#metric-view-types "Direct link to Metric-view types")
|
|
90
90
|
|
|
91
|
-
`generate-types` (and the Vite plugin) emit metric-view types **additively** — there is no separate command. When a `config/
|
|
91
|
+
`generate-types` (and the Vite plugin) emit metric-view types **additively** — there is no separate command. When a `config/metric-views/definitions.json` file is present, the same run that generates your query types also DESCRIBEs each declared [UC Metric View](./docs/plugins/analytics.md) and writes `metric-views.d.ts` into `shared/appkit-types/`:
|
|
92
92
|
|
|
93
93
|
* `metric-views.d.ts` — augments the `MetricRegistry` interface so `useMetricView('<key>', …)` is autocompleted and type-checked. Each view's measures, dimensions, and their semantic metadata (SQL type, display name, format, time grains) are encoded at the type level.
|
|
94
94
|
|
|
95
|
-
If `metric-views.json` is absent the metric path stays dormant (nothing is emitted). When present it follows the **same** warehouse-readiness contract as query types: in the default non-blocking run a view that can't be described yet — a cold warehouse, or a bad/unreachable source — is written with permissive types and a warning, while under `--wait` that same situation fails the build so CI never ships incomplete metric types. A malformed `
|
|
95
|
+
If `config/metric-views/definitions.json` is absent the metric path stays dormant (nothing is emitted). When present it follows the **same** warehouse-readiness contract as query types: in the default non-blocking run a view that can't be described yet — a cold warehouse, or a bad/unreachable source — is written with permissive types and a warning, while under `--wait` that same situation fails the build so CI never ships incomplete metric types. A malformed `definitions.json` (invalid JSON, or a source that isn't a three-part UC FQN) fails fast in every mode.
|
|
96
96
|
|
|
97
|
-
`
|
|
97
|
+
`definitions.json` is keyed by metric key; each entry names the three-part UC FQN of the view and, optionally, the executor it runs as (`app_service_principal`, the default, or `user`):
|
|
98
98
|
|
|
99
99
|
```json
|
|
100
100
|
{
|
|
@@ -86,12 +86,154 @@ The analytics plugin exposes these endpoints (mounted under `/api/analytics`):
|
|
|
86
86
|
|
|
87
87
|
* `POST /api/analytics/query/:query_key`
|
|
88
88
|
* `GET /api/analytics/arrow-result/:jobId`
|
|
89
|
+
* `POST /api/analytics/metric/:key` — measure a Unity Catalog Metric View (see [Metric views](#metric-views))
|
|
89
90
|
|
|
90
91
|
## Format options[](#format-options "Direct link to Format options")
|
|
91
92
|
|
|
92
93
|
* `format: "JSON"` (default) returns JSON rows
|
|
93
94
|
* `format: "ARROW"` returns an Arrow "statement\_id" payload over SSE, then the client fetches binary Arrow from `/api/analytics/arrow-result/:jobId`
|
|
94
95
|
|
|
96
|
+
## Metric views[](#metric-views "Direct link to Metric views")
|
|
97
|
+
|
|
98
|
+
`POST /api/analytics/metric/:key` measures a [Unity Catalog Metric View](https://docs.databricks.com/en/metric-views/index.html) that you declared in `config/metric-views/definitions.json`. Instead of writing SQL, the caller sends a structured request — which measures to aggregate, which dimensions to group by, and an optional filter — and the plugin builds and runs the `SELECT MEASURE(...) ... GROUP BY ALL` for you against the view.
|
|
99
|
+
|
|
100
|
+
The route is **dormant until `config/metric-views/definitions.json` exists**: with no config file, every metric key returns `404`. Declaring the file (and generating types) is covered in [Metric-view types](./docs/development/type-generation.md#metric-view-types); this section documents the runtime endpoint that config activates.
|
|
101
|
+
|
|
102
|
+
### Request body[](#request-body "Direct link to Request body")
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
POST /api/analytics/metric/:key
|
|
106
|
+
Content-Type: application/json
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
"measures": ["arr", "revenue"],
|
|
110
|
+
"dimensions": ["region", "order_date"],
|
|
111
|
+
"timeGrain": "month",
|
|
112
|
+
"timeDimension": "order_date",
|
|
113
|
+
"filter": { "member": "region", "operator": "in", "values": ["EMEA", "APAC"] },
|
|
114
|
+
"limit": 100
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`:key` is a metric key from `definitions.json`. The body fields:
|
|
120
|
+
|
|
121
|
+
| Field | Type | Required | Description |
|
|
122
|
+
| --------------- | ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
|
+
| `measures` | `string[]` | yes | Measures to aggregate. At least 1, at most 50. Each becomes `MEASURE(<name>) AS <name>`. |
|
|
124
|
+
| `dimensions` | `string[]` | no | Dimensions to group by (max 20). Selected verbatim and grouped via `GROUP BY ALL`. |
|
|
125
|
+
| `filter` | object | no | Structured predicate tree translated into a parameterized `WHERE` clause (see [Filters](#filters)). |
|
|
126
|
+
| `timeGrain` | `string` | no | Bucket a time dimension via `date_trunc('<grain>', …)` — e.g. `day`, `month`. Requires `timeDimension`. |
|
|
127
|
+
| `timeDimension` | `string` | no | The single dimension `timeGrain` buckets. Must be one of `dimensions`. Required whenever `timeGrain` is set. |
|
|
128
|
+
| `limit` | `number` | no | Positive integer row cap (max 100000). |
|
|
129
|
+
| `format` | `string` | no | `JSON_ARRAY` (default). `JSON` is accepted as a deprecated alias for it; Arrow formats (`ARROW`, `ARROW_STREAM`) are rejected on this route. |
|
|
130
|
+
|
|
131
|
+
Measures and dimensions must be unique across both lists — a name cannot repeat, nor appear as both a measure and a dimension.
|
|
132
|
+
|
|
133
|
+
### How the request becomes SQL[](#how-the-request-becomes-sql "Direct link to How the request becomes SQL")
|
|
134
|
+
|
|
135
|
+
Given a view registered as `catalog.schema.revenue_metrics`, the request above produces (measures and dimensions are sorted for a deterministic SELECT list):
|
|
136
|
+
|
|
137
|
+
```sql
|
|
138
|
+
SELECT MEASURE(`arr`) AS `arr`, MEASURE(`revenue`) AS `revenue`,
|
|
139
|
+
date_trunc('month', `order_date`) AS `order_date`, `region`
|
|
140
|
+
FROM `catalog`.`schema`.`revenue_metrics`
|
|
141
|
+
WHERE `region` IN (:f_0, :f_1)
|
|
142
|
+
GROUP BY ALL
|
|
143
|
+
LIMIT 100
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The metric view's FQN and every measure/dimension identifier are backtick-quoted; filter values are bound as parameters (`:f_0`, `:f_1`, …), never interpolated into the SQL string.
|
|
148
|
+
|
|
149
|
+
### Filters[](#filters "Direct link to Filters")
|
|
150
|
+
|
|
151
|
+
`filter` is a recursive tree. A leaf is a single predicate:
|
|
152
|
+
|
|
153
|
+
```json
|
|
154
|
+
{ "member": "region", "operator": "equals", "values": ["EMEA"] }
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Predicates combine with `and` / `or` groups, which can nest:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"and": [
|
|
163
|
+
{ "member": "region", "operator": "in", "values": ["EMEA", "APAC"] },
|
|
164
|
+
{
|
|
165
|
+
"or": [
|
|
166
|
+
{ "member": "segment", "operator": "equals", "values": ["Enterprise"] },
|
|
167
|
+
{ "member": "deal_size", "operator": "gt", "values": [50000] }
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
The operator vocabulary:
|
|
176
|
+
|
|
177
|
+
| Operator | SQL | Values |
|
|
178
|
+
| --------------------------- | ----------------------- | ------------------ |
|
|
179
|
+
| `equals` | `=` | exactly one |
|
|
180
|
+
| `notEquals` | `<>` | exactly one |
|
|
181
|
+
| `in` | `IN (…)` | one or more |
|
|
182
|
+
| `notIn` | `NOT IN (…)` | one or more |
|
|
183
|
+
| `gt` / `gte` / `lt` / `lte` | `>` / `>=` / `<` / `<=` | exactly one |
|
|
184
|
+
| `contains` | `LIKE :param` | exactly one string |
|
|
185
|
+
| `notContains` | `NOT LIKE :param` | exactly one string |
|
|
186
|
+
| `set` | `IS NOT NULL` | none |
|
|
187
|
+
| `notSet` | `IS NULL` | none |
|
|
188
|
+
|
|
189
|
+
For `contains` / `notContains`, the `%…%` wildcards are applied to the *bound parameter value* (`%value%`), not written into the SQL text — so the value is never interpolated, consistent with every other operator.
|
|
190
|
+
|
|
191
|
+
Empty groups of either kind (`{ "or": [] }`, `{ "and": [] }`) are rejected with `400` — every `and` / `or` group must contain at least one predicate. To send no filter, omit the `filter` field entirely rather than passing an empty group.
|
|
192
|
+
|
|
193
|
+
Filters are bounded to keep hostile input from exhausting the server: nesting depth ≤ 8, ≤ 100 children per `and` / `or` group, and ≤ 1000 values per predicate. A request that exceeds a cap is rejected with `400`.
|
|
194
|
+
|
|
195
|
+
### Executors (cache scope)[](#executors-cache-scope "Direct link to Executors (cache scope)")
|
|
196
|
+
|
|
197
|
+
Each entry in `definitions.json` names the executor the query runs as, which also sets the cache scope. This is fixed by config, not the request:
|
|
198
|
+
|
|
199
|
+
| `executor` | Runs as | Cache |
|
|
200
|
+
| --------------------------------- | ---------------------------------- | ----------------------- |
|
|
201
|
+
| `app_service_principal` (default) | The app service principal | Shared across all users |
|
|
202
|
+
| `user` | The requesting user (on-behalf-of) | Per user |
|
|
203
|
+
|
|
204
|
+
This mirrors the `<key>.sql` vs `<key>.obo.sql` distinction for [file-based queries](#execution-context).
|
|
205
|
+
|
|
206
|
+
### Response[](#response "Direct link to Response")
|
|
207
|
+
|
|
208
|
+
The response is the same SSE stream as `POST /api/analytics/query/:query_key`. If the SQL warehouse is cold it first emits `warehouse_status` events (see [Warehouse readiness](#warehouse-readiness)), then a single `result` event with the rows as objects:
|
|
209
|
+
|
|
210
|
+
```json
|
|
211
|
+
{
|
|
212
|
+
"type": "result",
|
|
213
|
+
"data": [
|
|
214
|
+
{
|
|
215
|
+
"region": "EMEA",
|
|
216
|
+
"order_date": "2025-01-01",
|
|
217
|
+
"arr": 1200000,
|
|
218
|
+
"revenue": 340000
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
On failure it emits an `error` event instead.
|
|
226
|
+
|
|
227
|
+
### Errors and behavior[](#errors-and-behavior "Direct link to Errors and behavior")
|
|
228
|
+
|
|
229
|
+
| Status | Body | When |
|
|
230
|
+
| ------ | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
|
231
|
+
| `404` | `{ "error": "Metric not found" }` | `:key` is not declared in `definitions.json` (also the response for every key when the file is absent). |
|
|
232
|
+
| `400` | `{ "error": "Invalid metric request body (fields: …)", "code": … }` | The request body fails validation. The message names only the offending field paths, never the submitted values. |
|
|
233
|
+
| `503` | `{ "error": "Metric registry not available", "code": "METRIC_REGISTRY_LOAD_FAILED" }` | `definitions.json` is present but malformed or unreadable. |
|
|
234
|
+
|
|
235
|
+
Editing `definitions.json` is picked up on the next request — no server restart is needed. A previously malformed file that you fix likewise starts working on the next request.
|
|
236
|
+
|
|
95
237
|
## Frontend usage[](#frontend-usage "Direct link to Frontend usage")
|
|
96
238
|
|
|
97
239
|
### useAnalyticsQuery[](#useanalyticsquery "Direct link to useAnalyticsQuery")
|
|
@@ -101,7 +243,11 @@ React hook that subscribes to an analytics query over SSE and returns its latest
|
|
|
101
243
|
```ts
|
|
102
244
|
import { useAnalyticsQuery } from "@databricks/appkit-ui/react";
|
|
103
245
|
|
|
104
|
-
const { data, loading, error } = useAnalyticsQuery(
|
|
246
|
+
const { data, loading, error } = useAnalyticsQuery(
|
|
247
|
+
queryKey,
|
|
248
|
+
parameters,
|
|
249
|
+
options,
|
|
250
|
+
);
|
|
105
251
|
|
|
106
252
|
```
|
|
107
253
|
|
|
@@ -109,8 +255,8 @@ const { data, loading, error } = useAnalyticsQuery(queryKey, parameters, options
|
|
|
109
255
|
|
|
110
256
|
```ts
|
|
111
257
|
{
|
|
112
|
-
data: T | null;
|
|
113
|
-
loading: boolean;
|
|
258
|
+
data: T | null; // query result (typed array for JSON, TypedArrowTable for ARROW)
|
|
259
|
+
loading: boolean; // true while the query is executing
|
|
114
260
|
error: string | null; // error message, or null on success
|
|
115
261
|
warehouseStatus: WarehouseStatus | null; // see "Warehouse readiness" below
|
|
116
262
|
}
|
|
@@ -139,8 +285,10 @@ This means a cold start no longer freezes the UI on a stalled spinner. Render th
|
|
|
139
285
|
import { useAnalyticsQuery } from "@databricks/appkit-ui/react";
|
|
140
286
|
|
|
141
287
|
function SpendTable() {
|
|
142
|
-
const { data, loading, error, warehouseStatus } =
|
|
143
|
-
|
|
288
|
+
const { data, loading, error, warehouseStatus } = useAnalyticsQuery(
|
|
289
|
+
"spend_summary",
|
|
290
|
+
params,
|
|
291
|
+
);
|
|
144
292
|
|
|
145
293
|
if (warehouseStatus && warehouseStatus.state !== "RUNNING") {
|
|
146
294
|
return <div>Warehouse is {warehouseStatus.state.toLowerCase()}…</div>;
|
|
@@ -184,10 +332,7 @@ export function AppShell({ children }) {
|
|
|
184
332
|
If you already render your own `<Toaster />` for unrelated app toasts, drop the indicator and call `useResourceStatusToaster()` instead so resource-status toasts share that single Toaster:
|
|
185
333
|
|
|
186
334
|
```tsx
|
|
187
|
-
import {
|
|
188
|
-
useResourceStatusToaster,
|
|
189
|
-
Toaster,
|
|
190
|
-
} from "@databricks/appkit-ui/react";
|
|
335
|
+
import { useResourceStatusToaster, Toaster } from "@databricks/appkit-ui/react";
|
|
191
336
|
|
|
192
337
|
function App() {
|
|
193
338
|
useResourceStatusToaster();
|
|
@@ -207,7 +352,8 @@ For a fully custom toast body, pass `render` (rendered through `toast.custom`):
|
|
|
207
352
|
<ResourceStatusIndicator
|
|
208
353
|
render={(agg) => (
|
|
209
354
|
<div className="rounded-lg border bg-background p-3 shadow">
|
|
210
|
-
{agg.worst?.kind} {agg.worst?.state.toLowerCase()} ({agg.activeCount}
|
|
355
|
+
{agg.worst?.kind} {agg.worst?.state.toLowerCase()} ({agg.activeCount}{" "}
|
|
356
|
+
waiting)
|
|
211
357
|
</div>
|
|
212
358
|
)}
|
|
213
359
|
/>
|
|
@@ -221,8 +367,7 @@ To override copy for a specific kind without rewriting the whole UI, pass `rende
|
|
|
221
367
|
renderers={{
|
|
222
368
|
warehouse: {
|
|
223
369
|
title: () => "Spinning up your data",
|
|
224
|
-
description: (_s, agg) =>
|
|
225
|
-
`${agg.affectedLabels.length} chart(s) waiting`,
|
|
370
|
+
description: (_s, agg) => `${agg.affectedLabels.length} chart(s) waiting`,
|
|
226
371
|
},
|
|
227
372
|
}}
|
|
228
373
|
/>
|
|
@@ -254,11 +399,9 @@ import { useEffect, useId } from "react";
|
|
|
254
399
|
|
|
255
400
|
function useLakebaseReadiness() {
|
|
256
401
|
const id = useId();
|
|
257
|
-
const { publish, unpublish } = useResourceStatusPublisher(
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
{ kindHint: "lakebase" },
|
|
261
|
-
);
|
|
402
|
+
const { publish, unpublish } = useResourceStatusPublisher(id, "lakebase", {
|
|
403
|
+
kindHint: "lakebase",
|
|
404
|
+
});
|
|
262
405
|
|
|
263
406
|
useEffect(() => {
|
|
264
407
|
publish({
|
|
@@ -288,21 +431,27 @@ import { sql } from "@databricks/appkit-ui/js";
|
|
|
288
431
|
import { Skeleton } from "@databricks/appkit-ui";
|
|
289
432
|
|
|
290
433
|
function SpendTable() {
|
|
291
|
-
const params = useMemo(
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
434
|
+
const params = useMemo(
|
|
435
|
+
() => ({
|
|
436
|
+
startDate: sql.date("2025-01-01"),
|
|
437
|
+
endDate: sql.date("2025-12-31"),
|
|
438
|
+
}),
|
|
439
|
+
[],
|
|
440
|
+
);
|
|
295
441
|
|
|
296
442
|
const { data, loading, error } = useAnalyticsQuery("spend_summary", params);
|
|
297
443
|
|
|
298
444
|
if (loading) return <Skeleton className="h-32 w-full" />;
|
|
299
445
|
if (error) return <div className="text-destructive">{error}</div>;
|
|
300
|
-
if (!data?.length)
|
|
446
|
+
if (!data?.length)
|
|
447
|
+
return <div className="text-muted-foreground">No results</div>;
|
|
301
448
|
|
|
302
449
|
return (
|
|
303
450
|
<ul>
|
|
304
451
|
{data.map((row) => (
|
|
305
|
-
<li key={row.id}>
|
|
452
|
+
<li key={row.id}>
|
|
453
|
+
{row.name}: ${row.cost_usd}
|
|
454
|
+
</li>
|
|
306
455
|
))}
|
|
307
456
|
</ul>
|
|
308
457
|
);
|
package/llms.txt
CHANGED
|
@@ -125,6 +125,7 @@ npx @databricks/appkit docs <query>
|
|
|
125
125
|
- [Interface: FileResource](./docs/api/appkit/Interface.FileResource.md): Describes the file or directory being acted upon.
|
|
126
126
|
- [Interface: FunctionTool](./docs/api/appkit/Interface.FunctionTool.md): Properties
|
|
127
127
|
- [Interface: GenerateDatabaseCredentialRequest](./docs/api/appkit/Interface.GenerateDatabaseCredentialRequest.md): Request parameters for generating database OAuth credentials
|
|
128
|
+
- [Interface: GenerationParams](./docs/api/appkit/Interface.GenerationParams.md): Optional generation parameters forwarded to the OpenAI-compatible serving
|
|
128
129
|
- [Interface: IJobsConfig](./docs/api/appkit/Interface.IJobsConfig.md): Configuration for the Jobs plugin.
|
|
129
130
|
- [Interface: ITelemetry](./docs/api/appkit/Interface.ITelemetry.md): Plugin-facing interface for OpenTelemetry instrumentation.
|
|
130
131
|
- [Interface: JobAPI](./docs/api/appkit/Interface.JobAPI.md): User-facing API for a single configured job.
|