@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.
Files changed (40) hide show
  1. package/dist/appkit/package.js +1 -1
  2. package/dist/plugins/ai-search/ai-search.d.ts +21 -0
  3. package/dist/plugins/ai-search/ai-search.d.ts.map +1 -1
  4. package/dist/plugins/ai-search/ai-search.js +74 -13
  5. package/dist/plugins/ai-search/ai-search.js.map +1 -1
  6. package/dist/plugins/ai-search/defaults.js +4 -1
  7. package/dist/plugins/ai-search/defaults.js.map +1 -1
  8. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  9. package/dist/plugins/analytics/analytics.js +9 -3
  10. package/dist/plugins/analytics/analytics.js.map +1 -1
  11. package/dist/plugins/analytics/metric.js +2 -1
  12. package/dist/plugins/analytics/mv/cache.js +2 -0
  13. package/dist/plugins/analytics/mv/cache.js.map +1 -1
  14. package/dist/plugins/analytics/mv/constants.js +40 -27
  15. package/dist/plugins/analytics/mv/constants.js.map +1 -1
  16. package/dist/plugins/analytics/mv/formatters.js +18 -2
  17. package/dist/plugins/analytics/mv/formatters.js.map +1 -1
  18. package/dist/plugins/analytics/mv/index.js +2 -1
  19. package/dist/plugins/analytics/mv/metadata.js +52 -10
  20. package/dist/plugins/analytics/mv/metadata.js.map +1 -1
  21. package/dist/plugins/analytics/mv/schemas.js +31 -1
  22. package/dist/plugins/analytics/mv/schemas.js.map +1 -1
  23. package/dist/plugins/analytics/types.d.ts +6 -5
  24. package/dist/plugins/analytics/types.d.ts.map +1 -1
  25. package/dist/plugins/analytics/types.js.map +1 -1
  26. package/dist/shared/src/schemas/manifest.d.ts +2 -2
  27. package/dist/shared/src/schemas/metric-metadata-bundle.js +24 -0
  28. package/dist/shared/src/schemas/metric-metadata-bundle.js.map +1 -0
  29. package/dist/shared/src/schemas/metric-source.js +1 -1
  30. package/dist/type-generator/index.js +10 -6
  31. package/dist/type-generator/index.js.map +1 -1
  32. package/dist/type-generator/mv-registry/render-types.js +35 -52
  33. package/dist/type-generator/mv-registry/render-types.js.map +1 -1
  34. package/dist/type-generator/vite-plugin.js +0 -1
  35. package/dist/type-generator/vite-plugin.js.map +1 -1
  36. package/docs/development/type-generation.md +7 -6
  37. package/docs/plugins/ai-search.md +10 -0
  38. package/docs/plugins/analytics.md +287 -12
  39. package/package.json +1 -1
  40. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n createWorkspaceClient,\n type WorkspaceClient,\n} from \"../workspace-client\";\nimport {\n isRevivableMetricCacheEntry,\n loadCache,\n type MetricCacheEntry,\n metricCacheHash,\n saveCache,\n} from \"./cache\";\nimport {\n classifyBlockingFailure,\n classifyEnvironmentalCause,\n getErrorDiagnostic,\n isConnectivityError,\n} from \"./errors\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"./migration\";\nimport { readMetricConfig, resolveMetricConfig } from \"./mv-registry/config\";\nimport { createWorkspaceDescribeFetcher } from \"./mv-registry/describe\";\nimport { generateMetricTypeDeclarations } from \"./mv-registry/render-types\";\nimport { emptyMetricSchema, syncMetrics } from \"./mv-registry/sync\";\nimport type {\n DescribeFetcher,\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n} from \"./mv-registry/types\";\nimport { decidePreflight, type PreflightMode } from \"./preflight\";\nimport { generateQueriesFromDescribe } from \"./query-registry\";\nimport { generateServingTypes as generateServingTypesImpl } from \"./serving/generator\";\nimport type { QueryFatalError, QuerySchema, QuerySyntaxError } from \"./types\";\nimport {\n getWarehouseState,\n startWarehouse,\n type WarehouseState,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\ndotenv.config();\n\nconst logger = createLogger(\"type-generator\");\n\n/**\n * Upper bound (~5 min) on how long the Metric Views path's `blocking`-mode preflight\n * waits for a warehouse to reach RUNNING. Mirrors the query path's (unexported)\n * `PREFLIGHT_WAIT_MAX_MS` in query-registry.ts.\n */\nconst MV_PREFLIGHT_WAIT_MAX_MS = 300_000;\n\nfunction determineWarningMessage(\n cause: \"auth\" | \"unreachable\" | \"unavailable\",\n warehouseId: string,\n): string {\n const causeLabel =\n cause === \"auth\"\n ? \"auth blocked\"\n : cause === \"unreachable\"\n ? \"warehouse unreachable\"\n : \"warehouse unavailable\";\n // Use a stable prefix for greppability and CI log matching.\n return `AppKit typegen: using committed types — warehouse ${warehouseId} ${causeLabel}; please check warehouse status and retry`;\n}\n\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\n}\n\nfunction isQueryDegraded(schema: QuerySchema): boolean {\n return schema.degraded === true;\n}\n\nfunction hasAnyDegradedMetrics(schemas: MetricSchema[]): boolean {\n return schemas.some((s) => s.degraded === true);\n}\n\nfunction formatFailureRows(\n label: string,\n queries: TypegenFailure[],\n color: (value: string) => string,\n) {\n if (queries.length === 0) return [];\n\n // Group by message so a shared failure — e.g. a warehouse-level fatal that\n // hits every query identically — prints once instead of repeating per row.\n const byMessage = new Map<string, string[]>();\n for (const { name, message } of queries) {\n const names = byMessage.get(message);\n if (names) names.push(name);\n else byMessage.set(message, [name]);\n }\n\n const maxNameLen = Math.max(...queries.map((query) => query.name.length));\n const tag = color(label.padEnd(7));\n const rows: string[] = [];\n for (const [message, names] of byMessage) {\n if (names.length === 1) {\n rows.push(\n ` ${tag} ${pc.bold(names[0].padEnd(maxNameLen))} ${pc.dim(message)}`,\n );\n continue;\n }\n // Shared message → print it once, then list the affected query names.\n rows.push(\n ` ${tag} ${pc.dim(message)} ${pc.dim(`(${names.length} ${plural(names.length, \"query\", \"queries\")})`)}`,\n );\n rows.push(\n ` ${names.map((name) => pc.bold(name)).join(pc.dim(\", \"))}`,\n );\n }\n return rows;\n}\n\nfunction formatTypegenFailureMessage(options: {\n syntaxErrors: QuerySyntaxError[];\n fatalErrors?: QueryFatalError[];\n warehouseId?: string;\n title: string;\n causes: string[];\n nextStep: string;\n}) {\n const { syntaxErrors, fatalErrors = [], warehouseId, title } = options;\n const total = syntaxErrors.length + fatalErrors.length;\n const separator = pc.dim(\"─\".repeat(60));\n const warehouse = warehouseId\n ? ` against ${pc.dim(`warehouse ${warehouseId}`)}`\n : \"\";\n\n return [\n ` ${pc.bold(pc.red(\"Type generation failed\"))}`,\n ` ${separator}`,\n ` ${title}: ${total} ${plural(total, \"query\", \"queries\")} could not be described${warehouse}.`,\n ` AppKit wrote generated types with ${pc.bold(\"result: unknown\")} for the failed ${plural(total, \"query\", \"queries\")}.`,\n \"\",\n ...formatFailureRows(\"SQL ERR\", syntaxErrors, pc.red),\n ...(syntaxErrors.length > 0 && fatalErrors.length > 0 ? [\"\"] : []),\n ...formatFailureRows(\"FATAL\", fatalErrors, pc.red),\n \"\",\n ` ${pc.bold(\"Common causes\")}`,\n ...options.causes.map((cause) => ` - ${cause}`),\n \"\",\n ` ${pc.bold(\"Next step\")}`,\n ` ${options.nextStep}`,\n ].join(\"\\n\");\n}\n\n/**\n * Thrown when one or more queries fail `DESCRIBE QUERY` against a *reachable*\n * warehouse — i.e. genuine SQL errors (bad table, syntax, incompatible type),\n * as opposed to a connectivity failure (warehouse unreachable), which degrades\n * silently. Whether this is fatal is the caller's decision: the Vite plugin and\n * CLI fail the build in production and warn-only in development.\n */\nexport class TypegenSyntaxError extends Error {\n readonly queries: QuerySyntaxError[];\n readonly fatalQueries: QueryFatalError[];\n\n constructor(\n queries: QuerySyntaxError[],\n warehouseId?: string,\n fatalQueries: QueryFatalError[] = [],\n ) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: queries,\n fatalErrors: fatalQueries,\n warehouseId,\n title: \"DESCRIBE QUERY failed\",\n causes: [\n \"SQL syntax errors\",\n \"missing tables or views\",\n \"warehouse format incompatibilities\",\n ],\n nextStep: warehouseId\n ? `Run each SQL ERR query directly in a Databricks SQL editor against warehouse ${pc.bold(warehouseId)}.`\n : \"Run each SQL ERR query directly in a Databricks SQL editor.\",\n }),\n );\n this.name = \"TypegenSyntaxError\";\n this.queries = queries;\n this.fatalQueries = fatalQueries;\n }\n}\n\n/**\n * Thrown when DESCRIBE QUERY could not be requested because of a non-SQL fatal\n * setup/request problem, such as missing permissions, invalid warehouse IDs, or\n * malformed SDK configuration. Like TypegenSyntaxError, this is thrown only\n * after the declaration file has been written with `result: unknown` entries.\n */\nexport class TypegenFatalError extends Error {\n readonly queries: QueryFatalError[];\n\n constructor(queries: QueryFatalError[], warehouseId?: string) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: [],\n fatalErrors: queries,\n warehouseId,\n title: \"DESCRIBE QUERY could not be requested\",\n causes: [\n \"missing warehouse permissions\",\n \"invalid warehouse ID\",\n \"authentication failure\",\n \"SDK configuration errors\",\n ],\n nextStep: warehouseId\n ? `Verify access to warehouse ${pc.bold(warehouseId)} and rerun type generation.`\n : \"Verify warehouse access and rerun type generation.\",\n }),\n );\n this.name = \"TypegenFatalError\";\n this.queries = queries;\n }\n}\n\n/**\n * Generate type declarations for QueryRegistry\n * Create the d.ts file from the plugin routes and query schemas\n * @param querySchemas - the list of query schemas\n * @returns - the type declarations as a string\n */\nfunction generateTypeDeclarations(querySchemas: QuerySchema[] = []): string {\n const queryEntries = querySchemas\n .map(({ name, type }) => {\n const indentedType = type\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : ` ${line}`))\n .join(\"\\n\");\n return ` ${name}: ${indentedType}`;\n })\n .join(\";\\n\");\n\n const querySection = queryEntries ? `\\n${queryEntries};\\n ` : \"\";\n\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\nimport type { SQLTypeMarker, SQLStringMarker, SQLNumberMarker, SQLBooleanMarker, SQLBinaryMarker, SQLDateMarker, SQLTimestampMarker } from \"@databricks/appkit-ui/js\";\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface QueryRegistry {${querySection}}\n}\n`;\n}\n\n/**\n * Status-only probe for the metric-view gate in {@link generateFromEntryPoint}\n *\n * Uses {@link getWarehouseState} (`warehouses.get`) —\n * a read-only GET that can never start the warehouse\n *\n * Takes the lazy client *getter* so the probe also absorbs client construction failure.\n * A connectivity blip returns `undefined`, which the gate reads as transient not-running;\n * a deterministic failure (auth, bad id) is re-thrown so the gate can classify it\n * fatal rather than silently degrading.\n */\nasync function probeWarehouseState(\n getClient: () => WorkspaceClient,\n warehouseId: string,\n): Promise<WarehouseState | undefined> {\n try {\n return await getWarehouseState(getClient(), warehouseId);\n } catch (err) {\n // Connectivity blip → undefined (gate degrades, retries next pass). A\n // deterministic failure (auth, bad warehouse id, client construction) must\n // not masquerade as not-running — re-throw so the gate pins it fatal, the\n // same split the query path's preflight makes.\n if (isConnectivityError(err)) return undefined;\n throw err;\n }\n}\n\n/**\n * Entry point for generating type declarations from all imported files\n * @param options - the options for the generation\n * @param options.entryPoint - the entry point file\n * @param options.outFile - the output file\n * @param options.noCache - skip the typegen cache entirely: every query is\n * re-described, and the metric path ignores its cached schemas (every\n * configured key becomes describe-needed) and overwrites the cache's\n * `metrics` section with this pass's results.\n * @param options.mode - preflight policy (see {@link PreflightMode}), default\n * `\"non-blocking\"`. For queries, `\"non-blocking\"` never touches the\n * warehouse. For metric views it makes one status-only probe and DESCRIBEs\n * only when the warehouse is already RUNNING, otherwise emits permissive\n * degraded types immediately. `\"blocking\"` waits for / starts the warehouse\n * first, failing the build only for a deleted/deleting one.\n * @param options.metricViewsFolder - folder that holds `definitions.json`\n * (`<root>/config/metric-views`). Optional and independent of `queryFolder`:\n * metric-view types generate whenever this folder holds a config, even if the\n * app has no `config/queries`. When omitted it defaults to a sibling\n * `metric-views` directory of `queryFolder` (so query-only callers keep\n * working); when neither is given, the metric path is skipped.\n * @param options.mvOutFile - optional output file for the MetricRegistry\n * augmentation. Defaults to a sibling `metric-views.ts` file under the same\n * directory as `outFile`. Skipped entirely if `definitions.json` is absent.\n * @param options.metricFetcher - optional DescribeFetcher used by\n * {@link syncMetrics} (tests inject a mock; production lazily builds a\n * default WorkspaceClient-backed one). An injected fetcher always runs: it\n * hits no warehouse, so it bypasses both the non-blocking gate and the\n * blocking preflight.\n */\nexport async function generateFromEntryPoint(options: {\n outFile: string;\n queryFolder?: string;\n metricViewsFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n mode?: PreflightMode;\n mvOutFile?: string;\n metricFetcher?: DescribeFetcher;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n mvOutFile,\n metricFetcher,\n } = options;\n\n // Metric config lives in `config/metric-views/`, a sibling of the queries\n // folder. Prefer the explicit option; otherwise derive the sibling of\n // `queryFolder` so callers that pass only `queryFolder` keep emitting metric\n // types. Undefined when neither is given → the metric path stays dormant.\n const metricViewsFolder =\n options.metricViewsFolder ??\n (queryFolder ? path.resolve(queryFolder, \"..\", \"metric-views\") : undefined);\n const resolvedMvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n // Deterministic fatal errors only (404/400).\n let fatalErrors: QueryFatalError[] = [];\n let queryHadEnvironmentalFailure = false;\n let metricsHadEnvironmentalFailure = false;\n // Track the coarse cause of the environmental failure for the warning message.\n let environmentalCause: \"auth\" | \"unreachable\" | \"unavailable\" | undefined;\n\n if (queryFolder) {\n const result = await generateQueriesFromDescribe(queryFolder, warehouseId, {\n noCache,\n mode,\n });\n queryRegistry = result.schemas;\n syntaxErrors = result.syntaxErrors ?? [];\n fatalErrors = result.fatalErrors ?? [];\n queryHadEnvironmentalFailure = result.hadEnvironmentalFailure ?? false;\n environmentalCause =\n environmentalCause ?? result.environmentalCause ?? undefined;\n }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n // In blocking mode, never overwrite committed types with a schema explicitly\n // marked degraded. Leave the committed .d.ts as the fallback of record.\n // Non-blocking mode always writes.\n const hasAnyDegradedQuery = queryRegistry.some(isQueryDegraded);\n if (mode === \"blocking\" && hasAnyDegradedQuery) {\n // A degraded schema always participates in the committed-types gate. Keep\n // this invariant next to write suppression so a new producer cannot update\n // one decision without the other.\n queryHadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unavailable\";\n }\n const shouldWriteQueries = mode !== \"blocking\" || !hasAnyDegradedQuery;\n\n if (shouldWriteQueries) {\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n }\n\n // Metric-view types: emit whenever a metric-views folder is resolved (gated\n // on the metric config's own dir, NOT the queries folder — an app can declare\n // metric views without any `.sql` queries). `syncMetricViewsTypes` still\n // returns `noConfig` when the folder holds no `definitions.json`.\n if (metricViewsFolder) {\n let mvResult: SyncMetricViewsTypesResult;\n try {\n mvResult = await syncMetricViewsTypes({\n metricViewsFolder,\n warehouseId,\n metricOutFile: resolvedMvFile,\n cache: !noCache,\n metricFetcher,\n mode,\n // In blocking mode, never overwrite committed metric types with a\n // degraded result — including on runs that go on to throw, so a failing\n // build leaves the committed type file intact. Non-blocking always writes.\n suppressDegradedWrite: mode === \"blocking\",\n });\n } catch (configError) {\n // syncMetricViewsTypes only throws for a malformed definitions.json — re-throw as a message-only TypegenFatalError.\n throw new TypegenFatalError(\n [\n {\n name: \"config/metric-views/definitions.json\",\n message: getErrorDiagnostic(configError),\n },\n ],\n warehouseId,\n );\n }\n\n // Deleted/deleting-warehouse fatal preflight (blocking mode only);\n // empty (no-op) when definitions.json is absent or in non-blocking mode.\n // Only deterministic fatals are recorded in fatalErrors.\n for (const fe of mvResult.fatalErrors) {\n fatalErrors.push(fe);\n }\n\n metricsHadEnvironmentalFailure =\n (mvResult.hadEnvironmentalFailure ?? false) ||\n (mode === \"blocking\" && hasAnyDegradedMetrics(mvResult.schemas));\n environmentalCause =\n environmentalCause ?? mvResult.environmentalCause ?? undefined;\n\n // Blocking (`--wait` / prod Vite) escalates only deterministic per-key\n // DESCRIBE failures. Transient connectivity failures are already recorded\n // as environmental by syncMetricViewsTypes and fall through to the\n // committed-types gate below.\n if (mode === \"blocking\") {\n for (const failure of mvResult.failures) {\n if (failure.transient) continue;\n fatalErrors.push({\n name: failure.key,\n message: `metric view ${failure.key} (${failure.source}) could not be described: ${failure.reason}`,\n });\n }\n }\n }\n\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Deterministic failures (SQL syntax errors or 404/400 HTTP) always crash regardless of mode.\n if (syntaxErrors.length > 0) {\n throw new TypegenSyntaxError(syntaxErrors, warehouseId, fatalErrors);\n }\n if (fatalErrors.length > 0) {\n throw new TypegenFatalError(fatalErrors, warehouseId);\n }\n\n if (\n mode === \"blocking\" &&\n (queryHadEnvironmentalFailure || metricsHadEnvironmentalFailure)\n ) {\n const missingCommittedTypes: string[] = [];\n if (queryHadEnvironmentalFailure && !existsSync(outFile)) {\n missingCommittedTypes.push(path.basename(outFile));\n }\n if (metricsHadEnvironmentalFailure && !existsSync(resolvedMvFile)) {\n missingCommittedTypes.push(path.basename(resolvedMvFile));\n }\n\n if (missingCommittedTypes.length === 0) {\n // Committed types present: emit loud warning and exit 0.\n const warningMessage = determineWarningMessage(\n environmentalCause ?? \"unavailable\",\n warehouseId,\n );\n logger.warn(warningMessage);\n } else {\n throw new TypegenFatalError(\n [\n {\n name: \"type-generator\",\n message: `Warehouse ${warehouseId} could not provide schemas and the required committed type ${plural(missingCommittedTypes.length, \"artifact is\", \"artifacts are\")} missing: ${missingCommittedTypes.join(\", \")}. Run 'npx @databricks/appkit generate-types --wait' locally and commit the generated type files.`,\n },\n ],\n warehouseId,\n );\n }\n }\n\n logger.debug(\"Type generation complete!\");\n}\n\n/**\n * Result of a {@link syncMetricViewsTypes} run, returned to the caller (the CLI\n * directly, or {@link generateFromEntryPoint} which delegates to it) so it can\n * report what happened and decide its exit code.\n */\nexport interface SyncMetricViewsTypesResult {\n metricOutFile?: string;\n schemas: MetricSchema[];\n failures: MetricSyncFailure[];\n /**\n * `true` when no `definitions.json` was found in the metric-views folder, so\n * nothing was synced.\n */\n noConfig: boolean;\n /**\n * Per-key fatal preflight errors (empty except in the `blocking`-mode\n * deleted/deleting-warehouse and deterministic-preflight-failure cases).\n * {@link generateFromEntryPoint} surfaces these by throwing\n * {@link TypegenFatalError}; when the run also degraded, `suppressDegradedWrite`\n * means no artifact was written and the committed types stand. A `\"describe-now\"`\n * run sets no blocking preflight, so for that mode this is always empty.\n * ONLY contains deterministic failures (404/400).\n */\n fatalErrors: Array<{ name: string; message: string }>;\n /**\n * `true` when an environmental failure occurred in blocking mode (auth, connectivity,\n * DELETED/DELETING, wait-timeout, or other unrecognized failures). Used by\n * {@link generateFromEntryPoint} to decide whether to apply the has-types gate.\n * Does not directly cause a throw — the gate decides that. Always false in\n * non-blocking or describe-now mode.\n */\n hadEnvironmentalFailure?: boolean;\n /**\n * Coarse cause label for the environmental failure, one of \"auth\" (401/403),\n * \"unreachable\" (connectivity), or \"unavailable\" (other). Only set when\n * hadEnvironmentalFailure is true; used by the warning message.\n */\n environmentalCause?: \"auth\" | \"unreachable\" | \"unavailable\";\n}\n\n/**\n * Unified metric-view type-generation pipeline behind {@link\n * generateFromEntryPoint}'s metric section (which forwards its\n * `\"non-blocking\"`/`\"blocking\"` mode). Also directly callable with the default\n * `\"describe-now\"` mode for a focused, always-converge metric refresh.\n *\n *\n * @param options.metricViewsFolder - folder that holds `definitions.json` (`<root>/config/metric-views`).\n * @param options.warehouseId - SQL warehouse used for `DESCRIBE TABLE EXTENDED`.\n * @param options.metricOutFile - output path for the MetricRegistry `.ts` (the\n * generated source carries both the `declare module` augmentation and the\n * runtime `metricViewsMetadata` const).\n * @param options.cache - cache toggle, default ON. Only `cache === false` disables it (so `undefined`/`true` keep caching).\n * @param options.metricFetcher - optional injected {@link DescribeFetcher}\n * @param options.mode - preflight/gate policy, default `\"describe-now\"`. When set to `\"blocking\"`,\n * metric-view `.ts` writes are suppressed if any metric is degraded (to preserve committed files).\n * @param options.suppressDegradedWrite - when true (only in `mode === \"blocking\"` context), skip\n * the metricOutFile write if any metric schema has `degraded === true`. Used to prevent\n * overwriting committed type files with degraded types in blocking mode.\n */\nexport async function syncMetricViewsTypes(options: {\n metricViewsFolder: string;\n warehouseId: string;\n metricOutFile: string;\n cache?: boolean;\n metricFetcher?: DescribeFetcher;\n mode?: \"describe-now\" | \"non-blocking\" | \"blocking\";\n suppressDegradedWrite?: boolean;\n}): Promise<SyncMetricViewsTypesResult> {\n const {\n metricViewsFolder,\n warehouseId,\n metricOutFile,\n cache: cacheEnabled,\n metricFetcher,\n mode = \"describe-now\",\n suppressDegradedWrite,\n } = options;\n\n // Only `cache === false` disables caching; `undefined`/`true` keep it on.\n const noCache = cacheEnabled === false;\n\n const mvConfig = await readMetricConfig(metricViewsFolder);\n if (!mvConfig) {\n // No definitions.json — additive path stays dormant. The CLI turns this\n // into a friendly \"nothing to sync\" message and exits 0;\n // generateFromEntryPoint simply ignores `noConfig`.\n return { schemas: [], failures: [], fatalErrors: [], noConfig: true };\n }\n\n const resolution = resolveMetricConfig(mvConfig);\n\n const fatalErrors: Array<{ name: string; message: string }> = [];\n\n // Load the shared typegen cache and copy its `metrics` section into a null-prototype map.\n const cache = await loadCache();\n const mvCacheSection: Record<string, MetricCacheEntry> = Object.create(null);\n if (!noCache && cache.metrics) {\n for (const key of Object.keys(cache.metrics)) {\n mvCacheSection[key] = cache.metrics[key];\n }\n }\n\n // Partition BEFORE any gate/preflight decision: a hit (a structurally valid,\n // hash-matching, NON-degraded cached entry) is served from cache no matter\n // what the warehouse is doing. The cache only ever holds successful describes\n // (a degraded outcome is never persisted — see the write block below), so the\n // `degraded !== true` guard is normally moot; it also defends against a stale\n // degraded entry left by an older writer, which re-describes instead of\n // serving. Everything else (new, edited, unrevivable, or degraded) is eligible\n // for DESCRIBE, so a fully-warm pass makes zero warehouse calls and constructs\n // zero clients. Mirrors the query path: only a good result is cache-servable.\n const hitSchemas = new Map<string, MetricSchema>();\n const describeNeeded: typeof resolution.entries = [];\n for (const entry of resolution.entries) {\n const prior = mvCacheSection[entry.key];\n if (\n prior !== undefined &&\n isRevivableMetricCacheEntry(prior) &&\n prior.hash === metricCacheHash(entry.source, entry.lane) &&\n prior.schema.degraded !== true\n ) {\n hitSchemas.set(entry.key, prior.schema);\n } else {\n describeNeeded.push(entry);\n }\n }\n\n let mvClient: WorkspaceClient | undefined;\n const getMvClient = (): WorkspaceClient => {\n mvClient ??= createWorkspaceClient();\n return mvClient;\n };\n\n // Blocking-mode preflight: ensure the warehouse is running before the MV DESCRIBE\n // batch (probe → decide → wait / start+wait; only DELETED/DELETING is fatal). Two softenings vs the query preflight: a failed probe and a timed-out wait are NOT fatal here — we fall through to syncMetrics, which classifies a still-not-ready warehouse as degraded rather than failing the build. Skipped for `describe-now`/`non-blocking` (only `mode === \"blocking\"` enters here).\n let preflightFatalMessage: string | undefined;\n let hadEnvironmentalFailure = false;\n let environmentalCause: \"auth\" | \"unreachable\" | \"unavailable\" | undefined;\n if (\n mode === \"blocking\" &&\n metricFetcher === undefined &&\n describeNeeded.length > 0\n ) {\n try {\n const state = await getWarehouseState(getMvClient(), warehouseId);\n const decision = decidePreflight(state, mode);\n if (decision === \"fatal\") {\n preflightFatalMessage = `warehouse ${warehouseId} is ${state}`;\n // State-based DELETED/DELETING is environmental, not deterministic.\n hadEnvironmentalFailure = true;\n environmentalCause = \"unavailable\";\n } else if (decision === \"startWaitProceed\") {\n // treatStoppedAsTransient rides out the stale pre-start STOPPED/STOPPING\n // reading, same as the query preflight.\n await startWarehouse(getMvClient(), warehouseId);\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n treatStoppedAsTransient: true,\n });\n if (settled !== \"RUNNING\") {\n // With treatStoppedAsTransient, a non-RUNNING resolve is exactly\n // DELETED/DELETING — the warehouse was deleted while we waited.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n hadEnvironmentalFailure = true;\n }\n } else if (decision === \"waitThenProceed\") {\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n });\n if (settled === \"DELETED\" || settled === \"DELETING\") {\n // Deleted mid-wait: fatal. Environmental (state-based).\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n hadEnvironmentalFailure = true;\n }\n }\n } catch (err) {\n // Connectivity blip: fall through to syncMetrics, whose DESCRIBEs degrade\n // a not-ready / unreachable warehouse rather than throwing.\n if (!isConnectivityError(err)) {\n // Deterministic failures become fatal errors; environmental failures\n // degrade for the committed-types gate.\n const classification = classifyBlockingFailure(err);\n if (classification === \"deterministic\") {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n } else {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n hadEnvironmentalFailure = true;\n environmentalCause = classifyEnvironmentalCause(err);\n }\n }\n }\n }\n\n let gateState: WarehouseState | undefined;\n let describeNow =\n metricFetcher !== undefined ||\n mode !== \"non-blocking\" ||\n describeNeeded.length === 0;\n if (!describeNow) {\n try {\n gateState = await probeWarehouseState(getMvClient, warehouseId);\n } catch (err) {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n describeNow = gateState === \"RUNNING\";\n }\n\n let described: MetricSchema[];\n let failures: MetricSyncFailure[] = [];\n if (preflightFatalMessage !== undefined) {\n // Environmental failures degrade for the committed-types gate; deterministic\n // failures record one fatal error per key. Degraded schemas are not cached.\n described = describeNeeded.map(emptyMetricSchema);\n if (!hadEnvironmentalFailure) {\n for (const entry of describeNeeded) {\n fatalErrors.push({ name: entry.key, message: preflightFatalMessage });\n }\n }\n } else if (describeNeeded.length === 0) {\n // Nothing left to describe — every configured key was a cache hit.\n // syncMetrics would be a no-op (and building its fetcher would construct a\n // client for nothing); artifacts regenerate from cache.\n described = [];\n } else if (describeNow) {\n const fetcher =\n metricFetcher ??\n createWorkspaceDescribeFetcher(getMvClient(), warehouseId);\n ({ schemas: described, failures } = await syncMetrics(\n { entries: describeNeeded },\n fetcher,\n ));\n\n // Surface DESCRIBE failures loudly: a misconfigured definitions.json would\n // otherwise silently ship an empty entry that the runtime fail-closed gate\n // 503s in production. syncMetrics is log-free; this caller is the single\n // owner of failure logging.\n if (failures.length > 0) {\n for (const f of failures) {\n logger.warn(\n \"metric sync failed for %s (%s): %s\",\n f.key,\n f.source,\n f.reason,\n );\n }\n }\n\n // A rejected DESCRIBE with a connectivity signal is expected to recover on\n // a later pass. In blocking mode, route it through the same committed-types\n // gate as preflight outages instead of treating it as a configuration error.\n if (mode === \"blocking\" && failures.some((failure) => failure.transient)) {\n hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unreachable\";\n }\n\n // Degraded-but-not-failed keys: the warehouse answered with a non-terminal\n // state (stopped / cold-starting), so their schemas are unknown.\n const failedKeys = new Set(failures.map((f) => f.key));\n const degradedKeys = described\n .filter((s) => s.degraded && !failedKeys.has(s.key))\n .map((s) => s.key);\n if (degradedKeys.length > 0) {\n logger.info(\n \"Warehouse %s did not return schemas for %d metric view(s) (%s) — wrote degraded metric types (permissive); they will refresh once the warehouse is available.\",\n warehouseId,\n degradedKeys.length,\n degradedKeys.join(\", \"),\n );\n hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unavailable\";\n }\n } else {\n // Un-probed DESCRIBEs emit degraded schemas; cache hits remain last-known-good.\n described = describeNeeded.map(emptyMetricSchema);\n logger.info(\n \"Warehouse %s is not running — wrote degraded metric types (permissive) for %d metric view(s) (%s); they will refresh once the warehouse is available.\",\n warehouseId,\n describeNeeded.length,\n describeNeeded.map((e) => e.key).join(\", \"),\n );\n hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unavailable\";\n }\n\n // Cache only successful schema results for describe-needed keys; remove stale cache for degraded ones.\n for (let i = 0; i < describeNeeded.length; i++) {\n // syncMetrics return one schema per entry in entry order, so described[i] always belongs to describeNeeded[i].\n const entry = describeNeeded[i];\n if (described[i].degraded === true) {\n delete mvCacheSection[entry.key];\n continue;\n }\n mvCacheSection[entry.key] = {\n hash: metricCacheHash(entry.source, entry.lane),\n schema: described[i],\n // Vestigial, mirrors the query path's only cache write (always false): a\n // persisted entry is by construction a good result, so it never needs a\n // re-describe flag. Kept for on-disk shape compatibility with existing\n // version-3 caches (isRevivableMetricCacheEntry gates on a boolean).\n retry: false,\n };\n }\n\n // Prune entries whose key is no longer configured\n const configuredKeys = new Set(resolution.entries.map((e) => e.key));\n let prunedCount = 0;\n for (const key of Object.keys(mvCacheSection)) {\n if (!configuredKeys.has(key)) {\n delete mvCacheSection[key];\n prunedCount++;\n }\n }\n\n // Save when this pass produced outcomes, bypassed the cache, or pruned.\n if (describeNeeded.length > 0 || noCache || prunedCount > 0) {\n cache.metrics = mvCacheSection;\n await saveCache(cache);\n }\n\n // Merge cached hits with fresh results back into config order.\n const describedByKey = new Map<string, MetricSchema>();\n for (const schema of described) {\n describedByKey.set(schema.key, schema);\n }\n const schemas = resolution.entries.map((entry) => {\n const schema = hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);\n if (schema !== undefined) return schema;\n logger.warn(\n \"no schema resolved for metric key %s — emitting degraded types (should not happen)\",\n entry.key,\n );\n return emptyMetricSchema(entry);\n });\n\n // Same anti-clobber rule as the query path: when suppressDegradedWrite is set\n // (blocking mode), skip the write if any metric degraded, preserving the\n // committed metric-views.ts. Non-blocking mode always writes.\n const shouldWriteMetrics =\n !suppressDegradedWrite || !hasAnyDegradedMetrics(schemas);\n\n if (shouldWriteMetrics) {\n await fs.mkdir(path.dirname(metricOutFile), { recursive: true });\n await fs.writeFile(\n metricOutFile,\n generateMetricTypeDeclarations(schemas),\n \"utf-8\",\n );\n }\n // Sweep the ambient `metric-views.d.ts` a pre-`.ts` version left behind,\n // which would otherwise duplicate the augmentation the new `.ts` emits.\n // Skipped unless the replacement was actually written, so a degraded\n // blocking pass leaves an app's only committed metric types in place.\n if (\n metricOutFile.endsWith(\".ts\") &&\n !metricOutFile.endsWith(\".d.ts\") &&\n existsSync(metricOutFile)\n ) {\n const staleDts = `${metricOutFile.slice(0, -\".ts\".length)}.d.ts`;\n try {\n await fs.unlink(staleDts);\n logger.debug(\"Removed stale generated types at %s\", staleDts);\n } catch {\n // No stale sibling — nothing to clean up.\n }\n }\n\n logger.debug(\n \"Wrote MetricRegistry augmentation for %d metric(s)%s\",\n schemas.length,\n failures.length > 0 ? ` (${failures.length} failure(s))` : \"\",\n );\n\n return {\n metricOutFile,\n schemas,\n failures,\n fatalErrors,\n noConfig: false,\n hadEnvironmentalFailure:\n mode === \"blocking\" ? hadEnvironmentalFailure : undefined,\n environmentalCause:\n mode === \"blocking\" && hadEnvironmentalFailure\n ? environmentalCause\n : undefined,\n };\n}\n\n// Rolldown tree-shaking only preserves \"own exports\" (locally defined) — not re-exports.\n// A local binding ensures the serving vite plugin's import keeps this in the dependency graph,\n// mirroring how generateFromEntryPoint (also defined here) is preserved via the analytics vite plugin.\nexport const generateServingTypes = generateServingTypesImpl;\n\n// Re-export the mv-registry types so consumers (CLI, the type-generator\n// .d.ts shim in `packages/shared`) can pick them up from this entry point —\n// the .d.ts shim documents these as part of the package's public surface.\nexport type {\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n};\n\nexport const TYPES_DIR = \"appkit-types\";\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\nexport const METRIC_TYPES_FILE = \"metric-views.ts\";\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport pc from \"picocolors\";\nimport { METRIC_METADATA_FILE } from \"../../../shared/src/schemas/metric-metadata-bundle\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n createWorkspaceClient,\n type WorkspaceClient,\n} from \"../workspace-client\";\nimport {\n isRevivableMetricCacheEntry,\n loadCache,\n type MetricCacheEntry,\n metricCacheHash,\n saveCache,\n} from \"./cache\";\nimport {\n classifyBlockingFailure,\n classifyEnvironmentalCause,\n getErrorDiagnostic,\n isConnectivityError,\n} from \"./errors\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"./migration\";\nimport { readMetricConfig, resolveMetricConfig } from \"./mv-registry/config\";\nimport { createWorkspaceDescribeFetcher } from \"./mv-registry/describe\";\nimport {\n buildMetricMetadataBundle,\n generateMetricTypeDeclarations,\n} from \"./mv-registry/render-types\";\nimport { emptyMetricSchema, syncMetrics } from \"./mv-registry/sync\";\nimport type {\n DescribeFetcher,\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n} from \"./mv-registry/types\";\nimport { decidePreflight, type PreflightMode } from \"./preflight\";\nimport { generateQueriesFromDescribe } from \"./query-registry\";\nimport { generateServingTypes as generateServingTypesImpl } from \"./serving/generator\";\nimport type { QueryFatalError, QuerySchema, QuerySyntaxError } from \"./types\";\nimport {\n getWarehouseState,\n startWarehouse,\n type WarehouseState,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\ndotenv.config();\n\nconst logger = createLogger(\"type-generator\");\n\n/**\n * Upper bound (~5 min) on how long the Metric Views path's `blocking`-mode preflight\n * waits for a warehouse to reach RUNNING. Mirrors the query path's (unexported)\n * `PREFLIGHT_WAIT_MAX_MS` in query-registry.ts.\n */\nconst MV_PREFLIGHT_WAIT_MAX_MS = 300_000;\n\nfunction determineWarningMessage(\n cause: \"auth\" | \"unreachable\" | \"unavailable\",\n warehouseId: string,\n): string {\n const causeLabel =\n cause === \"auth\"\n ? \"auth blocked\"\n : cause === \"unreachable\"\n ? \"warehouse unreachable\"\n : \"warehouse unavailable\";\n // Use a stable prefix for greppability and CI log matching.\n return `AppKit typegen: using committed types — warehouse ${warehouseId} ${causeLabel}; please check warehouse status and retry`;\n}\n\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\n}\n\nfunction isQueryDegraded(schema: QuerySchema): boolean {\n return schema.degraded === true;\n}\n\nfunction hasAnyDegradedMetrics(schemas: MetricSchema[]): boolean {\n return schemas.some((s) => s.degraded === true);\n}\n\nfunction formatFailureRows(\n label: string,\n queries: TypegenFailure[],\n color: (value: string) => string,\n) {\n if (queries.length === 0) return [];\n\n // Group by message so a shared failure — e.g. a warehouse-level fatal that\n // hits every query identically — prints once instead of repeating per row.\n const byMessage = new Map<string, string[]>();\n for (const { name, message } of queries) {\n const names = byMessage.get(message);\n if (names) names.push(name);\n else byMessage.set(message, [name]);\n }\n\n const maxNameLen = Math.max(...queries.map((query) => query.name.length));\n const tag = color(label.padEnd(7));\n const rows: string[] = [];\n for (const [message, names] of byMessage) {\n if (names.length === 1) {\n rows.push(\n ` ${tag} ${pc.bold(names[0].padEnd(maxNameLen))} ${pc.dim(message)}`,\n );\n continue;\n }\n // Shared message → print it once, then list the affected query names.\n rows.push(\n ` ${tag} ${pc.dim(message)} ${pc.dim(`(${names.length} ${plural(names.length, \"query\", \"queries\")})`)}`,\n );\n rows.push(\n ` ${names.map((name) => pc.bold(name)).join(pc.dim(\", \"))}`,\n );\n }\n return rows;\n}\n\nfunction formatTypegenFailureMessage(options: {\n syntaxErrors: QuerySyntaxError[];\n fatalErrors?: QueryFatalError[];\n warehouseId?: string;\n title: string;\n causes: string[];\n nextStep: string;\n}) {\n const { syntaxErrors, fatalErrors = [], warehouseId, title } = options;\n const total = syntaxErrors.length + fatalErrors.length;\n const separator = pc.dim(\"─\".repeat(60));\n const warehouse = warehouseId\n ? ` against ${pc.dim(`warehouse ${warehouseId}`)}`\n : \"\";\n\n return [\n ` ${pc.bold(pc.red(\"Type generation failed\"))}`,\n ` ${separator}`,\n ` ${title}: ${total} ${plural(total, \"query\", \"queries\")} could not be described${warehouse}.`,\n ` AppKit wrote generated types with ${pc.bold(\"result: unknown\")} for the failed ${plural(total, \"query\", \"queries\")}.`,\n \"\",\n ...formatFailureRows(\"SQL ERR\", syntaxErrors, pc.red),\n ...(syntaxErrors.length > 0 && fatalErrors.length > 0 ? [\"\"] : []),\n ...formatFailureRows(\"FATAL\", fatalErrors, pc.red),\n \"\",\n ` ${pc.bold(\"Common causes\")}`,\n ...options.causes.map((cause) => ` - ${cause}`),\n \"\",\n ` ${pc.bold(\"Next step\")}`,\n ` ${options.nextStep}`,\n ].join(\"\\n\");\n}\n\n/**\n * Thrown when one or more queries fail `DESCRIBE QUERY` against a *reachable*\n * warehouse — i.e. genuine SQL errors (bad table, syntax, incompatible type),\n * as opposed to a connectivity failure (warehouse unreachable), which degrades\n * silently. Whether this is fatal is the caller's decision: the Vite plugin and\n * CLI fail the build in production and warn-only in development.\n */\nexport class TypegenSyntaxError extends Error {\n readonly queries: QuerySyntaxError[];\n readonly fatalQueries: QueryFatalError[];\n\n constructor(\n queries: QuerySyntaxError[],\n warehouseId?: string,\n fatalQueries: QueryFatalError[] = [],\n ) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: queries,\n fatalErrors: fatalQueries,\n warehouseId,\n title: \"DESCRIBE QUERY failed\",\n causes: [\n \"SQL syntax errors\",\n \"missing tables or views\",\n \"warehouse format incompatibilities\",\n ],\n nextStep: warehouseId\n ? `Run each SQL ERR query directly in a Databricks SQL editor against warehouse ${pc.bold(warehouseId)}.`\n : \"Run each SQL ERR query directly in a Databricks SQL editor.\",\n }),\n );\n this.name = \"TypegenSyntaxError\";\n this.queries = queries;\n this.fatalQueries = fatalQueries;\n }\n}\n\n/**\n * Thrown when DESCRIBE QUERY could not be requested because of a non-SQL fatal\n * setup/request problem, such as missing permissions, invalid warehouse IDs, or\n * malformed SDK configuration. Like TypegenSyntaxError, this is thrown only\n * after the declaration file has been written with `result: unknown` entries.\n */\nexport class TypegenFatalError extends Error {\n readonly queries: QueryFatalError[];\n\n constructor(queries: QueryFatalError[], warehouseId?: string) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: [],\n fatalErrors: queries,\n warehouseId,\n title: \"DESCRIBE QUERY could not be requested\",\n causes: [\n \"missing warehouse permissions\",\n \"invalid warehouse ID\",\n \"authentication failure\",\n \"SDK configuration errors\",\n ],\n nextStep: warehouseId\n ? `Verify access to warehouse ${pc.bold(warehouseId)} and rerun type generation.`\n : \"Verify warehouse access and rerun type generation.\",\n }),\n );\n this.name = \"TypegenFatalError\";\n this.queries = queries;\n }\n}\n\n/**\n * Generate type declarations for QueryRegistry\n * Create the d.ts file from the plugin routes and query schemas\n * @param querySchemas - the list of query schemas\n * @returns - the type declarations as a string\n */\nfunction generateTypeDeclarations(querySchemas: QuerySchema[] = []): string {\n const queryEntries = querySchemas\n .map(({ name, type }) => {\n const indentedType = type\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : ` ${line}`))\n .join(\"\\n\");\n return ` ${name}: ${indentedType}`;\n })\n .join(\";\\n\");\n\n const querySection = queryEntries ? `\\n${queryEntries};\\n ` : \"\";\n\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\nimport type { SQLTypeMarker, SQLStringMarker, SQLNumberMarker, SQLBooleanMarker, SQLBinaryMarker, SQLDateMarker, SQLTimestampMarker } from \"@databricks/appkit-ui/js\";\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface QueryRegistry {${querySection}}\n}\n`;\n}\n\n/**\n * Status-only probe for the metric-view gate in {@link generateFromEntryPoint}\n *\n * Uses {@link getWarehouseState} (`warehouses.get`) —\n * a read-only GET that can never start the warehouse\n *\n * Takes the lazy client *getter* so the probe also absorbs client construction failure.\n * A connectivity blip returns `undefined`, which the gate reads as transient not-running;\n * a deterministic failure (auth, bad id) is re-thrown so the gate can classify it\n * fatal rather than silently degrading.\n */\nasync function probeWarehouseState(\n getClient: () => WorkspaceClient,\n warehouseId: string,\n): Promise<WarehouseState | undefined> {\n try {\n return await getWarehouseState(getClient(), warehouseId);\n } catch (err) {\n // Connectivity blip → undefined (gate degrades, retries next pass). A\n // deterministic failure (auth, bad warehouse id, client construction) must\n // not masquerade as not-running — re-throw so the gate pins it fatal, the\n // same split the query path's preflight makes.\n if (isConnectivityError(err)) return undefined;\n throw err;\n }\n}\n\n/**\n * Entry point for generating type declarations from all imported files\n * @param options - the options for the generation\n * @param options.entryPoint - the entry point file\n * @param options.outFile - the output file\n * @param options.noCache - skip the typegen cache entirely: every query is\n * re-described, and the metric path ignores its cached schemas (every\n * configured key becomes describe-needed) and overwrites the cache's\n * `metrics` section with this pass's results.\n * @param options.mode - preflight policy (see {@link PreflightMode}), default\n * `\"non-blocking\"`. For queries, `\"non-blocking\"` never touches the\n * warehouse. For metric views it makes one status-only probe and DESCRIBEs\n * only when the warehouse is already RUNNING, otherwise emits permissive\n * degraded types immediately. `\"blocking\"` waits for / starts the warehouse\n * first, failing the build only for a deleted/deleting one.\n * @param options.metricViewsFolder - folder that holds `definitions.json`\n * (`<root>/config/metric-views`). Optional and independent of `queryFolder`:\n * metric-view types generate whenever this folder holds a config, even if the\n * app has no `config/queries`. When omitted it defaults to a sibling\n * `metric-views` directory of `queryFolder` (so query-only callers keep\n * working); when neither is given, the metric path is skipped.\n * @param options.mvOutFile - optional output file for the MetricRegistry\n * augmentation. Defaults to a sibling `metric-views.ts` file under the same\n * directory as `outFile`. Skipped entirely if `definitions.json` is absent.\n * @param options.metricFetcher - optional DescribeFetcher used by\n * {@link syncMetrics} (tests inject a mock; production lazily builds a\n * default WorkspaceClient-backed one). An injected fetcher always runs: it\n * hits no warehouse, so it bypasses both the non-blocking gate and the\n * blocking preflight.\n */\nexport async function generateFromEntryPoint(options: {\n outFile: string;\n queryFolder?: string;\n metricViewsFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n mode?: PreflightMode;\n mvOutFile?: string;\n metricFetcher?: DescribeFetcher;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n mvOutFile,\n metricFetcher,\n } = options;\n\n // Metric config lives in `config/metric-views/`, a sibling of the queries\n // folder. Prefer the explicit option; otherwise derive the sibling of\n // `queryFolder` so callers that pass only `queryFolder` keep emitting metric\n // types. Undefined when neither is given → the metric path stays dormant.\n const metricViewsFolder =\n options.metricViewsFolder ??\n (queryFolder ? path.resolve(queryFolder, \"..\", \"metric-views\") : undefined);\n const resolvedMvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n // Deterministic fatal errors only (404/400).\n let fatalErrors: QueryFatalError[] = [];\n let queryHadEnvironmentalFailure = false;\n let metricsHadEnvironmentalFailure = false;\n // Track the coarse cause of the environmental failure for the warning message.\n let environmentalCause: \"auth\" | \"unreachable\" | \"unavailable\" | undefined;\n\n if (queryFolder) {\n const result = await generateQueriesFromDescribe(queryFolder, warehouseId, {\n noCache,\n mode,\n });\n queryRegistry = result.schemas;\n syntaxErrors = result.syntaxErrors ?? [];\n fatalErrors = result.fatalErrors ?? [];\n queryHadEnvironmentalFailure = result.hadEnvironmentalFailure ?? false;\n environmentalCause =\n environmentalCause ?? result.environmentalCause ?? undefined;\n }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n // In blocking mode, never overwrite committed types with a schema explicitly\n // marked degraded. Leave the committed .d.ts as the fallback of record.\n // Non-blocking mode always writes.\n const hasAnyDegradedQuery = queryRegistry.some(isQueryDegraded);\n if (mode === \"blocking\" && hasAnyDegradedQuery) {\n // A degraded schema always participates in the committed-types gate. Keep\n // this invariant next to write suppression so a new producer cannot update\n // one decision without the other.\n queryHadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unavailable\";\n }\n const shouldWriteQueries = mode !== \"blocking\" || !hasAnyDegradedQuery;\n\n if (shouldWriteQueries) {\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n }\n\n // Metric-view types: emit whenever a metric-views folder is resolved (gated\n // on the metric config's own dir, NOT the queries folder — an app can declare\n // metric views without any `.sql` queries). `syncMetricViewsTypes` still\n // returns `noConfig` when the folder holds no `definitions.json`.\n if (metricViewsFolder) {\n let mvResult: SyncMetricViewsTypesResult;\n try {\n mvResult = await syncMetricViewsTypes({\n metricViewsFolder,\n warehouseId,\n metricOutFile: resolvedMvFile,\n cache: !noCache,\n metricFetcher,\n mode,\n // In blocking mode, never overwrite committed metric types with a\n // degraded result — including on runs that go on to throw, so a failing\n // build leaves the committed type file intact. Non-blocking always writes.\n suppressDegradedWrite: mode === \"blocking\",\n });\n } catch (configError) {\n // syncMetricViewsTypes only throws for a malformed definitions.json — re-throw as a message-only TypegenFatalError.\n throw new TypegenFatalError(\n [\n {\n name: \"config/metric-views/definitions.json\",\n message: getErrorDiagnostic(configError),\n },\n ],\n warehouseId,\n );\n }\n\n // Deleted/deleting-warehouse fatal preflight (blocking mode only);\n // empty (no-op) when definitions.json is absent or in non-blocking mode.\n // Only deterministic fatals are recorded in fatalErrors.\n for (const fe of mvResult.fatalErrors) {\n fatalErrors.push(fe);\n }\n\n metricsHadEnvironmentalFailure =\n (mvResult.hadEnvironmentalFailure ?? false) ||\n (mode === \"blocking\" && hasAnyDegradedMetrics(mvResult.schemas));\n environmentalCause =\n environmentalCause ?? mvResult.environmentalCause ?? undefined;\n\n // Blocking (`--wait` / prod Vite) escalates only deterministic per-key\n // DESCRIBE failures. Transient connectivity failures are already recorded\n // as environmental by syncMetricViewsTypes and fall through to the\n // committed-types gate below.\n if (mode === \"blocking\") {\n for (const failure of mvResult.failures) {\n if (failure.transient) continue;\n fatalErrors.push({\n name: failure.key,\n message: `metric view ${failure.key} (${failure.source}) could not be described: ${failure.reason}`,\n });\n }\n }\n }\n\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Deterministic failures (SQL syntax errors or 404/400 HTTP) always crash regardless of mode.\n if (syntaxErrors.length > 0) {\n throw new TypegenSyntaxError(syntaxErrors, warehouseId, fatalErrors);\n }\n if (fatalErrors.length > 0) {\n throw new TypegenFatalError(fatalErrors, warehouseId);\n }\n\n if (\n mode === \"blocking\" &&\n (queryHadEnvironmentalFailure || metricsHadEnvironmentalFailure)\n ) {\n const missingCommittedTypes: string[] = [];\n if (queryHadEnvironmentalFailure && !existsSync(outFile)) {\n missingCommittedTypes.push(path.basename(outFile));\n }\n if (metricsHadEnvironmentalFailure && !existsSync(resolvedMvFile)) {\n missingCommittedTypes.push(path.basename(resolvedMvFile));\n }\n\n if (missingCommittedTypes.length === 0) {\n // Committed types present: emit loud warning and exit 0.\n const warningMessage = determineWarningMessage(\n environmentalCause ?? \"unavailable\",\n warehouseId,\n );\n logger.warn(warningMessage);\n } else {\n throw new TypegenFatalError(\n [\n {\n name: \"type-generator\",\n message: `Warehouse ${warehouseId} could not provide schemas and the required committed type ${plural(missingCommittedTypes.length, \"artifact is\", \"artifacts are\")} missing: ${missingCommittedTypes.join(\", \")}. Run 'npx @databricks/appkit generate-types --wait' locally and commit the generated type files.`,\n },\n ],\n warehouseId,\n );\n }\n }\n\n logger.debug(\"Type generation complete!\");\n}\n\n/**\n * Result of a {@link syncMetricViewsTypes} run, returned to the caller (the CLI\n * directly, or {@link generateFromEntryPoint} which delegates to it) so it can\n * report what happened and decide its exit code.\n */\nexport interface SyncMetricViewsTypesResult {\n metricOutFile?: string;\n schemas: MetricSchema[];\n failures: MetricSyncFailure[];\n /**\n * `true` when no `definitions.json` was found in the metric-views folder, so\n * nothing was synced.\n */\n noConfig: boolean;\n /**\n * Per-key fatal preflight errors (empty except in the `blocking`-mode\n * deleted/deleting-warehouse and deterministic-preflight-failure cases).\n * {@link generateFromEntryPoint} surfaces these by throwing\n * {@link TypegenFatalError}; when the run also degraded, `suppressDegradedWrite`\n * means no artifact was written and the committed types stand. A `\"describe-now\"`\n * run sets no blocking preflight, so for that mode this is always empty.\n * ONLY contains deterministic failures (404/400).\n */\n fatalErrors: Array<{ name: string; message: string }>;\n /**\n * `true` when an environmental failure occurred in blocking mode (auth, connectivity,\n * DELETED/DELETING, wait-timeout, or other unrecognized failures). Used by\n * {@link generateFromEntryPoint} to decide whether to apply the has-types gate.\n * Does not directly cause a throw — the gate decides that. Always false in\n * non-blocking or describe-now mode.\n */\n hadEnvironmentalFailure?: boolean;\n /**\n * Coarse cause label for the environmental failure, one of \"auth\" (401/403),\n * \"unreachable\" (connectivity), or \"unavailable\" (other). Only set when\n * hadEnvironmentalFailure is true; used by the warning message.\n */\n environmentalCause?: \"auth\" | \"unreachable\" | \"unavailable\";\n}\n\n/**\n * Unified metric-view type-generation pipeline behind {@link\n * generateFromEntryPoint}'s metric section (which forwards its\n * `\"non-blocking\"`/`\"blocking\"` mode). Also directly callable with the default\n * `\"describe-now\"` mode for a focused, always-converge metric refresh.\n *\n *\n * @param options.metricViewsFolder - folder that holds `definitions.json` (`<root>/config/metric-views`).\n * @param options.warehouseId - SQL warehouse used for `DESCRIBE TABLE EXTENDED`.\n * @param options.metricOutFile - output path for the MetricRegistry `.ts` (the\n * generated source carries both the `declare module` augmentation and the\n * runtime `metricViewsMetadata` const).\n * @param options.cache - cache toggle, default ON. Only `cache === false` disables it (so `undefined`/`true` keep caching).\n * @param options.metricFetcher - optional injected {@link DescribeFetcher}\n * @param options.mode - preflight/gate policy, default `\"describe-now\"`. When set to `\"blocking\"`,\n * metric-view `.ts` writes are suppressed if any metric is degraded (to preserve committed files).\n * @param options.suppressDegradedWrite - when true (only in `mode === \"blocking\"` context), skip\n * the metricOutFile write if any metric schema has `degraded === true`. Used to prevent\n * overwriting committed type files with degraded types in blocking mode.\n */\nexport async function syncMetricViewsTypes(options: {\n metricViewsFolder: string;\n warehouseId: string;\n metricOutFile: string;\n cache?: boolean;\n metricFetcher?: DescribeFetcher;\n mode?: \"describe-now\" | \"non-blocking\" | \"blocking\";\n suppressDegradedWrite?: boolean;\n}): Promise<SyncMetricViewsTypesResult> {\n const {\n metricViewsFolder,\n warehouseId,\n metricOutFile,\n cache: cacheEnabled,\n metricFetcher,\n mode = \"describe-now\",\n suppressDegradedWrite,\n } = options;\n\n // Only `cache === false` disables caching; `undefined`/`true` keep it on.\n const noCache = cacheEnabled === false;\n\n const mvConfig = await readMetricConfig(metricViewsFolder);\n if (!mvConfig) {\n // No definitions.json — additive path stays dormant. The CLI turns this\n // into a friendly \"nothing to sync\" message and exits 0;\n // generateFromEntryPoint simply ignores `noConfig`.\n return { schemas: [], failures: [], fatalErrors: [], noConfig: true };\n }\n\n const resolution = resolveMetricConfig(mvConfig);\n\n const fatalErrors: Array<{ name: string; message: string }> = [];\n\n // Load the shared typegen cache and copy its `metrics` section into a null-prototype map.\n const cache = await loadCache();\n const mvCacheSection: Record<string, MetricCacheEntry> = Object.create(null);\n if (!noCache && cache.metrics) {\n for (const key of Object.keys(cache.metrics)) {\n mvCacheSection[key] = cache.metrics[key];\n }\n }\n\n // Partition BEFORE any gate/preflight decision: a hit (a structurally valid,\n // hash-matching, NON-degraded cached entry) is served from cache no matter\n // what the warehouse is doing. The cache only ever holds successful describes\n // (a degraded outcome is never persisted — see the write block below), so the\n // `degraded !== true` guard is normally moot; it also defends against a stale\n // degraded entry left by an older writer, which re-describes instead of\n // serving. Everything else (new, edited, unrevivable, or degraded) is eligible\n // for DESCRIBE, so a fully-warm pass makes zero warehouse calls and constructs\n // zero clients. Mirrors the query path: only a good result is cache-servable.\n const hitSchemas = new Map<string, MetricSchema>();\n const describeNeeded: typeof resolution.entries = [];\n for (const entry of resolution.entries) {\n const prior = mvCacheSection[entry.key];\n if (\n prior !== undefined &&\n isRevivableMetricCacheEntry(prior) &&\n prior.hash === metricCacheHash(entry.source, entry.lane) &&\n prior.schema.degraded !== true\n ) {\n hitSchemas.set(entry.key, prior.schema);\n } else {\n describeNeeded.push(entry);\n }\n }\n\n let mvClient: WorkspaceClient | undefined;\n const getMvClient = (): WorkspaceClient => {\n mvClient ??= createWorkspaceClient();\n return mvClient;\n };\n\n // Blocking-mode preflight: ensure the warehouse is running before the MV DESCRIBE\n // batch (probe → decide → wait / start+wait; only DELETED/DELETING is fatal). Two softenings vs the query preflight: a failed probe and a timed-out wait are NOT fatal here — we fall through to syncMetrics, which classifies a still-not-ready warehouse as degraded rather than failing the build. Skipped for `describe-now`/`non-blocking` (only `mode === \"blocking\"` enters here).\n let preflightFatalMessage: string | undefined;\n let hadEnvironmentalFailure = false;\n let environmentalCause: \"auth\" | \"unreachable\" | \"unavailable\" | undefined;\n if (\n mode === \"blocking\" &&\n metricFetcher === undefined &&\n describeNeeded.length > 0\n ) {\n try {\n const state = await getWarehouseState(getMvClient(), warehouseId);\n const decision = decidePreflight(state, mode);\n if (decision === \"fatal\") {\n preflightFatalMessage = `warehouse ${warehouseId} is ${state}`;\n // State-based DELETED/DELETING is environmental, not deterministic.\n hadEnvironmentalFailure = true;\n environmentalCause = \"unavailable\";\n } else if (decision === \"startWaitProceed\") {\n // treatStoppedAsTransient rides out the stale pre-start STOPPED/STOPPING\n // reading, same as the query preflight.\n await startWarehouse(getMvClient(), warehouseId);\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n treatStoppedAsTransient: true,\n });\n if (settled !== \"RUNNING\") {\n // With treatStoppedAsTransient, a non-RUNNING resolve is exactly\n // DELETED/DELETING — the warehouse was deleted while we waited.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n hadEnvironmentalFailure = true;\n }\n } else if (decision === \"waitThenProceed\") {\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n });\n if (settled === \"DELETED\" || settled === \"DELETING\") {\n // Deleted mid-wait: fatal. Environmental (state-based).\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n hadEnvironmentalFailure = true;\n }\n }\n } catch (err) {\n // Connectivity blip: fall through to syncMetrics, whose DESCRIBEs degrade\n // a not-ready / unreachable warehouse rather than throwing.\n if (!isConnectivityError(err)) {\n // Deterministic failures become fatal errors; environmental failures\n // degrade for the committed-types gate.\n const classification = classifyBlockingFailure(err);\n if (classification === \"deterministic\") {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n } else {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n hadEnvironmentalFailure = true;\n environmentalCause = classifyEnvironmentalCause(err);\n }\n }\n }\n }\n\n let gateState: WarehouseState | undefined;\n let describeNow =\n metricFetcher !== undefined ||\n mode !== \"non-blocking\" ||\n describeNeeded.length === 0;\n if (!describeNow) {\n try {\n gateState = await probeWarehouseState(getMvClient, warehouseId);\n } catch (err) {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n describeNow = gateState === \"RUNNING\";\n }\n\n let described: MetricSchema[];\n let failures: MetricSyncFailure[] = [];\n if (preflightFatalMessage !== undefined) {\n // Environmental failures degrade for the committed-types gate; deterministic\n // failures record one fatal error per key. Degraded schemas are not cached.\n described = describeNeeded.map(emptyMetricSchema);\n if (!hadEnvironmentalFailure) {\n for (const entry of describeNeeded) {\n fatalErrors.push({ name: entry.key, message: preflightFatalMessage });\n }\n }\n } else if (describeNeeded.length === 0) {\n // Nothing left to describe — every configured key was a cache hit.\n // syncMetrics would be a no-op (and building its fetcher would construct a\n // client for nothing); artifacts regenerate from cache.\n described = [];\n } else if (describeNow) {\n const fetcher =\n metricFetcher ??\n createWorkspaceDescribeFetcher(getMvClient(), warehouseId);\n ({ schemas: described, failures } = await syncMetrics(\n { entries: describeNeeded },\n fetcher,\n ));\n\n // Surface DESCRIBE failures loudly: a misconfigured definitions.json would\n // otherwise silently ship an empty entry that the runtime fail-closed gate\n // 503s in production. syncMetrics is log-free; this caller is the single\n // owner of failure logging.\n if (failures.length > 0) {\n for (const f of failures) {\n logger.warn(\n \"metric sync failed for %s (%s): %s\",\n f.key,\n f.source,\n f.reason,\n );\n }\n }\n\n // A rejected DESCRIBE with a connectivity signal is expected to recover on\n // a later pass. In blocking mode, route it through the same committed-types\n // gate as preflight outages instead of treating it as a configuration error.\n if (mode === \"blocking\" && failures.some((failure) => failure.transient)) {\n hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unreachable\";\n }\n\n // Degraded-but-not-failed keys: the warehouse answered with a non-terminal\n // state (stopped / cold-starting), so their schemas are unknown.\n const failedKeys = new Set(failures.map((f) => f.key));\n const degradedKeys = described\n .filter((s) => s.degraded && !failedKeys.has(s.key))\n .map((s) => s.key);\n if (degradedKeys.length > 0) {\n logger.info(\n \"Warehouse %s did not return schemas for %d metric view(s) (%s) — wrote degraded metric types (permissive); they will refresh once the warehouse is available.\",\n warehouseId,\n degradedKeys.length,\n degradedKeys.join(\", \"),\n );\n hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unavailable\";\n }\n } else {\n // Un-probed DESCRIBEs emit degraded schemas; cache hits remain last-known-good.\n described = describeNeeded.map(emptyMetricSchema);\n logger.info(\n \"Warehouse %s is not running — wrote degraded metric types (permissive) for %d metric view(s) (%s); they will refresh once the warehouse is available.\",\n warehouseId,\n describeNeeded.length,\n describeNeeded.map((e) => e.key).join(\", \"),\n );\n hadEnvironmentalFailure = true;\n environmentalCause = environmentalCause ?? \"unavailable\";\n }\n\n // Cache only successful schema results for describe-needed keys; remove stale cache for degraded ones.\n for (let i = 0; i < describeNeeded.length; i++) {\n // syncMetrics return one schema per entry in entry order, so described[i] always belongs to describeNeeded[i].\n const entry = describeNeeded[i];\n if (described[i].degraded === true) {\n delete mvCacheSection[entry.key];\n continue;\n }\n mvCacheSection[entry.key] = {\n hash: metricCacheHash(entry.source, entry.lane),\n schema: described[i],\n // Vestigial, mirrors the query path's only cache write (always false): a\n // persisted entry is by construction a good result, so it never needs a\n // re-describe flag. Kept for on-disk shape compatibility with existing\n // version-3 caches (isRevivableMetricCacheEntry gates on a boolean).\n retry: false,\n };\n }\n\n // Prune entries whose key is no longer configured\n const configuredKeys = new Set(resolution.entries.map((e) => e.key));\n let prunedCount = 0;\n for (const key of Object.keys(mvCacheSection)) {\n if (!configuredKeys.has(key)) {\n delete mvCacheSection[key];\n prunedCount++;\n }\n }\n\n // Save when this pass produced outcomes, bypassed the cache, or pruned.\n if (describeNeeded.length > 0 || noCache || prunedCount > 0) {\n cache.metrics = mvCacheSection;\n await saveCache(cache);\n }\n\n // Merge cached hits with fresh results back into config order.\n const describedByKey = new Map<string, MetricSchema>();\n for (const schema of described) {\n describedByKey.set(schema.key, schema);\n }\n const schemas = resolution.entries.map((entry) => {\n const schema = hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);\n if (schema !== undefined) return schema;\n logger.warn(\n \"no schema resolved for metric key %s — emitting degraded types (should not happen)\",\n entry.key,\n );\n return emptyMetricSchema(entry);\n });\n\n // Same anti-clobber rule as the query path: when suppressDegradedWrite is set\n // (blocking mode), skip the write if any metric degraded, preserving the\n // committed metric-views.d.ts. Non-blocking mode always writes.\n const shouldWriteMetrics =\n !suppressDegradedWrite || !hasAnyDegradedMetrics(schemas);\n\n if (shouldWriteMetrics) {\n await fs.mkdir(path.dirname(metricOutFile), { recursive: true });\n await fs.writeFile(\n metricOutFile,\n generateMetricTypeDeclarations(schemas),\n \"utf-8\",\n );\n\n const bundlePath = path.join(metricViewsFolder, METRIC_METADATA_FILE);\n await fs.writeFile(\n bundlePath,\n `${JSON.stringify(buildMetricMetadataBundle(schemas), null, 2)}\\n`,\n \"utf-8\",\n );\n logger.debug(\"Wrote metric metadata bundle to %s\", bundlePath);\n }\n // Sweep the `metric-views.ts` an interim version left behind, which would\n // otherwise duplicate the augmentation the `.d.ts` emits. Skipped unless the\n // replacement was actually written, so a degraded blocking pass leaves an\n // app's only committed metric types in place.\n if (metricOutFile.endsWith(\".d.ts\") && existsSync(metricOutFile)) {\n const staleTs = `${metricOutFile.slice(0, -\".d.ts\".length)}.ts`;\n try {\n await fs.unlink(staleTs);\n logger.debug(\"Removed stale generated types at %s\", staleTs);\n } catch {\n // No stale sibling — nothing to clean up.\n }\n }\n\n logger.debug(\n \"Wrote MetricRegistry augmentation for %d metric(s)%s\",\n schemas.length,\n failures.length > 0 ? ` (${failures.length} failure(s))` : \"\",\n );\n\n return {\n metricOutFile,\n schemas,\n failures,\n fatalErrors,\n noConfig: false,\n hadEnvironmentalFailure:\n mode === \"blocking\" ? hadEnvironmentalFailure : undefined,\n environmentalCause:\n mode === \"blocking\" && hadEnvironmentalFailure\n ? environmentalCause\n : undefined,\n };\n}\n\n// Rolldown tree-shaking only preserves \"own exports\" (locally defined) — not re-exports.\n// A local binding ensures the serving vite plugin's import keeps this in the dependency graph,\n// mirroring how generateFromEntryPoint (also defined here) is preserved via the analytics vite plugin.\nexport const generateServingTypes = generateServingTypesImpl;\n\n// Re-export the mv-registry types so consumers (CLI, the type-generator\n// .d.ts shim in `packages/shared`) can pick them up from this entry point —\n// the .d.ts shim documents these as part of the package's public surface.\nexport type {\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n};\n\nexport const TYPES_DIR = \"appkit-types\";\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\nexport const METRIC_TYPES_FILE = \"metric-views.d.ts\";\n"],"mappings":"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"}
