@databricks/appkit 0.61.0 → 0.62.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/NOTICE.md +1 -0
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js.map +1 -1
- package/dist/agents/supervisor-api.d.ts.map +1 -1
- package/dist/agents/supervisor-api.js.map +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/storage/memory.js.map +1 -1
- package/dist/cache/storage/persistent.js.map +1 -1
- package/dist/cli/commands/codemod/index.js.map +1 -1
- package/dist/cli/commands/codemod/on-plugins-ready.js.map +1 -1
- package/dist/cli/commands/docs.js.map +1 -1
- package/dist/cli/commands/doctor/bundle.js.map +1 -1
- package/dist/cli/commands/doctor/index.js.map +1 -1
- package/dist/cli/commands/doctor/report.js.map +1 -1
- package/dist/cli/commands/doctor/resolve-targets.js +4 -6
- package/dist/cli/commands/doctor/resolve-targets.js.map +1 -1
- package/dist/cli/commands/doctor/run.js.map +1 -1
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/commands/plugin/add-resource/add-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/create.js.map +1 -1
- package/dist/cli/commands/plugin/create/prompt-resource.js.map +1 -1
- package/dist/cli/commands/plugin/create/scaffold.js.map +1 -1
- package/dist/cli/commands/plugin/index.js.map +1 -1
- package/dist/cli/commands/plugin/list/list.js.map +1 -1
- package/dist/cli/commands/plugin/promote/promote.js.map +1 -1
- package/dist/cli/commands/plugin/sync/sync.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate-manifest.js.map +1 -1
- package/dist/cli/commands/plugin/validate/validate.js.map +1 -1
- package/dist/cli/commands/registry/add.js.map +1 -1
- package/dist/cli/commands/registry/client.js.map +1 -1
- package/dist/cli/commands/registry/config-writer.js.map +1 -1
- package/dist/cli/commands/registry/env-reconcile.js.map +1 -1
- package/dist/cli/commands/registry/env-writer.js.map +1 -1
- package/dist/cli/commands/registry/index.js.map +1 -1
- package/dist/cli/commands/registry/info.js.map +1 -1
- package/dist/cli/commands/registry/list.js.map +1 -1
- package/dist/cli/commands/registry/requirements.js.map +1 -1
- package/dist/cli/commands/registry/server-register.js.map +1 -1
- package/dist/cli/commands/registry/workspace-picker.js.map +1 -1
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/lakebase/index.d.ts.map +1 -1
- package/dist/connectors/lakebase/index.js.map +1 -1
- package/dist/connectors/lakebase/pool-manager.d.ts.map +1 -1
- package/dist/connectors/lakebase/pool-manager.js.map +1 -1
- package/dist/connectors/lakebase/routing-pool.d.ts.map +1 -1
- package/dist/connectors/lakebase/routing-pool.js.map +1 -1
- package/dist/connectors/mcp/client.d.ts.map +1 -1
- package/dist/connectors/mcp/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/context/client-options.js.map +1 -1
- package/dist/context/execution-context.d.ts.map +1 -1
- package/dist/context/execution-context.js.map +1 -1
- package/dist/context/index.d.ts +1 -1
- package/dist/context/service-context.d.ts +55 -3
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js.map +1 -1
- package/dist/core/agent/build-toolkit.js.map +1 -1
- package/dist/core/agent/load-agents.d.ts.map +1 -1
- package/dist/core/agent/load-agents.js.map +1 -1
- package/dist/core/agent/run-agent.d.ts.map +1 -1
- package/dist/core/agent/run-agent.js.map +1 -1
- package/dist/core/agent/toolkit-resolver.js.map +1 -1
- package/dist/core/agent/tools/define-tool.d.ts.map +1 -1
- package/dist/core/agent/tools/define-tool.js.map +1 -1
- package/dist/core/agent/tools/tool.d.ts.map +1 -1
- package/dist/core/agent/tools/tool.js.map +1 -1
- 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.map +1 -1
- package/dist/core/lifecycle-manager.js.map +1 -1
- package/dist/core/plugin-context.d.ts +13 -0
- package/dist/core/plugin-context.d.ts.map +1 -1
- package/dist/core/plugin-context.js +12 -1
- package/dist/core/plugin-context.js.map +1 -1
- package/dist/errors/configuration.d.ts.map +1 -1
- package/dist/errors/configuration.js.map +1 -1
- package/dist/logging/logger.js.map +1 -1
- package/dist/logging/wide-event-emitter.js.map +1 -1
- package/dist/plugin/dev-reader.d.ts.map +1 -1
- package/dist/plugin/dev-reader.js.map +1 -1
- package/dist/plugin/interceptors/cache.js.map +1 -1
- package/dist/plugin/interceptors/retry.js.map +1 -1
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/plugin.d.ts +2 -2
- package/dist/plugin/plugin.d.ts.map +1 -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.map +1 -1
- package/dist/plugins/agents/event-translator.js.map +1 -1
- package/dist/plugins/agents/thread-store.js.map +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/mv/cache.js.map +1 -1
- package/dist/plugins/analytics/mv/constants.js.map +1 -1
- package/dist/plugins/analytics/mv/formatters.js.map +1 -1
- package/dist/plugins/analytics/mv/metadata.js.map +1 -1
- package/dist/plugins/analytics/mv/registry.js.map +1 -1
- package/dist/plugins/analytics/mv/schemas.js.map +1 -1
- package/dist/plugins/analytics/query.js.map +1 -1
- package/dist/plugins/analytics/result-delivery.js.map +1 -1
- package/dist/plugins/files/helpers.js.map +1 -1
- package/dist/plugins/files/plugin.d.ts.map +1 -1
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/files/types.d.ts.map +1 -1
- package/dist/plugins/genie/genie.d.ts.map +1 -1
- package/dist/plugins/genie/genie.js.map +1 -1
- package/dist/plugins/jobs/plugin.d.ts.map +1 -1
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/jobs/types.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/plugins/lakebase/types.d.ts.map +1 -1
- package/dist/plugins/server/base-server.js.map +1 -1
- package/dist/plugins/server/client-config-sanitizer.js.map +1 -1
- package/dist/plugins/server/index.d.ts.map +1 -1
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/react-source-loc-vite-plugin.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/plugins/serving/schema-filter.js.map +1 -1
- package/dist/plugins/serving/serving.d.ts.map +1 -1
- package/dist/plugins/serving/serving.js.map +1 -1
- package/dist/plugins/ui-variants/index.js.map +1 -1
- package/dist/registry/manifest-loader.d.ts.map +1 -1
- package/dist/registry/manifest-loader.js.map +1 -1
- package/dist/registry/resource-registry.d.ts.map +1 -1
- package/dist/registry/resource-registry.js.map +1 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry/types.js.map +1 -1
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js.map +1 -1
- package/dist/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/plugin.d.ts.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts.map +1 -1
- package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/schemas/metric-metadata-bundle.js.map +1 -1
- package/dist/shared/src/schemas/metric-source.js.map +1 -1
- package/dist/shared/src/sse/analytics.js.map +1 -1
- package/dist/shared/src/workspace-client/legacy.js.map +1 -1
- package/dist/shared/src/workspace-client/types.d.ts.map +1 -1
- package/dist/stream/sse-writer.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/types.js.map +1 -1
- package/dist/telemetry/instrumentations.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/telemetry/telemetry-provider.js.map +1 -1
- package/dist/telemetry/trace-sampler.js.map +1 -1
- package/dist/testing/expect-stream.d.ts +111 -0
- package/dist/testing/expect-stream.d.ts.map +1 -0
- package/dist/testing/expect-stream.js +154 -0
- package/dist/testing/expect-stream.js.map +1 -0
- package/dist/testing/fixtures.d.ts +257 -0
- package/dist/testing/fixtures.d.ts.map +1 -0
- package/dist/testing/fixtures.js +385 -0
- package/dist/testing/fixtures.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/test-plugin-context.d.ts +158 -0
- package/dist/testing/test-plugin-context.d.ts.map +1 -0
- package/dist/testing/test-plugin-context.js +139 -0
- package/dist/testing/test-plugin-context.js.map +1 -0
- package/dist/type-generator/cache.js.map +1 -1
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/mv-registry/config.js.map +1 -1
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/serving/cache.js.map +1 -1
- package/dist/type-generator/serving/generator.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.d.ts.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/dist/type-generator/serving/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/serving/vite-plugin.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/workspace-client/legacy.js.map +1 -1
- package/dist/workspace-client/types.d.ts.map +1 -1
- package/docs/plugins/testing.md +245 -0
- package/llms.txt +1 -0
- package/package.json +15 -2
- package/sbom.cdx.json +1 -1
- package/skills/appkit-ui-variants/SKILL.md +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-search.js","names":["manifest"],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type express from \"express\";\nimport type { CacheConfig, IAppRouter, PluginExecutionSettings } from \"shared\";\nimport { AiSearchConnector } from \"../../connectors/ai-search/client\";\nimport type {\n VsQueryParams,\n VsRawResponse,\n} from \"../../connectors/ai-search/types\";\nimport { getCurrentUserId, getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { formatWarningBanner } from \"../../utils/banner\";\nimport { aiSearchDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport type {\n IAiSearchConfig,\n IndexConfig,\n IndexSummary,\n SearchQueryType,\n SearchRequest,\n SearchResponse,\n SearchResult,\n} from \"./types\";\n\nconst logger = createLogger(\"ai-search\");\n\nconst querySettings: PluginExecutionSettings = {\n default: aiSearchDefaults,\n};\n\nexport class AiSearchPlugin extends Plugin<IAiSearchConfig> {\n static manifest = manifest as PluginManifest<\"aiSearch\">;\n\n protected static description =\n \"Query Databricks Vector Search indexes with hybrid search, reranking, and pagination\";\n protected declare config: IAiSearchConfig;\n\n private connector: AiSearchConnector;\n\n constructor(config: IAiSearchConfig) {\n super(config);\n this.config = {\n ...config,\n indexes: config.indexes ?? this._defaultIndexes(),\n };\n this.connector = new AiSearchConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n /**\n * Seeds a `default` index from `DATABRICKS_VS_INDEX_NAME` when no `indexes`\n * are configured, so `aiSearch()` works with just the env var.\n */\n private _defaultIndexes(): Record<string, IndexConfig> {\n const indexName = process.env.DATABRICKS_VS_INDEX_NAME;\n return indexName ? { default: { indexName } } : {};\n }\n\n async setup(): Promise<void> {\n // pagination needs an endpointName the framework's resource validation\n // can't see, so check it here.\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.pagination && !idx.endpointName) {\n throw new Error(\n `Index \"${alias}\" has pagination enabled but is missing \"endpointName\"`,\n );\n }\n }\n\n // Dev fills in missing `columns` from the source table; prod can't query\n // without them (VS requires `columns`), so fail fast at boot.\n if (process.env.NODE_ENV === \"development\") {\n await this._autoDiscoverColumns();\n } else {\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (!idx.columns || idx.columns.length === 0) {\n throw new Error(\n `Index \"${alias}\" has no columns configured. Vector Search queries require \"columns\"; set them explicitly (auto-discovered only in development).`,\n );\n }\n }\n }\n }\n\n /**\n * For each configured index missing `columns`, fill them from its Delta-Sync\n * source table (all source columns minus embedding vectors). Best-effort:\n * failures are logged and skipped, never thrown. A partial `columns_to_sync`\n * isn't honored, so the discovered list is a starting point to trim.\n */\n private async _autoDiscoverColumns(): Promise<void> {\n const discovered: Record<string, string[]> = {};\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.columns && idx.columns.length > 0) continue;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) continue;\n try {\n const client = getWorkspaceClient();\n const info = await this.connector.getIndex(client, indexName);\n const sourceTable = info.delta_sync_index_spec?.source_table;\n if (!sourceTable) continue;\n const excluded = new Set(\n (info.delta_sync_index_spec?.embedding_vector_columns ?? []).map(\n (c) => c.name,\n ),\n );\n const columns = (\n await this.connector.getSourceColumns(client, sourceTable)\n ).filter((c) => !excluded.has(c));\n if (columns.length > 0) {\n idx.columns = columns;\n discovered[alias] = columns;\n }\n } catch (error) {\n logger.warn(\n 'Could not auto-discover columns for index \"%s\": %s',\n alias,\n error instanceof Error ? error.message : String(error),\n );\n }\n }\n if (Object.keys(discovered).length > 0) {\n logger.warn(\"\\n%s\", this._formatColumnDiscoveryBanner(discovered));\n }\n }\n\n private _formatColumnDiscoveryBanner(\n discovered: Record<string, string[]>,\n ): string {\n const lines = [\n \"AI SEARCH: columns auto-discovered (dev mode — would fail in production)\",\n \"\",\n ];\n for (const [alias, columns] of Object.entries(discovered)) {\n lines.push(` ${alias}: ${columns.join(\", \")}`);\n }\n lines.push(\"\");\n lines.push(\n \"Set `columns` explicitly in the plugin config before deploying.\",\n );\n\n return formatWarningBanner(lines);\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"query\",\n method: \"post\",\n path: \"/:alias/query\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n const body: SearchRequest = req.body;\n if (!body.queryText && !body.queryVector) {\n res.status(400).json({\n error: \"queryText or queryVector is required\",\n plugin: this.name,\n });\n return;\n }\n\n // Drop client-supplied `columns` so an HTTP caller can't widen the\n // projection past what the app configured. (query() callers are\n // trusted and keep the override.)\n const { columns: _clientColumns, ...safeBody } = body;\n const isAsUser = indexConfig.auth === \"on-behalf-of-user\";\n const plugin = isAsUser ? this.asUser(req) : this;\n const queryType =\n safeBody.queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n // Key per user for OBO so results never leak across users; SP shares \"global\".\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n try {\n // Prepare inside execute so a self-managed embeddingFn runs in the\n // same OBO context as the query, not as the service principal.\n const result = await plugin.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(safeBody, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(safeBody, indexConfig, executorKey),\n );\n\n this._sendResult(res, result, queryType);\n } catch (error) {\n this._handleError(res, error, \"Query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"queryNextPage\",\n method: \"post\",\n path: \"/:alias/next-page\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n if (!indexConfig.pagination) {\n res.status(400).json({\n error: `Pagination is not enabled for index \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return;\n }\n\n if (!indexConfig.endpointName) {\n res.status(400).json({\n error: `Index \"${req.params.alias}\" is missing endpointName required for pagination`,\n plugin: this.name,\n });\n return;\n }\n\n const { pageToken, queryType } = req.body;\n if (!pageToken) {\n res.status(400).json({\n error: \"pageToken is required\",\n plugin: this.name,\n });\n return;\n }\n // Echo the original query's queryType so paged responses stay\n // consistent with page 1; fall back to the index default.\n const pageQueryType = queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n try {\n const plugin =\n indexConfig.auth === \"on-behalf-of-user\" ? this.asUser(req) : this;\n\n // Uncached: a page token is a single-use cursor. `querySettings` has\n // no `cacheKey`, so caching stays off.\n const result = await plugin.execute(\n async (signal) =>\n this.connector.queryNextPage(\n getWorkspaceClient(),\n {\n indexName: indexConfig.indexName,\n endpointName: indexConfig.endpointName as string,\n pageToken,\n },\n signal,\n ),\n querySettings,\n );\n\n this._sendResult(res, result, pageQueryType);\n } catch (error) {\n this._handleError(res, error, \"Next-page query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"getConfig\",\n method: \"get\",\n path: \"/:alias/config\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n res.json({\n alias: req.params.alias,\n columns: indexConfig.columns,\n queryType: indexConfig.queryType ?? \"hybrid\",\n numResults: indexConfig.numResults ?? 20,\n reranker: !!indexConfig.reranker,\n pagination: !!indexConfig.pagination,\n });\n },\n });\n }\n\n /**\n * Index aliases + non-sensitive query metadata, serialized to the client so\n * the UI can discover available indexes instead of hardcoding an alias.\n */\n clientConfig(): { indexes: IndexSummary[] } {\n const indexes = Object.entries(this.config.indexes ?? {}).map(\n ([alias, idx]) => ({\n alias,\n queryType: idx.queryType ?? \"hybrid\",\n pagination: !!idx.pagination,\n }),\n );\n return { indexes };\n }\n\n /**\n * Programmatic query API — available as `appkit.aiSearch.query()`.\n * When called through `asUser(req)`, executes with the user's credentials.\n *\n * @remarks `T` types each result's `data` but is an unchecked cast — the row\n * shape isn't validated at runtime.\n */\n async query<T extends Record<string, unknown> = Record<string, unknown>>(\n alias: string,\n request: SearchRequest,\n ): Promise<SearchResponse<T>> {\n const indexConfig = this._resolveIndex(alias);\n if (!indexConfig) {\n throw new Error(`No index configured with alias \"${alias}\"`);\n }\n\n // Resolve queryType for the response here; _prepareQuery runs inside\n // execute so a cache hit skips the embedding and the VS call.\n const { queryType } = this._resolveQueryParams(request, indexConfig);\n\n // getCurrentUserId() is the user's id under asUser(), the service id\n // otherwise — keying per caller like the route's executorKey.\n const result = await this.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(request, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(request, indexConfig, getCurrentUserId()),\n );\n\n if (!result.ok) {\n throw new Error(\n `Vector search query failed for index \"${alias}\": ${result.message}`,\n );\n }\n\n return this._parseResponse(result.data, queryType);\n }\n\n async shutdown(): Promise<void> {\n // No streams or persistent connections to clean up\n }\n\n exports() {\n return {\n query: this.query.bind(this),\n };\n }\n\n private _resolveIndex(\n alias: string,\n ): (IndexConfig & { indexName: string }) | undefined {\n const idx = this.config.indexes?.[alias];\n if (!idx) return undefined;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) return undefined;\n return { ...idx, indexName };\n }\n\n /** Resolve an index by route alias, or send a 404 and return null. */\n private _resolveOr404(\n req: express.Request,\n res: express.Response,\n ): (IndexConfig & { indexName: string }) | null {\n const indexConfig = this._resolveIndex(req.params.alias);\n if (!indexConfig) {\n res.status(404).json({\n error: `No index configured with alias \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return null;\n }\n return indexConfig;\n }\n\n /** Send an execution result as JSON, or its error status/message. */\n private _sendResult(\n res: express.Response,\n result: Awaited<ReturnType<typeof this.execute<VsRawResponse>>>,\n queryType: SearchQueryType,\n ): void {\n if (!result.ok) {\n res\n .status(result.status)\n .json({ error: result.message, plugin: this.name });\n return;\n }\n res.json(this._parseResponse(result.data, queryType));\n }\n\n /**\n * Resolve request-vs-index defaults for the result-determining fields.\n * Shared by `_prepareQuery` (the payload) and `_cacheKeyFor` (the key) so\n * the two can't drift.\n */\n private _resolveQueryParams(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Pick<VsQueryParams, \"queryType\" | \"columns\" | \"numResults\" | \"reranker\"> {\n const queryType = request.queryType ?? indexConfig.queryType ?? \"hybrid\";\n const columns = request.columns ?? indexConfig.columns ?? [];\n return {\n queryType,\n columns,\n numResults: request.numResults ?? indexConfig.numResults ?? 20,\n reranker: this._resolveReranker(request.reranker, indexConfig, columns),\n };\n }\n\n private async _prepareQuery(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Promise<Omit<VsQueryParams, \"indexName\">> {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n let queryText = request.queryText;\n let queryVector = request.queryVector;\n\n // full_text uses no vector; hybrid keeps the text for its keyword half.\n if (\n indexConfig.embeddingFn &&\n queryText &&\n !queryVector &&\n queryType !== \"full_text\"\n ) {\n try {\n queryVector = await indexConfig.embeddingFn(queryText);\n if (queryType === \"ann\") queryText = undefined;\n } catch (error) {\n throw new Error(\n `Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return {\n queryText,\n queryVector,\n queryType,\n columns,\n numResults,\n filters: request.filters,\n reranker,\n };\n }\n\n /**\n * Cache key for a query: every input that changes the VS result, resolved\n * via `_resolveQueryParams` so the key matches the payload (post-allowlist\n * `columns`, not the raw request). `queryVector` is hashed (vectors are\n * large); the key uses `queryText`, not the derived embedding, since the\n * embedding is a function of `queryText` and hasn't run at key-build time.\n * `columns`/`filters` are order-normalized so equivalent requests share an\n * entry.\n */\n private _cacheKeyFor(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): CacheConfig[\"cacheKey\"] {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n return [\n \"ai-search:query\",\n indexConfig.indexName,\n request.queryText ?? \"\",\n request.queryVector ? this._hashVector(request.queryVector) : \"\",\n queryType,\n numResults,\n // columns is a projection; order doesn't affect results, so sort a copy.\n JSON.stringify([...columns].sort()),\n this._stableStringify(request.filters ?? null),\n String(!!reranker),\n executorKey,\n ];\n }\n\n private _hashVector(vector: number[]): string {\n return createHash(\"sha256\").update(JSON.stringify(vector)).digest(\"hex\");\n }\n\n /** Execute settings with the per-call cache key folded in. */\n private _executeSettings(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): PluginExecutionSettings {\n return {\n default: {\n ...aiSearchDefaults,\n cache: {\n ...aiSearchDefaults.cache,\n cacheKey: this._cacheKeyFor(request, indexConfig, executorKey),\n },\n },\n };\n }\n\n /** `JSON.stringify` with object keys sorted recursively; array order kept. */\n private _stableStringify(value: unknown): string {\n return JSON.stringify(value, (_key, val) =>\n val && typeof val === \"object\" && !Array.isArray(val)\n ? Object.fromEntries(\n Object.entries(val).sort(([a], [b]) => a.localeCompare(b)),\n )\n : val,\n );\n }\n\n private _resolveReranker(\n requestReranker: boolean | undefined,\n indexConfig: IndexConfig,\n columns: string[],\n ): { columnsToRerank: string[] } | undefined {\n const shouldRerank = requestReranker ?? indexConfig.reranker;\n if (!shouldRerank) return undefined;\n\n if (typeof indexConfig.reranker === \"object\") {\n return indexConfig.reranker;\n }\n // Auto-derive from returnable columns (excluding the id). With no columns\n // resolved there's nothing to rerank on, so skip it.\n const columnsToRerank = columns.filter((c) => c !== \"id\");\n return columnsToRerank.length > 0 ? { columnsToRerank } : undefined;\n }\n\n private _parseResponse<\n T extends Record<string, unknown> = Record<string, unknown>,\n >(raw: VsRawResponse, queryType: SearchQueryType): SearchResponse<T> {\n const columnNames = raw.manifest.columns.map((c) => c.name);\n const scoreIndex = columnNames.indexOf(\"score\");\n\n const results: SearchResult<T>[] = raw.result.data_array.map((row) => {\n const data: Record<string, unknown> = {};\n for (let i = 0; i < columnNames.length; i++) {\n if (i !== scoreIndex) data[columnNames[i]] = row[i];\n }\n return {\n score: scoreIndex >= 0 ? (row[scoreIndex] as number) : 0,\n data: data as T,\n };\n });\n\n return {\n results,\n totalCount: raw.result.row_count,\n queryTimeMs:\n raw.debug_info?.response_time ?? raw.debug_info?.latency_ms ?? 0,\n queryType,\n nextPageToken: raw.next_page_token ?? null,\n };\n }\n\n private _handleError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n logger.error(\"%s: %O\", fallbackMessage, error);\n // Match Plugin.execute(): the raw message is only exposed outside production.\n const isDev = process.env.NODE_ENV !== \"production\";\n const message =\n isDev && error instanceof Error ? error.message : fallbackMessage;\n res.status(500).json({ error: message, plugin: this.name });\n }\n}\n\nexport const aiSearch = toPlugin(AiSearchPlugin);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ai-search.js","names":["manifest"],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport type express from \"express\";\nimport type { CacheConfig, IAppRouter, PluginExecutionSettings } from \"shared\";\n\nimport { AiSearchConnector } from \"../../connectors/ai-search/client\";\nimport type {\n VsQueryParams,\n VsRawResponse,\n} from \"../../connectors/ai-search/types\";\nimport { getCurrentUserId, getWorkspaceClient } from \"../../context\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { formatWarningBanner } from \"../../utils/banner\";\nimport { aiSearchDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport type {\n IAiSearchConfig,\n IndexConfig,\n IndexSummary,\n SearchQueryType,\n SearchRequest,\n SearchResponse,\n SearchResult,\n} from \"./types\";\n\nconst logger = createLogger(\"ai-search\");\n\nconst querySettings: PluginExecutionSettings = {\n default: aiSearchDefaults,\n};\n\nexport class AiSearchPlugin extends Plugin<IAiSearchConfig> {\n static manifest = manifest as PluginManifest<\"aiSearch\">;\n\n protected static description =\n \"Query Databricks Vector Search indexes with hybrid search, reranking, and pagination\";\n declare protected config: IAiSearchConfig;\n\n private connector: AiSearchConnector;\n\n constructor(config: IAiSearchConfig) {\n super(config);\n this.config = {\n ...config,\n indexes: config.indexes ?? this._defaultIndexes(),\n };\n this.connector = new AiSearchConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n /**\n * Seeds a `default` index from `DATABRICKS_VS_INDEX_NAME` when no `indexes`\n * are configured, so `aiSearch()` works with just the env var.\n */\n private _defaultIndexes(): Record<string, IndexConfig> {\n const indexName = process.env.DATABRICKS_VS_INDEX_NAME;\n return indexName ? { default: { indexName } } : {};\n }\n\n async setup(): Promise<void> {\n // pagination needs an endpointName the framework's resource validation\n // can't see, so check it here.\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.pagination && !idx.endpointName) {\n throw new Error(\n `Index \"${alias}\" has pagination enabled but is missing \"endpointName\"`,\n );\n }\n }\n\n // Dev fills in missing `columns` from the source table; prod can't query\n // without them (VS requires `columns`), so fail fast at boot.\n if (process.env.NODE_ENV === \"development\") {\n await this._autoDiscoverColumns();\n } else {\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (!idx.columns || idx.columns.length === 0) {\n throw new Error(\n `Index \"${alias}\" has no columns configured. Vector Search queries require \"columns\"; set them explicitly (auto-discovered only in development).`,\n );\n }\n }\n }\n }\n\n /**\n * For each configured index missing `columns`, fill them from its Delta-Sync\n * source table (all source columns minus embedding vectors). Best-effort:\n * failures are logged and skipped, never thrown. A partial `columns_to_sync`\n * isn't honored, so the discovered list is a starting point to trim.\n */\n private async _autoDiscoverColumns(): Promise<void> {\n const discovered: Record<string, string[]> = {};\n for (const [alias, idx] of Object.entries(this.config.indexes ?? {})) {\n if (idx.columns && idx.columns.length > 0) continue;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) continue;\n try {\n const client = getWorkspaceClient();\n const info = await this.connector.getIndex(client, indexName);\n const sourceTable = info.delta_sync_index_spec?.source_table;\n if (!sourceTable) continue;\n const excluded = new Set(\n (info.delta_sync_index_spec?.embedding_vector_columns ?? []).map(\n (c) => c.name,\n ),\n );\n const columns = (\n await this.connector.getSourceColumns(client, sourceTable)\n ).filter((c) => !excluded.has(c));\n if (columns.length > 0) {\n idx.columns = columns;\n discovered[alias] = columns;\n }\n } catch (error) {\n logger.warn(\n 'Could not auto-discover columns for index \"%s\": %s',\n alias,\n error instanceof Error ? error.message : String(error),\n );\n }\n }\n if (Object.keys(discovered).length > 0) {\n logger.warn(\"\\n%s\", this._formatColumnDiscoveryBanner(discovered));\n }\n }\n\n private _formatColumnDiscoveryBanner(\n discovered: Record<string, string[]>,\n ): string {\n const lines = [\n \"AI SEARCH: columns auto-discovered (dev mode — would fail in production)\",\n \"\",\n ];\n for (const [alias, columns] of Object.entries(discovered)) {\n lines.push(` ${alias}: ${columns.join(\", \")}`);\n }\n lines.push(\"\");\n lines.push(\n \"Set `columns` explicitly in the plugin config before deploying.\",\n );\n\n return formatWarningBanner(lines);\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"query\",\n method: \"post\",\n path: \"/:alias/query\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n const body: SearchRequest = req.body;\n if (!body.queryText && !body.queryVector) {\n res.status(400).json({\n error: \"queryText or queryVector is required\",\n plugin: this.name,\n });\n return;\n }\n\n // Drop client-supplied `columns` so an HTTP caller can't widen the\n // projection past what the app configured. (query() callers are\n // trusted and keep the override.)\n const { columns: _clientColumns, ...safeBody } = body;\n const isAsUser = indexConfig.auth === \"on-behalf-of-user\";\n const plugin = isAsUser ? this.asUser(req) : this;\n const queryType =\n safeBody.queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n // Key per user for OBO so results never leak across users; SP shares \"global\".\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n try {\n // Prepare inside execute so a self-managed embeddingFn runs in the\n // same OBO context as the query, not as the service principal.\n const result = await plugin.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(safeBody, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(safeBody, indexConfig, executorKey),\n );\n\n this._sendResult(res, result, queryType);\n } catch (error) {\n this._handleError(res, error, \"Query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"queryNextPage\",\n method: \"post\",\n path: \"/:alias/next-page\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n\n if (!indexConfig.pagination) {\n res.status(400).json({\n error: `Pagination is not enabled for index \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return;\n }\n\n if (!indexConfig.endpointName) {\n res.status(400).json({\n error: `Index \"${req.params.alias}\" is missing endpointName required for pagination`,\n plugin: this.name,\n });\n return;\n }\n\n const { pageToken, queryType } = req.body;\n if (!pageToken) {\n res.status(400).json({\n error: \"pageToken is required\",\n plugin: this.name,\n });\n return;\n }\n // Echo the original query's queryType so paged responses stay\n // consistent with page 1; fall back to the index default.\n const pageQueryType = queryType ?? indexConfig.queryType ?? \"hybrid\";\n\n try {\n const plugin =\n indexConfig.auth === \"on-behalf-of-user\" ? this.asUser(req) : this;\n\n // Uncached: a page token is a single-use cursor. `querySettings` has\n // no `cacheKey`, so caching stays off.\n const result = await plugin.execute(\n async (signal) =>\n this.connector.queryNextPage(\n getWorkspaceClient(),\n {\n indexName: indexConfig.indexName,\n endpointName: indexConfig.endpointName as string,\n pageToken,\n },\n signal,\n ),\n querySettings,\n );\n\n this._sendResult(res, result, pageQueryType);\n } catch (error) {\n this._handleError(res, error, \"Next-page query failed\");\n }\n },\n });\n\n this.route(router, {\n name: \"getConfig\",\n method: \"get\",\n path: \"/:alias/config\",\n handler: async (req: express.Request, res: express.Response) => {\n const indexConfig = this._resolveOr404(req, res);\n if (!indexConfig) return;\n res.json({\n alias: req.params.alias,\n columns: indexConfig.columns,\n queryType: indexConfig.queryType ?? \"hybrid\",\n numResults: indexConfig.numResults ?? 20,\n reranker: !!indexConfig.reranker,\n pagination: !!indexConfig.pagination,\n });\n },\n });\n }\n\n /**\n * Index aliases + non-sensitive query metadata, serialized to the client so\n * the UI can discover available indexes instead of hardcoding an alias.\n */\n clientConfig(): { indexes: IndexSummary[] } {\n const indexes = Object.entries(this.config.indexes ?? {}).map(\n ([alias, idx]) => ({\n alias,\n queryType: idx.queryType ?? \"hybrid\",\n pagination: !!idx.pagination,\n }),\n );\n return { indexes };\n }\n\n /**\n * Programmatic query API — available as `appkit.aiSearch.query()`.\n * When called through `asUser(req)`, executes with the user's credentials.\n *\n * @remarks `T` types each result's `data` but is an unchecked cast — the row\n * shape isn't validated at runtime.\n */\n async query<T extends Record<string, unknown> = Record<string, unknown>>(\n alias: string,\n request: SearchRequest,\n ): Promise<SearchResponse<T>> {\n const indexConfig = this._resolveIndex(alias);\n if (!indexConfig) {\n throw new Error(`No index configured with alias \"${alias}\"`);\n }\n\n // Resolve queryType for the response here; _prepareQuery runs inside\n // execute so a cache hit skips the embedding and the VS call.\n const { queryType } = this._resolveQueryParams(request, indexConfig);\n\n // getCurrentUserId() is the user's id under asUser(), the service id\n // otherwise — keying per caller like the route's executorKey.\n const result = await this.execute(\n async (signal) => {\n const prepared = await this._prepareQuery(request, indexConfig);\n return this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n );\n },\n this._executeSettings(request, indexConfig, getCurrentUserId()),\n );\n\n if (!result.ok) {\n throw new Error(\n `Vector search query failed for index \"${alias}\": ${result.message}`,\n );\n }\n\n return this._parseResponse(result.data, queryType);\n }\n\n async shutdown(): Promise<void> {\n // No streams or persistent connections to clean up\n }\n\n exports() {\n return {\n query: this.query.bind(this),\n };\n }\n\n private _resolveIndex(\n alias: string,\n ): (IndexConfig & { indexName: string }) | undefined {\n const idx = this.config.indexes?.[alias];\n if (!idx) return undefined;\n const indexName = idx.indexName ?? process.env.DATABRICKS_VS_INDEX_NAME;\n if (!indexName) return undefined;\n return { ...idx, indexName };\n }\n\n /** Resolve an index by route alias, or send a 404 and return null. */\n private _resolveOr404(\n req: express.Request,\n res: express.Response,\n ): (IndexConfig & { indexName: string }) | null {\n const indexConfig = this._resolveIndex(req.params.alias);\n if (!indexConfig) {\n res.status(404).json({\n error: `No index configured with alias \"${req.params.alias}\"`,\n plugin: this.name,\n });\n return null;\n }\n return indexConfig;\n }\n\n /** Send an execution result as JSON, or its error status/message. */\n private _sendResult(\n res: express.Response,\n result: Awaited<ReturnType<typeof this.execute<VsRawResponse>>>,\n queryType: SearchQueryType,\n ): void {\n if (!result.ok) {\n res\n .status(result.status)\n .json({ error: result.message, plugin: this.name });\n return;\n }\n res.json(this._parseResponse(result.data, queryType));\n }\n\n /**\n * Resolve request-vs-index defaults for the result-determining fields.\n * Shared by `_prepareQuery` (the payload) and `_cacheKeyFor` (the key) so\n * the two can't drift.\n */\n private _resolveQueryParams(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Pick<VsQueryParams, \"queryType\" | \"columns\" | \"numResults\" | \"reranker\"> {\n const queryType = request.queryType ?? indexConfig.queryType ?? \"hybrid\";\n const columns = request.columns ?? indexConfig.columns ?? [];\n return {\n queryType,\n columns,\n numResults: request.numResults ?? indexConfig.numResults ?? 20,\n reranker: this._resolveReranker(request.reranker, indexConfig, columns),\n };\n }\n\n private async _prepareQuery(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Promise<Omit<VsQueryParams, \"indexName\">> {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n let queryText = request.queryText;\n let queryVector = request.queryVector;\n\n // full_text uses no vector; hybrid keeps the text for its keyword half.\n if (\n indexConfig.embeddingFn &&\n queryText &&\n !queryVector &&\n queryType !== \"full_text\"\n ) {\n try {\n queryVector = await indexConfig.embeddingFn(queryText);\n if (queryType === \"ann\") queryText = undefined;\n } catch (error) {\n throw new Error(\n `Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n }\n\n return {\n queryText,\n queryVector,\n queryType,\n columns,\n numResults,\n filters: request.filters,\n reranker,\n };\n }\n\n /**\n * Cache key for a query: every input that changes the VS result, resolved\n * via `_resolveQueryParams` so the key matches the payload (post-allowlist\n * `columns`, not the raw request). `queryVector` is hashed (vectors are\n * large); the key uses `queryText`, not the derived embedding, since the\n * embedding is a function of `queryText` and hasn't run at key-build time.\n * `columns`/`filters` are order-normalized so equivalent requests share an\n * entry.\n */\n private _cacheKeyFor(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): CacheConfig[\"cacheKey\"] {\n const { queryType, columns, numResults, reranker } =\n this._resolveQueryParams(request, indexConfig);\n return [\n \"ai-search:query\",\n indexConfig.indexName,\n request.queryText ?? \"\",\n request.queryVector ? this._hashVector(request.queryVector) : \"\",\n queryType,\n numResults,\n // columns is a projection; order doesn't affect results, so sort a copy.\n JSON.stringify([...columns].sort()),\n this._stableStringify(request.filters ?? null),\n String(!!reranker),\n executorKey,\n ];\n }\n\n private _hashVector(vector: number[]): string {\n return createHash(\"sha256\").update(JSON.stringify(vector)).digest(\"hex\");\n }\n\n /** Execute settings with the per-call cache key folded in. */\n private _executeSettings(\n request: SearchRequest,\n indexConfig: IndexConfig & { indexName: string },\n executorKey: string,\n ): PluginExecutionSettings {\n return {\n default: {\n ...aiSearchDefaults,\n cache: {\n ...aiSearchDefaults.cache,\n cacheKey: this._cacheKeyFor(request, indexConfig, executorKey),\n },\n },\n };\n }\n\n /** `JSON.stringify` with object keys sorted recursively; array order kept. */\n private _stableStringify(value: unknown): string {\n return JSON.stringify(value, (_key, val) =>\n val && typeof val === \"object\" && !Array.isArray(val)\n ? Object.fromEntries(\n Object.entries(val).sort(([a], [b]) => a.localeCompare(b)),\n )\n : val,\n );\n }\n\n private _resolveReranker(\n requestReranker: boolean | undefined,\n indexConfig: IndexConfig,\n columns: string[],\n ): { columnsToRerank: string[] } | undefined {\n const shouldRerank = requestReranker ?? indexConfig.reranker;\n if (!shouldRerank) return undefined;\n\n if (typeof indexConfig.reranker === \"object\") {\n return indexConfig.reranker;\n }\n // Auto-derive from returnable columns (excluding the id). With no columns\n // resolved there's nothing to rerank on, so skip it.\n const columnsToRerank = columns.filter((c) => c !== \"id\");\n return columnsToRerank.length > 0 ? { columnsToRerank } : undefined;\n }\n\n private _parseResponse<\n T extends Record<string, unknown> = Record<string, unknown>,\n >(raw: VsRawResponse, queryType: SearchQueryType): SearchResponse<T> {\n const columnNames = raw.manifest.columns.map((c) => c.name);\n const scoreIndex = columnNames.indexOf(\"score\");\n\n const results: SearchResult<T>[] = raw.result.data_array.map((row) => {\n const data: Record<string, unknown> = {};\n for (let i = 0; i < columnNames.length; i++) {\n if (i !== scoreIndex) data[columnNames[i]] = row[i];\n }\n return {\n score: scoreIndex >= 0 ? (row[scoreIndex] as number) : 0,\n data: data as T,\n };\n });\n\n return {\n results,\n totalCount: raw.result.row_count,\n queryTimeMs:\n raw.debug_info?.response_time ?? raw.debug_info?.latency_ms ?? 0,\n queryType,\n nextPageToken: raw.next_page_token ?? null,\n };\n }\n\n private _handleError(\n res: express.Response,\n error: unknown,\n fallbackMessage: string,\n ): void {\n logger.error(\"%s: %O\", fallbackMessage, error);\n // Match Plugin.execute(): the raw message is only exposed outside production.\n const isDev = process.env.NODE_ENV !== \"production\";\n const message =\n isDev && error instanceof Error ? error.message : fallbackMessage;\n res.status(500).json({ error: message, plugin: this.name });\n }\n}\n\nexport const aiSearch = toPlugin(AiSearchPlugin);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"analytics.d.ts","names":[],"sources":["../../../src/plugins/analytics/analytics.ts"],"mappings":";;;;;;;;;;;;;;cA8Ga,eAAA,SAAwB,MAAA,YAAkB,YAAA;;SAE9C,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UACC,MAAA,EAAQ,gBAAA;EAAA,QAGlB,SAAA;EAAA,QACA,cAAA;;;AATV;;;;;;UAmBU,gBAAA;cAEI,MAAA,EAAQ,gBAAA;EAWpB,YAAA,CAAa,MAAA,EAAQ,UAAA;EA2ClB;;;;;;EAHG,mBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA4RA;;;;;;;EAAA,QAvQW,mBAAA;EA22BI;;;;;EA/0BZ,eAAA,CAAgB,WAAA,WAAsB,OAAA;EA63BkB;;;;EAr3BxD,iBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EA+yBA;;;;;;;;;;;;;;;EAllBG,kBAAA,CACJ,GAAA,EAAK,OAAA,CAAQ,OAAA,EACb,GAAA,EAAK,OAAA,CAAQ,QAAA,GACZ,OAAA;EAlViB;;;;;;EAAA,QAgnBN,qBAAA;EA5jBC;;;;;;;;;EAAA,QAylBP,sBAAA;EA9hBF;;;;;;;;;;;EAAA,QA8jBQ,uBAAA;EA5VP;;;;;;;EAifD,0BAAA,CAA2B,MAAA,EAAQ,WAAA,GAAc,OAAA;EAAd;;;;;;;;;;;;;;;;;;EAAA,QAiCjC,qBAAA;EAkHF;;;;;;;;;;;;;;;EAzDA,KAAA,CACJ,KAAA,UACA,UAAA,GAAa,MAAA,SAAe,aAAA,sBAC5B,gBAAA,GAAmB,MAAA,eACnB,MAAA,GAAS,WAAA,GACR,OAAA;EAqBG,QAAA,CAAA,GAAY,OAAA;EAAA,QAIV,KAAA;EAuBR,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EA1DqC;;;;AA2J1C;;;EAtFE,OAAA,CAAQ,IAAA,GAXE,cAAA,GAWoD,MAAA,SAAA,YAAA;EAsF1C;;;;EA9EpB,OAAA,CAAA;IA8EoB;;;2BA7JL,UAAA,GACA,MAAA,SAAe,aAAA,sBAAiC,gBAAA,GAC1C,MAAA,eAAmB,MAAA,GAC7B,WAAA,KACR,OAAA;EAAA;AAAA;;;;cAyJQ,SAAA,EAAS,QAAA,QAAA,eAAA,EAAA,gBAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type express from \"express\";\nimport {\n type AgentToolDefinition,\n type AnalyticsSseMessage,\n type IAppRouter,\n makeResultMessage,\n type PluginExecuteConfig,\n type SQLTypeMarker,\n type StreamExecutionSettings,\n type ToolProvider,\n} from \"shared\";\nimport { z } from \"zod\";\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n type WarehouseStatusUpdate,\n} from \"../../connectors/sql-warehouse/client\";\nimport { getWarehouseId, getWorkspaceClient } from \"../../context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport {\n buildMetricSql,\n composeMetricCacheKey,\n deriveMetricExecutorKey,\n loadMetricMetadata,\n loadMetricRegistry,\n METRIC_METADATA_FILE,\n selectMetricMetadata,\n validateMetricRequest,\n} from \"./metric\";\nimport { QueryProcessor } from \"./query\";\nimport {\n type ArrowCapability,\n deliverArrowBytes,\n deliverJsonResult,\n type QueryExecutor,\n} from \"./result-delivery\";\nimport {\n type AnalyticsQueryResponse,\n type AnalyticsStreamMessage,\n type IAnalyticsConfig,\n type IAnalyticsQueryRequest,\n type MetricRegistration,\n normalizeAnalyticsFormat,\n type WarehouseStatus,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\n/**\n * Bridges a callback-emitting async function into an async iterable.\n *\n * `start(emit)` runs concurrently; every value passed to `emit` is yielded\n * in order. The iterable completes when `start`'s promise resolves and\n * re-throws (after draining) if it rejects. Lets a callback-based progress\n * API (e.g. SQL warehouse readiness) be consumed with `for await`.\n */\nasync function* streamCallbacks<T>(\n start: (emit: (value: T) => void) => Promise<void>,\n): AsyncGenerator<T, void, unknown> {\n const queue: T[] = [];\n let wake: (() => void) | null = null;\n let settled = false;\n let error: unknown = null;\n\n const notify = (): void => {\n wake?.();\n wake = null;\n };\n\n // The .then(_, err => ...) chain converts a rejection into a resolved\n // promise; the consumer surfaces `error` after draining the queue.\n void start((value) => {\n queue.push(value);\n notify();\n }).then(\n () => {\n settled = true;\n notify();\n },\n (err) => {\n error = err;\n settled = true;\n notify();\n },\n );\n\n while (!settled || queue.length > 0) {\n while (queue.length > 0) yield queue.shift() as T;\n if (settled) break;\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n if (error) throw error;\n}\n\nexport class AnalyticsPlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"analytics\">;\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n /**\n * In-process memo of which arrow delivery mode each warehouse supports\n * (keyed by warehouse id). A standard warehouse rejects `INLINE+ARROW_STREAM`\n * on every query, so once learned we skip that doomed probe; Reyden stays\n * `\"inline\"`. Capability is a property of the warehouse, not the user, so it\n * is not user-scoped. Bounded by the number of distinct warehouses a process\n * talks to (effectively one).\n */\n private _arrowCapability = new Map<string, ArrowCapability>();\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // Metric-view route. Registered parallel to `/query`\n // measures a registered UC Metric View over the same SSE envelope.\n this.route(router, {\n name: \"metric\",\n method: \"post\",\n path: \"/metric/:key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleMetricRoute(req, res);\n },\n });\n\n // Column-names fallback for very wide Arrow schemas whose names don't fit\n // in the `X-Appkit-Arrow-Columns` response header.\n // The client hits this with the statement id from `X-Appkit-Arrow-Columns-Ref`.\n this.route(router, {\n name: \"arrow-columns\",\n method: \"get\",\n path: \"/columns/:statementId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleColumnsRoute(req, res);\n },\n });\n }\n\n /**\n * Column-names fallback endpoint. Re-derives the real column names from the\n * statement's result manifest (stateless — no server cache), for the client\n * to relabel a positional Arrow schema when the names were too large for the\n * response header.\n */\n async _handleColumnsRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { statementId } = req.params;\n const columns = await this._resolveColumnNames(req, statementId);\n if (columns && columns.length > 0) {\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.json({ columns });\n return;\n }\n res.status(404).json({\n error: \"Column names unavailable\",\n plugin: this.name,\n });\n }\n\n /**\n * Resolve a statement's real column names, trying the user's identity first\n * (required for `.obo.sql` statements, which the service principal cannot\n * `getStatement`) then falling back to the service principal (for\n * SP-executed statements). Returns undefined if neither identity can read it,\n * so the client falls back to the raw positional Arrow schema names.\n */\n private async _resolveColumnNames(\n req: express.Request,\n statementId: string,\n ): Promise<string[] | undefined> {\n const attempts: Array<() => Promise<string[] | undefined>> = [\n () => this.asUser(req)._getColumnNames(statementId),\n () => this._getColumnNames(statementId),\n ];\n for (const attempt of attempts) {\n try {\n const columns = await attempt();\n if (columns && columns.length > 0) return columns;\n } catch (error) {\n logger.debug(\n \"Arrow column-names lookup attempt failed for %s: %O\",\n statementId,\n error,\n );\n }\n }\n return undefined;\n }\n\n /**\n * Fetch column names in the current execution context. Proxied by `asUser`,\n * so `getWorkspaceClient()` resolves to the user's client when invoked via\n * `this.asUser(req)` and the service principal's otherwise.\n */\n async _getColumnNames(statementId: string): Promise<string[] | undefined> {\n return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format: rawFormat = \"JSON_ARRAY\" } =\n req.body as IAnalyticsQueryRequest;\n\n if (\n rawFormat !== \"JSON_ARRAY\" &&\n rawFormat !== \"ARROW_STREAM\" &&\n rawFormat !== \"JSON\" &&\n rawFormat !== \"ARROW\"\n ) {\n res.status(400).json({\n error: `Invalid format: ${String(rawFormat)}. Expected \"JSON_ARRAY\" or \"ARROW_STREAM\".`,\n });\n return;\n }\n\n const format = normalizeAnalyticsFormat(rawFormat);\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // ARROW_STREAM streams the raw Arrow IPC bytes back as the HTTP response\n // body — no SSE, no server-side stash, no second /arrow-result request.\n // INLINE attachments are piped straight through (the bytes are already in\n // hand from executeStatement); a warehouse that refuses INLINE falls back\n // to EXTERNAL_LINKS and streams those chunks. JSON keeps the SSE path\n // below (it carries warehouse-readiness progress + cached rows).\n if (format === \"ARROW_STREAM\") {\n await this._handleArrowStreamQuery(\n req,\n res,\n query_key,\n query,\n isAsUser,\n parameters,\n );\n return;\n }\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n format,\n hashedQuery,\n executorKey,\n ],\n };\n\n // Cache/retry/timeout are scoped to the SQL execution itself (inner\n // `execute`) so the warehouse-readiness phase isn't subject to retries\n // and the generator value never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // user-scoped trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`) the\n // fallback's `ExecutionError`s carry. Capture the original here so\n // we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const processedParams =\n await self.queryProcessor.processQueryParams(query, parameters);\n // JSON_ARRAY path: tries INLINE + JSON_ARRAY and, if the\n // warehouse only accepts ARROW_STREAM for INLINE, retries as\n // ARROW_STREAM and decodes server-side — returning the SSE\n // `result` message with plain rows. (ARROW_STREAM requests are\n // handled earlier via `_handleArrowStreamQuery`.)\n return await self._executeJsonArrayPath(\n executor,\n query,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` (e.g.\n // RESULT_TOO_LARGE_FOR_JSON_FALLBACK) and sanitized `clientMessage`\n // survive to the SSE error payload. Fall back to a generic\n // statement failure only if the original wasn't an AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Handle metric-view execution requests (`POST /api/analytics/metric/:key`).\n *\n * Mirrors {@link _handleQueryRoute}'s JSON SSE path: the outer\n * `executeStream` disables cache/retry and streams warehouse-readiness\n * (`warehouse_status`) events, then the inner `execute` builds the metric SQL\n * and delivers rows through {@link deliverJsonResult} as a `result` message.\n * The `originalError` re-throw discipline preserves each error's structured\n * `errorCode`/`clientMessage` for the SSE error payload.\n *\n * Lane dispatch is driven by the registration: an SP-lane metric runs as the\n * app service principal (shared cache); an OBO-lane metric runs\n * on-behalf-of the requesting user via `asUser(req)` (per-user cache keyed by\n * a hash of the user identity).\n */\n async _handleMetricRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { key } = req.params;\n\n logger.debug(req, \"Executing metric: %s\", key);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeMetric\").setContext(\"analytics\", {\n metric_key: key,\n plugin: this.name,\n });\n\n if (!key) {\n res.status(400).json({ error: \"metric key is required\" });\n return;\n }\n\n // Resolve the registry from disk: read + parse `definitions.json` once per\n // request (no memoization). Reads through the plugin's shared `this.app`\n // (the base `Plugin`'s `AppManager`) from `config/metric-views/` under the\n // process cwd, so this is dev-tunnel aware and inherits the traversal\n // guard — the same mechanism as the sibling `.sql` query path.\n let registry: Record<string, MetricRegistration>;\n try {\n registry = await loadMetricRegistry(this.app, req, this.devFileReader);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logger.warn(req, \"Failed to load metric registry: %s\", reason);\n event?.setContext(\"analytics\", {\n metric_registry_load_error: reason,\n });\n res.status(503).json({\n error: \"Metric registry not available\",\n code: \"METRIC_REGISTRY_LOAD_FAILED\",\n });\n return;\n }\n\n // Own-property lookup: never resolve `key` to an inherited `Object.prototype` member.\n const registration = Object.hasOwn(registry, key)\n ? registry[key]\n : undefined;\n if (!registration) {\n // Don't echo the user-supplied `key` back in the public response.\n event?.setContext(\"analytics\", { unknown_metric_key: key });\n res.status(404).json({ error: \"Metric not found\" });\n return;\n }\n\n // Validate the body on the canonical error path.\n // `validateMetricRequest` throws a `ValidationError` (400) whose message names only field paths, never raw values.\n let request: ReturnType<typeof validateMetricRequest>;\n try {\n request = validateMetricRequest(req.body ?? {});\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n event?.setContext(\"analytics\", {\n unexpected_error: err instanceof Error ? err.message : String(err),\n metric_key: key,\n });\n logger.warn(\n req,\n \"Unexpected throw during metric request validation for %s: %s\",\n key,\n err instanceof Error ? err.message : String(err),\n );\n res.status(400).json({ error: \"Invalid request body\" });\n return;\n }\n\n // Lane dispatch. The lane comes from the registration (the entry's\n // `executor` in definitions.json), NOT a URL segment or `.obo.sql`\n // filename: an OBO-lane metric runs on-behalf-of the requesting user\n // (per-user cache via `asUser(req)`), an SP-lane metric as the app service\n // principal (shared cache).\n let executor: AnalyticsPlugin;\n let executorKey: string;\n try {\n const isObo = registration.lane === \"obo\";\n executor = isObo ? this.asUser(req) : this;\n executorKey = deriveMetricExecutorKey({\n lane: registration.lane,\n userIdentity: isObo ? this.resolveUserId(req) : undefined,\n });\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n throw err;\n }\n\n const injectedMetadata = this.config.metricViewsMetadata;\n const allMetadata =\n injectedMetadata ??\n (await loadMetricMetadata(this.app, req, this.devFileReader));\n\n if (allMetadata !== undefined && !Object.hasOwn(allMetadata, key)) {\n if (injectedMetadata !== undefined) {\n logger.warn(\n req,\n \"No display metadata for metric key %s in the injected metricViewsMetadata\",\n key,\n );\n } else {\n logger.warn(\n req,\n \"No display metadata for metric key %s — regenerate types to refresh %s\",\n key,\n METRIC_METADATA_FILE,\n );\n }\n }\n\n // Computed here, outside the cached execute below, so a cache hit still\n // serves the current metadata. Absent config → `undefined` → the `result`\n // message omits the field (envelope-identical to `/query`).\n const metadata = selectMetricMetadata(\n allMetadata,\n key,\n request.measures,\n request.dimensions,\n );\n\n // Cache key. Composed over the canonicalized args (sorted measures/\n // dimensions, stable-sorted predicates, grain, timeDimension, limit, orderBy) plus\n // the `executorKey` — `\"sp\"` shares the cache across all users, a per-user\n // identity hash isolates OBO callers.\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: composeMetricCacheKey({\n metricKey: key,\n source: registration.source,\n measures: request.measures,\n dimensions: request.dimensions,\n timeGrain: request.timeGrain,\n timeDimension: request.timeDimension,\n filter: request.filter,\n format: \"JSON_ARRAY\",\n executorKey,\n limit: request.limit,\n orderBy: request.orderBy,\n }),\n };\n\n // Cache/retry/timeout scoped to the SQL execution itself (inner `execute`)\n // so the warehouse-readiness phase isn't retried and the generator value\n // never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`). Capture the\n // original here so we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const { statement, parameters } = buildMetricSql(\n registration,\n request,\n );\n const processedParams =\n await self.queryProcessor.processQueryParams(\n statement,\n Object.keys(parameters).length > 0 ? parameters : undefined,\n );\n // Reuse the query route's JSON delivery: INLINE JSON_ARRAY with\n // an ARROW_STREAM-inline fallback, returning plain rows in a\n // `result` message — byte-identical envelope to `/query`.\n return await self._executeJsonArrayPath(\n executor,\n statement,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` and\n // sanitized `clientMessage` survive to the SSE error payload. Fall\n // back to a generic statement failure only if it wasn't an\n // AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n // Stamp the metadata onto the (possibly cached) result message; the\n // cached message never carries it.\n const resultMessage = sqlResult.data as AnalyticsSseMessage;\n yield (\n metadata !== undefined\n ? { ...resultMessage, metadata }\n : resultMessage\n ) as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * JSON_ARRAY SSE path. Delegates the disposition/format fallback to\n * {@link deliverJsonResult} (INLINE JSON_ARRAY → on `needs-arrow-inline`,\n * INLINE ARROW_STREAM decoded to rows) and wraps the rows in a `result`\n * message. External links are never used for the JSON fallback.\n */\n private async _executeJsonArrayPath(\n executor: AnalyticsPlugin,\n query: string,\n processedParams:\n | Record<string, SQLTypeMarker | null | undefined>\n | undefined,\n signal?: AbortSignal,\n ): Promise<AnalyticsSseMessage> {\n const result = await deliverJsonResult(\n executor,\n query,\n processedParams,\n signal,\n );\n return makeResultMessage(result.data, {\n status: result.status,\n statement_id: result.statement_id,\n });\n }\n\n /**\n * Attach the real column names so the client can relabel the positional\n * Arrow schema (Databricks encodes ARROW_STREAM columns as col_0, …).\n *\n * Small schemas ride `X-Appkit-Arrow-Columns` directly. A very wide schema\n * whose URL-encoded names would blow the HTTP header size limit instead\n * advertises the statement id in `X-Appkit-Arrow-Columns-Ref`, and the\n * client fetches the names from `GET /columns/:statementId`.\n */\n private _setArrowColumnsHeader(\n res: express.Response,\n columnsRef: { columnNames?: string[]; statementId?: string },\n ): void {\n const names = columnsRef.columnNames;\n if (!names || names.length === 0) return;\n\n const encoded = encodeURIComponent(JSON.stringify(names));\n if (encoded.length <= MAX_ARROW_COLUMNS_HEADER_BYTES) {\n res.setHeader(\"X-Appkit-Arrow-Columns\", encoded);\n return;\n }\n if (columnsRef.statementId) {\n res.setHeader(\"X-Appkit-Arrow-Columns-Ref\", columnsRef.statementId);\n } else {\n logger.warn(\n \"Arrow column names exceed the header limit and no statement id is available for the fallback endpoint; client will fall back to the raw schema names\",\n );\n }\n }\n\n /**\n * ARROW_STREAM query handler: stream the raw Arrow IPC bytes back as the\n * HTTP response body — no SSE, no server-side stash, no second\n * `/arrow-result` request.\n *\n * The first chunk is pulled before headers are sent so a failure still\n * yields a clean JSON error; once bytes are in flight a mid-stream failure\n * can only abort the socket. Warehouse readiness is awaited (no SSE\n * progress on this path) — a no-op for a warm warehouse, a blocking wait\n * on a cold start. Runs under the user's context for `.obo.sql` queries.\n */\n private async _handleArrowStreamQuery(\n req: express.Request,\n res: express.Response,\n query_key: string,\n query: string,\n isAsUser: boolean,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n ): Promise<void> {\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n const abortController = new AbortController();\n const onClose = () => abortController.abort();\n res.on(\"close\", onClose);\n const signal = abortController.signal;\n\n // Fail-fast: bound the wait for the first byte (warehouse readiness +\n // execute + first chunk) so a stuck/overloaded warehouse returns a clear\n // 503 instead of hanging until the client gives up. Cleared once the\n // first chunk arrives — a legitimately long stream is never interrupted.\n const firstByteTimeoutMs =\n this.config.arrowFirstByteTimeoutMs ??\n DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS;\n let timedOut = false;\n const failFast = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, firstByteTimeoutMs);\n\n try {\n // Run warehouse readiness in the SAME identity context as the query:\n // for `.obo.sql`, `executor` is the `asUser(req)` proxy, so\n // `getWorkspaceClient()` inside resolves to the user's client (matching\n // the SSE path). Calling it bare here would auto-start the warehouse as\n // the service principal even for OBO requests.\n await executor._ensureArrowWarehouseReady(signal);\n\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const warehouseId = await getWarehouseId();\n\n // Populated by `deliverArrowBytes` from the result manifest before the\n // first chunk is yielded, so the header below carries the real names.\n const columnsRef: { columnNames?: string[]; statementId?: string } = {};\n const bytes = deliverArrowBytes(\n // Wrap the executor so the INLINE attempt runs through the interceptor\n // chain (cache + retry), matching the JSON path. Only inline attachments\n // are cached; EXTERNAL_LINKS carry expiring pre-signed URLs and are\n // never cached (see `_arrowCachingExecutor`).\n this._arrowCachingExecutor(\n executor,\n query_key,\n query,\n parameters,\n executorKey,\n ),\n this.SQLClient,\n query,\n processedParams,\n columnsRef,\n signal,\n {\n // Skip the doomed INLINE probe on a warehouse already known to need\n // EXTERNAL_LINKS; remember the resolved mode for next time.\n capabilityHint: this._arrowCapability.get(warehouseId),\n onCapabilityResolved: (capability) =>\n this._arrowCapability.set(warehouseId, capability),\n },\n );\n const first = await bytes.next();\n // First byte in hand — stop the fail-fast clock.\n clearTimeout(failFast);\n\n res.setHeader(\"Content-Type\", \"application/vnd.apache.arrow.stream\");\n res.setHeader(\"Cache-Control\", \"no-store\");\n this._setArrowColumnsHeader(res, columnsRef);\n\n if (!first.done) {\n await writeChunk(res, first.value);\n for await (const buf of bytes) {\n await writeChunk(res, buf);\n }\n }\n res.end();\n } catch (error) {\n clearTimeout(failFast);\n // Fail-fast timeout: the warehouse never produced a first byte. This also\n // aborts the signal, so it must be handled before the generic\n // `signal.aborted` branch below. Headers aren't sent yet (we time out\n // before the first chunk), so a clean 503 is still possible.\n if (timedOut) {\n logger.warn(\n \"Arrow query timed out before first byte after %dms\",\n firstByteTimeoutMs,\n );\n res.status(503).json({\n error:\n \"The SQL warehouse is starting or overloaded and did not respond in time. Please retry.\",\n errorCode: \"WAREHOUSE_UNAVAILABLE\",\n plugin: this.name,\n });\n return;\n }\n // Client disconnect / unmount aborts the signal (see `onClose`). That's\n // routine UI behavior, not a server error — tear down quietly whether it\n // fires before or after headers. Checked before the headersSent branch so\n // a mid-stream disconnect doesn't spam ERROR logs / alerting.\n if (signal.aborted) {\n if (res.headersSent) res.destroy();\n else res.end();\n return;\n }\n if (res.headersSent) {\n logger.error(\"Arrow query stream failed mid-flight: %O\", error);\n res.destroy(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n logger.error(\"Arrow query error: %O\", error);\n // Do not echo upstream / SDK error text — it can include statement\n // fragments and correlation ids. Keep the structured code so the\n // client can branch (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // ARROW_DELIVERY_UNSUPPORTED).\n const errorCode =\n error instanceof ExecutionError ? error.errorCode : undefined;\n res.status(500).json({\n // `clientMessage` is the sanitized, actionable text (e.g. \"Re-run with\n // JSON_ARRAY\" for ARROW_DELIVERY_UNSUPPORTED); it never carries raw\n // warehouse/SDK strings. Fall back to a generic message otherwise.\n error:\n error instanceof AppKitError\n ? error.clientMessage\n : \"Unable to execute query\",\n errorCode,\n plugin: this.name,\n });\n } finally {\n res.off(\"close\", onClose);\n }\n }\n\n /**\n * Await SQL warehouse readiness for the direct-binary Arrow path. There is no\n * SSE progress channel here — readiness is simply awaited, bounded by the\n * caller's abort signal / fail-fast timeout. Invoked via the request executor\n * (`asUser(req)` for `.obo.sql`) so `getWorkspaceClient()` resolves in the\n * correct identity context rather than defaulting to the service principal.\n */\n async _ensureArrowWarehouseReady(signal: AbortSignal): Promise<void> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStart = this.config.autoStartWarehouse ?? true;\n await this.SQLClient.ensureWarehouseRunning(workspaceClient, warehouseId, {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart,\n onStatus: () => {},\n });\n }\n\n /**\n * Wrap an executor so the `INLINE + ARROW_STREAM` attempt is served from\n * (and populates) the same per-user TTL cache the JSON path uses — otherwise\n * every arrow chart render is a fresh warehouse execution, unlike its JSON\n * twin. Caching is deliberately scoped to inline attachments:\n *\n * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the\n * same lifecycle as cached JSON rows — safe to cache.\n * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in\n * minutes; caching them would serve dead links, so those pass through\n * uncached (only the tiny link metadata would be cached anyway).\n *\n * Uses `this.cache.getOrExecute` directly rather than `this.execute()`\n * because `execute()` reduces a thrown error to `{ ok:false, message }`,\n * dropping the `errorCode` the capability fallback classifies on. The cache\n * re-throws `AppKitError`s intact and never caches a rejection, so the\n * INLINE→EXTERNAL_LINKS fallback still sees the structured rejection.\n */\n private _arrowCachingExecutor(\n executor: AnalyticsPlugin,\n query_key: string,\n query: string,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n executorKey: string,\n ): QueryExecutor {\n const hashedQuery = this.queryProcessor.hashQuery(query);\n const cache = this.cache;\n const ttl = queryDefaults.cache?.ttl;\n return {\n query: (q, params, formatParameters, signal) => {\n // Only the inline-arrow attempt is cacheable — EXTERNAL_LINKS carry\n // short-lived pre-signed URLs, so those pass straight through.\n if (\n formatParameters.disposition !== \"INLINE\" ||\n formatParameters.format !== \"ARROW_STREAM\"\n ) {\n return executor.query(q, params, formatParameters, signal);\n }\n // On a standard warehouse this throws a capability rejection — the\n // cache never stores a rejection, so the fallback still sees the\n // structured error. On Reyden it returns a bounded (<=25 MiB)\n // attachment that caches like the JSON path's rows. The shared signal\n // dedupes concurrent renders (e.g. React StrictMode double-mount).\n return cache.getOrExecute(\n [\n \"analytics:query:arrow\",\n query_key,\n JSON.stringify(parameters),\n hashedQuery,\n executorKey,\n ],\n (sharedSignal) =>\n executor.query(q, params, formatParameters, sharedSignal ?? signal),\n executorKey,\n { ttl, callerSignal: signal },\n );\n },\n };\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n private tools = {\n query: defineTool({\n description:\n \"Execute a read-only SQL query against the Databricks SQL warehouse. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted; writes are rejected. Returns the query results as JSON.\",\n schema: z.object({\n query: z\n .string()\n .describe(\n \"The SQL query to execute. Must be a SELECT, WITH, SHOW, EXPLAIN, or DESCRIBE statement.\",\n ),\n }),\n annotations: {\n effect: \"read\",\n requiresUserContext: true,\n },\n autoInheritable: true,\n execute: (args, signal) => {\n assertReadOnlySql(args.query);\n return this.query(args.query, undefined, undefined, signal);\n },\n }),\n };\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n /**\n * Returns the plugin's tools as a keyed record of `ToolkitEntry` markers.\n * Called by the agents plugin (via `resolveToolkitFromProvider`) to spread\n * a filtered, renamed view of the plugin's tools into an agent's tool\n * index. Inside the function form of `AgentDefinition.tools`, callers\n * reach this method via `plugins.analytics.toolkit(opts)`.\n */\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * Write one chunk to the response honoring backpressure: if the socket\n * buffer is full (`res.write` returns false), wait for `drain` before\n * resolving so a slow client can't balloon Node's internal write queue and\n * defeat the constant-memory goal of streaming.\n *\n * @internal exported for unit testing the backpressure/disconnect behavior.\n */\nexport function writeChunk(\n res: express.Response,\n bytes: Uint8Array,\n): Promise<void> {\n const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n // If the socket is already gone, `res.write` won't return true and the\n // `drain`/`close`/`error` events have already fired — so the promise below\n // would never settle, wedging the for-await loop and the upstream reader.\n // Reject up front instead.\n if (res.destroyed || res.writableEnded) {\n return Promise.reject(\n new DOMException(\"The response stream closed\", \"AbortError\"),\n );\n }\n if (res.write(buf)) return Promise.resolve();\n // Backpressured: resolve on `drain`, but also settle on `close`/`error`. A\n // client that disconnects mid-backpressure never emits `drain` on the\n // destroyed socket, so waiting on `drain` alone would wedge this promise —\n // and with it the awaiting for-await loop and the upstream Arrow reader —\n // forever. Rejecting instead unwinds the stream so its `finally` can cancel\n // the reader.\n return new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n res.off(\"error\", onClose);\n };\n const onDrain = () => {\n cleanup();\n resolve();\n };\n const onClose = () => {\n cleanup();\n reject(new DOMException(\"The response stream closed\", \"AbortError\"));\n };\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n res.once(\"error\", onClose);\n });\n}\n\n/**\n * Fail-fast ceiling on the wait for the first Arrow byte (warehouse\n * readiness + execute + first chunk). Past this a stuck/overloaded warehouse\n * yields a clear 503 instead of hanging. Override per plugin via\n * `arrowFirstByteTimeoutMs`.\n */\nconst DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000;\n\n/**\n * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a\n * very wide schema) the names are served via the `/columns/:statementId`\n * fallback endpoint instead of the header. Kept well under the common ~8 KiB\n * per-header limit.\n */\nconst MAX_ARROW_COLUMNS_HEADER_BYTES = 6000;\n\n/**\n * @internal\n */\nexport const analytics = toPlugin(AnalyticsPlugin);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"analytics.js","names":["manifest"],"sources":["../../../src/plugins/analytics/analytics.ts"],"sourcesContent":["import type express from \"express\";\nimport {\n type AgentToolDefinition,\n type AnalyticsSseMessage,\n type IAppRouter,\n makeResultMessage,\n type PluginExecuteConfig,\n type SQLTypeMarker,\n type StreamExecutionSettings,\n type ToolProvider,\n} from \"shared\";\nimport { z } from \"zod\";\n\nimport { SQLWarehouseConnector } from \"../../connectors\";\nimport {\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS,\n type WarehouseStatusUpdate,\n} from \"../../connectors/sql-warehouse/client\";\nimport { getWarehouseId, getWorkspaceClient } from \"../../context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { AppKitError, ExecutionError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport type { WorkspaceClient } from \"../../workspace-client\";\nimport { queryDefaults } from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport {\n buildMetricSql,\n composeMetricCacheKey,\n deriveMetricExecutorKey,\n loadMetricMetadata,\n loadMetricRegistry,\n METRIC_METADATA_FILE,\n selectMetricMetadata,\n validateMetricRequest,\n} from \"./metric\";\nimport { QueryProcessor } from \"./query\";\nimport {\n type ArrowCapability,\n deliverArrowBytes,\n deliverJsonResult,\n type QueryExecutor,\n} from \"./result-delivery\";\nimport {\n type AnalyticsQueryResponse,\n type AnalyticsStreamMessage,\n type IAnalyticsConfig,\n type IAnalyticsQueryRequest,\n type MetricRegistration,\n normalizeAnalyticsFormat,\n type WarehouseStatus,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\n/**\n * Bridges a callback-emitting async function into an async iterable.\n *\n * `start(emit)` runs concurrently; every value passed to `emit` is yielded\n * in order. The iterable completes when `start`'s promise resolves and\n * re-throws (after draining) if it rejects. Lets a callback-based progress\n * API (e.g. SQL warehouse readiness) be consumed with `for await`.\n */\nasync function* streamCallbacks<T>(\n start: (emit: (value: T) => void) => Promise<void>,\n): AsyncGenerator<T, void, unknown> {\n const queue: T[] = [];\n let wake: (() => void) | null = null;\n let settled = false;\n let error: unknown = null;\n\n const notify = (): void => {\n wake?.();\n wake = null;\n };\n\n // The .then(_, err => ...) chain converts a rejection into a resolved\n // promise; the consumer surfaces `error` after draining the queue.\n void start((value) => {\n queue.push(value);\n notify();\n }).then(\n () => {\n settled = true;\n notify();\n },\n (err) => {\n error = err;\n settled = true;\n notify();\n },\n );\n\n while (!settled || queue.length > 0) {\n while (queue.length > 0) yield queue.shift() as T;\n if (settled) break;\n await new Promise<void>((resolve) => {\n wake = resolve;\n });\n }\n if (error) throw error;\n}\n\nexport class AnalyticsPlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"analytics\">;\n\n protected static description = \"Analytics plugin for data analysis\";\n declare protected config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n /**\n * In-process memo of which arrow delivery mode each warehouse supports\n * (keyed by warehouse id). A standard warehouse rejects `INLINE+ARROW_STREAM`\n * on every query, so once learned we skip that doomed probe; Reyden stays\n * `\"inline\"`. Capability is a property of the warehouse, not the user, so it\n * is not user-scoped. Bounded by the number of distinct warehouses a process\n * talks to (effectively one).\n */\n private _arrowCapability = new Map<string, ArrowCapability>();\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // Metric-view route. Registered parallel to `/query`\n // measures a registered UC Metric View over the same SSE envelope.\n this.route(router, {\n name: \"metric\",\n method: \"post\",\n path: \"/metric/:key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleMetricRoute(req, res);\n },\n });\n\n // Column-names fallback for very wide Arrow schemas whose names don't fit\n // in the `X-Appkit-Arrow-Columns` response header.\n // The client hits this with the statement id from `X-Appkit-Arrow-Columns-Ref`.\n this.route(router, {\n name: \"arrow-columns\",\n method: \"get\",\n path: \"/columns/:statementId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleColumnsRoute(req, res);\n },\n });\n }\n\n /**\n * Column-names fallback endpoint. Re-derives the real column names from the\n * statement's result manifest (stateless — no server cache), for the client\n * to relabel a positional Arrow schema when the names were too large for the\n * response header.\n */\n async _handleColumnsRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { statementId } = req.params;\n const columns = await this._resolveColumnNames(req, statementId);\n if (columns && columns.length > 0) {\n res.setHeader(\"Cache-Control\", \"no-store\");\n res.json({ columns });\n return;\n }\n res.status(404).json({\n error: \"Column names unavailable\",\n plugin: this.name,\n });\n }\n\n /**\n * Resolve a statement's real column names, trying the user's identity first\n * (required for `.obo.sql` statements, which the service principal cannot\n * `getStatement`) then falling back to the service principal (for\n * SP-executed statements). Returns undefined if neither identity can read it,\n * so the client falls back to the raw positional Arrow schema names.\n */\n private async _resolveColumnNames(\n req: express.Request,\n statementId: string,\n ): Promise<string[] | undefined> {\n const attempts: Array<() => Promise<string[] | undefined>> = [\n () => this.asUser(req)._getColumnNames(statementId),\n () => this._getColumnNames(statementId),\n ];\n for (const attempt of attempts) {\n try {\n const columns = await attempt();\n if (columns && columns.length > 0) return columns;\n } catch (error) {\n logger.debug(\n \"Arrow column-names lookup attempt failed for %s: %O\",\n statementId,\n error,\n );\n }\n }\n return undefined;\n }\n\n /**\n * Fetch column names in the current execution context. Proxied by `asUser`,\n * so `getWorkspaceClient()` resolves to the user's client when invoked via\n * `this.asUser(req)` and the service principal's otherwise.\n */\n async _getColumnNames(statementId: string): Promise<string[] | undefined> {\n return this.SQLClient.getColumnNames(getWorkspaceClient(), statementId);\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format: rawFormat = \"JSON_ARRAY\" } =\n req.body as IAnalyticsQueryRequest;\n\n if (\n rawFormat !== \"JSON_ARRAY\" &&\n rawFormat !== \"ARROW_STREAM\" &&\n rawFormat !== \"JSON\" &&\n rawFormat !== \"ARROW\"\n ) {\n res.status(400).json({\n error: `Invalid format: ${String(rawFormat)}. Expected \"JSON_ARRAY\" or \"ARROW_STREAM\".`,\n });\n return;\n }\n\n const format = normalizeAnalyticsFormat(rawFormat);\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const queryResult = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!queryResult) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const { query, isAsUser } = queryResult;\n\n // ARROW_STREAM streams the raw Arrow IPC bytes back as the HTTP response\n // body — no SSE, no server-side stash, no second /arrow-result request.\n // INLINE attachments are piped straight through (the bytes are already in\n // hand from executeStatement); a warehouse that refuses INLINE falls back\n // to EXTERNAL_LINKS and streams those chunks. JSON keeps the SSE path\n // below (it carries warehouse-readiness progress + cached rows).\n if (format === \"ARROW_STREAM\") {\n await this._handleArrowStreamQuery(\n req,\n res,\n query_key,\n query,\n isAsUser,\n parameters,\n );\n return;\n }\n\n // get execution context - user-scoped if .obo.sql, otherwise service principal\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n format,\n hashedQuery,\n executorKey,\n ],\n };\n\n // Cache/retry/timeout are scoped to the SQL execution itself (inner\n // `execute`) so the warehouse-readiness phase isn't subject to retries\n // and the generator value never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // user-scoped trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`) the\n // fallback's `ExecutionError`s carry. Capture the original here so\n // we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const processedParams =\n await self.queryProcessor.processQueryParams(query, parameters);\n // JSON_ARRAY path: tries INLINE + JSON_ARRAY and, if the\n // warehouse only accepts ARROW_STREAM for INLINE, retries as\n // ARROW_STREAM and decodes server-side — returning the SSE\n // `result` message with plain rows. (ARROW_STREAM requests are\n // handled earlier via `_handleArrowStreamQuery`.)\n return await self._executeJsonArrayPath(\n executor,\n query,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` (e.g.\n // RESULT_TOO_LARGE_FOR_JSON_FALLBACK) and sanitized `clientMessage`\n // survive to the SSE error payload. Fall back to a generic\n // statement failure only if the original wasn't an AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n yield sqlResult.data as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * Handle metric-view execution requests (`POST /api/analytics/metric/:key`).\n *\n * Mirrors {@link _handleQueryRoute}'s JSON SSE path: the outer\n * `executeStream` disables cache/retry and streams warehouse-readiness\n * (`warehouse_status`) events, then the inner `execute` builds the metric SQL\n * and delivers rows through {@link deliverJsonResult} as a `result` message.\n * The `originalError` re-throw discipline preserves each error's structured\n * `errorCode`/`clientMessage` for the SSE error payload.\n *\n * Lane dispatch is driven by the registration: an SP-lane metric runs as the\n * app service principal (shared cache); an OBO-lane metric runs\n * on-behalf-of the requesting user via `asUser(req)` (per-user cache keyed by\n * a hash of the user identity).\n */\n async _handleMetricRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { key } = req.params;\n\n logger.debug(req, \"Executing metric: %s\", key);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeMetric\").setContext(\"analytics\", {\n metric_key: key,\n plugin: this.name,\n });\n\n if (!key) {\n res.status(400).json({ error: \"metric key is required\" });\n return;\n }\n\n // Resolve the registry from disk: read + parse `definitions.json` once per\n // request (no memoization). Reads through the plugin's shared `this.app`\n // (the base `Plugin`'s `AppManager`) from `config/metric-views/` under the\n // process cwd, so this is dev-tunnel aware and inherits the traversal\n // guard — the same mechanism as the sibling `.sql` query path.\n let registry: Record<string, MetricRegistration>;\n try {\n registry = await loadMetricRegistry(this.app, req, this.devFileReader);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n logger.warn(req, \"Failed to load metric registry: %s\", reason);\n event?.setContext(\"analytics\", {\n metric_registry_load_error: reason,\n });\n res.status(503).json({\n error: \"Metric registry not available\",\n code: \"METRIC_REGISTRY_LOAD_FAILED\",\n });\n return;\n }\n\n // Own-property lookup: never resolve `key` to an inherited `Object.prototype` member.\n const registration = Object.hasOwn(registry, key)\n ? registry[key]\n : undefined;\n if (!registration) {\n // Don't echo the user-supplied `key` back in the public response.\n event?.setContext(\"analytics\", { unknown_metric_key: key });\n res.status(404).json({ error: \"Metric not found\" });\n return;\n }\n\n // Validate the body on the canonical error path.\n // `validateMetricRequest` throws a `ValidationError` (400) whose message names only field paths, never raw values.\n let request: ReturnType<typeof validateMetricRequest>;\n try {\n request = validateMetricRequest(req.body ?? {});\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n event?.setContext(\"analytics\", {\n unexpected_error: err instanceof Error ? err.message : String(err),\n metric_key: key,\n });\n logger.warn(\n req,\n \"Unexpected throw during metric request validation for %s: %s\",\n key,\n err instanceof Error ? err.message : String(err),\n );\n res.status(400).json({ error: \"Invalid request body\" });\n return;\n }\n\n // Lane dispatch. The lane comes from the registration (the entry's\n // `executor` in definitions.json), NOT a URL segment or `.obo.sql`\n // filename: an OBO-lane metric runs on-behalf-of the requesting user\n // (per-user cache via `asUser(req)`), an SP-lane metric as the app service\n // principal (shared cache).\n let executor: AnalyticsPlugin;\n let executorKey: string;\n try {\n const isObo = registration.lane === \"obo\";\n executor = isObo ? this.asUser(req) : this;\n executorKey = deriveMetricExecutorKey({\n lane: registration.lane,\n userIdentity: isObo ? this.resolveUserId(req) : undefined,\n });\n } catch (err) {\n if (err instanceof AppKitError) {\n res.status(err.statusCode).json({ error: err.message, code: err.code });\n return;\n }\n throw err;\n }\n\n const injectedMetadata = this.config.metricViewsMetadata;\n const allMetadata =\n injectedMetadata ??\n (await loadMetricMetadata(this.app, req, this.devFileReader));\n\n if (allMetadata !== undefined && !Object.hasOwn(allMetadata, key)) {\n if (injectedMetadata !== undefined) {\n logger.warn(\n req,\n \"No display metadata for metric key %s in the injected metricViewsMetadata\",\n key,\n );\n } else {\n logger.warn(\n req,\n \"No display metadata for metric key %s — regenerate types to refresh %s\",\n key,\n METRIC_METADATA_FILE,\n );\n }\n }\n\n // Computed here, outside the cached execute below, so a cache hit still\n // serves the current metadata. Absent config → `undefined` → the `result`\n // message omits the field (envelope-identical to `/query`).\n const metadata = selectMetricMetadata(\n allMetadata,\n key,\n request.measures,\n request.dimensions,\n );\n\n // Cache key. Composed over the canonicalized args (sorted measures/\n // dimensions, stable-sorted predicates, grain, timeDimension, limit, orderBy) plus\n // the `executorKey` — `\"sp\"` shares the cache across all users, a per-user\n // identity hash isolates OBO callers.\n const cacheConfig = {\n ...queryDefaults.cache,\n cacheKey: composeMetricCacheKey({\n metricKey: key,\n source: registration.source,\n measures: request.measures,\n dimensions: request.dimensions,\n timeGrain: request.timeGrain,\n timeDimension: request.timeDimension,\n filter: request.filter,\n format: \"JSON_ARRAY\",\n executorKey,\n limit: request.limit,\n orderBy: request.orderBy,\n }),\n };\n\n // Cache/retry/timeout scoped to the SQL execution itself (inner `execute`)\n // so the warehouse-readiness phase isn't retried and the generator value\n // never leaks into the cache.\n const sqlConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: cacheConfig,\n };\n\n // Outer stream: no cache/retry — `executeStream` would otherwise wrap the\n // generator factory and cache the generator object itself. Telemetry +\n // trace context still apply.\n const streamExecutionSettings: StreamExecutionSettings = {\n default: {\n cache: { enabled: false },\n retry: { enabled: false },\n },\n };\n\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStartWarehouse = this.config.autoStartWarehouse ?? true;\n\n const self = this;\n\n await executor.executeStream(\n res,\n async function* (\n signal,\n ): AsyncGenerator<AnalyticsStreamMessage, void, unknown> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n // Stream warehouse-readiness updates as SSE events, then run SQL.\n const readinessUpdates = streamCallbacks<WarehouseStatusUpdate>(\n (emit) =>\n self.SQLClient.ensureWarehouseRunning(\n workspaceClient,\n warehouseId,\n {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart: autoStartWarehouse,\n onStatus: emit,\n },\n ),\n );\n for await (const update of readinessUpdates) {\n yield {\n type: \"warehouse_status\",\n status: {\n state: update.state as WarehouseStatus[\"state\"],\n elapsedMs: update.elapsedMs,\n },\n };\n }\n\n // `execute()` reduces a thrown error to `{ status, message }`,\n // dropping the rich fields (`errorCode`, `clientMessage`). Capture the\n // original here so we can re-throw it intact — the SSE error path\n // (`StreamManager`) reads `errorCode`/`clientMessage` off it.\n let originalError: unknown;\n const sqlResult = await executor.execute(\n async (sig) => {\n try {\n const { statement, parameters } = buildMetricSql(\n registration,\n request,\n );\n const processedParams =\n await self.queryProcessor.processQueryParams(\n statement,\n Object.keys(parameters).length > 0 ? parameters : undefined,\n );\n // Reuse the query route's JSON delivery: INLINE JSON_ARRAY with\n // an ARROW_STREAM-inline fallback, returning plain rows in a\n // `result` message — byte-identical envelope to `/query`.\n return await self._executeJsonArrayPath(\n executor,\n statement,\n processedParams,\n sig,\n );\n } catch (err) {\n originalError = err;\n throw err;\n }\n },\n { default: sqlConfig },\n executorKey,\n );\n\n if (!sqlResult.ok) {\n const msg = sqlResult.message;\n const lower = msg.toLowerCase();\n if (\n lower.includes(\"operation was aborted\") ||\n lower.includes(\"the request was aborted\") ||\n lower.includes(\"statement was canceled\")\n ) {\n const err = new DOMException(\n lower.includes(\"canceled\") ? msg : \"The operation was aborted.\",\n \"AbortError\",\n );\n throw err;\n }\n // Re-throw the original error so its structured `errorCode` and\n // sanitized `clientMessage` survive to the SSE error payload. Fall\n // back to a generic statement failure only if it wasn't an\n // AppKitError.\n if (originalError instanceof AppKitError) {\n throw originalError;\n }\n const inner = msg.startsWith(\"Statement failed: \")\n ? msg.slice(\"Statement failed: \".length)\n : msg;\n throw ExecutionError.statementFailed(inner);\n }\n\n // Stamp the metadata onto the (possibly cached) result message; the\n // cached message never carries it.\n const resultMessage = sqlResult.data as AnalyticsSseMessage;\n yield (\n metadata !== undefined\n ? { ...resultMessage, metadata }\n : resultMessage\n ) as AnalyticsStreamMessage;\n },\n streamExecutionSettings,\n executorKey,\n );\n }\n\n /**\n * JSON_ARRAY SSE path. Delegates the disposition/format fallback to\n * {@link deliverJsonResult} (INLINE JSON_ARRAY → on `needs-arrow-inline`,\n * INLINE ARROW_STREAM decoded to rows) and wraps the rows in a `result`\n * message. External links are never used for the JSON fallback.\n */\n private async _executeJsonArrayPath(\n executor: AnalyticsPlugin,\n query: string,\n processedParams:\n | Record<string, SQLTypeMarker | null | undefined>\n | undefined,\n signal?: AbortSignal,\n ): Promise<AnalyticsSseMessage> {\n const result = await deliverJsonResult(\n executor,\n query,\n processedParams,\n signal,\n );\n return makeResultMessage(result.data, {\n status: result.status,\n statement_id: result.statement_id,\n });\n }\n\n /**\n * Attach the real column names so the client can relabel the positional\n * Arrow schema (Databricks encodes ARROW_STREAM columns as col_0, …).\n *\n * Small schemas ride `X-Appkit-Arrow-Columns` directly. A very wide schema\n * whose URL-encoded names would blow the HTTP header size limit instead\n * advertises the statement id in `X-Appkit-Arrow-Columns-Ref`, and the\n * client fetches the names from `GET /columns/:statementId`.\n */\n private _setArrowColumnsHeader(\n res: express.Response,\n columnsRef: { columnNames?: string[]; statementId?: string },\n ): void {\n const names = columnsRef.columnNames;\n if (!names || names.length === 0) return;\n\n const encoded = encodeURIComponent(JSON.stringify(names));\n if (encoded.length <= MAX_ARROW_COLUMNS_HEADER_BYTES) {\n res.setHeader(\"X-Appkit-Arrow-Columns\", encoded);\n return;\n }\n if (columnsRef.statementId) {\n res.setHeader(\"X-Appkit-Arrow-Columns-Ref\", columnsRef.statementId);\n } else {\n logger.warn(\n \"Arrow column names exceed the header limit and no statement id is available for the fallback endpoint; client will fall back to the raw schema names\",\n );\n }\n }\n\n /**\n * ARROW_STREAM query handler: stream the raw Arrow IPC bytes back as the\n * HTTP response body — no SSE, no server-side stash, no second\n * `/arrow-result` request.\n *\n * The first chunk is pulled before headers are sent so a failure still\n * yields a clean JSON error; once bytes are in flight a mid-stream failure\n * can only abort the socket. Warehouse readiness is awaited (no SSE\n * progress on this path) — a no-op for a warm warehouse, a blocking wait\n * on a cold start. Runs under the user's context for `.obo.sql` queries.\n */\n private async _handleArrowStreamQuery(\n req: express.Request,\n res: express.Response,\n query_key: string,\n query: string,\n isAsUser: boolean,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n ): Promise<void> {\n const executor = isAsUser ? this.asUser(req) : this;\n const executorKey = isAsUser ? this.resolveUserId(req) : \"global\";\n const abortController = new AbortController();\n const onClose = () => abortController.abort();\n res.on(\"close\", onClose);\n const signal = abortController.signal;\n\n // Fail-fast: bound the wait for the first byte (warehouse readiness +\n // execute + first chunk) so a stuck/overloaded warehouse returns a clear\n // 503 instead of hanging until the client gives up. Cleared once the\n // first chunk arrives — a legitimately long stream is never interrupted.\n const firstByteTimeoutMs =\n this.config.arrowFirstByteTimeoutMs ??\n DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS;\n let timedOut = false;\n const failFast = setTimeout(() => {\n timedOut = true;\n abortController.abort();\n }, firstByteTimeoutMs);\n\n try {\n // Run warehouse readiness in the SAME identity context as the query:\n // for `.obo.sql`, `executor` is the `asUser(req)` proxy, so\n // `getWorkspaceClient()` inside resolves to the user's client (matching\n // the SSE path). Calling it bare here would auto-start the warehouse as\n // the service principal even for OBO requests.\n await executor._ensureArrowWarehouseReady(signal);\n\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const warehouseId = await getWarehouseId();\n\n // Populated by `deliverArrowBytes` from the result manifest before the\n // first chunk is yielded, so the header below carries the real names.\n const columnsRef: { columnNames?: string[]; statementId?: string } = {};\n const bytes = deliverArrowBytes(\n // Wrap the executor so the INLINE attempt runs through the interceptor\n // chain (cache + retry), matching the JSON path. Only inline attachments\n // are cached; EXTERNAL_LINKS carry expiring pre-signed URLs and are\n // never cached (see `_arrowCachingExecutor`).\n this._arrowCachingExecutor(\n executor,\n query_key,\n query,\n parameters,\n executorKey,\n ),\n this.SQLClient,\n query,\n processedParams,\n columnsRef,\n signal,\n {\n // Skip the doomed INLINE probe on a warehouse already known to need\n // EXTERNAL_LINKS; remember the resolved mode for next time.\n capabilityHint: this._arrowCapability.get(warehouseId),\n onCapabilityResolved: (capability) =>\n this._arrowCapability.set(warehouseId, capability),\n },\n );\n const first = await bytes.next();\n // First byte in hand — stop the fail-fast clock.\n clearTimeout(failFast);\n\n res.setHeader(\"Content-Type\", \"application/vnd.apache.arrow.stream\");\n res.setHeader(\"Cache-Control\", \"no-store\");\n this._setArrowColumnsHeader(res, columnsRef);\n\n if (!first.done) {\n await writeChunk(res, first.value);\n for await (const buf of bytes) {\n await writeChunk(res, buf);\n }\n }\n res.end();\n } catch (error) {\n clearTimeout(failFast);\n // Fail-fast timeout: the warehouse never produced a first byte. This also\n // aborts the signal, so it must be handled before the generic\n // `signal.aborted` branch below. Headers aren't sent yet (we time out\n // before the first chunk), so a clean 503 is still possible.\n if (timedOut) {\n logger.warn(\n \"Arrow query timed out before first byte after %dms\",\n firstByteTimeoutMs,\n );\n res.status(503).json({\n error:\n \"The SQL warehouse is starting or overloaded and did not respond in time. Please retry.\",\n errorCode: \"WAREHOUSE_UNAVAILABLE\",\n plugin: this.name,\n });\n return;\n }\n // Client disconnect / unmount aborts the signal (see `onClose`). That's\n // routine UI behavior, not a server error — tear down quietly whether it\n // fires before or after headers. Checked before the headersSent branch so\n // a mid-stream disconnect doesn't spam ERROR logs / alerting.\n if (signal.aborted) {\n if (res.headersSent) res.destroy();\n else res.end();\n return;\n }\n if (res.headersSent) {\n logger.error(\"Arrow query stream failed mid-flight: %O\", error);\n res.destroy(error instanceof Error ? error : new Error(String(error)));\n return;\n }\n logger.error(\"Arrow query error: %O\", error);\n // Do not echo upstream / SDK error text — it can include statement\n // fragments and correlation ids. Keep the structured code so the\n // client can branch (e.g. RESULT_TOO_LARGE_FOR_JSON_FALLBACK,\n // ARROW_DELIVERY_UNSUPPORTED).\n const errorCode =\n error instanceof ExecutionError ? error.errorCode : undefined;\n res.status(500).json({\n // `clientMessage` is the sanitized, actionable text (e.g. \"Re-run with\n // JSON_ARRAY\" for ARROW_DELIVERY_UNSUPPORTED); it never carries raw\n // warehouse/SDK strings. Fall back to a generic message otherwise.\n error:\n error instanceof AppKitError\n ? error.clientMessage\n : \"Unable to execute query\",\n errorCode,\n plugin: this.name,\n });\n } finally {\n res.off(\"close\", onClose);\n }\n }\n\n /**\n * Await SQL warehouse readiness for the direct-binary Arrow path. There is no\n * SSE progress channel here — readiness is simply awaited, bounded by the\n * caller's abort signal / fail-fast timeout. Invoked via the request executor\n * (`asUser(req)` for `.obo.sql`) so `getWorkspaceClient()` resolves in the\n * correct identity context rather than defaulting to the service principal.\n */\n async _ensureArrowWarehouseReady(signal: AbortSignal): Promise<void> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n const startupTimeoutMs =\n this.config.warehouseStartupTimeoutMs ??\n DEFAULT_WAREHOUSE_STARTUP_TIMEOUT_MS;\n const autoStart = this.config.autoStartWarehouse ?? true;\n await this.SQLClient.ensureWarehouseRunning(workspaceClient, warehouseId, {\n signal,\n timeoutMs: startupTimeoutMs,\n autoStart,\n onStatus: () => {},\n });\n }\n\n /**\n * Wrap an executor so the `INLINE + ARROW_STREAM` attempt is served from\n * (and populates) the same per-user TTL cache the JSON path uses — otherwise\n * every arrow chart render is a fresh warehouse execution, unlike its JSON\n * twin. Caching is deliberately scoped to inline attachments:\n *\n * - INLINE results carry a bounded (<=25 MiB) base64 `attachment` with the\n * same lifecycle as cached JSON rows — safe to cache.\n * - EXTERNAL_LINKS results carry short-lived pre-signed URLs that expire in\n * minutes; caching them would serve dead links, so those pass through\n * uncached (only the tiny link metadata would be cached anyway).\n *\n * Uses `this.cache.getOrExecute` directly rather than `this.execute()`\n * because `execute()` reduces a thrown error to `{ ok:false, message }`,\n * dropping the `errorCode` the capability fallback classifies on. The cache\n * re-throws `AppKitError`s intact and never caches a rejection, so the\n * INLINE→EXTERNAL_LINKS fallback still sees the structured rejection.\n */\n private _arrowCachingExecutor(\n executor: AnalyticsPlugin,\n query_key: string,\n query: string,\n parameters: IAnalyticsQueryRequest[\"parameters\"],\n executorKey: string,\n ): QueryExecutor {\n const hashedQuery = this.queryProcessor.hashQuery(query);\n const cache = this.cache;\n const ttl = queryDefaults.cache?.ttl;\n return {\n query: (q, params, formatParameters, signal) => {\n // Only the inline-arrow attempt is cacheable — EXTERNAL_LINKS carry\n // short-lived pre-signed URLs, so those pass straight through.\n if (\n formatParameters.disposition !== \"INLINE\" ||\n formatParameters.format !== \"ARROW_STREAM\"\n ) {\n return executor.query(q, params, formatParameters, signal);\n }\n // On a standard warehouse this throws a capability rejection — the\n // cache never stores a rejection, so the fallback still sees the\n // structured error. On Reyden it returns a bounded (<=25 MiB)\n // attachment that caches like the JSON path's rows. The shared signal\n // dedupes concurrent renders (e.g. React StrictMode double-mount).\n return cache.getOrExecute(\n [\n \"analytics:query:arrow\",\n query_key,\n JSON.stringify(parameters),\n hashedQuery,\n executorKey,\n ],\n (sharedSignal) =>\n executor.query(q, params, formatParameters, sharedSignal ?? signal),\n executorKey,\n { ttl, callerSignal: signal },\n );\n },\n };\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n\n private tools = {\n query: defineTool({\n description:\n \"Execute a read-only SQL query against the Databricks SQL warehouse. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted; writes are rejected. Returns the query results as JSON.\",\n schema: z.object({\n query: z\n .string()\n .describe(\n \"The SQL query to execute. Must be a SELECT, WITH, SHOW, EXPLAIN, or DESCRIBE statement.\",\n ),\n }),\n annotations: {\n effect: \"read\",\n requiresUserContext: true,\n },\n autoInheritable: true,\n execute: (args, signal) => {\n assertReadOnlySql(args.query);\n return this.query(args.query, undefined, undefined, signal);\n },\n }),\n };\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n /**\n * Returns the plugin's tools as a keyed record of `ToolkitEntry` markers.\n * Called by the agents plugin (via `resolveToolkitFromProvider`) to spread\n * a filtered, renamed view of the plugin's tools into an agent's tool\n * index. Inside the function form of `AgentDefinition.tools`, callers\n * reach this method via `plugins.analytics.toolkit(opts)`.\n */\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the public exports for the analytics plugin.\n * Note: `asUser()` is automatically added by AppKit.\n */\n exports() {\n return {\n /**\n * Execute a SQL query using service principal credentials.\n */\n query: this.query,\n };\n }\n}\n\n/**\n * Write one chunk to the response honoring backpressure: if the socket\n * buffer is full (`res.write` returns false), wait for `drain` before\n * resolving so a slow client can't balloon Node's internal write queue and\n * defeat the constant-memory goal of streaming.\n *\n * @internal exported for unit testing the backpressure/disconnect behavior.\n */\nexport function writeChunk(\n res: express.Response,\n bytes: Uint8Array,\n): Promise<void> {\n const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);\n // If the socket is already gone, `res.write` won't return true and the\n // `drain`/`close`/`error` events have already fired — so the promise below\n // would never settle, wedging the for-await loop and the upstream reader.\n // Reject up front instead.\n if (res.destroyed || res.writableEnded) {\n return Promise.reject(\n new DOMException(\"The response stream closed\", \"AbortError\"),\n );\n }\n if (res.write(buf)) return Promise.resolve();\n // Backpressured: resolve on `drain`, but also settle on `close`/`error`. A\n // client that disconnects mid-backpressure never emits `drain` on the\n // destroyed socket, so waiting on `drain` alone would wedge this promise —\n // and with it the awaiting for-await loop and the upstream Arrow reader —\n // forever. Rejecting instead unwinds the stream so its `finally` can cancel\n // the reader.\n return new Promise<void>((resolve, reject) => {\n const cleanup = () => {\n res.off(\"drain\", onDrain);\n res.off(\"close\", onClose);\n res.off(\"error\", onClose);\n };\n const onDrain = () => {\n cleanup();\n resolve();\n };\n const onClose = () => {\n cleanup();\n reject(new DOMException(\"The response stream closed\", \"AbortError\"));\n };\n res.once(\"drain\", onDrain);\n res.once(\"close\", onClose);\n res.once(\"error\", onClose);\n });\n}\n\n/**\n * Fail-fast ceiling on the wait for the first Arrow byte (warehouse\n * readiness + execute + first chunk). Past this a stuck/overloaded warehouse\n * yields a clear 503 instead of hanging. Override per plugin via\n * `arrowFirstByteTimeoutMs`.\n */\nconst DEFAULT_ARROW_FIRST_BYTE_TIMEOUT_MS = 120_000;\n\n/**\n * Byte ceiling for the `X-Appkit-Arrow-Columns` header value. Beyond this (a\n * very wide schema) the names are served via the `/columns/:statementId`\n * fallback endpoint instead of the header. Kept well under the common ~8 KiB\n * per-header limit.\n */\nconst MAX_ARROW_COLUMNS_HEADER_BYTES = 6000;\n\n/**\n * @internal\n */\nexport const analytics = toPlugin(AnalyticsPlugin);\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache.js","names":[],"sources":["../../../../src/plugins/analytics/mv/cache.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport { AuthenticationError } from \"../../../errors\";\nimport type { MetricFilter, MetricLane, MetricPredicate } from \"../types\";\nimport { sortFilterChildren } from \"./formatters\";\nimport type { MetricCacheKeyInput } from \"./types\";\n\nexport function composeMetricCacheKey(input: MetricCacheKeyInput): string[] {\n const sortedMeasures = [...input.measures].sort();\n const sortedDimensions = [...(input.dimensions ?? [])].sort();\n const filterFingerprint =\n input.filter !== undefined ? canonicalizeFilter(input.filter) : \"_\";\n // `timeDimension` only changes the SQL when `timeGrain` is set (see renderDimensionClause)\n const timeDimensionPart =\n input.timeGrain != null ? (input.timeDimension ?? \"_\") : \"_\";\n // Preserve `orderBy` sequence: under LIMIT, `a, b` and `b, a` return different rows.\n const orderByPart =\n input.orderBy !== undefined && input.orderBy.length > 0\n ? JSON.stringify(\n input.orderBy.map((o) => [o.field, o.direction ?? \"ASC\"]),\n )\n : \"_\";\n return [\n \"metric\",\n input.metricKey,\n input.source,\n input.format,\n // Commas are valid in identifiers, so JSON-encode: `[\"a,b\"]` must not\n // collide with `[\"a\",\"b\"]`.\n JSON.stringify(sortedMeasures),\n JSON.stringify(sortedDimensions),\n input.timeGrain ?? \"_\",\n timeDimensionPart,\n filterFingerprint,\n typeof input.limit === \"number\" ? String(input.limit) : \"_\",\n orderByPart,\n input.executorKey,\n ];\n}\n\nexport function deriveMetricExecutorKey(input: {\n lane: MetricLane;\n userIdentity?: string | null;\n}): string {\n if (input.lane === \"sp\") {\n return \"sp\";\n }\n const identity = input.userIdentity?.trim();\n if (!identity) {\n throw AuthenticationError.missingUserId();\n }\n return createHash(\"sha256\").update(identity).digest(\"hex\");\n}\n\nfunction canonicalizeFilter(node: MetricFilter): string {\n if (node === null || typeof node !== \"object\") {\n return \"_\";\n }\n\n if (\"and\" in node || \"or\" in node) {\n const groupKey = \"and\" in node ? \"and\" : \"or\";\n const children = (\n node as { and?: ReadonlyArray<MetricFilter> } & {\n or?: ReadonlyArray<MetricFilter>;\n }\n )[groupKey];\n\n if (!Array.isArray(children) || children.length === 0) {\n return `${groupKey}()`;\n }\n\n const sorted = sortFilterChildren(children);\n const childFingerprints = sorted.map(canonicalizeFilter);\n return `${groupKey}(${childFingerprints.join(\",\")})`;\n }\n\n const p = node as MetricPredicate;\n const valuesPart = (p.values ?? []).map((v) => [typeof v, v]);\n return `p(${JSON.stringify([p.member, p.operator, valuesPart])})`;\n}\n"],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"cache.js","names":[],"sources":["../../../../src/plugins/analytics/mv/cache.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\n\nimport { AuthenticationError } from \"../../../errors\";\nimport type { MetricFilter, MetricLane, MetricPredicate } from \"../types\";\nimport { sortFilterChildren } from \"./formatters\";\nimport type { MetricCacheKeyInput } from \"./types\";\n\nexport function composeMetricCacheKey(input: MetricCacheKeyInput): string[] {\n const sortedMeasures = [...input.measures].sort();\n const sortedDimensions = [...(input.dimensions ?? [])].sort();\n const filterFingerprint =\n input.filter !== undefined ? canonicalizeFilter(input.filter) : \"_\";\n // `timeDimension` only changes the SQL when `timeGrain` is set (see renderDimensionClause)\n const timeDimensionPart =\n input.timeGrain != null ? (input.timeDimension ?? \"_\") : \"_\";\n // Preserve `orderBy` sequence: under LIMIT, `a, b` and `b, a` return different rows.\n const orderByPart =\n input.orderBy !== undefined && input.orderBy.length > 0\n ? JSON.stringify(\n input.orderBy.map((o) => [o.field, o.direction ?? \"ASC\"]),\n )\n : \"_\";\n return [\n \"metric\",\n input.metricKey,\n input.source,\n input.format,\n // Commas are valid in identifiers, so JSON-encode: `[\"a,b\"]` must not\n // collide with `[\"a\",\"b\"]`.\n JSON.stringify(sortedMeasures),\n JSON.stringify(sortedDimensions),\n input.timeGrain ?? \"_\",\n timeDimensionPart,\n filterFingerprint,\n typeof input.limit === \"number\" ? String(input.limit) : \"_\",\n orderByPart,\n input.executorKey,\n ];\n}\n\nexport function deriveMetricExecutorKey(input: {\n lane: MetricLane;\n userIdentity?: string | null;\n}): string {\n if (input.lane === \"sp\") {\n return \"sp\";\n }\n const identity = input.userIdentity?.trim();\n if (!identity) {\n throw AuthenticationError.missingUserId();\n }\n return createHash(\"sha256\").update(identity).digest(\"hex\");\n}\n\nfunction canonicalizeFilter(node: MetricFilter): string {\n if (node === null || typeof node !== \"object\") {\n return \"_\";\n }\n\n if (\"and\" in node || \"or\" in node) {\n const groupKey = \"and\" in node ? \"and\" : \"or\";\n const children = (\n node as { and?: ReadonlyArray<MetricFilter> } & {\n or?: ReadonlyArray<MetricFilter>;\n }\n )[groupKey];\n\n if (!Array.isArray(children) || children.length === 0) {\n return `${groupKey}()`;\n }\n\n const sorted = sortFilterChildren(children);\n const childFingerprints = sorted.map(canonicalizeFilter);\n return `${groupKey}(${childFingerprints.join(\",\")})`;\n }\n\n const p = node as MetricPredicate;\n const valuesPart = (p.values ?? []).map((v) => [typeof v, v]);\n return `p(${JSON.stringify([p.member, p.operator, valuesPart])})`;\n}\n"],"mappings":";;;;;;AAOA,SAAgB,sBAAsB,OAAsC;CAC1E,MAAM,iBAAiB,CAAC,GAAG,MAAM,SAAS,CAAC,MAAM;CACjD,MAAM,mBAAmB,CAAC,GAAI,MAAM,cAAc,EAAE,CAAE,CAAC,MAAM;CAC7D,MAAM,oBACJ,MAAM,WAAW,SAAY,mBAAmB,MAAM,OAAO,GAAG;CAElE,MAAM,oBACJ,MAAM,aAAa,OAAQ,MAAM,iBAAiB,MAAO;CAE3D,MAAM,cACJ,MAAM,YAAY,UAAa,MAAM,QAAQ,SAAS,IAClD,KAAK,UACH,MAAM,QAAQ,KAAK,MAAM,CAAC,EAAE,OAAO,EAAE,aAAa,MAAM,CAAC,CAC1D,GACD;AACN,QAAO;EACL;EACA,MAAM;EACN,MAAM;EACN,MAAM;EAGN,KAAK,UAAU,eAAe;EAC9B,KAAK,UAAU,iBAAiB;EAChC,MAAM,aAAa;EACnB;EACA;EACA,OAAO,MAAM,UAAU,WAAW,OAAO,MAAM,MAAM,GAAG;EACxD;EACA,MAAM;EACP;;AAGH,SAAgB,wBAAwB,OAG7B;AACT,KAAI,MAAM,SAAS,KACjB,QAAO;CAET,MAAM,WAAW,MAAM,cAAc,MAAM;AAC3C,KAAI,CAAC,SACH,OAAM,oBAAoB,eAAe;AAE3C,QAAO,WAAW,SAAS,CAAC,OAAO,SAAS,CAAC,OAAO,MAAM;;AAG5D,SAAS,mBAAmB,MAA4B;AACtD,KAAI,SAAS,QAAQ,OAAO,SAAS,SACnC,QAAO;AAGT,KAAI,SAAS,QAAQ,QAAQ,MAAM;EACjC,MAAM,WAAW,SAAS,OAAO,QAAQ;EACzC,MAAM,WACJ,KAGA;AAEF,MAAI,CAAC,MAAM,QAAQ,SAAS,IAAI,SAAS,WAAW,EAClD,QAAO,GAAG,SAAS;AAKrB,SAAO,GAAG,SAAS,GAFJ,mBAAmB,SAAS,CACV,IAAI,mBAAmB,CAChB,KAAK,IAAI,CAAC;;CAGpD,MAAM,IAAI;CACV,MAAM,cAAc,EAAE,UAAU,EAAE,EAAE,KAAK,MAAM,CAAC,OAAO,GAAG,EAAE,CAAC;AAC7D,QAAO,KAAK,KAAK,UAAU;EAAC,EAAE;EAAQ,EAAE;EAAU;EAAW,CAAC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.js","names":[],"sources":["../../../../src/plugins/analytics/mv/constants.ts"],"sourcesContent":["import type { MetricFilterOperatorName, MetricOrderDirection } from \"shared\";\nimport { METRIC_CONFIG_FILE } from \"../../../../../shared/src/schemas/metric-fqn\";\nimport type { MetricLane } from \"../types\";\n\n// Re-exported from the shared zod-free module (single source of truth for the\n// `definitions.json` basename) so analytics-local callers keep importing it\n// from this barrel.\nexport { METRIC_CONFIG_FILE };\n\n/** Runtime vocabulary accepted by the analytics metric-view validator. */\nexport const METRIC_FILTER_OPERATORS = [\n \"equals\",\n \"notEquals\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"contains\",\n \"notContains\",\n \"in\",\n \"notIn\",\n \"set\",\n \"notSet\",\n] as const satisfies readonly MetricFilterOperatorName[];\n\n/** Operators that require at least one value. */\nexport const LIST_VALUE_OPERATORS = new Set<MetricFilterOperatorName>([\n \"in\",\n \"notIn\",\n]);\n\n/** Operators that reject `values` entirely. */\nexport const NULL_OPERATORS = new Set<MetricFilterOperatorName>([\n \"set\",\n \"notSet\",\n]);\n\n/** Operators that emit `LIKE` / `NOT LIKE` and require a string value. */\nexport const STRING_OPERATORS = new Set<MetricFilterOperatorName>([\n \"contains\",\n \"notContains\",\n]);\n\n/** Operators that require exactly one value. */\nexport const SINGLE_VALUE_OPERATORS = new Set<MetricFilterOperatorName>([\n \"equals\",\n \"notEquals\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n ...STRING_OPERATORS,\n]);\n\n/** Runtime vocabulary accepted by the metric-view `orderBy` validator. */\nexport const METRIC_ORDER_DIRECTIONS = [\n \"ASC\",\n \"DESC\",\n] as const satisfies readonly MetricOrderDirection[];\n\n/**\n * Measure, dimension, and filter-member names are **column identifiers**: they\n * are validated by the shared {@link isValidColumnName} (rejects only control\n * characters / newlines) and backtick-quoted via {@link quoteIdentifier} at\n * every interpolation point.\n *\n * Time-grain token shape. Unlike the column identifiers above, the grain is\n * interpolated as a single-quoted `date_trunc` unit LITERAL in {@link renderDimensionClause},\n * so it keeps a narrow keyword-shaped gate — that pattern is what keeps a hostile token out\n * of the quoted-literal position.\n */\nexport const TIME_GRAIN_PATTERN = /^[a-z][a-z_]*$/;\n\n/**\n * The depth count is the number of nested `{ and }` / `{ or }` wrappers\n * encountered while descending; leaf predicates do not count toward depth.\n * Prevents stack-overflowing the recursive validator or translator.\n */\nexport const METRIC_FILTER_MAX_DEPTH = 8;\n\n/**\n * Cardinality caps on user-controlled arrays.\n * Prevents stack-overflowing the recursive validator or translator.\n */\nexport const METRIC_MEASURES_MAX = 50;\nexport const METRIC_DIMENSIONS_MAX = 20;\nexport const METRIC_FILTER_VALUES_MAX = 1000;\nexport const METRIC_LIMIT_MAX = 100_000;\n/**\n * Cap on the request's `orderBy` array. The rendered clause can exceed this,\n * since a `limit`ed query also appends the unnamed dimensions as tie-breakers —\n * the cap bounds what a caller sends, not the emitted key count.\n */\nexport const METRIC_ORDER_BY_MAX = 20;\n\n/**\n * Maximum number of children per AND/OR group node.\n * Prevents stack-overflowing the recursive validator or translator.\n */\nexport const METRIC_FILTER_GROUP_MAX = 100;\n\n/**\n * Map an entry's declared `executor` to the internal execution lane:\n * - `\"user\"` → `\"obo\"` (per-user cache, on-behalf-of)\n * - `\"app_service_principal\"` (default) → `\"sp\"` (shared cache)\n */\nexport function laneFromExecutor(\n executor: \"app_service_principal\" | \"user\",\n): MetricLane {\n return executor === \"user\" ? \"obo\" : \"sp\";\n}\n"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../../../../src/plugins/analytics/mv/constants.ts"],"sourcesContent":["import type { MetricFilterOperatorName, MetricOrderDirection } from \"shared\";\n\nimport { METRIC_CONFIG_FILE } from \"../../../../../shared/src/schemas/metric-fqn\";\nimport type { MetricLane } from \"../types\";\n\n// Re-exported from the shared zod-free module (single source of truth for the\n// `definitions.json` basename) so analytics-local callers keep importing it\n// from this barrel.\nexport { METRIC_CONFIG_FILE };\n\n/** Runtime vocabulary accepted by the analytics metric-view validator. */\nexport const METRIC_FILTER_OPERATORS = [\n \"equals\",\n \"notEquals\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n \"contains\",\n \"notContains\",\n \"in\",\n \"notIn\",\n \"set\",\n \"notSet\",\n] as const satisfies readonly MetricFilterOperatorName[];\n\n/** Operators that require at least one value. */\nexport const LIST_VALUE_OPERATORS = new Set<MetricFilterOperatorName>([\n \"in\",\n \"notIn\",\n]);\n\n/** Operators that reject `values` entirely. */\nexport const NULL_OPERATORS = new Set<MetricFilterOperatorName>([\n \"set\",\n \"notSet\",\n]);\n\n/** Operators that emit `LIKE` / `NOT LIKE` and require a string value. */\nexport const STRING_OPERATORS = new Set<MetricFilterOperatorName>([\n \"contains\",\n \"notContains\",\n]);\n\n/** Operators that require exactly one value. */\nexport const SINGLE_VALUE_OPERATORS = new Set<MetricFilterOperatorName>([\n \"equals\",\n \"notEquals\",\n \"gt\",\n \"gte\",\n \"lt\",\n \"lte\",\n ...STRING_OPERATORS,\n]);\n\n/** Runtime vocabulary accepted by the metric-view `orderBy` validator. */\nexport const METRIC_ORDER_DIRECTIONS = [\n \"ASC\",\n \"DESC\",\n] as const satisfies readonly MetricOrderDirection[];\n\n/**\n * Measure, dimension, and filter-member names are **column identifiers**: they\n * are validated by the shared {@link isValidColumnName} (rejects only control\n * characters / newlines) and backtick-quoted via {@link quoteIdentifier} at\n * every interpolation point.\n *\n * Time-grain token shape. Unlike the column identifiers above, the grain is\n * interpolated as a single-quoted `date_trunc` unit LITERAL in {@link renderDimensionClause},\n * so it keeps a narrow keyword-shaped gate — that pattern is what keeps a hostile token out\n * of the quoted-literal position.\n */\nexport const TIME_GRAIN_PATTERN = /^[a-z][a-z_]*$/;\n\n/**\n * The depth count is the number of nested `{ and }` / `{ or }` wrappers\n * encountered while descending; leaf predicates do not count toward depth.\n * Prevents stack-overflowing the recursive validator or translator.\n */\nexport const METRIC_FILTER_MAX_DEPTH = 8;\n\n/**\n * Cardinality caps on user-controlled arrays.\n * Prevents stack-overflowing the recursive validator or translator.\n */\nexport const METRIC_MEASURES_MAX = 50;\nexport const METRIC_DIMENSIONS_MAX = 20;\nexport const METRIC_FILTER_VALUES_MAX = 1000;\nexport const METRIC_LIMIT_MAX = 100_000;\n/**\n * Cap on the request's `orderBy` array. The rendered clause can exceed this,\n * since a `limit`ed query also appends the unnamed dimensions as tie-breakers —\n * the cap bounds what a caller sends, not the emitted key count.\n */\nexport const METRIC_ORDER_BY_MAX = 20;\n\n/**\n * Maximum number of children per AND/OR group node.\n * Prevents stack-overflowing the recursive validator or translator.\n */\nexport const METRIC_FILTER_GROUP_MAX = 100;\n\n/**\n * Map an entry's declared `executor` to the internal execution lane:\n * - `\"user\"` → `\"obo\"` (per-user cache, on-behalf-of)\n * - `\"app_service_principal\"` (default) → `\"sp\"` (shared cache)\n */\nexport function laneFromExecutor(\n executor: \"app_service_principal\" | \"user\",\n): MetricLane {\n return executor === \"user\" ? \"obo\" : \"sp\";\n}\n"],"mappings":";;;;AAWA,MAAa,0BAA0B;CACrC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;AAGD,MAAa,uBAAuB,IAAI,IAA8B,CACpE,MACA,QACD,CAAC;;AAGF,MAAa,iBAAiB,IAAI,IAA8B,CAC9D,OACA,SACD,CAAC;;AAGF,MAAa,mBAAmB,IAAI,IAA8B,CAChE,YACA,cACD,CAAC;;AAGF,MAAa,yBAAyB,IAAI,IAA8B;CACtE;CACA;CACA;CACA;CACA;CACA;CACA,GAAG;CACJ,CAAC;;AAGF,MAAa,0BAA0B,CACrC,OACA,OACD;;;;;;;;;;;;AAaD,MAAa,qBAAqB;;;;;;AAOlC,MAAa,0BAA0B;;;;;AAMvC,MAAa,sBAAsB;AACnC,MAAa,wBAAwB;AACrC,MAAa,2BAA2B;AACxC,MAAa,mBAAmB;;;;;;AAMhC,MAAa,sBAAsB;;;;;AAMnC,MAAa,0BAA0B;;;;;;AAOvC,SAAgB,iBACd,UACY;AACZ,QAAO,aAAa,SAAS,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"formatters.js","names":["sqlHelpers"],"sources":["../../../../src/plugins/analytics/mv/formatters.ts"],"sourcesContent":["import { type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport {\n isValidColumnName,\n isValidFqn,\n quoteFqnForSql,\n quoteIdentifier,\n} from \"../../../../../shared/src/schemas/metric-fqn\";\nimport type {\n IAnalyticsMetricRequest,\n MetricFilter,\n MetricFilterOperatorName,\n MetricPredicate,\n MetricRegistration,\n} from \"../types\";\nimport {\n METRIC_FILTER_MAX_DEPTH,\n METRIC_FILTER_OPERATORS,\n TIME_GRAIN_PATTERN,\n} from \"./constants\";\nimport type { FilterRenderState } from \"./types\";\n\nfunction quoteSafeFqn(fqn: string): string {\n if (!isValidFqn(fqn)) {\n throw new Error(\n `Refusing to build SQL: \"${fqn}\" is not a valid three-part UC FQN.`,\n );\n }\n return quoteFqnForSql(fqn);\n}\n\nexport function buildMetricSql(\n registration: MetricRegistration,\n request: IAnalyticsMetricRequest,\n): {\n statement: string;\n parameters: Record<string, SQLTypeMarker>;\n} {\n const quotedSource = quoteSafeFqn(registration.source);\n\n if (request.measures.length === 0) {\n throw new Error(\"buildMetricSql requires at least one measure.\");\n }\n\n for (const m of request.measures) {\n if (!isValidColumnName(m)) {\n throw new Error(\n `Refusing to build SQL: measure \"${m}\" is not a valid identifier.`,\n );\n }\n }\n\n const dimensions = request.dimensions ?? [];\n for (const d of dimensions) {\n if (!isValidColumnName(d)) {\n throw new Error(\n `Refusing to build SQL: dimension \"${d}\" is not a valid identifier.`,\n );\n }\n }\n\n const measureClauses = [...request.measures]\n .sort()\n .map((m) => `MEASURE(${quoteIdentifier(m)}) AS ${quoteIdentifier(m)}`);\n\n const sortedDimensions = [...dimensions].sort();\n const dimensionClauses = sortedDimensions.map((d) =>\n renderDimensionClause(d, request.timeGrain, request.timeDimension),\n );\n\n const selectList = [...measureClauses, ...dimensionClauses].join(\", \");\n const groupByClause = dimensions.length > 0 ? \" GROUP BY ALL\" : \"\";\n const orderByClause = renderOrderByClause(request, sortedDimensions);\n\n const limitClause =\n typeof request.limit === \"number\" && request.limit > 0\n ? ` LIMIT ${Math.floor(request.limit)}`\n : \"\";\n\n const parameters: Record<string, SQLTypeMarker> = {};\n let whereClause = \"\";\n if (request.filter !== undefined) {\n const fragment = renderFilter(request.filter, parameters, {\n counter: 0,\n depth: 0,\n });\n if (fragment !== null && fragment.length > 0) {\n whereClause = ` WHERE ${fragment}`;\n }\n }\n\n const statement = `SELECT ${selectList} FROM ${quotedSource}${whereClause}${groupByClause}${orderByClause}${limitClause}`;\n return { statement, parameters };\n}\n\nfunction renderFilter(\n node: MetricFilter,\n params: Record<string, SQLTypeMarker>,\n state: FilterRenderState,\n): string | null {\n if (node === null || typeof node !== \"object\") {\n throw new Error(\n \"Refusing to build SQL: filter node must be an object Predicate or { and } / { or } group.\",\n );\n }\n\n if (\"and\" in node || \"or\" in node) {\n const groupKey = \"and\" in node ? \"and\" : \"or\";\n if (state.depth + 1 > METRIC_FILTER_MAX_DEPTH) {\n throw new Error(\n `Refusing to build SQL: filter AND/OR nesting exceeds the maximum depth of ${METRIC_FILTER_MAX_DEPTH}.`,\n );\n }\n\n const children = (\n node as { and?: ReadonlyArray<MetricFilter> } & {\n or?: ReadonlyArray<MetricFilter>;\n }\n )[groupKey];\n\n if (!Array.isArray(children) || children.length === 0) {\n // Empty group → the Boolean identity element for the operator, so the\n // result is correct in ANY position (including nested inside the other\n // operator). An empty OR is vacuously false (`1 = 0`); an empty AND is\n // vacuously true (`1 = 1`). Returning `null` for empty-AND (dropped by the\n // parent) would only be correct at the top level — nested in an OR it\n // would silently vanish and turn `TRUE OR P` into `P`, under-returning\n // rows. (The validator rejects empty groups outright; this is the\n // defense-in-depth fallback and must be semantically correct too.)\n return groupKey === \"or\" ? \"1 = 0\" : \"1 = 1\";\n }\n\n const sortedChildren = sortFilterChildren(children);\n\n const fragments: string[] = [];\n const childState: FilterRenderState = {\n counter: state.counter,\n depth: state.depth + 1,\n };\n for (const child of sortedChildren) {\n const rendered = renderFilter(child, params, childState);\n if (rendered != null && rendered.length > 0) {\n fragments.push(rendered);\n }\n }\n state.counter = childState.counter;\n\n if (fragments.length === 0) return null;\n if (fragments.length === 1) return fragments[0];\n const joiner = groupKey === \"and\" ? \" AND \" : \" OR \";\n return `(${fragments.join(joiner)})`;\n }\n\n const predicate = node as MetricPredicate;\n\n if (!isValidColumnName(predicate.member)) {\n throw new Error(\n `Refusing to build SQL: filter member \"${predicate.member}\" is not a valid identifier.`,\n );\n }\n if (\n !METRIC_FILTER_OPERATORS.includes(\n predicate.operator as MetricFilterOperatorName,\n )\n ) {\n throw new Error(\n `Refusing to build SQL: unknown filter operator \"${predicate.operator}\".`,\n );\n }\n\n return renderPredicate(predicate, params, state);\n}\n\nexport function sortFilterChildren(\n children: ReadonlyArray<MetricFilter>,\n): MetricFilter[] {\n const indexed = children.map((child, idx) => {\n let key: string;\n let isPredicate: boolean;\n if (\n child !== null &&\n typeof child === \"object\" &&\n !(\"and\" in child) &&\n !(\"or\" in child)\n ) {\n const p = child as MetricPredicate;\n key = JSON.stringify([p.member, p.operator]);\n isPredicate = true;\n } else {\n key = \"\";\n isPredicate = false;\n }\n return { child, idx, key, isPredicate };\n });\n\n indexed.sort((a, b) => {\n if (a.isPredicate && !b.isPredicate) return -1;\n if (!a.isPredicate && b.isPredicate) return 1;\n if (a.isPredicate && b.isPredicate) {\n if (a.key < b.key) return -1;\n if (a.key > b.key) return 1;\n }\n return a.idx - b.idx;\n });\n\n return indexed.map((entry) => entry.child);\n}\n\nfunction renderPredicate(\n predicate: MetricPredicate,\n params: Record<string, SQLTypeMarker>,\n state: FilterRenderState,\n): string {\n const col = quoteIdentifier(predicate.member);\n const op = predicate.operator;\n const values = predicate.values ?? [];\n\n switch (op) {\n case \"equals\":\n return `${col} = ${bindValue(values[0], params, state)}`;\n case \"notEquals\":\n return `${col} <> ${bindValue(values[0], params, state)}`;\n case \"gt\":\n return `${col} > ${bindValue(values[0], params, state)}`;\n case \"gte\":\n return `${col} >= ${bindValue(values[0], params, state)}`;\n case \"lt\":\n return `${col} < ${bindValue(values[0], params, state)}`;\n case \"lte\":\n return `${col} <= ${bindValue(values[0], params, state)}`;\n case \"in\": {\n const placeholders = values.map((v) => bindValue(v, params, state));\n return `${col} IN (${placeholders.join(\", \")})`;\n }\n case \"notIn\": {\n const placeholders = values.map((v) => bindValue(v, params, state));\n return `${col} NOT IN (${placeholders.join(\", \")})`;\n }\n case \"contains\": {\n const raw = values[0];\n if (typeof raw !== \"string\") {\n throw new Error(\n `Refusing to build SQL: filter operator \"contains\" requires a string value (got ${typeof raw}).`,\n );\n }\n return `${col} LIKE ${bindLikeValue(raw, params, state)}`;\n }\n case \"notContains\": {\n const raw = values[0];\n if (typeof raw !== \"string\") {\n throw new Error(\n `Refusing to build SQL: filter operator \"notContains\" requires a string value (got ${typeof raw}).`,\n );\n }\n return `${col} NOT LIKE ${bindLikeValue(raw, params, state)}`;\n }\n case \"set\":\n return `${col} IS NOT NULL`;\n case \"notSet\":\n return `${col} IS NULL`;\n default: {\n const _exhaustive: never = op;\n throw new Error(\n `Refusing to build SQL: unhandled filter operator \"${_exhaustive as string}\".`,\n );\n }\n }\n}\n\nfunction bindValue(\n value: string | number | undefined,\n params: Record<string, SQLTypeMarker>,\n state: FilterRenderState,\n): string {\n if (value === undefined) {\n throw new Error(\n \"Refusing to build SQL: filter predicate is missing a required value.\",\n );\n }\n const name = `f_${state.counter}`;\n state.counter += 1;\n if (typeof value === \"number\") {\n params[name] = sqlHelpers.number(value);\n } else if (typeof value === \"string\") {\n params[name] = sqlHelpers.string(value);\n } else {\n throw new Error(\n `Refusing to build SQL: filter value must be a string or number (got ${typeof value}).`,\n );\n }\n return `:${name}`;\n}\n\nfunction bindLikeValue(\n value: string,\n params: Record<string, SQLTypeMarker>,\n state: FilterRenderState,\n): string {\n const name = `f_${state.counter}`;\n state.counter += 1;\n params[name] = sqlHelpers.string(`%${value}%`);\n return `:${name}`;\n}\n\nfunction renderDimensionClause(\n dim: string,\n timeGrain: string | undefined,\n timeDimension: string | undefined,\n): string {\n if (timeGrain != null && dim === timeDimension) {\n if (!isValidColumnName(dim)) {\n throw new Error(\n `Refusing to build SQL: timeDimension \"${dim}\" is not a valid identifier.`,\n );\n }\n if (!TIME_GRAIN_PATTERN.test(timeGrain)) {\n throw new Error(\n `Refusing to build SQL: timeGrain \"${timeGrain}\" is not a valid grain token.`,\n );\n }\n const quoted = quoteIdentifier(dim);\n return `date_trunc('${timeGrain}', ${quoted}) AS ${quoted}`;\n }\n return quoteIdentifier(dim);\n}\n\nfunction renderOrderByClause(\n request: IAnalyticsMetricRequest,\n sortedDimensions: string[],\n): string {\n const keyList: string[] = [];\n\n if (request.orderBy != null && request.orderBy.length > 0) {\n for (const entry of request.orderBy) {\n if (!isValidColumnName(entry.field)) {\n throw new Error(\n `Refusing to build SQL: orderBy field \"${entry.field}\" is not a valid identifier.`,\n );\n }\n const direction = entry.direction === \"DESC\" ? \" DESC\" : \"\";\n keyList.push(`${quoteIdentifier(entry.field)}${direction}`);\n }\n }\n\n // Tie-breaker completion: when limit is set, append all dimensions not\n // already named in orderBy. Under GROUP BY ALL the full dimension tuple is\n // unique per row, so ordering by all dimensions gives a TOTAL order. A\n // partial ordering still leaves ties, and ties + LIMIT = non-determinism.\n if (typeof request.limit === \"number\" && request.limit > 0) {\n const orderByFields = new Set(request.orderBy?.map((e) => e.field) ?? []);\n for (const dim of sortedDimensions) {\n if (!orderByFields.has(dim)) {\n keyList.push(quoteIdentifier(dim));\n }\n }\n }\n\n // Return empty string when there is nothing to order by. This covers:\n // no orderBy + no limit; and no orderBy + limit but zero dimensions\n // (a pure aggregate returns exactly one row, ordering is pointless).\n if (keyList.length === 0) {\n return \"\";\n }\n\n return ` ORDER BY ${keyList.join(\", \")}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"formatters.js","names":["sqlHelpers"],"sources":["../../../../src/plugins/analytics/mv/formatters.ts"],"sourcesContent":["import { type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\n\nimport {\n isValidColumnName,\n isValidFqn,\n quoteFqnForSql,\n quoteIdentifier,\n} from \"../../../../../shared/src/schemas/metric-fqn\";\nimport type {\n IAnalyticsMetricRequest,\n MetricFilter,\n MetricFilterOperatorName,\n MetricPredicate,\n MetricRegistration,\n} from \"../types\";\nimport {\n METRIC_FILTER_MAX_DEPTH,\n METRIC_FILTER_OPERATORS,\n TIME_GRAIN_PATTERN,\n} from \"./constants\";\nimport type { FilterRenderState } from \"./types\";\n\nfunction quoteSafeFqn(fqn: string): string {\n if (!isValidFqn(fqn)) {\n throw new Error(\n `Refusing to build SQL: \"${fqn}\" is not a valid three-part UC FQN.`,\n );\n }\n return quoteFqnForSql(fqn);\n}\n\nexport function buildMetricSql(\n registration: MetricRegistration,\n request: IAnalyticsMetricRequest,\n): {\n statement: string;\n parameters: Record<string, SQLTypeMarker>;\n} {\n const quotedSource = quoteSafeFqn(registration.source);\n\n if (request.measures.length === 0) {\n throw new Error(\"buildMetricSql requires at least one measure.\");\n }\n\n for (const m of request.measures) {\n if (!isValidColumnName(m)) {\n throw new Error(\n `Refusing to build SQL: measure \"${m}\" is not a valid identifier.`,\n );\n }\n }\n\n const dimensions = request.dimensions ?? [];\n for (const d of dimensions) {\n if (!isValidColumnName(d)) {\n throw new Error(\n `Refusing to build SQL: dimension \"${d}\" is not a valid identifier.`,\n );\n }\n }\n\n const measureClauses = [...request.measures]\n .sort()\n .map((m) => `MEASURE(${quoteIdentifier(m)}) AS ${quoteIdentifier(m)}`);\n\n const sortedDimensions = [...dimensions].sort();\n const dimensionClauses = sortedDimensions.map((d) =>\n renderDimensionClause(d, request.timeGrain, request.timeDimension),\n );\n\n const selectList = [...measureClauses, ...dimensionClauses].join(\", \");\n const groupByClause = dimensions.length > 0 ? \" GROUP BY ALL\" : \"\";\n const orderByClause = renderOrderByClause(request, sortedDimensions);\n\n const limitClause =\n typeof request.limit === \"number\" && request.limit > 0\n ? ` LIMIT ${Math.floor(request.limit)}`\n : \"\";\n\n const parameters: Record<string, SQLTypeMarker> = {};\n let whereClause = \"\";\n if (request.filter !== undefined) {\n const fragment = renderFilter(request.filter, parameters, {\n counter: 0,\n depth: 0,\n });\n if (fragment !== null && fragment.length > 0) {\n whereClause = ` WHERE ${fragment}`;\n }\n }\n\n const statement = `SELECT ${selectList} FROM ${quotedSource}${whereClause}${groupByClause}${orderByClause}${limitClause}`;\n return { statement, parameters };\n}\n\nfunction renderFilter(\n node: MetricFilter,\n params: Record<string, SQLTypeMarker>,\n state: FilterRenderState,\n): string | null {\n if (node === null || typeof node !== \"object\") {\n throw new Error(\n \"Refusing to build SQL: filter node must be an object Predicate or { and } / { or } group.\",\n );\n }\n\n if (\"and\" in node || \"or\" in node) {\n const groupKey = \"and\" in node ? \"and\" : \"or\";\n if (state.depth + 1 > METRIC_FILTER_MAX_DEPTH) {\n throw new Error(\n `Refusing to build SQL: filter AND/OR nesting exceeds the maximum depth of ${METRIC_FILTER_MAX_DEPTH}.`,\n );\n }\n\n const children = (\n node as { and?: ReadonlyArray<MetricFilter> } & {\n or?: ReadonlyArray<MetricFilter>;\n }\n )[groupKey];\n\n if (!Array.isArray(children) || children.length === 0) {\n // Empty group → the Boolean identity element for the operator, so the\n // result is correct in ANY position (including nested inside the other\n // operator). An empty OR is vacuously false (`1 = 0`); an empty AND is\n // vacuously true (`1 = 1`). Returning `null` for empty-AND (dropped by the\n // parent) would only be correct at the top level — nested in an OR it\n // would silently vanish and turn `TRUE OR P` into `P`, under-returning\n // rows. (The validator rejects empty groups outright; this is the\n // defense-in-depth fallback and must be semantically correct too.)\n return groupKey === \"or\" ? \"1 = 0\" : \"1 = 1\";\n }\n\n const sortedChildren = sortFilterChildren(children);\n\n const fragments: string[] = [];\n const childState: FilterRenderState = {\n counter: state.counter,\n depth: state.depth + 1,\n };\n for (const child of sortedChildren) {\n const rendered = renderFilter(child, params, childState);\n if (rendered != null && rendered.length > 0) {\n fragments.push(rendered);\n }\n }\n state.counter = childState.counter;\n\n if (fragments.length === 0) return null;\n if (fragments.length === 1) return fragments[0];\n const joiner = groupKey === \"and\" ? \" AND \" : \" OR \";\n return `(${fragments.join(joiner)})`;\n }\n\n const predicate = node as MetricPredicate;\n\n if (!isValidColumnName(predicate.member)) {\n throw new Error(\n `Refusing to build SQL: filter member \"${predicate.member}\" is not a valid identifier.`,\n );\n }\n if (\n !METRIC_FILTER_OPERATORS.includes(\n predicate.operator as MetricFilterOperatorName,\n )\n ) {\n throw new Error(\n `Refusing to build SQL: unknown filter operator \"${predicate.operator}\".`,\n );\n }\n\n return renderPredicate(predicate, params, state);\n}\n\nexport function sortFilterChildren(\n children: ReadonlyArray<MetricFilter>,\n): MetricFilter[] {\n const indexed = children.map((child, idx) => {\n let key: string;\n let isPredicate: boolean;\n if (\n child !== null &&\n typeof child === \"object\" &&\n !(\"and\" in child) &&\n !(\"or\" in child)\n ) {\n const p = child as MetricPredicate;\n key = JSON.stringify([p.member, p.operator]);\n isPredicate = true;\n } else {\n key = \"\";\n isPredicate = false;\n }\n return { child, idx, key, isPredicate };\n });\n\n indexed.sort((a, b) => {\n if (a.isPredicate && !b.isPredicate) return -1;\n if (!a.isPredicate && b.isPredicate) return 1;\n if (a.isPredicate && b.isPredicate) {\n if (a.key < b.key) return -1;\n if (a.key > b.key) return 1;\n }\n return a.idx - b.idx;\n });\n\n return indexed.map((entry) => entry.child);\n}\n\nfunction renderPredicate(\n predicate: MetricPredicate,\n params: Record<string, SQLTypeMarker>,\n state: FilterRenderState,\n): string {\n const col = quoteIdentifier(predicate.member);\n const op = predicate.operator;\n const values = predicate.values ?? [];\n\n switch (op) {\n case \"equals\":\n return `${col} = ${bindValue(values[0], params, state)}`;\n case \"notEquals\":\n return `${col} <> ${bindValue(values[0], params, state)}`;\n case \"gt\":\n return `${col} > ${bindValue(values[0], params, state)}`;\n case \"gte\":\n return `${col} >= ${bindValue(values[0], params, state)}`;\n case \"lt\":\n return `${col} < ${bindValue(values[0], params, state)}`;\n case \"lte\":\n return `${col} <= ${bindValue(values[0], params, state)}`;\n case \"in\": {\n const placeholders = values.map((v) => bindValue(v, params, state));\n return `${col} IN (${placeholders.join(\", \")})`;\n }\n case \"notIn\": {\n const placeholders = values.map((v) => bindValue(v, params, state));\n return `${col} NOT IN (${placeholders.join(\", \")})`;\n }\n case \"contains\": {\n const raw = values[0];\n if (typeof raw !== \"string\") {\n throw new Error(\n `Refusing to build SQL: filter operator \"contains\" requires a string value (got ${typeof raw}).`,\n );\n }\n return `${col} LIKE ${bindLikeValue(raw, params, state)}`;\n }\n case \"notContains\": {\n const raw = values[0];\n if (typeof raw !== \"string\") {\n throw new Error(\n `Refusing to build SQL: filter operator \"notContains\" requires a string value (got ${typeof raw}).`,\n );\n }\n return `${col} NOT LIKE ${bindLikeValue(raw, params, state)}`;\n }\n case \"set\":\n return `${col} IS NOT NULL`;\n case \"notSet\":\n return `${col} IS NULL`;\n default: {\n const _exhaustive: never = op;\n throw new Error(\n `Refusing to build SQL: unhandled filter operator \"${_exhaustive as string}\".`,\n );\n }\n }\n}\n\nfunction bindValue(\n value: string | number | undefined,\n params: Record<string, SQLTypeMarker>,\n state: FilterRenderState,\n): string {\n if (value === undefined) {\n throw new Error(\n \"Refusing to build SQL: filter predicate is missing a required value.\",\n );\n }\n const name = `f_${state.counter}`;\n state.counter += 1;\n if (typeof value === \"number\") {\n params[name] = sqlHelpers.number(value);\n } else if (typeof value === \"string\") {\n params[name] = sqlHelpers.string(value);\n } else {\n throw new Error(\n `Refusing to build SQL: filter value must be a string or number (got ${typeof value}).`,\n );\n }\n return `:${name}`;\n}\n\nfunction bindLikeValue(\n value: string,\n params: Record<string, SQLTypeMarker>,\n state: FilterRenderState,\n): string {\n const name = `f_${state.counter}`;\n state.counter += 1;\n params[name] = sqlHelpers.string(`%${value}%`);\n return `:${name}`;\n}\n\nfunction renderDimensionClause(\n dim: string,\n timeGrain: string | undefined,\n timeDimension: string | undefined,\n): string {\n if (timeGrain != null && dim === timeDimension) {\n if (!isValidColumnName(dim)) {\n throw new Error(\n `Refusing to build SQL: timeDimension \"${dim}\" is not a valid identifier.`,\n );\n }\n if (!TIME_GRAIN_PATTERN.test(timeGrain)) {\n throw new Error(\n `Refusing to build SQL: timeGrain \"${timeGrain}\" is not a valid grain token.`,\n );\n }\n const quoted = quoteIdentifier(dim);\n return `date_trunc('${timeGrain}', ${quoted}) AS ${quoted}`;\n }\n return quoteIdentifier(dim);\n}\n\nfunction renderOrderByClause(\n request: IAnalyticsMetricRequest,\n sortedDimensions: string[],\n): string {\n const keyList: string[] = [];\n\n if (request.orderBy != null && request.orderBy.length > 0) {\n for (const entry of request.orderBy) {\n if (!isValidColumnName(entry.field)) {\n throw new Error(\n `Refusing to build SQL: orderBy field \"${entry.field}\" is not a valid identifier.`,\n );\n }\n const direction = entry.direction === \"DESC\" ? \" DESC\" : \"\";\n keyList.push(`${quoteIdentifier(entry.field)}${direction}`);\n }\n }\n\n // Tie-breaker completion: when limit is set, append all dimensions not\n // already named in orderBy. Under GROUP BY ALL the full dimension tuple is\n // unique per row, so ordering by all dimensions gives a TOTAL order. A\n // partial ordering still leaves ties, and ties + LIMIT = non-determinism.\n if (typeof request.limit === \"number\" && request.limit > 0) {\n const orderByFields = new Set(request.orderBy?.map((e) => e.field) ?? []);\n for (const dim of sortedDimensions) {\n if (!orderByFields.has(dim)) {\n keyList.push(quoteIdentifier(dim));\n }\n }\n }\n\n // Return empty string when there is nothing to order by. This covers:\n // no orderBy + no limit; and no orderBy + limit but zero dimensions\n // (a pure aggregate returns exactly one row, ordering is pointless).\n if (keyList.length === 0) {\n return \"\";\n }\n\n return ` ORDER BY ${keyList.join(\", \")}`;\n}\n"],"mappings":";;;;;AAsBA,SAAS,aAAa,KAAqB;AACzC,KAAI,CAAC,WAAW,IAAI,CAClB,OAAM,IAAI,MACR,2BAA2B,IAAI,qCAChC;AAEH,QAAO,eAAe,IAAI;;AAG5B,SAAgB,eACd,cACA,SAIA;CACA,MAAM,eAAe,aAAa,aAAa,OAAO;AAEtD,KAAI,QAAQ,SAAS,WAAW,EAC9B,OAAM,IAAI,MAAM,gDAAgD;AAGlE,MAAK,MAAM,KAAK,QAAQ,SACtB,KAAI,CAAC,kBAAkB,EAAE,CACvB,OAAM,IAAI,MACR,mCAAmC,EAAE,8BACtC;CAIL,MAAM,aAAa,QAAQ,cAAc,EAAE;AAC3C,MAAK,MAAM,KAAK,WACd,KAAI,CAAC,kBAAkB,EAAE,CACvB,OAAM,IAAI,MACR,qCAAqC,EAAE,8BACxC;CAIL,MAAM,iBAAiB,CAAC,GAAG,QAAQ,SAAS,CACzC,MAAM,CACN,KAAK,MAAM,WAAW,gBAAgB,EAAE,CAAC,OAAO,gBAAgB,EAAE,GAAG;CAExE,MAAM,mBAAmB,CAAC,GAAG,WAAW,CAAC,MAAM;CAC/C,MAAM,mBAAmB,iBAAiB,KAAK,MAC7C,sBAAsB,GAAG,QAAQ,WAAW,QAAQ,cAAc,CACnE;CAED,MAAM,aAAa,CAAC,GAAG,gBAAgB,GAAG,iBAAiB,CAAC,KAAK,KAAK;CACtE,MAAM,gBAAgB,WAAW,SAAS,IAAI,kBAAkB;CAChE,MAAM,gBAAgB,oBAAoB,SAAS,iBAAiB;CAEpE,MAAM,cACJ,OAAO,QAAQ,UAAU,YAAY,QAAQ,QAAQ,IACjD,UAAU,KAAK,MAAM,QAAQ,MAAM,KACnC;CAEN,MAAM,aAA4C,EAAE;CACpD,IAAI,cAAc;AAClB,KAAI,QAAQ,WAAW,QAAW;EAChC,MAAM,WAAW,aAAa,QAAQ,QAAQ,YAAY;GACxD,SAAS;GACT,OAAO;GACR,CAAC;AACF,MAAI,aAAa,QAAQ,SAAS,SAAS,EACzC,eAAc,UAAU;;AAK5B,QAAO;EAAE,WADS,UAAU,WAAW,QAAQ,eAAe,cAAc,gBAAgB,gBAAgB;EACxF;EAAY;;AAGlC,SAAS,aACP,MACA,QACA,OACe;AACf,KAAI,SAAS,QAAQ,OAAO,SAAS,SACnC,OAAM,IAAI,MACR,4FACD;AAGH,KAAI,SAAS,QAAQ,QAAQ,MAAM;EACjC,MAAM,WAAW,SAAS,OAAO,QAAQ;AACzC,MAAI,MAAM,QAAQ,IAAI,wBACpB,OAAM,IAAI,MACR,6EAA6E,wBAAwB,GACtG;EAGH,MAAM,WACJ,KAGA;AAEF,MAAI,CAAC,MAAM,QAAQ,SAAS,IAAI,SAAS,WAAW,EASlD,QAAO,aAAa,OAAO,UAAU;EAGvC,MAAM,iBAAiB,mBAAmB,SAAS;EAEnD,MAAM,YAAsB,EAAE;EAC9B,MAAM,aAAgC;GACpC,SAAS,MAAM;GACf,OAAO,MAAM,QAAQ;GACtB;AACD,OAAK,MAAM,SAAS,gBAAgB;GAClC,MAAM,WAAW,aAAa,OAAO,QAAQ,WAAW;AACxD,OAAI,YAAY,QAAQ,SAAS,SAAS,EACxC,WAAU,KAAK,SAAS;;AAG5B,QAAM,UAAU,WAAW;AAE3B,MAAI,UAAU,WAAW,EAAG,QAAO;AACnC,MAAI,UAAU,WAAW,EAAG,QAAO,UAAU;EAC7C,MAAM,SAAS,aAAa,QAAQ,UAAU;AAC9C,SAAO,IAAI,UAAU,KAAK,OAAO,CAAC;;CAGpC,MAAM,YAAY;AAElB,KAAI,CAAC,kBAAkB,UAAU,OAAO,CACtC,OAAM,IAAI,MACR,yCAAyC,UAAU,OAAO,8BAC3D;AAEH,KACE,CAAC,wBAAwB,SACvB,UAAU,SACX,CAED,OAAM,IAAI,MACR,mDAAmD,UAAU,SAAS,IACvE;AAGH,QAAO,gBAAgB,WAAW,QAAQ,MAAM;;AAGlD,SAAgB,mBACd,UACgB;CAChB,MAAM,UAAU,SAAS,KAAK,OAAO,QAAQ;EAC3C,IAAI;EACJ,IAAI;AACJ,MACE,UAAU,QACV,OAAO,UAAU,YACjB,EAAE,SAAS,UACX,EAAE,QAAQ,QACV;GACA,MAAM,IAAI;AACV,SAAM,KAAK,UAAU,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC;AAC5C,iBAAc;SACT;AACL,SAAM;AACN,iBAAc;;AAEhB,SAAO;GAAE;GAAO;GAAK;GAAK;GAAa;GACvC;AAEF,SAAQ,MAAM,GAAG,MAAM;AACrB,MAAI,EAAE,eAAe,CAAC,EAAE,YAAa,QAAO;AAC5C,MAAI,CAAC,EAAE,eAAe,EAAE,YAAa,QAAO;AAC5C,MAAI,EAAE,eAAe,EAAE,aAAa;AAClC,OAAI,EAAE,MAAM,EAAE,IAAK,QAAO;AAC1B,OAAI,EAAE,MAAM,EAAE,IAAK,QAAO;;AAE5B,SAAO,EAAE,MAAM,EAAE;GACjB;AAEF,QAAO,QAAQ,KAAK,UAAU,MAAM,MAAM;;AAG5C,SAAS,gBACP,WACA,QACA,OACQ;CACR,MAAM,MAAM,gBAAgB,UAAU,OAAO;CAC7C,MAAM,KAAK,UAAU;CACrB,MAAM,SAAS,UAAU,UAAU,EAAE;AAErC,SAAQ,IAAR;EACE,KAAK,SACH,QAAO,GAAG,IAAI,KAAK,UAAU,OAAO,IAAI,QAAQ,MAAM;EACxD,KAAK,YACH,QAAO,GAAG,IAAI,MAAM,UAAU,OAAO,IAAI,QAAQ,MAAM;EACzD,KAAK,KACH,QAAO,GAAG,IAAI,KAAK,UAAU,OAAO,IAAI,QAAQ,MAAM;EACxD,KAAK,MACH,QAAO,GAAG,IAAI,MAAM,UAAU,OAAO,IAAI,QAAQ,MAAM;EACzD,KAAK,KACH,QAAO,GAAG,IAAI,KAAK,UAAU,OAAO,IAAI,QAAQ,MAAM;EACxD,KAAK,MACH,QAAO,GAAG,IAAI,MAAM,UAAU,OAAO,IAAI,QAAQ,MAAM;EACzD,KAAK,KAEH,QAAO,GAAG,IAAI,OADO,OAAO,KAAK,MAAM,UAAU,GAAG,QAAQ,MAAM,CAAC,CACjC,KAAK,KAAK,CAAC;EAE/C,KAAK,QAEH,QAAO,GAAG,IAAI,WADO,OAAO,KAAK,MAAM,UAAU,GAAG,QAAQ,MAAM,CAAC,CAC7B,KAAK,KAAK,CAAC;EAEnD,KAAK,YAAY;GACf,MAAM,MAAM,OAAO;AACnB,OAAI,OAAO,QAAQ,SACjB,OAAM,IAAI,MACR,kFAAkF,OAAO,IAAI,IAC9F;AAEH,UAAO,GAAG,IAAI,QAAQ,cAAc,KAAK,QAAQ,MAAM;;EAEzD,KAAK,eAAe;GAClB,MAAM,MAAM,OAAO;AACnB,OAAI,OAAO,QAAQ,SACjB,OAAM,IAAI,MACR,qFAAqF,OAAO,IAAI,IACjG;AAEH,UAAO,GAAG,IAAI,YAAY,cAAc,KAAK,QAAQ,MAAM;;EAE7D,KAAK,MACH,QAAO,GAAG,IAAI;EAChB,KAAK,SACH,QAAO,GAAG,IAAI;EAChB,SAAS;GACP,MAAM,cAAqB;AAC3B,SAAM,IAAI,MACR,qDAAqD,YAAsB,IAC5E;;;;AAKP,SAAS,UACP,OACA,QACA,OACQ;AACR,KAAI,UAAU,OACZ,OAAM,IAAI,MACR,uEACD;CAEH,MAAM,OAAO,KAAK,MAAM;AACxB,OAAM,WAAW;AACjB,KAAI,OAAO,UAAU,SACnB,QAAO,QAAQA,IAAW,OAAO,MAAM;UAC9B,OAAO,UAAU,SAC1B,QAAO,QAAQA,IAAW,OAAO,MAAM;KAEvC,OAAM,IAAI,MACR,uEAAuE,OAAO,MAAM,IACrF;AAEH,QAAO,IAAI;;AAGb,SAAS,cACP,OACA,QACA,OACQ;CACR,MAAM,OAAO,KAAK,MAAM;AACxB,OAAM,WAAW;AACjB,QAAO,QAAQA,IAAW,OAAO,IAAI,MAAM,GAAG;AAC9C,QAAO,IAAI;;AAGb,SAAS,sBACP,KACA,WACA,eACQ;AACR,KAAI,aAAa,QAAQ,QAAQ,eAAe;AAC9C,MAAI,CAAC,kBAAkB,IAAI,CACzB,OAAM,IAAI,MACR,yCAAyC,IAAI,8BAC9C;AAEH,MAAI,CAAC,mBAAmB,KAAK,UAAU,CACrC,OAAM,IAAI,MACR,qCAAqC,UAAU,+BAChD;EAEH,MAAM,SAAS,gBAAgB,IAAI;AACnC,SAAO,eAAe,UAAU,KAAK,OAAO,OAAO;;AAErD,QAAO,gBAAgB,IAAI;;AAG7B,SAAS,oBACP,SACA,kBACQ;CACR,MAAM,UAAoB,EAAE;AAE5B,KAAI,QAAQ,WAAW,QAAQ,QAAQ,QAAQ,SAAS,EACtD,MAAK,MAAM,SAAS,QAAQ,SAAS;AACnC,MAAI,CAAC,kBAAkB,MAAM,MAAM,CACjC,OAAM,IAAI,MACR,yCAAyC,MAAM,MAAM,8BACtD;EAEH,MAAM,YAAY,MAAM,cAAc,SAAS,UAAU;AACzD,UAAQ,KAAK,GAAG,gBAAgB,MAAM,MAAM,GAAG,YAAY;;AAQ/D,KAAI,OAAO,QAAQ,UAAU,YAAY,QAAQ,QAAQ,GAAG;EAC1D,MAAM,gBAAgB,IAAI,IAAI,QAAQ,SAAS,KAAK,MAAM,EAAE,MAAM,IAAI,EAAE,CAAC;AACzE,OAAK,MAAM,OAAO,iBAChB,KAAI,CAAC,cAAc,IAAI,IAAI,CACzB,SAAQ,KAAK,gBAAgB,IAAI,CAAC;;AAQxC,KAAI,QAAQ,WAAW,EACrB,QAAO;AAGT,QAAO,aAAa,QAAQ,KAAK,KAAK"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.js","names":[],"sources":["../../../../src/plugins/analytics/mv/metadata.ts"],"sourcesContent":["import type { MetricViewColumnDisplay, MetricViewsMetadata } from \"shared\";\nimport {\n METRIC_METADATA_BUNDLE_VERSION,\n METRIC_METADATA_FILE,\n metricMetadataBundleSchema,\n} from \"../../../../../shared/src/schemas/metric-metadata-bundle\";\nimport type { AppManager, DevFileReader, RequestLike } from \"../../../app\";\nimport { createLogger } from \"../../../logging/logger\";\n\nconst logger = createLogger(\"analytics:metric-views\");\n\nexport { METRIC_METADATA_FILE };\n\n/**\n * Parsed-bundle cache, keyed on the raw file contents.\n */\nlet parsedBundleCache: { raw: string; metadata: MetricViewsMetadata } | null =\n null;\n\n/**\n * The runtime twin of the generated `MetricRegistry` augmentation: the type\n * generator emits both from one `DESCRIBE` pass, this side being JSON so the\n * plugin can discover it instead of the app importing and injecting it.\n *\n * Read through {@link AppManager.readMetricViewsConfig} for the same reasons as\n * {@link loadMetricRegistry} — dev-tunnel awareness and the traversal guard.\n */\nexport async function loadMetricMetadata(\n app: AppManager,\n req?: RequestLike,\n devFileReader?: DevFileReader,\n): Promise<MetricViewsMetadata | undefined> {\n let raw: string | null;\n try {\n raw = await app.readMetricViewsConfig(\n METRIC_METADATA_FILE,\n req,\n devFileReader,\n );\n } catch (err) {\n logger.warn(\n \"Failed to read %s: %s\",\n METRIC_METADATA_FILE,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n\n // Absent file (ENOENT / dev-tunnel not-found) or a rejected traversal path.\n // Types were never generated, or generation predates the bundle → dormant.\n if (raw === null) {\n return undefined;\n }\n\n if (parsedBundleCache?.raw === raw) {\n return parsedBundleCache.metadata;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n logger.warn(\n \"Ignoring malformed %s: %s\",\n METRIC_METADATA_FILE,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n\n const result = metricMetadataBundleSchema.safeParse(parsed);\n if (!result.success) {\n logger.warn(\n \"Ignoring invalid %s: %s\",\n METRIC_METADATA_FILE,\n result.error.issues\n .map((i) => `${i.path.join(\".\")}: ${i.message}`)\n .join(\"; \"),\n );\n return undefined;\n }\n\n if (result.data.version !== METRIC_METADATA_BUNDLE_VERSION) {\n logger.warn(\n \"Ignoring %s written for bundle version %d (this runtime reads version %d) — regenerate types\",\n METRIC_METADATA_FILE,\n result.data.version,\n METRIC_METADATA_BUNDLE_VERSION,\n );\n return undefined;\n }\n\n // Null-prototype map for the same reason as the registry: a metric key that\n // collides with an inherited `Object.prototype` member must not resolve to a\n // truthy non-entry at the lookup site in `selectMetricMetadata`.\n const metadata: MetricViewsMetadata = Object.create(null);\n for (const [key, entry] of Object.entries(result.data.metricViews)) {\n metadata[key] = entry;\n }\n\n parsedBundleCache = { raw, metadata };\n return metadata;\n}\n\n/**\n * Flatten the resolved {@link MetricViewsMetadata} for `key` into a single\n * `Record<column, meta>` covering only the requested measures and dimensions,\n * so the client can label/format just the columns it queried.\n *\n * Returns `undefined` rather than an empty object when there is nothing to\n * stamp, so the caller can omit the field and keep the message byte-identical\n * to a plain `/query` result.\n */\nexport function selectMetricMetadata(\n all: MetricViewsMetadata | undefined,\n key: string,\n measures: string[],\n dimensions: string[] | undefined,\n): Record<string, MetricViewColumnDisplay> | undefined {\n if (!all || !Object.hasOwn(all, key)) {\n return undefined;\n }\n\n const entry = all[key];\n const slice: Record<string, MetricViewColumnDisplay> = {};\n\n for (const measure of measures) {\n if (Object.hasOwn(entry.measures, measure)) {\n slice[measure] = entry.measures[measure];\n }\n }\n for (const dimension of dimensions ?? []) {\n if (Object.hasOwn(entry.dimensions, dimension)) {\n slice[dimension] = entry.dimensions[dimension];\n }\n }\n\n return Object.keys(slice).length > 0 ? slice : undefined;\n}\n"],"mappings":";;;;
|
|
1
|
+
{"version":3,"file":"metadata.js","names":[],"sources":["../../../../src/plugins/analytics/mv/metadata.ts"],"sourcesContent":["import type { MetricViewColumnDisplay, MetricViewsMetadata } from \"shared\";\n\nimport {\n METRIC_METADATA_BUNDLE_VERSION,\n METRIC_METADATA_FILE,\n metricMetadataBundleSchema,\n} from \"../../../../../shared/src/schemas/metric-metadata-bundle\";\nimport type { AppManager, DevFileReader, RequestLike } from \"../../../app\";\nimport { createLogger } from \"../../../logging/logger\";\n\nconst logger = createLogger(\"analytics:metric-views\");\n\nexport { METRIC_METADATA_FILE };\n\n/**\n * Parsed-bundle cache, keyed on the raw file contents.\n */\nlet parsedBundleCache: { raw: string; metadata: MetricViewsMetadata } | null =\n null;\n\n/**\n * The runtime twin of the generated `MetricRegistry` augmentation: the type\n * generator emits both from one `DESCRIBE` pass, this side being JSON so the\n * plugin can discover it instead of the app importing and injecting it.\n *\n * Read through {@link AppManager.readMetricViewsConfig} for the same reasons as\n * {@link loadMetricRegistry} — dev-tunnel awareness and the traversal guard.\n */\nexport async function loadMetricMetadata(\n app: AppManager,\n req?: RequestLike,\n devFileReader?: DevFileReader,\n): Promise<MetricViewsMetadata | undefined> {\n let raw: string | null;\n try {\n raw = await app.readMetricViewsConfig(\n METRIC_METADATA_FILE,\n req,\n devFileReader,\n );\n } catch (err) {\n logger.warn(\n \"Failed to read %s: %s\",\n METRIC_METADATA_FILE,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n\n // Absent file (ENOENT / dev-tunnel not-found) or a rejected traversal path.\n // Types were never generated, or generation predates the bundle → dormant.\n if (raw === null) {\n return undefined;\n }\n\n if (parsedBundleCache?.raw === raw) {\n return parsedBundleCache.metadata;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n logger.warn(\n \"Ignoring malformed %s: %s\",\n METRIC_METADATA_FILE,\n err instanceof Error ? err.message : String(err),\n );\n return undefined;\n }\n\n const result = metricMetadataBundleSchema.safeParse(parsed);\n if (!result.success) {\n logger.warn(\n \"Ignoring invalid %s: %s\",\n METRIC_METADATA_FILE,\n result.error.issues\n .map((i) => `${i.path.join(\".\")}: ${i.message}`)\n .join(\"; \"),\n );\n return undefined;\n }\n\n if (result.data.version !== METRIC_METADATA_BUNDLE_VERSION) {\n logger.warn(\n \"Ignoring %s written for bundle version %d (this runtime reads version %d) — regenerate types\",\n METRIC_METADATA_FILE,\n result.data.version,\n METRIC_METADATA_BUNDLE_VERSION,\n );\n return undefined;\n }\n\n // Null-prototype map for the same reason as the registry: a metric key that\n // collides with an inherited `Object.prototype` member must not resolve to a\n // truthy non-entry at the lookup site in `selectMetricMetadata`.\n const metadata: MetricViewsMetadata = Object.create(null);\n for (const [key, entry] of Object.entries(result.data.metricViews)) {\n metadata[key] = entry;\n }\n\n parsedBundleCache = { raw, metadata };\n return metadata;\n}\n\n/**\n * Flatten the resolved {@link MetricViewsMetadata} for `key` into a single\n * `Record<column, meta>` covering only the requested measures and dimensions,\n * so the client can label/format just the columns it queried.\n *\n * Returns `undefined` rather than an empty object when there is nothing to\n * stamp, so the caller can omit the field and keep the message byte-identical\n * to a plain `/query` result.\n */\nexport function selectMetricMetadata(\n all: MetricViewsMetadata | undefined,\n key: string,\n measures: string[],\n dimensions: string[] | undefined,\n): Record<string, MetricViewColumnDisplay> | undefined {\n if (!all || !Object.hasOwn(all, key)) {\n return undefined;\n }\n\n const entry = all[key];\n const slice: Record<string, MetricViewColumnDisplay> = {};\n\n for (const measure of measures) {\n if (Object.hasOwn(entry.measures, measure)) {\n slice[measure] = entry.measures[measure];\n }\n }\n for (const dimension of dimensions ?? []) {\n if (Object.hasOwn(entry.dimensions, dimension)) {\n slice[dimension] = entry.dimensions[dimension];\n }\n }\n\n return Object.keys(slice).length > 0 ? slice : undefined;\n}\n"],"mappings":";;;;AAUA,MAAM,SAAS,aAAa,yBAAyB;;;;AAOrD,IAAI,oBACF;;;;;;;;;AAUF,eAAsB,mBACpB,KACA,KACA,eAC0C;CAC1C,IAAI;AACJ,KAAI;AACF,QAAM,MAAM,IAAI,sBACd,sBACA,KACA,cACD;UACM,KAAK;AACZ,SAAO,KACL,yBACA,sBACA,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CACjD;AACD;;AAKF,KAAI,QAAQ,KACV;AAGF,KAAI,mBAAmB,QAAQ,IAC7B,QAAO,kBAAkB;CAG3B,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;UACjB,KAAK;AACZ,SAAO,KACL,6BACA,sBACA,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,CACjD;AACD;;CAGF,MAAM,SAAS,2BAA2B,UAAU,OAAO;AAC3D,KAAI,CAAC,OAAO,SAAS;AACnB,SAAO,KACL,2BACA,sBACA,OAAO,MAAM,OACV,KAAK,MAAM,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC,IAAI,EAAE,UAAU,CAC/C,KAAK,KAAK,CACd;AACD;;AAGF,KAAI,OAAO,KAAK,YAAY,gCAAgC;AAC1D,SAAO,KACL,gGACA,sBACA,OAAO,KAAK,SACZ,+BACD;AACD;;CAMF,MAAM,WAAgC,OAAO,OAAO,KAAK;AACzD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,KAAK,YAAY,CAChE,UAAS,OAAO;AAGlB,qBAAoB;EAAE;EAAK;EAAU;AACrC,QAAO;;;;;;;;;;;AAYT,SAAgB,qBACd,KACA,KACA,UACA,YACqD;AACrD,KAAI,CAAC,OAAO,CAAC,OAAO,OAAO,KAAK,IAAI,CAClC;CAGF,MAAM,QAAQ,IAAI;CAClB,MAAM,QAAiD,EAAE;AAEzD,MAAK,MAAM,WAAW,SACpB,KAAI,OAAO,OAAO,MAAM,UAAU,QAAQ,CACxC,OAAM,WAAW,MAAM,SAAS;AAGpC,MAAK,MAAM,aAAa,cAAc,EAAE,CACtC,KAAI,OAAO,OAAO,MAAM,YAAY,UAAU,CAC5C,OAAM,aAAa,MAAM,WAAW;AAIxC,QAAO,OAAO,KAAK,MAAM,CAAC,SAAS,IAAI,QAAQ"}
|