@databricks/appkit 0.57.0 → 0.59.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/dist/appkit/package.js +1 -1
- package/dist/plugins/ai-search/ai-search.d.ts +21 -0
- package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
- package/dist/plugins/ai-search/ai-search.js +74 -13
- package/dist/plugins/ai-search/ai-search.js.map +1 -1
- package/dist/plugins/ai-search/defaults.js +4 -1
- package/dist/plugins/ai-search/defaults.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +9 -3
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/metric.js +2 -1
- package/dist/plugins/analytics/mv/cache.js +2 -0
- package/dist/plugins/analytics/mv/cache.js.map +1 -1
- package/dist/plugins/analytics/mv/constants.js +40 -27
- package/dist/plugins/analytics/mv/constants.js.map +1 -1
- package/dist/plugins/analytics/mv/formatters.js +18 -2
- package/dist/plugins/analytics/mv/formatters.js.map +1 -1
- package/dist/plugins/analytics/mv/index.js +2 -1
- package/dist/plugins/analytics/mv/metadata.js +52 -10
- package/dist/plugins/analytics/mv/metadata.js.map +1 -1
- package/dist/plugins/analytics/mv/schemas.js +31 -1
- package/dist/plugins/analytics/mv/schemas.js.map +1 -1
- package/dist/plugins/analytics/types.d.ts +6 -5
- package/dist/plugins/analytics/types.d.ts.map +1 -1
- package/dist/plugins/analytics/types.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +2 -2
- package/dist/shared/src/schemas/metric-metadata-bundle.js +24 -0
- package/dist/shared/src/schemas/metric-metadata-bundle.js.map +1 -0
- package/dist/shared/src/schemas/metric-source.js +1 -1
- package/dist/type-generator/index.js +10 -6
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/mv-registry/render-types.js +35 -52
- package/dist/type-generator/mv-registry/render-types.js.map +1 -1
- package/dist/type-generator/vite-plugin.js +0 -1
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/docs/development/type-generation.md +7 -6
- package/docs/plugins/ai-search.md +10 -0
- package/docs/plugins/analytics.md +287 -12
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
package/dist/appkit/package.js
CHANGED
|
@@ -52,7 +52,28 @@ declare class AiSearchPlugin extends Plugin<IAiSearchConfig> {
|
|
|
52
52
|
private _resolveOr404;
|
|
53
53
|
/** Send an execution result as JSON, or its error status/message. */
|
|
54
54
|
private _sendResult;
|
|
55
|
+
/**
|
|
56
|
+
* Resolve request-vs-index defaults for the result-determining fields.
|
|
57
|
+
* Shared by `_prepareQuery` (the payload) and `_cacheKeyFor` (the key) so
|
|
58
|
+
* the two can't drift.
|
|
59
|
+
*/
|
|
60
|
+
private _resolveQueryParams;
|
|
55
61
|
private _prepareQuery;
|
|
62
|
+
/**
|
|
63
|
+
* Cache key for a query: every input that changes the VS result, resolved
|
|
64
|
+
* via `_resolveQueryParams` so the key matches the payload (post-allowlist
|
|
65
|
+
* `columns`, not the raw request). `queryVector` is hashed (vectors are
|
|
66
|
+
* large); the key uses `queryText`, not the derived embedding, since the
|
|
67
|
+
* embedding is a function of `queryText` and hasn't run at key-build time.
|
|
68
|
+
* `columns`/`filters` are order-normalized so equivalent requests share an
|
|
69
|
+
* entry.
|
|
70
|
+
*/
|
|
71
|
+
private _cacheKeyFor;
|
|
72
|
+
private _hashVector;
|
|
73
|
+
/** Execute settings with the per-call cache key folded in. */
|
|
74
|
+
private _executeSettings;
|
|
75
|
+
/** `JSON.stringify` with object keys sorted recursively; array order kept. */
|
|
76
|
+
private _stableStringify;
|
|
56
77
|
private _resolveReranker;
|
|
57
78
|
private _parseResponse;
|
|
58
79
|
private _handleError;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-search.d.ts","names":[],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"ai-search.d.ts","names":[],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"mappings":";;;;;;;;;cA+Ba,cAAA,SAAuB,MAAA,CAAO,eAAA;EAAA,OAClC,QAAA,EAAuB,cAAA;EAAA,iBAEb,WAAA;EAAA,UAEC,MAAA,EAAQ,eAAA;EAAA,QAElB,SAAA;cAEI,MAAA,EAAQ,eAAA;EATM;;;;EAAA,QAyBlB,eAAA;EAKF,KAAA,CAAA,GAAS,OAAA;EAAA;;;;;;EAAA,QAgCD,oBAAA;EAAA,QAoCN,4BAAA;EAkBR,YAAA,CAAa,MAAA,EAAQ,UAAA;EAgMH;;;;EAtDlB,YAAA,CAAA;IAAkB,OAAA,EAAS,YAAA;EAAA;EA9PO;;;;;;;EAgR5B,KAAA,WAAgB,MAAA,oBAA0B,MAAA,kBAAA,CAC9C,KAAA,UACA,OAAA,EAAS,aAAA,GACR,OAAA,CAAQ,cAAA,CAAe,CAAA;EAiCpB,QAAA,CAAA,GAAY,OAAA;EAIlB,OAAA,CAAA;sBAxCsB,MAAA,oBAAuB,MAAA,mBAAA,KAAA,UAC9B,OAAA,EACJ,aAAA,KACR,OAAA,CAAQ,cAAA,CAAe,CAAA;EAAA;EAAA,QA2ClB,aAAA;EArTI;EAAA,QAgUJ,aAAA;EA3SF;EAAA,QA2TE,WAAA;EA3RM;;;;;EAAA,QA8SN,mBAAA;EAAA,QAcM,aAAA;EA5Ha;;;;;;;;;EAAA,QA0KnB,YAAA;EAAA,QAsBA,WAAA;EA1IF;EAAA,QA+IE,gBAAA;EA3IR;EAAA,QA4JQ,gBAAA;EAAA,QAUA,gBAAA;EAAA,QAiBA,cAAA;EAAA,QA2BA,YAAA;AAAA;AAAA,cAcG,QAAA,EAAQ,QAAA,QAAA,cAAA,EAAA,eAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
|
-
import { getWorkspaceClient } from "../../context/execution-context.js";
|
|
2
|
+
import { getCurrentUserId, getWorkspaceClient } from "../../context/execution-context.js";
|
|
3
3
|
import { formatWarningBanner } from "../../utils/banner.js";
|
|
4
4
|
import "../../context/index.js";
|
|
5
5
|
import { Plugin } from "../../plugin/plugin.js";
|
|
@@ -8,6 +8,7 @@ import "../../plugin/index.js";
|
|
|
8
8
|
import { AiSearchConnector } from "../../connectors/ai-search/client.js";
|
|
9
9
|
import { aiSearchDefaults } from "./defaults.js";
|
|
10
10
|
import manifest_default from "./manifest.js";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
11
12
|
|
|
12
13
|
//#region src/plugins/ai-search/ai-search.ts
|
|
13
14
|
const logger = createLogger("ai-search");
|
|
@@ -93,8 +94,10 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
95
96
|
const { columns: _clientColumns, ...safeBody } = body;
|
|
96
|
-
const
|
|
97
|
+
const isAsUser = indexConfig.auth === "on-behalf-of-user";
|
|
98
|
+
const plugin = isAsUser ? this.asUser(req) : this;
|
|
97
99
|
const queryType = safeBody.queryType ?? indexConfig.queryType ?? "hybrid";
|
|
100
|
+
const executorKey = isAsUser ? this.resolveUserId(req) : "global";
|
|
98
101
|
try {
|
|
99
102
|
const result = await plugin.execute(async (signal) => {
|
|
100
103
|
const prepared = await this._prepareQuery(safeBody, indexConfig);
|
|
@@ -102,7 +105,7 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
102
105
|
indexName: indexConfig.indexName,
|
|
103
106
|
...prepared
|
|
104
107
|
}, signal);
|
|
105
|
-
},
|
|
108
|
+
}, this._executeSettings(safeBody, indexConfig, executorKey));
|
|
106
109
|
this._sendResult(res, result, queryType);
|
|
107
110
|
} catch (error) {
|
|
108
111
|
this._handleError(res, error, "Query failed");
|
|
@@ -190,13 +193,16 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
190
193
|
async query(alias, request) {
|
|
191
194
|
const indexConfig = this._resolveIndex(alias);
|
|
192
195
|
if (!indexConfig) throw new Error(`No index configured with alias "${alias}"`);
|
|
193
|
-
const
|
|
194
|
-
const result = await this.execute(async (signal) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
const { queryType } = this._resolveQueryParams(request, indexConfig);
|
|
197
|
+
const result = await this.execute(async (signal) => {
|
|
198
|
+
const prepared = await this._prepareQuery(request, indexConfig);
|
|
199
|
+
return this.connector.query(getWorkspaceClient(), {
|
|
200
|
+
indexName: indexConfig.indexName,
|
|
201
|
+
...prepared
|
|
202
|
+
}, signal);
|
|
203
|
+
}, this._executeSettings(request, indexConfig, getCurrentUserId()));
|
|
198
204
|
if (!result.ok) throw new Error(`Vector search query failed for index "${alias}": ${result.message}`);
|
|
199
|
-
return this._parseResponse(result.data,
|
|
205
|
+
return this._parseResponse(result.data, queryType);
|
|
200
206
|
}
|
|
201
207
|
async shutdown() {}
|
|
202
208
|
exports() {
|
|
@@ -235,8 +241,23 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
235
241
|
}
|
|
236
242
|
res.json(this._parseResponse(result.data, queryType));
|
|
237
243
|
}
|
|
238
|
-
|
|
244
|
+
/**
|
|
245
|
+
* Resolve request-vs-index defaults for the result-determining fields.
|
|
246
|
+
* Shared by `_prepareQuery` (the payload) and `_cacheKeyFor` (the key) so
|
|
247
|
+
* the two can't drift.
|
|
248
|
+
*/
|
|
249
|
+
_resolveQueryParams(request, indexConfig) {
|
|
239
250
|
const queryType = request.queryType ?? indexConfig.queryType ?? "hybrid";
|
|
251
|
+
const columns = request.columns ?? indexConfig.columns ?? [];
|
|
252
|
+
return {
|
|
253
|
+
queryType,
|
|
254
|
+
columns,
|
|
255
|
+
numResults: request.numResults ?? indexConfig.numResults ?? 20,
|
|
256
|
+
reranker: this._resolveReranker(request.reranker, indexConfig, columns)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async _prepareQuery(request, indexConfig) {
|
|
260
|
+
const { queryType, columns, numResults, reranker } = this._resolveQueryParams(request, indexConfig);
|
|
240
261
|
let queryText = request.queryText;
|
|
241
262
|
let queryVector = request.queryVector;
|
|
242
263
|
if (indexConfig.embeddingFn && queryText && !queryVector && queryType !== "full_text") try {
|
|
@@ -245,17 +266,57 @@ var AiSearchPlugin = class extends Plugin {
|
|
|
245
266
|
} catch (error) {
|
|
246
267
|
throw new Error(`Embedding generation failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
247
268
|
}
|
|
248
|
-
const columns = request.columns ?? indexConfig.columns ?? [];
|
|
249
269
|
return {
|
|
250
270
|
queryText,
|
|
251
271
|
queryVector,
|
|
252
272
|
queryType,
|
|
253
273
|
columns,
|
|
254
|
-
numResults
|
|
274
|
+
numResults,
|
|
255
275
|
filters: request.filters,
|
|
256
|
-
reranker
|
|
276
|
+
reranker
|
|
257
277
|
};
|
|
258
278
|
}
|
|
279
|
+
/**
|
|
280
|
+
* Cache key for a query: every input that changes the VS result, resolved
|
|
281
|
+
* via `_resolveQueryParams` so the key matches the payload (post-allowlist
|
|
282
|
+
* `columns`, not the raw request). `queryVector` is hashed (vectors are
|
|
283
|
+
* large); the key uses `queryText`, not the derived embedding, since the
|
|
284
|
+
* embedding is a function of `queryText` and hasn't run at key-build time.
|
|
285
|
+
* `columns`/`filters` are order-normalized so equivalent requests share an
|
|
286
|
+
* entry.
|
|
287
|
+
*/
|
|
288
|
+
_cacheKeyFor(request, indexConfig, executorKey) {
|
|
289
|
+
const { queryType, columns, numResults, reranker } = this._resolveQueryParams(request, indexConfig);
|
|
290
|
+
return [
|
|
291
|
+
"ai-search:query",
|
|
292
|
+
indexConfig.indexName,
|
|
293
|
+
request.queryText ?? "",
|
|
294
|
+
request.queryVector ? this._hashVector(request.queryVector) : "",
|
|
295
|
+
queryType,
|
|
296
|
+
numResults,
|
|
297
|
+
JSON.stringify([...columns].sort()),
|
|
298
|
+
this._stableStringify(request.filters ?? null),
|
|
299
|
+
String(!!reranker),
|
|
300
|
+
executorKey
|
|
301
|
+
];
|
|
302
|
+
}
|
|
303
|
+
_hashVector(vector) {
|
|
304
|
+
return createHash("sha256").update(JSON.stringify(vector)).digest("hex");
|
|
305
|
+
}
|
|
306
|
+
/** Execute settings with the per-call cache key folded in. */
|
|
307
|
+
_executeSettings(request, indexConfig, executorKey) {
|
|
308
|
+
return { default: {
|
|
309
|
+
...aiSearchDefaults,
|
|
310
|
+
cache: {
|
|
311
|
+
...aiSearchDefaults.cache,
|
|
312
|
+
cacheKey: this._cacheKeyFor(request, indexConfig, executorKey)
|
|
313
|
+
}
|
|
314
|
+
} };
|
|
315
|
+
}
|
|
316
|
+
/** `JSON.stringify` with object keys sorted recursively; array order kept. */
|
|
317
|
+
_stableStringify(value) {
|
|
318
|
+
return JSON.stringify(value, (_key, val) => val && typeof val === "object" && !Array.isArray(val) ? Object.fromEntries(Object.entries(val).sort(([a], [b]) => a.localeCompare(b))) : val);
|
|
319
|
+
}
|
|
259
320
|
_resolveReranker(requestReranker, indexConfig, columns) {
|
|
260
321
|
if (!(requestReranker ?? indexConfig.reranker)) return void 0;
|
|
261
322
|
if (typeof indexConfig.reranker === "object") return indexConfig.reranker;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai-search.js","names":["manifest"],"sources":["../../../src/plugins/ai-search/ai-search.ts"],"sourcesContent":["import type express from \"express\";\nimport type { IAppRouter, PluginExecutionSettings } from \"shared\";\nimport { AiSearchConnector } from \"../../connectors/ai-search/client\";\nimport type {\n VsQueryParams,\n VsRawResponse,\n} from \"../../connectors/ai-search/types\";\nimport { 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 plugin =\n indexConfig.auth === \"on-behalf-of-user\" ? this.asUser(req) : this;\n const queryType =\n safeBody.queryType ?? indexConfig.queryType ?? \"hybrid\";\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(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 }, querySettings);\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 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 const prepared = await this._prepareQuery(request, indexConfig);\n\n const result = await this.execute(\n async (signal) =>\n this.connector.query(\n getWorkspaceClient(),\n { indexName: indexConfig.indexName, ...prepared },\n signal,\n ),\n querySettings,\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, prepared.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 private async _prepareQuery(\n request: SearchRequest,\n indexConfig: IndexConfig,\n ): Promise<Omit<VsQueryParams, \"indexName\">> {\n const queryType = request.queryType ?? indexConfig.queryType ?? \"hybrid\";\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 const columns = request.columns ?? indexConfig.columns ?? [];\n return {\n queryText,\n queryVector,\n queryType,\n columns,\n numResults: request.numResults ?? indexConfig.numResults ?? 20,\n filters: request.filters,\n reranker: this._resolveReranker(request.reranker, indexConfig, columns),\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\";\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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/plugins/ai-search/defaults.ts"],"sourcesContent":["import type { PluginExecuteConfig } from \"shared\";\n\nexport const aiSearchDefaults: PluginExecuteConfig = {\n cache: { enabled:
|
|
1
|
+
{"version":3,"file":"defaults.js","names":[],"sources":["../../../src/plugins/ai-search/defaults.ts"],"sourcesContent":["import type { PluginExecuteConfig } from \"shared\";\n\nexport const aiSearchDefaults: PluginExecuteConfig = {\n // Short TTL: VS results shift as the index resyncs, so cache only long\n // enough to absorb bursts without serving stale results.\n cache: { enabled: true, ttl: 60 },\n retry: { enabled: true, initialDelay: 1000, attempts: 3 },\n timeout: 30_000,\n};\n"],"mappings":";AAEA,MAAa,mBAAwC;CAGnD,OAAO;EAAE,SAAS;EAAM,KAAK;EAAI;CACjC,OAAO;EAAE,SAAS;EAAM,cAAc;EAAM,UAAU;EAAG;CACzD,SAAS;CACV"}
|
|
@@ -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":";;;;;;;;;;;;;;cA6Ga,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"}
|
|
@@ -17,7 +17,8 @@ import { queryDefaults } from "./defaults.js";
|
|
|
17
17
|
import manifest_default from "./manifest.js";
|
|
18
18
|
import { buildMetricSql } from "./mv/formatters.js";
|
|
19
19
|
import { composeMetricCacheKey, deriveMetricExecutorKey } from "./mv/cache.js";
|
|
20
|
-
import {
|
|
20
|
+
import { METRIC_METADATA_FILE } from "../../shared/src/schemas/metric-metadata-bundle.js";
|
|
21
|
+
import { loadMetricMetadata, selectMetricMetadata } from "./mv/metadata.js";
|
|
21
22
|
import { loadMetricRegistry } from "./mv/registry.js";
|
|
22
23
|
import { normalizeAnalyticsFormat } from "./types.js";
|
|
23
24
|
import { validateMetricRequest } from "./mv/schemas.js";
|
|
@@ -337,7 +338,11 @@ var AnalyticsPlugin = class extends Plugin {
|
|
|
337
338
|
}
|
|
338
339
|
throw err;
|
|
339
340
|
}
|
|
340
|
-
const
|
|
341
|
+
const injectedMetadata = this.config.metricViewsMetadata;
|
|
342
|
+
const allMetadata = injectedMetadata ?? await loadMetricMetadata(this.app, req, this.devFileReader);
|
|
343
|
+
if (allMetadata !== void 0 && !Object.hasOwn(allMetadata, key)) if (injectedMetadata !== void 0) logger.warn(req, "No display metadata for metric key %s in the injected metricViewsMetadata", key);
|
|
344
|
+
else logger.warn(req, "No display metadata for metric key %s — regenerate types to refresh %s", key, METRIC_METADATA_FILE);
|
|
345
|
+
const metadata = selectMetricMetadata(allMetadata, key, request.measures, request.dimensions);
|
|
341
346
|
const cacheConfig = {
|
|
342
347
|
...queryDefaults.cache,
|
|
343
348
|
cacheKey: composeMetricCacheKey({
|
|
@@ -350,7 +355,8 @@ var AnalyticsPlugin = class extends Plugin {
|
|
|
350
355
|
filter: request.filter,
|
|
351
356
|
format: "JSON_ARRAY",
|
|
352
357
|
executorKey,
|
|
353
|
-
limit: request.limit
|
|
358
|
+
limit: request.limit,
|
|
359
|
+
orderBy: request.orderBy
|
|
354
360
|
})
|
|
355
361
|
};
|
|
356
362
|
const sqlConfig = {
|