@@ -1,25 +1,12 @@
1
+ import { METRIC_METADATA_BUNDLE_VERSION } from "../../shared/src/schemas/metric-metadata-bundle.js";
2
+
1
3
  //#region src/type-generator/mv-registry/render-types.ts
2
4
  /**
3
- * @todo unify with query-registry.ts
4
- * Map a Databricks SQL type to a TypeScript primitive.
5
- * Centralized here (not imported from query-registry) so this module
6
- * stays self-contained.
5
+ * Metric results use Databricks' JSON_ARRAY delivery, whose scalar cells are
6
+ * strings regardless of their SQL type. Every selected column can also be SQL
7
+ * NULL.
7
8
  */
8
- function tsTypeFor(sqlType) {
9
- switch (sqlType.toUpperCase().replace(/\(.*\)$/, "").replace(/<.*>$/, "").split(" ")[0]) {
10
- case "BOOLEAN": return "boolean";
11
- case "TINYINT":
12
- case "SMALLINT":
13
- case "INT":
14
- case "INTEGER":
15
- case "BIGINT":
16
- case "FLOAT":
17
- case "DOUBLE":
18
- case "DECIMAL":
19
- case "NUMERIC": return "number";
20
- default: return "string";
21
- }
22
- }
9
+ const JSON_ARRAY_WIRE_TYPE = "string | null";
23
10
  function renderMetricEntry(schema) {
24
11
  if (schema.degraded) return renderDegradedMetricEntry(schema);
25
12
  const indent = " ";
@@ -29,7 +16,7 @@ function renderMetricEntry(schema) {
29
16
  ${cols.map((col) => {
30
17
  const grainComment = col.timeGrains?.length ? ` @timeGrain ${col.timeGrains.join("|")}` : "";
31
18
  return `${indent}/** @sqlType ${col.type.replace(/\*\//g, "* /")}${grainComment} */
32
- ${indent}${JSON.stringify(col.name)}: ${tsTypeFor(col.type)}`;
19
+ ${indent}${JSON.stringify(col.name)}: ${JSON_ARRAY_WIRE_TYPE}`;
33
20
  }).join(";\n")};
34
21
  }`;
35
22
  };
@@ -100,28 +87,33 @@ ${indent}}`;
100
87
  }).join(";\n")};
101
88
  }`;
102
89
  }
103
- function renderMetadataValueField(col) {
104
- return `{ ${metadataFields(col).map(([name, value]) => `${name}: ${value}`).join(", ")} }`;
90
+ function metadataValue(col) {
91
+ const value = { type: col.type };
92
+ if (col.displayName) value.display_name = col.displayName;
93
+ if (col.format) value.format = col.format;
94
+ if (col.description) value.description = col.description;
95
+ return value;
105
96
  }
106
- function renderMetadataValueMap(cols, indent) {
107
- if (cols.length === 0) return "{}";
108
- return `{
109
- ${cols.map((col) => `${indent} ${JSON.stringify(col.name)}: ${renderMetadataValueField(col)}`).join(",\n")},
110
- ${indent}}`;
97
+ function metadataValueMap(cols) {
98
+ const map = {};
99
+ for (const col of cols) map[col.name] = metadataValue(col);
100
+ return map;
111
101
  }
112
- function renderMetricViewsMetadata(schemas) {
113
- if (schemas.length === 0) return "export const metricViewsMetadata = {} as const;\n";
114
- return `export const metricViewsMetadata = {
115
- ${schemas.map((schema) => {
116
- const measures = renderMetadataValueMap(schema.measures, " ");
117
- const dimensions = renderMetadataValueMap(schema.dimensions, " ");
118
- return ` ${JSON.stringify(schema.key)}: {
119
- measures: ${measures},
120
- dimensions: ${dimensions},
121
- }`;
122
- }).join(",\n")},
123
- } as const;
124
- `;
102
+ /**
103
+ * Entries keep the same key order as the augmentation. Degraded schemas
104
+ * contribute empty maps — the warehouse could not describe their columns, so
105
+ * there is no display metadata to stamp.
106
+ */
107
+ function buildMetricMetadataBundle(schemas) {
108
+ const metricViews = {};
109
+ for (const schema of schemas) metricViews[schema.key] = {
110
+ measures: metadataValueMap(schema.measures),
111
+ dimensions: metadataValueMap(schema.dimensions)
112
+ };
113
+ return {
114
+ version: METRIC_METADATA_BUNDLE_VERSION,
115
+ metricViews
116
+ };
125
117
  }
126
118
  function renderMetricRegistry(schemas) {
127
119
  if (schemas.length === 0) return `declare module "@databricks/appkit-ui/react" {
@@ -135,22 +127,13 @@ ${schemas.map(renderMetricEntry).join(";\n")};
135
127
  }
136
128
  `;
137
129
  }
138
- /**
139
- * Build the full metric-views.ts file from a list of metric schemas.
140
- *
141
- * The header must stay a type-only `import type {} from`: it anchors the module
142
- * so the augmentation resolves while compiling to zero runtime code, whereas a
143
- * bare `import "@databricks/appkit-ui/react"` would execute the client package
144
- * entry on the Node server.
145
- */
146
130
  function generateMetricTypeDeclarations(schemas) {
147
131
  return `// Auto-generated by AppKit - DO NOT EDIT
148
132
  // Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build
149
- import type {} from "@databricks/appkit-ui/react";
150
- ${renderMetricRegistry(schemas)}
151
- ${renderMetricViewsMetadata(schemas)}`;
133
+ import "@databricks/appkit-ui/react";
134
+ ${renderMetricRegistry(schemas)}`;
152
135
  }
153
136
 
154
137
  //#endregion
155
- export { generateMetricTypeDeclarations };
138
+ export { buildMetricMetadataBundle, generateMetricTypeDeclarations };
156
139
  //# sourceMappingURL=render-types.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"render-types.js","names":[],"sources":["../../../src/type-generator/mv-registry/render-types.ts"],"sourcesContent":["import type { MetricColumnMetadata, MetricSchema } from \"./types\";\n\n/**\n * @todo unify with query-registry.ts\n * Map a Databricks SQL type to a TypeScript primitive.\n * Centralized here (not imported from query-registry) so this module\n * stays self-contained.\n */\nfunction tsTypeFor(sqlType: string): string {\n const normalized = sqlType\n .toUpperCase()\n .replace(/\\(.*\\)$/, \"\")\n .replace(/<.*>$/, \"\")\n .split(\" \")[0];\n\n switch (normalized) {\n case \"BOOLEAN\":\n return \"boolean\";\n case \"TINYINT\":\n case \"SMALLINT\":\n case \"INT\":\n case \"INTEGER\":\n case \"BIGINT\":\n case \"FLOAT\":\n case \"DOUBLE\":\n case \"DECIMAL\":\n case \"NUMERIC\":\n return \"number\";\n default:\n return \"string\";\n }\n}\n\n// Render a MetricRegistry interface entry from a MetricSchema.\nfunction renderMetricEntry(schema: MetricSchema): string {\n if (schema.degraded) {\n return renderDegradedMetricEntry(schema);\n }\n const indent = \" \";\n const colsBlock = (cols: MetricColumnMetadata[]): string => {\n if (cols.length === 0) return \"Record<string, never>\";\n const fields = cols\n .map((col) => {\n const grainComment = col.timeGrains?.length\n ? ` @timeGrain ${col.timeGrains.join(\"|\")}`\n : \"\";\n return `${indent}/** @sqlType ${col.type.replace(/\\*\\//g, \"* /\")}${grainComment} */\n${indent}${JSON.stringify(col.name)}: ${tsTypeFor(col.type)}`;\n })\n .join(\";\\n\");\n return `{\n${fields};\n }`;\n };\n const unionOf = (keys: string[]): string =>\n keys.length > 0 ? keys.join(\" | \") : \"never\";\n\n const measuresBlock = colsBlock(schema.measures);\n const dimensionsBlock = colsBlock(schema.dimensions);\n const measureUnion = unionOf(\n schema.measures.map((m) => JSON.stringify(m.name)),\n );\n const dimensionUnion = unionOf(\n schema.dimensions.map((d) => JSON.stringify(d.name)),\n );\n\n const timeGrainSet = new Set<string>();\n for (const d of schema.dimensions) {\n for (const g of d.timeGrains ?? []) {\n timeGrainSet.add(g);\n }\n }\n const timeGrainUnion =\n timeGrainSet.size > 0\n ? [...timeGrainSet]\n .sort()\n .map((g) => JSON.stringify(g))\n .join(\" | \")\n : \"never\";\n\n const measureMetadata = renderMetadataMap(schema.measures, indent);\n const dimensionMetadata = renderMetadataMap(schema.dimensions, indent, true);\n\n return ` ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: ${measuresBlock};\n dimensions: ${dimensionsBlock};\n measureKeys: ${measureUnion};\n dimensionKeys: ${dimensionUnion};\n timeGrains: ${timeGrainUnion};\n metadata: {\n measures: ${measureMetadata};\n dimensions: ${dimensionMetadata};\n };\n }`;\n}\n\n// Render the permissive (\"degraded-open\") entry for a schema the warehouse could not describe.\nfunction renderDegradedMetricEntry(schema: MetricSchema): string {\n return ` /** Degraded: schema unavailable at type-generation time — permissive types until a successful DESCRIBE refreshes them. */\n ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: Record<string, unknown>;\n dimensions: Record<string, unknown>;\n measureKeys: string;\n dimensionKeys: string;\n timeGrains: string;\n metadata: {\n measures: Record<string, never>;\n dimensions: Record<string, never>;\n };\n }`;\n}\n\ntype RenderedMetadataField = readonly [name: string, value: string];\n\n// Rendered per-column fields shared by the type-level and runtime metadata.\n// `time_grain` is type-only, so it is included only when requested.\nfunction metadataFields(\n col: MetricColumnMetadata,\n includeTimeGrain = false,\n): RenderedMetadataField[] {\n const fields: RenderedMetadataField[] = [[\"type\", JSON.stringify(col.type)]];\n const optionalFields = [\n [\"display_name\", col.displayName],\n [\"format\", col.format],\n [\"description\", col.description],\n ] as const;\n\n for (const [name, value] of optionalFields) {\n if (value) {\n fields.push([name, JSON.stringify(value)]);\n }\n }\n\n if (includeTimeGrain && col.timeGrains && col.timeGrains.length > 0) {\n const grainTuple = col.timeGrains.map((g) => JSON.stringify(g)).join(\", \");\n fields.push([\"time_grain\", `readonly [${grainTuple}]`]);\n }\n\n return fields;\n}\n\n// Render the type-level shape of a column's semantic-metadata map\n// for the `metadata` field of a MetricRegistry entry.\nfunction renderMetadataMap(\n cols: MetricColumnMetadata[],\n indent: string,\n includeTimeGrain = false,\n): string {\n if (cols.length === 0) return \"Record<string, never>\";\n\n const inner = cols\n .map((col) => {\n const fieldsBlock = metadataFields(col, includeTimeGrain)\n .map(([name, value]) => `${indent} ${name}: ${value}`)\n .join(\";\\n\");\n return `${indent}${JSON.stringify(col.name)}: {\n${fieldsBlock};\n${indent}}`;\n })\n .join(\";\\n\");\n\n return `{\n${inner};\n }`;\n}\n\n// Value-side twin of a `renderMetadataMap` entry, minus `time_grain` (not part\n// of MetricViewColumnDisplay).\nfunction renderMetadataValueField(col: MetricColumnMetadata): string {\n const fields = metadataFields(col).map(\n ([name, value]) => `${name}: ${value}`,\n );\n return `{ ${fields.join(\", \")} }`;\n}\n\n// Render one metric's runtime measures/dimensions map, keyed by column name.\nfunction renderMetadataValueMap(\n cols: MetricColumnMetadata[],\n indent: string,\n): string {\n if (cols.length === 0) return \"{}\";\n const inner = cols\n .map(\n (col) =>\n `${indent} ${JSON.stringify(col.name)}: ${renderMetadataValueField(col)}`,\n )\n .join(\",\\n\");\n return `{\n${inner},\n${indent}}`;\n}\n\n// Render the runtime `metricViewsMetadata` const, emitted `as const` in the\n// same key order as the augmentation.\nfunction renderMetricViewsMetadata(schemas: MetricSchema[]): string {\n if (schemas.length === 0) {\n return \"export const metricViewsMetadata = {} as const;\\n\";\n }\n const entries = schemas\n .map((schema) => {\n const measures = renderMetadataValueMap(schema.measures, \" \");\n const dimensions = renderMetadataValueMap(schema.dimensions, \" \");\n return ` ${JSON.stringify(schema.key)}: {\n measures: ${measures},\n dimensions: ${dimensions},\n }`;\n })\n .join(\",\\n\");\n return `export const metricViewsMetadata = {\n${entries},\n} as const;\n`;\n}\n\n// Render the augmentation block for the appkit-ui MetricRegistry interface.\nfunction renderMetricRegistry(schemas: MetricSchema[]): string {\n if (schemas.length === 0) {\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {}\n}\n`;\n }\n const entries = schemas.map(renderMetricEntry).join(\";\\n\");\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {\n${entries};\n }\n}\n`;\n}\n\n/**\n * Build the full metric-views.ts file from a list of metric schemas.\n *\n * The header must stay a type-only `import type {} from`: it anchors the module\n * so the augmentation resolves while compiling to zero runtime code, whereas a\n * bare `import \"@databricks/appkit-ui/react\"` would execute the client package\n * entry on the Node server.\n */\nexport function generateMetricTypeDeclarations(\n schemas: MetricSchema[],\n): string {\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport type {} from \"@databricks/appkit-ui/react\";\n${renderMetricRegistry(schemas)}\n${renderMetricViewsMetadata(schemas)}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"render-types.js","names":[],"sources":["../../../src/type-generator/mv-registry/render-types.ts"],"sourcesContent":["import type { MetricViewColumnDisplay } from \"../../../../shared/src/metric-metadata\";\nimport {\n METRIC_METADATA_BUNDLE_VERSION,\n type MetricMetadataBundle,\n} from \"../../../../shared/src/schemas/metric-metadata-bundle\";\nimport type { MetricColumnMetadata, MetricSchema } from \"./types\";\n\n/**\n * Metric results use Databricks' JSON_ARRAY delivery, whose scalar cells are\n * strings regardless of their SQL type. Every selected column can also be SQL\n * NULL.\n */\nconst JSON_ARRAY_WIRE_TYPE = \"string | null\";\n\n// Render a MetricRegistry interface entry from a MetricSchema.\nfunction renderMetricEntry(schema: MetricSchema): string {\n if (schema.degraded) {\n return renderDegradedMetricEntry(schema);\n }\n const indent = \" \";\n const colsBlock = (cols: MetricColumnMetadata[]): string => {\n if (cols.length === 0) return \"Record<string, never>\";\n const fields = cols\n .map((col) => {\n const grainComment = col.timeGrains?.length\n ? ` @timeGrain ${col.timeGrains.join(\"|\")}`\n : \"\";\n return `${indent}/** @sqlType ${col.type.replace(/\\*\\//g, \"* /\")}${grainComment} */\n${indent}${JSON.stringify(col.name)}: ${JSON_ARRAY_WIRE_TYPE}`;\n })\n .join(\";\\n\");\n return `{\n${fields};\n }`;\n };\n const unionOf = (keys: string[]): string =>\n keys.length > 0 ? keys.join(\" | \") : \"never\";\n\n const measuresBlock = colsBlock(schema.measures);\n const dimensionsBlock = colsBlock(schema.dimensions);\n const measureUnion = unionOf(\n schema.measures.map((m) => JSON.stringify(m.name)),\n );\n const dimensionUnion = unionOf(\n schema.dimensions.map((d) => JSON.stringify(d.name)),\n );\n\n const timeGrainSet = new Set<string>();\n for (const d of schema.dimensions) {\n for (const g of d.timeGrains ?? []) {\n timeGrainSet.add(g);\n }\n }\n const timeGrainUnion =\n timeGrainSet.size > 0\n ? [...timeGrainSet]\n .sort()\n .map((g) => JSON.stringify(g))\n .join(\" | \")\n : \"never\";\n\n const measureMetadata = renderMetadataMap(schema.measures, indent);\n const dimensionMetadata = renderMetadataMap(schema.dimensions, indent, true);\n\n return ` ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: ${measuresBlock};\n dimensions: ${dimensionsBlock};\n measureKeys: ${measureUnion};\n dimensionKeys: ${dimensionUnion};\n timeGrains: ${timeGrainUnion};\n metadata: {\n measures: ${measureMetadata};\n dimensions: ${dimensionMetadata};\n };\n }`;\n}\n\n// Render the permissive (\"degraded-open\") entry for a schema the warehouse could not describe.\nfunction renderDegradedMetricEntry(schema: MetricSchema): string {\n return ` /** Degraded: schema unavailable at type-generation time — permissive types until a successful DESCRIBE refreshes them. */\n ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: Record<string, unknown>;\n dimensions: Record<string, unknown>;\n measureKeys: string;\n dimensionKeys: string;\n timeGrains: string;\n metadata: {\n measures: Record<string, never>;\n dimensions: Record<string, never>;\n };\n }`;\n}\n\ntype RenderedMetadataField = readonly [name: string, value: string];\n\n// Rendered per-column fields shared by the type-level and runtime metadata.\n// `time_grain` is type-only, so it is included only when requested.\nfunction metadataFields(\n col: MetricColumnMetadata,\n includeTimeGrain = false,\n): RenderedMetadataField[] {\n const fields: RenderedMetadataField[] = [[\"type\", JSON.stringify(col.type)]];\n const optionalFields = [\n [\"display_name\", col.displayName],\n [\"format\", col.format],\n [\"description\", col.description],\n ] as const;\n\n for (const [name, value] of optionalFields) {\n if (value) {\n fields.push([name, JSON.stringify(value)]);\n }\n }\n\n if (includeTimeGrain && col.timeGrains && col.timeGrains.length > 0) {\n const grainTuple = col.timeGrains.map((g) => JSON.stringify(g)).join(\", \");\n fields.push([\"time_grain\", `readonly [${grainTuple}]`]);\n }\n\n return fields;\n}\n\n// Render the type-level shape of a column's semantic-metadata map\n// for the `metadata` field of a MetricRegistry entry.\nfunction renderMetadataMap(\n cols: MetricColumnMetadata[],\n indent: string,\n includeTimeGrain = false,\n): string {\n if (cols.length === 0) return \"Record<string, never>\";\n\n const inner = cols\n .map((col) => {\n const fieldsBlock = metadataFields(col, includeTimeGrain)\n .map(([name, value]) => `${indent} ${name}: ${value}`)\n .join(\";\\n\");\n return `${indent}${JSON.stringify(col.name)}: {\n${fieldsBlock};\n${indent}}`;\n })\n .join(\";\\n\");\n\n return `{\n${inner};\n }`;\n}\n\n// Value-side twin of a `renderMetadataMap` entry, minus `time_grain` (which is\n// type-only and not part of MetricViewColumnDisplay).\nfunction metadataValue(col: MetricColumnMetadata): MetricViewColumnDisplay {\n const value: MetricViewColumnDisplay = { type: col.type };\n if (col.displayName) value.display_name = col.displayName;\n if (col.format) value.format = col.format;\n if (col.description) value.description = col.description;\n return value;\n}\n\nfunction metadataValueMap(\n cols: MetricColumnMetadata[],\n): Record<string, MetricViewColumnDisplay> {\n const map: Record<string, MetricViewColumnDisplay> = {};\n for (const col of cols) {\n map[col.name] = metadataValue(col);\n }\n return map;\n}\n\n/**\n * Entries keep the same key order as the augmentation. Degraded schemas\n * contribute empty maps — the warehouse could not describe their columns, so\n * there is no display metadata to stamp.\n */\nexport function buildMetricMetadataBundle(\n schemas: MetricSchema[],\n): MetricMetadataBundle {\n const metricViews: MetricMetadataBundle[\"metricViews\"] = {};\n for (const schema of schemas) {\n metricViews[schema.key] = {\n measures: metadataValueMap(schema.measures),\n dimensions: metadataValueMap(schema.dimensions),\n };\n }\n return { version: METRIC_METADATA_BUNDLE_VERSION, metricViews };\n}\n\n// Render the augmentation block for the appkit-ui MetricRegistry interface.\nfunction renderMetricRegistry(schemas: MetricSchema[]): string {\n if (schemas.length === 0) {\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {}\n}\n`;\n }\n const entries = schemas.map(renderMetricEntry).join(\";\\n\");\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {\n${entries};\n }\n}\n`;\n}\n\nexport function generateMetricTypeDeclarations(\n schemas: MetricSchema[],\n): string {\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\n${renderMetricRegistry(schemas)}`;\n}\n"],"mappings":"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"}
@@ -180,7 +180,6 @@ function appKitTypesPlugin(options) {
180
180
  configResolved(config) {
181
181
  const projectRoot = path.resolve(config.root, "..");
182
182
  outFile = path.resolve(projectRoot, options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`);
183
- if (options?.mvOutFile?.endsWith(".d.ts")) throw new Error(`appKitTypesPlugin: mvOutFile must be a .ts file, not a .d.ts (got "${options.mvOutFile}"). The metric-views file carries a runtime const, which cannot live in an ambient .d.ts.`);
184
183
  mvOutFile = options?.mvOutFile !== void 0 ? path.resolve(projectRoot, options.mvOutFile) : void 0;
185
184
  const defaultQueryFolder = path.join(process.cwd(), "config", "queries");
186
185
  const defaultMetricViewsFolder = path.join(process.cwd(), "config", "metric-views");
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { METRIC_CONFIG_FILE } from \"../../../shared/src/schemas/metric-fqn\";\nimport { createLogger } from \"../logging/logger\";\nimport { createWorkspaceClient } from \"../workspace-client\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n TypegenFatalError,\n TypegenSyntaxError,\n} from \"./index\";\nimport type { PreflightMode } from \"./preflight\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * How long the DEV background watcher waits for a STARTING warehouse to reach\n * RUNNING before giving up. Short relative to the CLI's preflight budget: this\n * is a best-effort \"regenerate once the warehouse warms up\" convenience, not a\n * gate, so we'd rather stop polling than hold a detached task open for minutes.\n */\nconst DEV_WAREHOUSE_WATCH_MAX_MS = 60_000;\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /**\n * Path to the metric registry `.ts` file (relative to client folder).\n * Defaults to a sibling of `outFile`, computed by the generator. The\n * generated source carries both the `declare module` augmentation and the\n * runtime `metricViewsMetadata` const, so it is a real `.ts`, not a `.d.ts`.\n */\n mvOutFile?: string;\n /**\n * Folders to watch for changes. Defaults to `config/queries` and\n * `config/metric-views`. When overridden, include a `queries` folder and/or a\n * `metric-views` folder — they are resolved by their trailing path segment.\n */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let mvOutFile: string | undefined;\n let watchFolders: string[];\n // The queries + metric-views config folders, resolved in `configResolved`.\n // Passed explicitly into generateFromEntryPoint so neither is inferred from\n // `watchFolders` ordering (which used to assume queries was `watchFolders[0]`).\n let queryFolder: string | undefined;\n let metricViewsFolder: string | undefined;\n\n // Single-flight state for runGenerate(). `inFlight` is the promise of the\n // currently-running drain (null when idle); `queued` records that a trigger\n // arrived while a run was active so exactly ONE trailing run fires afterwards\n // (latest-wins — coalesces any number of overlapping triggers into a single\n // rerun). `queued` is read/cleared synchronously inside the drain loop so a\n // trigger landing in any window is caught before the drain exits.\n //\n // `pendingMode` is the mode the next generate should run in (latest-wins, like\n // `queued`): the foreground build runs non-blocking in dev (instant degrade)\n // while the background warehouse watch runs blocking (real DESCRIBEs). A\n // blocking watch trigger that lands while a non-blocking foreground run is in\n // flight therefore still describes when its trailing run fires.\n let inFlight: Promise<void> | null = null;\n let queued = false;\n let pendingMode: PreflightMode = \"non-blocking\";\n\n // The currently-armed DEV background warehouse watch, if any. Aborting it\n // stops a pending waitUntilRunning (server shutdown, or a newer arm replacing\n // an older one).\n let watchController: AbortController | null = null;\n\n /**\n * Generate types once in the given preflight {@link PreflightMode}. Never\n * throws in dev (logs instead); in production it rethrows so the build fails.\n * This is the un-guarded core — callers should go through {@link runGenerate}\n * so concurrent triggers can't race-write the .d.ts.\n *\n * @param mode - preflight policy for this run. The foreground build passes a\n * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it\n * degrades instantly); the background warehouse watch passes \"blocking\" so\n * its regenerate actually DESCRIBEs and lands real (non-degraded) types.\n */\n async function generateOnce(mode: PreflightMode) {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder,\n metricViewsFolder,\n warehouseId,\n noCache: false,\n mode,\n mvOutFile,\n });\n } catch (error) {\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // report in their message. Their stack frames and attached query arrays\n // point into appkit internals and only add noise, so surface just the\n // message — both when failing the prod build and when logging in dev.\n const isTypegenError =\n error instanceof TypegenSyntaxError ||\n error instanceof TypegenFatalError;\n\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n if (isTypegenError) error.stack = error.message;\n throw error;\n }\n\n if (isTypegenError) {\n logger.error(\"%s\", error.message);\n } else {\n logger.error(\"Error generating types: %O\", error);\n }\n }\n }\n\n /**\n * Single-flight wrapper around {@link generateOnce}. The initial build, the\n * .sql watcher, and the DEV warehouse watch all route through here so they can\n * never run typegen concurrently (which would race-write the .d.ts).\n *\n * If a run is already in flight, this does NOT start a second one — it records\n * the requested mode and sets a trailing flag so exactly one more run fires\n * after the current finishes, coalescing any number of overlapping triggers\n * (latest-wins, including the mode: a blocking watch trigger that arrives mid\n * non-blocking foreground run still describes when its trailing run fires).\n *\n * @param mode - preflight policy for this run. Recorded into `pendingMode`,\n * which the drain reads for each generate (latest trigger wins).\n * @returns A promise that resolves when this trigger's work (including any\n * trailing run it scheduled) has completed.\n */\n function runGenerate(mode: PreflightMode): Promise<void> {\n pendingMode = mode;\n\n if (inFlight) {\n // A run is active: remember that another trigger arrived and ride out the\n // current run. One trailing run then covers all coalesced triggers and\n // runs in the latest requested mode (recorded above).\n queued = true;\n return inFlight;\n }\n\n // Drain in a loop rather than recursing after a single queued-check: a\n // trigger can land in the window between generateOnce() resolving and the\n // check, so we re-test `queued` until it's clear. Critically, `inFlight` is\n // cleared synchronously in the SAME tick as the final `queued === false`\n // observation — never deferred to a .finally microtask — so there's no\n // window where a trigger sees `inFlight` set but the drain has already\n // decided to exit. The guard stays held for the whole drain, so concurrent\n // triggers only ever set the flag; they never start a parallel generate.\n const drain = async (): Promise<void> => {\n while (true) {\n queued = false;\n // Snapshot the mode synchronously alongside clearing `queued` so a\n // trigger landing during this generate is observed (via `queued`) on the\n // next loop with its own mode, not silently dropped.\n const runMode = pendingMode;\n await generateOnce(runMode);\n // Synchronous check + clear, atomic w.r.t. other (synchronous) callers.\n if (!queued) {\n inFlight = null;\n return;\n }\n }\n };\n\n inFlight = drain();\n return inFlight;\n }\n\n /**\n * DEV-only: get the warehouse to RUNNING in the background and regenerate with\n * real (non-degraded) types once it is — without blocking dev startup. The\n * foreground build only ever degrades in dev (instant `unknown`/cached types),\n * so this is what lands actual DESCRIBE results in the editor for EVERY\n * reachable warehouse state, not just one that happens to already be warm.\n *\n * Post-probe behaviour by state:\n * - RUNNING → describe right away (the dev foreground degraded, so a running\n * warehouse would otherwise never get real types). `waitUntilRunning`\n * returns immediately for an already-running warehouse, then the blocking\n * regenerate fires.\n * - STARTING → it's already coming up; just wait for RUNNING, then describe.\n * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.\n * - DELETED / DELETING → return (a deleted warehouse can't be started, and\n * blocking typegen would treat it as fatal); leave the degraded types.\n *\n * No-op in production or without a warehouse id. Replaces any previously-armed\n * watch (aborting it first). Fully self-contained: it never throws into the\n * caller and never re-arms itself. The whole lifecycle is abortable via the\n * shared {@link watchController} — its signal is threaded into\n * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the\n * regenerate routes through {@link runGenerate} so it can't race-write the\n * .d.ts with the foreground degrade or a `.sql` re-trigger.\n *\n * The regenerate runs in \"blocking\" mode (not the foreground's non-blocking)\n * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —\n * the whole point of warming the warehouse in the background.\n */\n function armWarehouseWatch(): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n if (!warehouseId) return;\n\n // Supersede any in-flight watch so we never run two concurrently.\n watchController?.abort();\n const controller = new AbortController();\n watchController = controller;\n const { signal } = controller;\n\n void (async () => {\n try {\n const client = createWorkspaceClient();\n const state = await getWarehouseState(client, warehouseId);\n\n // A deleted/deleting warehouse can't be started and blocking typegen\n // would treat it as fatal — leave the degraded types and stop. Every\n // other state (including RUNNING) proceeds to wait-then-describe so the\n // dev editor gets real types, not just the foreground's degraded ones.\n if (state === \"DELETED\" || state === \"DELETING\") {\n return;\n }\n\n // Stopped/stopping won't reach RUNNING on its own — nudge it. RUNNING and\n // STARTING need no start (RUNNING is already up; STARTING is coming up),\n // so don't issue a redundant one. A failed start is non-fatal: give up\n // silently rather than throw out of the detached task (the developer\n // still has degraded/cached types).\n let startedByUs = false;\n if (state === \"STOPPED\" || state === \"STOPPING\") {\n try {\n logger.debug(\"Warehouse is %s; starting it.\", state);\n await startWarehouse(client, warehouseId);\n startedByUs = true;\n } catch {\n return;\n }\n }\n\n // Wait for RUNNING. For an already-RUNNING warehouse this returns on the\n // first poll; for STARTING/STOPPED it polls (abortably) until the\n // warehouse warms up, a terminal state, or the deadline.\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,\n signal,\n // We just issued the start, so the first poll(s) often still report\n // STOPPED/STOPPING before the start propagates. Poll through those\n // instead of bailing, or the regenerate would never fire. When we\n // didn't start it (RUNNING/STARTING branch), keep the default terminal\n // states.\n treatStoppedAsTransient: startedByUs,\n });\n\n if (final === \"RUNNING\" && !signal.aborted) {\n logger.debug(\"Warehouse is RUNNING; regenerating types.\");\n // Blocking: the warehouse is RUNNING now, so describe it and emit real\n // (non-degraded) types — unlike the foreground dev run, which degraded.\n // Routed through the single-flight guard so it coalesces with the\n // foreground degrade / any `.sql` re-trigger instead of racing them.\n await runGenerate(\"blocking\");\n }\n } catch {\n // Detached background task: any failure (timeout, abort, connectivity,\n // auth) is non-fatal — the developer still has degraded/cached types.\n }\n })();\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n // Run when either config surface exists. Metric-view types are\n // independent of `.sql` queries, so a metric-only project (a\n // `config/metric-views/` with no `config/queries/`) must still activate\n // the plugin.\n const hasQueries = existsSync(\n path.join(process.cwd(), \"config\", \"queries\"),\n );\n const hasMetricViews = existsSync(\n path.join(process.cwd(), \"config\", \"metric-views\"),\n );\n if (!hasQueries && !hasMetricViews) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n // The metric out-path resolves against projectRoot only when explicitly\n // provided; an unset option passes through as undefined so the generator\n // computes its sibling-of-outFile default. In the all-defaults case the\n // final path is identical (the default outFile above lives in\n // shared/<TYPES_DIR>/), and a customized outFile now keeps its metric\n // sibling next to it instead of pinning it under shared/.\n //\n // Reject a `.d.ts` metric out-path up front: the metric file is a real\n // `.ts` source carrying a runtime `const` (metricViewsMetadata), which is\n // illegal inside an ambient declaration file (TS1039). Fail fast with a\n // clear message rather than emitting a file that won't compile.\n if (options?.mvOutFile?.endsWith(\".d.ts\")) {\n throw new Error(\n `appKitTypesPlugin: mvOutFile must be a .ts file, not a .d.ts (got \"${options.mvOutFile}\"). ` +\n \"The metric-views file carries a runtime const, which cannot live in an ambient .d.ts.\",\n );\n }\n mvOutFile =\n options?.mvOutFile !== undefined\n ? path.resolve(projectRoot, options.mvOutFile)\n : undefined;\n\n const defaultQueryFolder = path.join(process.cwd(), \"config\", \"queries\");\n const defaultMetricViewsFolder = path.join(\n process.cwd(),\n \"config\",\n \"metric-views\",\n );\n watchFolders = options?.watchFolders ?? [\n defaultQueryFolder,\n defaultMetricViewsFolder,\n ];\n\n // Resolve the two config folders explicitly rather than assuming a\n // position in `watchFolders`. With a custom `watchFolders`, match by the\n // trailing segment; otherwise use the computed defaults.\n if (options?.watchFolders) {\n queryFolder = watchFolders.find((f) => path.basename(f) === \"queries\");\n metricViewsFolder = watchFolders.find(\n (f) => path.basename(f) === \"metric-views\",\n );\n } else {\n queryFolder = defaultQueryFolder;\n metricViewsFolder = defaultMetricViewsFolder;\n }\n },\n\n buildStart() {\n // Production: block the build on this generate (and surface failures).\n // The watch is a dev-only no-op, so just run typegen.\n if (process.env.NODE_ENV === \"production\") {\n return runGenerate(\"blocking\");\n }\n\n // Dev: don't block startup waiting on typegen. The foreground generate runs\n // non-blocking — it skips the warehouse entirely and writes degraded\n // (cached/`unknown`) types instantly. Then arm the warehouse watch so the\n // warehouse gets a one-shot BLOCKING regenerate (real types) in the\n // background for EVERY reachable state: RUNNING describes right away, while\n // STARTING/STOPPED are waited (and started) until they reach RUNNING.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n // The metric config is `definitions.json` — a far more generic name\n // than the old `metric-views.json`. Match it by DIRECTORY, not bare\n // basename: only a `definitions.json` sitting directly in the\n // metric-views folder is the config (a `definitions.json` elsewhere in\n // a watched tree must not trigger a regenerate).\n const isMetricConfig =\n metricViewsFolder !== undefined &&\n path.basename(changedFile) === METRIC_CONFIG_FILE &&\n path.dirname(path.resolve(changedFile)) ===\n path.resolve(metricViewsFolder);\n\n if (isWatchedFile && (changedFile.endsWith(\".sql\") || isMetricConfig)) {\n // Route through the single-flight runner (was fire-and-forget\n // generate(), which could race the initial build / watch). This is a\n // dev-only hook, so degrade instantly (non-blocking), then re-arm the\n // warehouse watch so the edited query or metric-view source is\n // re-described in the background against the running warehouse (or\n // once a still-starting one warms up), landing fresh\n // blocking-described types.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n }\n });\n\n // Tear down any pending warehouse watch when the dev server closes so a\n // long backoff can't keep the process alive after shutdown.\n server.httpServer?.once(\"close\", () => {\n watchController?.abort();\n });\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport type { Plugin } from \"vite\";\nimport { METRIC_CONFIG_FILE } from \"../../../shared/src/schemas/metric-fqn\";\nimport { createLogger } from \"../logging/logger\";\nimport { createWorkspaceClient } from \"../workspace-client\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n TypegenFatalError,\n TypegenSyntaxError,\n} from \"./index\";\nimport type { PreflightMode } from \"./preflight\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * How long the DEV background watcher waits for a STARTING warehouse to reach\n * RUNNING before giving up. Short relative to the CLI's preflight budget: this\n * is a best-effort \"regenerate once the warehouse warms up\" convenience, not a\n * gate, so we'd rather stop polling than hold a detached task open for minutes.\n */\nconst DEV_WAREHOUSE_WATCH_MAX_MS = 60_000;\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /**\n * Path to the metric registry `.ts` file (relative to client folder).\n * Defaults to a sibling of `outFile`, computed by the generator. The\n * generated source carries both the `declare module` augmentation and the\n * runtime `metricViewsMetadata` const, so it is a real `.ts`, not a `.d.ts`.\n */\n mvOutFile?: string;\n /**\n * Folders to watch for changes. Defaults to `config/queries` and\n * `config/metric-views`. When overridden, include a `queries` folder and/or a\n * `metric-views` folder — they are resolved by their trailing path segment.\n */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let mvOutFile: string | undefined;\n let watchFolders: string[];\n // The queries + metric-views config folders, resolved in `configResolved`.\n // Passed explicitly into generateFromEntryPoint so neither is inferred from\n // `watchFolders` ordering (which used to assume queries was `watchFolders[0]`).\n let queryFolder: string | undefined;\n let metricViewsFolder: string | undefined;\n\n // Single-flight state for runGenerate(). `inFlight` is the promise of the\n // currently-running drain (null when idle); `queued` records that a trigger\n // arrived while a run was active so exactly ONE trailing run fires afterwards\n // (latest-wins — coalesces any number of overlapping triggers into a single\n // rerun). `queued` is read/cleared synchronously inside the drain loop so a\n // trigger landing in any window is caught before the drain exits.\n //\n // `pendingMode` is the mode the next generate should run in (latest-wins, like\n // `queued`): the foreground build runs non-blocking in dev (instant degrade)\n // while the background warehouse watch runs blocking (real DESCRIBEs). A\n // blocking watch trigger that lands while a non-blocking foreground run is in\n // flight therefore still describes when its trailing run fires.\n let inFlight: Promise<void> | null = null;\n let queued = false;\n let pendingMode: PreflightMode = \"non-blocking\";\n\n // The currently-armed DEV background warehouse watch, if any. Aborting it\n // stops a pending waitUntilRunning (server shutdown, or a newer arm replacing\n // an older one).\n let watchController: AbortController | null = null;\n\n /**\n * Generate types once in the given preflight {@link PreflightMode}. Never\n * throws in dev (logs instead); in production it rethrows so the build fails.\n * This is the un-guarded core — callers should go through {@link runGenerate}\n * so concurrent triggers can't race-write the .d.ts.\n *\n * @param mode - preflight policy for this run. The foreground build passes a\n * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it\n * degrades instantly); the background warehouse watch passes \"blocking\" so\n * its regenerate actually DESCRIBEs and lands real (non-degraded) types.\n */\n async function generateOnce(mode: PreflightMode) {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder,\n metricViewsFolder,\n warehouseId,\n noCache: false,\n mode,\n mvOutFile,\n });\n } catch (error) {\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // report in their message. Their stack frames and attached query arrays\n // point into appkit internals and only add noise, so surface just the\n // message — both when failing the prod build and when logging in dev.\n const isTypegenError =\n error instanceof TypegenSyntaxError ||\n error instanceof TypegenFatalError;\n\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n if (isTypegenError) error.stack = error.message;\n throw error;\n }\n\n if (isTypegenError) {\n logger.error(\"%s\", error.message);\n } else {\n logger.error(\"Error generating types: %O\", error);\n }\n }\n }\n\n /**\n * Single-flight wrapper around {@link generateOnce}. The initial build, the\n * .sql watcher, and the DEV warehouse watch all route through here so they can\n * never run typegen concurrently (which would race-write the .d.ts).\n *\n * If a run is already in flight, this does NOT start a second one — it records\n * the requested mode and sets a trailing flag so exactly one more run fires\n * after the current finishes, coalescing any number of overlapping triggers\n * (latest-wins, including the mode: a blocking watch trigger that arrives mid\n * non-blocking foreground run still describes when its trailing run fires).\n *\n * @param mode - preflight policy for this run. Recorded into `pendingMode`,\n * which the drain reads for each generate (latest trigger wins).\n * @returns A promise that resolves when this trigger's work (including any\n * trailing run it scheduled) has completed.\n */\n function runGenerate(mode: PreflightMode): Promise<void> {\n pendingMode = mode;\n\n if (inFlight) {\n // A run is active: remember that another trigger arrived and ride out the\n // current run. One trailing run then covers all coalesced triggers and\n // runs in the latest requested mode (recorded above).\n queued = true;\n return inFlight;\n }\n\n // Drain in a loop rather than recursing after a single queued-check: a\n // trigger can land in the window between generateOnce() resolving and the\n // check, so we re-test `queued` until it's clear. Critically, `inFlight` is\n // cleared synchronously in the SAME tick as the final `queued === false`\n // observation — never deferred to a .finally microtask — so there's no\n // window where a trigger sees `inFlight` set but the drain has already\n // decided to exit. The guard stays held for the whole drain, so concurrent\n // triggers only ever set the flag; they never start a parallel generate.\n const drain = async (): Promise<void> => {\n while (true) {\n queued = false;\n // Snapshot the mode synchronously alongside clearing `queued` so a\n // trigger landing during this generate is observed (via `queued`) on the\n // next loop with its own mode, not silently dropped.\n const runMode = pendingMode;\n await generateOnce(runMode);\n // Synchronous check + clear, atomic w.r.t. other (synchronous) callers.\n if (!queued) {\n inFlight = null;\n return;\n }\n }\n };\n\n inFlight = drain();\n return inFlight;\n }\n\n /**\n * DEV-only: get the warehouse to RUNNING in the background and regenerate with\n * real (non-degraded) types once it is — without blocking dev startup. The\n * foreground build only ever degrades in dev (instant `unknown`/cached types),\n * so this is what lands actual DESCRIBE results in the editor for EVERY\n * reachable warehouse state, not just one that happens to already be warm.\n *\n * Post-probe behaviour by state:\n * - RUNNING → describe right away (the dev foreground degraded, so a running\n * warehouse would otherwise never get real types). `waitUntilRunning`\n * returns immediately for an already-running warehouse, then the blocking\n * regenerate fires.\n * - STARTING → it's already coming up; just wait for RUNNING, then describe.\n * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.\n * - DELETED / DELETING → return (a deleted warehouse can't be started, and\n * blocking typegen would treat it as fatal); leave the degraded types.\n *\n * No-op in production or without a warehouse id. Replaces any previously-armed\n * watch (aborting it first). Fully self-contained: it never throws into the\n * caller and never re-arms itself. The whole lifecycle is abortable via the\n * shared {@link watchController} — its signal is threaded into\n * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the\n * regenerate routes through {@link runGenerate} so it can't race-write the\n * .d.ts with the foreground degrade or a `.sql` re-trigger.\n *\n * The regenerate runs in \"blocking\" mode (not the foreground's non-blocking)\n * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —\n * the whole point of warming the warehouse in the background.\n */\n function armWarehouseWatch(): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n if (!warehouseId) return;\n\n // Supersede any in-flight watch so we never run two concurrently.\n watchController?.abort();\n const controller = new AbortController();\n watchController = controller;\n const { signal } = controller;\n\n void (async () => {\n try {\n const client = createWorkspaceClient();\n const state = await getWarehouseState(client, warehouseId);\n\n // A deleted/deleting warehouse can't be started and blocking typegen\n // would treat it as fatal — leave the degraded types and stop. Every\n // other state (including RUNNING) proceeds to wait-then-describe so the\n // dev editor gets real types, not just the foreground's degraded ones.\n if (state === \"DELETED\" || state === \"DELETING\") {\n return;\n }\n\n // Stopped/stopping won't reach RUNNING on its own — nudge it. RUNNING and\n // STARTING need no start (RUNNING is already up; STARTING is coming up),\n // so don't issue a redundant one. A failed start is non-fatal: give up\n // silently rather than throw out of the detached task (the developer\n // still has degraded/cached types).\n let startedByUs = false;\n if (state === \"STOPPED\" || state === \"STOPPING\") {\n try {\n logger.debug(\"Warehouse is %s; starting it.\", state);\n await startWarehouse(client, warehouseId);\n startedByUs = true;\n } catch {\n return;\n }\n }\n\n // Wait for RUNNING. For an already-RUNNING warehouse this returns on the\n // first poll; for STARTING/STOPPED it polls (abortably) until the\n // warehouse warms up, a terminal state, or the deadline.\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,\n signal,\n // We just issued the start, so the first poll(s) often still report\n // STOPPED/STOPPING before the start propagates. Poll through those\n // instead of bailing, or the regenerate would never fire. When we\n // didn't start it (RUNNING/STARTING branch), keep the default terminal\n // states.\n treatStoppedAsTransient: startedByUs,\n });\n\n if (final === \"RUNNING\" && !signal.aborted) {\n logger.debug(\"Warehouse is RUNNING; regenerating types.\");\n // Blocking: the warehouse is RUNNING now, so describe it and emit real\n // (non-degraded) types — unlike the foreground dev run, which degraded.\n // Routed through the single-flight guard so it coalesces with the\n // foreground degrade / any `.sql` re-trigger instead of racing them.\n await runGenerate(\"blocking\");\n }\n } catch {\n // Detached background task: any failure (timeout, abort, connectivity,\n // auth) is non-fatal — the developer still has degraded/cached types.\n }\n })();\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n // Run when either config surface exists. Metric-view types are\n // independent of `.sql` queries, so a metric-only project (a\n // `config/metric-views/` with no `config/queries/`) must still activate\n // the plugin.\n const hasQueries = existsSync(\n path.join(process.cwd(), \"config\", \"queries\"),\n );\n const hasMetricViews = existsSync(\n path.join(process.cwd(), \"config\", \"metric-views\"),\n );\n if (!hasQueries && !hasMetricViews) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n // The metric out-path resolves against projectRoot only when explicitly\n // provided; an unset option passes through as undefined so the generator\n // computes its sibling-of-outFile default. In the all-defaults case the\n // final path is identical (the default outFile above lives in\n // shared/<TYPES_DIR>/), and a customized outFile now keeps its metric\n // sibling next to it instead of pinning it under shared/.\n mvOutFile =\n options?.mvOutFile !== undefined\n ? path.resolve(projectRoot, options.mvOutFile)\n : undefined;\n\n const defaultQueryFolder = path.join(process.cwd(), \"config\", \"queries\");\n const defaultMetricViewsFolder = path.join(\n process.cwd(),\n \"config\",\n \"metric-views\",\n );\n watchFolders = options?.watchFolders ?? [\n defaultQueryFolder,\n defaultMetricViewsFolder,\n ];\n\n // Resolve the two config folders explicitly rather than assuming a\n // position in `watchFolders`. With a custom `watchFolders`, match by the\n // trailing segment; otherwise use the computed defaults.\n if (options?.watchFolders) {\n queryFolder = watchFolders.find((f) => path.basename(f) === \"queries\");\n metricViewsFolder = watchFolders.find(\n (f) => path.basename(f) === \"metric-views\",\n );\n } else {\n queryFolder = defaultQueryFolder;\n metricViewsFolder = defaultMetricViewsFolder;\n }\n },\n\n buildStart() {\n // Production: block the build on this generate (and surface failures).\n // The watch is a dev-only no-op, so just run typegen.\n if (process.env.NODE_ENV === \"production\") {\n return runGenerate(\"blocking\");\n }\n\n // Dev: don't block startup waiting on typegen. The foreground generate runs\n // non-blocking — it skips the warehouse entirely and writes degraded\n // (cached/`unknown`) types instantly. Then arm the warehouse watch so the\n // warehouse gets a one-shot BLOCKING regenerate (real types) in the\n // background for EVERY reachable state: RUNNING describes right away, while\n // STARTING/STOPPED are waited (and started) until they reach RUNNING.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n // The metric config is `definitions.json` — a far more generic name\n // than the old `metric-views.json`. Match it by DIRECTORY, not bare\n // basename: only a `definitions.json` sitting directly in the\n // metric-views folder is the config (a `definitions.json` elsewhere in\n // a watched tree must not trigger a regenerate).\n const isMetricConfig =\n metricViewsFolder !== undefined &&\n path.basename(changedFile) === METRIC_CONFIG_FILE &&\n path.dirname(path.resolve(changedFile)) ===\n path.resolve(metricViewsFolder);\n\n if (isWatchedFile && (changedFile.endsWith(\".sql\") || isMetricConfig)) {\n // Route through the single-flight runner (was fire-and-forget\n // generate(), which could race the initial build / watch). This is a\n // dev-only hook, so degrade instantly (non-blocking), then re-arm the\n // warehouse watch so the edited query or metric-view source is\n // re-described in the background against the running warehouse (or\n // once a still-starting one warms up), landing fresh\n // blocking-described types.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n }\n });\n\n // Tear down any pending warehouse watch when the dev server closes so a\n // long backoff can't keep the process alive after shutdown.\n server.httpServer?.once(\"close\", () => {\n watchController?.abort();\n });\n },\n };\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ AppKit can automatically generate TypeScript types for your SQL queries, providi
6
6
 
7
7
  Generate type-safe TypeScript declarations for query keys, parameters, and result rows.
8
8
 
9
- All generated files live in `shared/appkit-types/`, one per concern: `analytics.d.ts` (SQL query types), `serving.d.ts` (model-serving endpoint types), and `metric-views.ts` — a real source file rather than a `.d.ts` because it also carries a runtime `metricViewsMetadata` constant alongside the augmentation. A single command (and the Vite plugin) produces them all in one pass; see [Metric-view types](#metric-view-types). The files use [`declare module`](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to augment existing interfaces, so the types apply globally — you never need to import them. TypeScript auto-discovers them through `"include": ["shared/appkit-types"]` in your tsconfig.
9
+ All generated files live in `shared/appkit-types/`, one per concern: `analytics.d.ts` (SQL query types), `serving.d.ts` (model-serving endpoint types), and `metric-views.d.ts` (metric-view types). A single command (and the Vite plugin) produces them all in one pass; see [Metric-view types](#metric-view-types). The files use [`declare module`](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to augment existing interfaces, so the types apply globally — you never need to import them. TypeScript auto-discovers them through `"include": ["shared/appkit-types"]` in your tsconfig.
10
10
 
11
11
  ## Vite plugin: `appKitTypesPlugin`[​](#vite-plugin-appkittypesplugin "Direct link to vite-plugin-appkittypesplugin")
12
12
 
@@ -86,7 +86,7 @@ npx @databricks/appkit generate-types --wait
86
86
 
87
87
  #### CI resilience: committed types as fallback[​](#ci-resilience-committed-types-as-fallback "Direct link to CI resilience: committed types as fallback")
88
88
 
89
- In blocking mode (`--wait`), the generator attempts to fetch real types from your warehouse, but delegates to **committed type files** (`shared/appkit-types/analytics.d.ts` and, when Metric Views are configured, `shared/appkit-types/metric-views.ts`) as the fallback when the warehouse is unreachable. These generated files should be part of your repository. On a fresh CI checkout, every build attempts to DESCRIBE against the warehouse; the committed types are used only when that cannot complete.
89
+ In blocking mode (`--wait`), the generator attempts to fetch real types from your warehouse, but delegates to **committed type files** (`shared/appkit-types/analytics.d.ts` and, when Metric Views are configured, `shared/appkit-types/metric-views.d.ts`) as the fallback when the warehouse is unreachable. These generated files should be part of your repository. On a fresh CI checkout, every build attempts to DESCRIBE against the warehouse; the committed types are used only when that cannot complete.
90
90
 
91
91
  The generator **never overwrites committed types with degraded (`result: unknown`) types** — it writes real types, or it does not write at all.
92
92
 
@@ -97,17 +97,18 @@ A **two-bucket failure taxonomy** determines whether the build crashes or falls
97
97
 
98
98
  The loud warning is a single greppable stderr line naming the coarse cause (auth blocked / warehouse unreachable / warehouse unavailable) and the warehouse ID, so CI logs surface that the build fell back to committed types.
99
99
 
100
- For a Metric Views app, `metric-views.ts` must already exist before an environmental failure can fall back successfully. Unlike a declaration-only artifact, this file also exports the runtime `metricViewsMetadata` value consumed by the server, so `analytics.d.ts` alone cannot satisfy the gate.
100
+ For a Metric Views app, `metric-views.d.ts` must already exist before an environmental failure can fall back successfully — `analytics.d.ts` alone cannot satisfy the gate.
101
101
 
102
102
  The app template wires this up for you: `postinstall` and `predev` run the non-blocking default, while `prebuild` runs `--wait`.
103
103
 
104
104
  ## Metric-view types[​](#metric-view-types "Direct link to Metric-view types")
105
105
 
106
- `generate-types` (and the Vite plugin) emit metric-view types **additively** — there is no separate command. When a `config/metric-views/definitions.json` file is present, the same run that generates your query types also DESCRIBEs each declared [UC Metric View](./docs/plugins/analytics.md) and writes `metric-views.ts` into `shared/appkit-types/`:
106
+ `generate-types` (and the Vite plugin) emit metric-view types **additively** — there is no separate command. When a `config/metric-views/definitions.json` file is present, the same run that generates your query types also DESCRIBEs each declared [UC Metric View](./docs/plugins/analytics.md) and writes two artifacts:
107
107
 
108
- * `metric-views.ts` — augments the `MetricRegistry` interface so `useMetricView('<key>', …)` is autocompleted and type-checked. Each view's measures, dimensions, and their semantic metadata (SQL type, display name, format, time grains) are encoded at the type level. The same file also exports a runtime `metricViewsMetadata` constant carrying that metadata as a value — inject it via `analytics({ metricViewsMetadata })` so the [metric route](./docs/plugins/analytics.md#metric-views) can attach per-column display metadata to its response payload.
108
+ * `shared/appkit-types/metric-views.d.ts` — augments the `MetricRegistry` interface so `useMetricView('<key>', …)` is autocompleted and type-checked. Each view's measures, dimensions, and their semantic metadata (SQL type, display name, format, time grains) are encoded at the type level. Selected row keys use the actual JSON\_ARRAY wire value type (`string | null`); the SQL type remains available in metadata for deliberate parsing/formatting.
109
+ * `config/metric-views/metadata.generated.json` — the runtime half of the same pass, carrying that per-column metadata as a value beside your hand-authored `definitions.json`. The [metric route](./docs/plugins/analytics.md#metric-views) discovers it automatically and attaches the requested columns' metadata to its response payload, so no plugin wiring is needed. Commit it with your generated types; it is generated, so do not hand-edit it.
109
110
 
110
- If `config/metric-views/definitions.json` is absent the metric path stays dormant (nothing is emitted). When present it follows the **same** warehouse-readiness contract as query types: in the default non-blocking run a view that can't be described yet — a cold warehouse, or a bad/unreachable source — is written with permissive types and a warning, while under `--wait` metric views obey the [two-bucket taxonomy](#ci-resilience-committed-types-as-fallback) (environmental failures gate to committed `metric-views.ts` + warn; deterministic failures like malformed definitions crash the build). A malformed `definitions.json` (invalid JSON, or a source that isn't a three-part UC FQN) fails fast in every mode.
111
+ If `config/metric-views/definitions.json` is absent the metric path stays dormant (nothing is emitted). When present it follows the **same** warehouse-readiness contract as query types: in the default non-blocking run a view that can't be described yet — a cold warehouse, or a bad/unreachable source — is written with permissive types and a warning, while under `--wait` metric views obey the [two-bucket taxonomy](#ci-resilience-committed-types-as-fallback) (environmental failures gate to committed `metric-views.d.ts` + warn; deterministic failures like malformed definitions crash the build). A malformed `definitions.json` (invalid JSON, or a source that isn't a three-part UC FQN) fails fast in every mode.
111
112
 
112
113
  `definitions.json` is keyed by metric key; each entry names the three-part UC FQN of the view and, optionally, the executor it runs as (`app_service_principal`, the default, or `user`):
113
114
 
@@ -264,6 +264,16 @@ console.log(result.results);
264
264
 
265
265
  Pass optional overrides as a second argument to `query` to adjust `numResults` or other per-call settings.
266
266
 
267
+ ## Caching[​](#caching "Direct link to Caching")
268
+
269
+ Query results are cached with a short TTL (60s) so repeated identical queries — including a component that re-renders or mounts twice — reuse a single Vector Search call instead of hitting the index each time. The next-page route is not cached: a page token is a single-use cursor and already identifies the exact page.
270
+
271
+ The cache key covers everything that changes results: the resolved index, `queryText`, `queryVector` (hashed), `queryType`, `numResults`, the resolved `columns`, `filters`, and whether reranking is on. Two queries that differ in any of these are cached separately.
272
+
273
+ ### Per-user isolation[​](#per-user-isolation "Direct link to Per-user isolation")
274
+
275
+ For `auth: "on-behalf-of-user"` indexes the caller's identity is part of the cache key, so one user never sees another user's cached results — and an on-behalf-of-user query never reads a service-principal-populated entry. Service-principal indexes share a single cache entry across callers.
276
+
267
277
  ## React hook[​](#react-hook "Direct link to React hook")
268
278
 
269
279
  `useAiSearchQuery` reads the configured indexes from the plugin's client config and posts to the right `/:alias/query` route, so the UI never hardcodes an alias. With one index configured it needs no arguments; pass `{ alias }` to target a specific one.