@databricks/appkit 0.45.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/agents/databricks.d.ts +25 -1
  3. package/dist/agents/databricks.d.ts.map +1 -1
  4. package/dist/agents/databricks.js +20 -1
  5. package/dist/agents/databricks.js.map +1 -1
  6. package/dist/app/index.d.ts +49 -2
  7. package/dist/app/index.d.ts.map +1 -1
  8. package/dist/app/index.js +87 -10
  9. package/dist/app/index.js.map +1 -1
  10. package/dist/appkit/package.js +1 -1
  11. package/dist/beta.d.ts +2 -2
  12. package/dist/cli/commands/generate-types.js +9 -4
  13. package/dist/cli/commands/generate-types.js.map +1 -1
  14. package/dist/core/agent/load-agents.d.ts.map +1 -1
  15. package/dist/core/agent/load-agents.js +52 -0
  16. package/dist/core/agent/load-agents.js.map +1 -1
  17. package/dist/core/agent/types.d.ts +11 -0
  18. package/dist/core/agent/types.d.ts.map +1 -1
  19. package/dist/core/agent/types.js.map +1 -1
  20. package/dist/core/appkit.d.ts.map +1 -1
  21. package/dist/core/appkit.js +2 -0
  22. package/dist/core/appkit.js.map +1 -1
  23. package/dist/core/lifecycle-manager.js +183 -0
  24. package/dist/core/lifecycle-manager.js.map +1 -0
  25. package/dist/core/plugin-context.d.ts +19 -1
  26. package/dist/core/plugin-context.d.ts.map +1 -1
  27. package/dist/core/plugin-context.js +9 -3
  28. package/dist/core/plugin-context.js.map +1 -1
  29. package/dist/plugin/plugin.d.ts.map +1 -1
  30. package/dist/plugin/plugin.js +2 -1
  31. package/dist/plugin/plugin.js.map +1 -1
  32. package/dist/plugins/agents/agents.d.ts.map +1 -1
  33. package/dist/plugins/agents/agents.js +3 -3
  34. package/dist/plugins/agents/agents.js.map +1 -1
  35. package/dist/plugins/analytics/analytics.d.ts +16 -0
  36. package/dist/plugins/analytics/analytics.d.ts.map +1 -1
  37. package/dist/plugins/analytics/analytics.js +162 -1
  38. package/dist/plugins/analytics/analytics.js.map +1 -1
  39. package/dist/plugins/analytics/metric.js +7 -0
  40. package/dist/plugins/analytics/mv/cache.js +51 -0
  41. package/dist/plugins/analytics/mv/cache.js.map +1 -0
  42. package/dist/plugins/analytics/mv/constants.js +72 -0
  43. package/dist/plugins/analytics/mv/constants.js.map +1 -0
  44. package/dist/plugins/analytics/mv/formatters.js +151 -0
  45. package/dist/plugins/analytics/mv/formatters.js.map +1 -0
  46. package/dist/plugins/analytics/mv/index.js +6 -0
  47. package/dist/plugins/analytics/mv/registry.js +55 -0
  48. package/dist/plugins/analytics/mv/registry.js.map +1 -0
  49. package/dist/plugins/analytics/mv/schemas.js +178 -0
  50. package/dist/plugins/analytics/mv/schemas.js.map +1 -0
  51. package/dist/plugins/analytics/types.js.map +1 -1
  52. package/dist/plugins/lakebase/lakebase.d.ts +10 -2
  53. package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
  54. package/dist/plugins/lakebase/lakebase.js +18 -7
  55. package/dist/plugins/lakebase/lakebase.js.map +1 -1
  56. package/dist/plugins/server/index.d.ts +51 -1
  57. package/dist/plugins/server/index.d.ts.map +1 -1
  58. package/dist/plugins/server/index.js +110 -23
  59. package/dist/plugins/server/index.js.map +1 -1
  60. package/dist/registry/resource-registry.d.ts +9 -1
  61. package/dist/registry/resource-registry.d.ts.map +1 -1
  62. package/dist/registry/resource-registry.js +22 -5
  63. package/dist/registry/resource-registry.js.map +1 -1
  64. package/dist/schemas/metric-fqn.js +14 -0
  65. package/dist/schemas/metric-fqn.js.map +1 -0
  66. package/dist/shared/src/execute.d.ts +1 -3
  67. package/dist/shared/src/execute.d.ts.map +1 -1
  68. package/dist/shared/src/plugin.d.ts +16 -0
  69. package/dist/shared/src/plugin.d.ts.map +1 -1
  70. package/dist/shared/src/schemas/metric-fqn.js +78 -46
  71. package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
  72. package/dist/shared/src/schemas/metric-source.js +90 -0
  73. package/dist/shared/src/schemas/metric-source.js.map +1 -0
  74. package/dist/stream/buffers.js +1 -1
  75. package/dist/stream/buffers.js.map +1 -1
  76. package/dist/stream/defaults.js +0 -2
  77. package/dist/stream/defaults.js.map +1 -1
  78. package/dist/stream/stream-manager.d.ts +2 -2
  79. package/dist/stream/stream-manager.d.ts.map +1 -1
  80. package/dist/stream/stream-manager.js +26 -24
  81. package/dist/stream/stream-manager.js.map +1 -1
  82. package/dist/stream/stream-registry.js +30 -23
  83. package/dist/stream/stream-registry.js.map +1 -1
  84. package/dist/stream/timers.js +17 -0
  85. package/dist/stream/timers.js.map +1 -0
  86. package/dist/stream/types.js.map +1 -1
  87. package/dist/telemetry/telemetry-manager.js +19 -13
  88. package/dist/telemetry/telemetry-manager.js.map +1 -1
  89. package/dist/type-generator/index.js +14 -7
  90. package/dist/type-generator/index.js.map +1 -1
  91. package/dist/type-generator/mv-registry/config.js +13 -31
  92. package/dist/type-generator/mv-registry/config.js.map +1 -1
  93. package/dist/type-generator/mv-registry/describe.js +1 -31
  94. package/dist/type-generator/mv-registry/describe.js.map +1 -1
  95. package/dist/type-generator/mv-registry/sync.js +1 -1
  96. package/dist/type-generator/mv-registry/sync.js.map +1 -1
  97. package/dist/type-generator/vite-plugin.d.ts +5 -1
  98. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  99. package/dist/type-generator/vite-plugin.js +21 -4
  100. package/dist/type-generator/vite-plugin.js.map +1 -1
  101. package/dist/utils/safe-handler.js +28 -0
  102. package/dist/utils/safe-handler.js.map +1 -0
  103. package/docs/api/appkit/Class.Plugin.md +2 -0
  104. package/docs/api/appkit/Class.ResourceRegistry.md +1 -1
  105. package/docs/api/appkit/Interface.AgentDefinition.md +11 -0
  106. package/docs/api/appkit/Interface.GenerationParams.md +58 -0
  107. package/docs/api/appkit/Interface.RegisteredAgent.md +11 -0
  108. package/docs/api/appkit.md +1 -0
  109. package/docs/development/type-generation.md +3 -3
  110. package/docs/plugins/analytics.md +172 -23
  111. package/llms.txt +1 -0
  112. package/package.json +1 -1
  113. package/sbom.cdx.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport dotenv from \"dotenv\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n isRevivableMetricCacheEntry,\n loadCache,\n type MetricCacheEntry,\n metricCacheHash,\n saveCache,\n} from \"./cache\";\nimport { getErrorDiagnostic, isConnectivityError } 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\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\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.mvOutFile - optional output file for the MetricRegistry\n * augmentation. Defaults to a sibling `metric-views.d.ts` file under the same\n * directory as `outFile`. Skipped entirely if `metric-views.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 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 const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n let fatalErrors: QueryFatalError[] = [];\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 }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n\n // Metric-view types: only emit when metric-views.json exists.\n if (queryFolder) {\n const mvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n\n let mvResult: SyncMetricViewsTypesResult;\n try {\n mvResult = await syncMetricViewsTypes({\n queryFolder,\n warehouseId,\n metricOutFile: mvFile,\n cache: !noCache,\n metricFetcher,\n mode,\n });\n } catch (configError) {\n // syncMetricViewsTypes only throws for a malformed metric-views.json — re-throw as a message-only TypegenFatalError.\n throw new TypegenFatalError(\n [\n {\n name: \"metric-views.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 metric-views.json is absent or in non-blocking mode.\n for (const fe of mvResult.fatalErrors) {\n fatalErrors.push(fe);\n }\n\n // Blocking (`--wait` / prod Vite) escalates per-key DESCRIBE failures — a bad or unreachable source, i.e. a config error\n // to build failures so the end-of-run throw fails after the writes.\n if (mode === \"blocking\") {\n for (const failure of mvResult.failures) {\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 // Types are always written above — including `result: unknown` for any Metric View that could not be described.\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 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 `metric-views.json` was found in the query folder, so nothing\n * 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). The\n * artifacts are still written; {@link generateFromEntryPoint} surfaces these\n * by throwing {@link TypegenFatalError} after the writes. A `\"describe-now\"`\n * run sets no blocking preflight, so for that mode this is always empty.\n */\n fatalErrors: Array<{ name: string; message: string }>;\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.queryFolder - folder that holds `metric-views.json` (`<root>/config/queries`).\n * @param options.warehouseId - SQL warehouse used for `DESCRIBE TABLE EXTENDED`.\n * @param options.metricOutFile - output path for the MetricRegistry `.d.ts`.\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\"`.\n */\nexport async function syncMetricViewsTypes(options: {\n queryFolder: string;\n warehouseId: string;\n metricOutFile: string;\n cache?: boolean;\n metricFetcher?: DescribeFetcher;\n mode?: \"describe-now\" | \"non-blocking\" | \"blocking\";\n}): Promise<SyncMetricViewsTypesResult> {\n const {\n queryFolder,\n warehouseId,\n metricOutFile,\n cache: cacheEnabled,\n metricFetcher,\n mode = \"describe-now\",\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(queryFolder);\n if (!mvConfig) {\n // No metric-views.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 ??= new WorkspaceClient({});\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 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 } 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 }\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.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\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 preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\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 // Fatal preflight (deleted/deleting warehouse): fail like the query path —\n // skip DESCRIBE, emit degraded schemas so both artifacts are still written,\n // and record one fatal error per describe-needed key (cache hits are\n // unaffected). The caller surfaces them after the writes. The degraded\n // schemas are not cached (see the write block), so a later pass re-probes.\n described = describeNeeded.map(emptyMetricSchema);\n for (const entry of describeNeeded) {\n fatalErrors.push({ name: entry.key, message: preflightFatalMessage });\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 metric-views.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 // 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 }\n } else {\n // Un-probed DESCRIBEs deliberately skipped, not failures: emit each\n // describe-needed key as a degraded schema so both artifacts exist; cache\n // hits keep serving 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 }\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 await fs.mkdir(path.dirname(metricOutFile), { recursive: true });\n await fs.writeFile(\n metricOutFile,\n generateMetricTypeDeclarations(schemas),\n \"utf-8\",\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 };\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
+ {"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport dotenv from \"dotenv\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n isRevivableMetricCacheEntry,\n loadCache,\n type MetricCacheEntry,\n metricCacheHash,\n saveCache,\n} from \"./cache\";\nimport { getErrorDiagnostic, isConnectivityError } 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\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\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.d.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\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n let fatalErrors: QueryFatalError[] = [];\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 }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\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 const mvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n\n let mvResult: SyncMetricViewsTypesResult;\n try {\n mvResult = await syncMetricViewsTypes({\n metricViewsFolder,\n warehouseId,\n metricOutFile: mvFile,\n cache: !noCache,\n metricFetcher,\n mode,\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 for (const fe of mvResult.fatalErrors) {\n fatalErrors.push(fe);\n }\n\n // Blocking (`--wait` / prod Vite) escalates per-key DESCRIBE failures — a bad or unreachable source, i.e. a config error\n // to build failures so the end-of-run throw fails after the writes.\n if (mode === \"blocking\") {\n for (const failure of mvResult.failures) {\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 // Types are always written above — including `result: unknown` for any Metric View that could not be described.\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 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). The\n * artifacts are still written; {@link generateFromEntryPoint} surfaces these\n * by throwing {@link TypegenFatalError} after the writes. A `\"describe-now\"`\n * run sets no blocking preflight, so for that mode this is always empty.\n */\n fatalErrors: Array<{ name: string; message: string }>;\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 `.d.ts`.\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\"`.\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}): Promise<SyncMetricViewsTypesResult> {\n const {\n metricViewsFolder,\n warehouseId,\n metricOutFile,\n cache: cacheEnabled,\n metricFetcher,\n mode = \"describe-now\",\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 ??= new WorkspaceClient({});\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 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 } 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 }\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.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\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 preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\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 // Fatal preflight (deleted/deleting warehouse): fail like the query path —\n // skip DESCRIBE, emit degraded schemas so both artifacts are still written,\n // and record one fatal error per describe-needed key (cache hits are\n // unaffected). The caller surfaces them after the writes. The degraded\n // schemas are not cached (see the write block), so a later pass re-probes.\n described = describeNeeded.map(emptyMetricSchema);\n for (const entry of describeNeeded) {\n fatalErrors.push({ name: entry.key, message: preflightFatalMessage });\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 // 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 }\n } else {\n // Un-probed DESCRIBEs deliberately skipped, not failures: emit each\n // describe-needed key as a degraded schema so both artifacts exist; cache\n // hits keep serving 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 }\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 await fs.mkdir(path.dirname(metricOutFile), { recursive: true });\n await fs.writeFile(\n metricOutFile,\n generateMetricTypeDeclarations(schemas),\n \"utf-8\",\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 };\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,9 +1,8 @@
1
- import { MAX_UC_OBJECT_NAME_LENGTH, UC_FQN_PATTERN } from "../../shared/src/schemas/metric-fqn.js";
1
+ import { MAX_UC_OBJECT_NAME_LENGTH, METRIC_CONFIG_FILE, UC_FQN_PATTERN } from "../../shared/src/schemas/metric-fqn.js";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
5
  //#region src/type-generator/mv-registry/config.ts
6
- const MV_CONFIG_FILE = "metric-views.json";
7
6
  /**
8
7
  * Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog
9
8
  * limit. Enforced by {@link resolveMetricConfig}.
@@ -30,16 +29,17 @@ function compareKeys(a, b) {
30
29
  return a < b ? -1 : a > b ? 1 : 0;
31
30
  }
32
31
  /**
33
- * Read {@link MV_CONFIG_FILE} from a queries folder.
32
+ * Read {@link METRIC_CONFIG_FILE} from a metric-views folder
33
+ * (`config/metric-views/`).
34
34
  *
35
35
  * Returns `null` if the file does not exist (the metric-view path is
36
- * additive — apps without metric-views.json must not be penalized). There is
37
- * deliberately no fallback to the legacy `metric.json` filename.
36
+ * additive — apps without definitions.json must not be penalized). There is
37
+ * deliberately no fallback to a legacy filename.
38
38
  *
39
39
  * Throws on JSON parse errors so misconfiguration surfaces loudly.
40
40
  */
41
- async function readMetricConfig(queryFolder) {
42
- const metricPath = path.join(queryFolder, MV_CONFIG_FILE);
41
+ async function readMetricConfig(metricViewsFolder) {
42
+ const metricPath = path.join(metricViewsFolder, METRIC_CONFIG_FILE);
43
43
  let raw;
44
44
  try {
45
45
  raw = await fs.readFile(metricPath, "utf8");
@@ -51,9 +51,9 @@ async function readMetricConfig(queryFolder) {
51
51
  try {
52
52
  parsed = JSON.parse(raw);
53
53
  } catch (err) {
54
- throw new Error(`Failed to parse metric-views.json at ${metricPath}: ${err.message}`);
54
+ throw new Error(`Failed to parse definitions.json at ${metricPath}: ${err.message}`);
55
55
  }
56
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`Invalid metric-views.json at ${metricPath}: expected an object with a 'metricViews' map.`);
56
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`Invalid definitions.json at ${metricPath}: expected an object with a 'metricViews' map.`);
57
57
  return parsed;
58
58
  }
59
59
  /**
@@ -66,24 +66,6 @@ function isValidMetricKey(key) {
66
66
  return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
67
67
  }
68
68
  /**
69
- * Total predicate: is `fqn` a well-formed three-part UC metric view FQN?
70
- *
71
- * Well-formed = exactly three non-empty, dot-separated segments, each a valid
72
- * Unity Catalog object name per the shared {@link UC_FQN_PATTERN} (the single
73
- * source of truth, also used by the canonical Zod schema). Used as the
74
- * defense-in-depth re-check at the describe fetcher seam; {@link resolveMetricConfig}
75
- * runs the same checks but with specific, staged error messages.
76
- *
77
- * @note Segment length ({@link MAX_FQN_SEGMENT_LENGTH}) is NOT checked here —
78
- * an over-long but otherwise legal name is still "valid shape". The length cap
79
- * is a separate concern enforced (with its own message) in resolveMetricConfig.
80
- */
81
- function isValidFqn(fqn) {
82
- const segments = fqn.split(".");
83
- if (segments.length !== FQN_SEGMENT_COUNT) return false;
84
- return segments.every((segment) => UC_FQN_PATTERN.test(segment));
85
- }
86
- /**
87
69
  * Field allowlists enforced by {@link resolveMetricConfig}.
88
70
  */
89
71
  const ALLOWED_TOP_LEVEL_FIELDS = new Set(["$schema", "metricViews"]);
@@ -92,12 +74,12 @@ const ALLOWED_ENTRY_FIELDS = new Set(["source", "executor"]);
92
74
  * Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.
93
75
  */
94
76
  function resolveMetricConfig(config) {
95
- for (const field of Object.keys(config)) if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) throw new Error(`Invalid top-level field "${field}" in metric-views.json: only '$schema' and 'metricViews' are allowed.`);
77
+ for (const field of Object.keys(config)) if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) throw new Error(`Invalid top-level field "${field}" in definitions.json: only '$schema' and 'metricViews' are allowed.`);
96
78
  const metricViews = config.metricViews === void 0 ? {} : config.metricViews;
97
- if (typeof metricViews !== "object" || metricViews === null || Array.isArray(metricViews)) throw new Error(`Invalid 'metricViews' in metric-views.json: expected an object map of metric entries.`);
79
+ if (typeof metricViews !== "object" || metricViews === null || Array.isArray(metricViews)) throw new Error(`Invalid 'metricViews' in definitions.json: expected an object map of metric entries.`);
98
80
  const entries = [];
99
81
  const sortedKeys = Object.keys(metricViews).sort(compareKeys);
100
- if (sortedKeys.length > MAX_METRIC_VIEWS) throw new Error(`Invalid 'metricViews' in metric-views.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`);
82
+ if (sortedKeys.length > MAX_METRIC_VIEWS) throw new Error(`Invalid 'metricViews' in definitions.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`);
101
83
  for (const key of sortedKeys) {
102
84
  if (!isValidMetricKey(key)) throw new Error(`Invalid metric key "${key}" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`);
103
85
  const entry = metricViews[key];
@@ -127,5 +109,5 @@ function resolveMetricConfig(config) {
127
109
  }
128
110
 
129
111
  //#endregion
130
- export { isValidFqn, readMetricConfig, resolveMetricConfig };
112
+ export { readMetricConfig, resolveMetricConfig };
131
113
  //# sourceMappingURL=config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","names":[],"sources":["../../../src/type-generator/mv-registry/config.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n// Plain, zod-free value imports — single source of truth for the UC object-name\n// grammar. The Zod schema (packages/shared/src/schemas/metric-source.ts) imports\n// the SAME UC_FQN_PATTERN for its `source` .regex(...), so the runtime and the\n// canonical schema validate identically without the type-generator pulling the\n// shared Zod schema package into its runtime path (locked dependency-graph\n// ruling — see the comment in ../cache.ts). The relative specifier resolves the\n// shared source directly and drags in no zod.\nimport {\n MAX_UC_OBJECT_NAME_LENGTH,\n UC_FQN_PATTERN,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport type {\n MetricConfigResolution,\n MetricLane,\n MetricSourceConfig,\n ResolvedMetricEntry,\n} from \"./types\";\n\nconst MV_CONFIG_FILE = \"metric-views.json\";\n\n/**\n * Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog\n * limit. Enforced by {@link resolveMetricConfig}.\n */\nconst MAX_METRIC_VIEWS = 200;\n/** Per-segment cap = UC's object-name length limit (255). */\nconst MAX_FQN_SEGMENT_LENGTH = MAX_UC_OBJECT_NAME_LENGTH;\n/** Whole-FQN cap: three max-length segments plus the two separating dots. */\nconst MAX_FQN_LENGTH = MAX_FQN_SEGMENT_LENGTH * 3 + 2;\n/** A metric view FQN is exactly catalog.schema.metric_view. */\nconst FQN_SEGMENT_NAMES = [\"catalog\", \"schema\", \"metric_view\"] as const;\nconst FQN_SEGMENT_COUNT = FQN_SEGMENT_NAMES.length;\n\n/**\n * Locale-independent comparator (UTF-16 code-unit order) for metric-view key\n * ordering. Plain `sort()` is locale-sensitive, so keys could order differently\n * across environments and invalidate the cache hash — this keeps the ordering\n * stable everywhere.\n */\nfunction compareKeys(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Read {@link MV_CONFIG_FILE} from a queries folder.\n *\n * Returns `null` if the file does not exist (the metric-view path is\n * additive — apps without metric-views.json must not be penalized). There is\n * deliberately no fallback to the legacy `metric.json` filename.\n *\n * Throws on JSON parse errors so misconfiguration surfaces loudly.\n */\nexport async function readMetricConfig(\n queryFolder: string,\n): Promise<MetricSourceConfig | null> {\n const metricPath = path.join(queryFolder, MV_CONFIG_FILE);\n let raw: string;\n try {\n raw = await fs.readFile(metricPath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `Failed to parse metric-views.json at ${metricPath}: ${(err as Error).message}`,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\n `Invalid metric-views.json at ${metricPath}: expected an object with a 'metricViews' map.`,\n );\n }\n\n return parsed as MetricSourceConfig;\n}\n\n/**\n * Validate a key against the JSON Schema's metricKey pattern. Kept\n * lightweight — the shared Zod schema ({@link metricSourceSchema}) is the\n * canonical contract for IDE/CI; this regex is identical to its\n * {@link metricKeySchema}.\n */\nfunction isValidMetricKey(key: string): boolean {\n return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);\n}\n\n/**\n * Total predicate: is `fqn` a well-formed three-part UC metric view FQN?\n *\n * Well-formed = exactly three non-empty, dot-separated segments, each a valid\n * Unity Catalog object name per the shared {@link UC_FQN_PATTERN} (the single\n * source of truth, also used by the canonical Zod schema). Used as the\n * defense-in-depth re-check at the describe fetcher seam; {@link resolveMetricConfig}\n * runs the same checks but with specific, staged error messages.\n *\n * @note Segment length ({@link MAX_FQN_SEGMENT_LENGTH}) is NOT checked here —\n * an over-long but otherwise legal name is still \"valid shape\". The length cap\n * is a separate concern enforced (with its own message) in resolveMetricConfig.\n */\nexport function isValidFqn(fqn: string): boolean {\n const segments = fqn.split(\".\");\n if (segments.length !== FQN_SEGMENT_COUNT) {\n return false;\n }\n return segments.every((segment) => UC_FQN_PATTERN.test(segment));\n}\n\n/**\n * Field allowlists enforced by {@link resolveMetricConfig}.\n */\nconst ALLOWED_TOP_LEVEL_FIELDS = new Set([\"$schema\", \"metricViews\"]);\nconst ALLOWED_ENTRY_FIELDS = new Set([\"source\", \"executor\"]);\n\n/**\n * Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.\n */\nexport function resolveMetricConfig(\n config: MetricSourceConfig,\n): MetricConfigResolution {\n for (const field of Object.keys(config)) {\n if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) {\n throw new Error(\n `Invalid top-level field \"${field}\" in metric-views.json: only '$schema' and 'metricViews' are allowed.`,\n );\n }\n }\n\n // Default to {} only when metricViews is genuinely absent. A `null` must fall\n // through to the type check below and throw — the canonical Zod schema rejects\n // null.\n const metricViews =\n config.metricViews === undefined ? {} : config.metricViews;\n if (\n typeof metricViews !== \"object\" ||\n metricViews === null ||\n Array.isArray(metricViews)\n ) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: expected an object map of metric entries.`,\n );\n }\n\n const entries: ResolvedMetricEntry[] = [];\n const sortedKeys = Object.keys(metricViews).sort(compareKeys);\n if (sortedKeys.length > MAX_METRIC_VIEWS) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`,\n );\n }\n for (const key of sortedKeys) {\n if (!isValidMetricKey(key)) {\n throw new Error(\n `Invalid metric key \"${key}\" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`,\n );\n }\n\n const entry = metricViews[key];\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new Error(\n `Invalid metric entry \"${key}\": expected an object with a 'source' field.`,\n );\n }\n\n for (const field of Object.keys(entry)) {\n if (!ALLOWED_ENTRY_FIELDS.has(field)) {\n throw new Error(\n `Invalid field \"${field}\" on metric entry \"${key}\": only 'source' and 'executor' are allowed at v1.`,\n );\n }\n }\n\n if (typeof entry.source !== \"string\" || entry.source.trim() === \"\") {\n throw new Error(\n `Invalid metric entry \"${key}\": 'source' must be a non-empty string.`,\n );\n }\n\n if (entry.source.length > MAX_FQN_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": FQN is ${entry.source.length} characters, exceeding the maximum of ${MAX_FQN_LENGTH}.`,\n );\n }\n\n // Staged, specific validation against the UC object-name grammar\n // (UC_FQN_PATTERN — shared with the canonical Zod schema). Reported in\n // order of increasing specificity so the message names the exact problem.\n const segments = entry.source.split(\".\");\n\n // Arity: exactly catalog.schema.metric_view. A wrong part count almost\n // always means a name contains a dot — which the dotted `source` cannot\n // express, since every dot is a segment boundary.\n if (segments.length !== FQN_SEGMENT_COUNT) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view> (got ${segments.length} dot-separated part${segments.length === 1 ? \"\" : \"s\"}). A catalog, schema, or metric view name cannot itself contain a dot.`,\n );\n }\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n const segmentName = FQN_SEGMENT_NAMES[i];\n\n // Empty part: a leading/trailing/double dot (e.g. \"a..c\", \".b.c\").\n if (segment.length === 0) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part is empty. A three-part UC FQN needs a non-empty name in each position: <catalog>.<schema>.<metric_view>.`,\n );\n }\n\n // Length cap (UC: object names are at most 255 characters).\n if (segment.length > MAX_FQN_SEGMENT_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": the ${segmentName} segment is ${segment.length} characters, exceeding the maximum of ${MAX_FQN_SEGMENT_LENGTH} per segment.`,\n );\n }\n\n // Character set: must be a valid UC object name (the FQN is always\n // backtick-quoted before it reaches SQL, so UC's *delimited* identifier\n // rules apply — anything but space, '/', and control characters).\n if (!UC_FQN_PATTERN.test(segment)) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part \"${segment}\" contains a character Unity Catalog does not allow in an object name (no spaces, '/', or control characters).`,\n );\n }\n }\n\n const executor = entry.executor;\n if (\n executor !== undefined &&\n executor !== \"app_service_principal\" &&\n executor !== \"user\"\n ) {\n throw new Error(\n `Invalid executor \"${String(executor)}\" on metric entry \"${key}\": must be \"app_service_principal\" or \"user\".`,\n );\n }\n\n const lane: MetricLane = executor === \"user\" ? \"obo\" : \"sp\";\n\n entries.push({ key, source: entry.source, lane });\n }\n\n return { entries };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"config.js","names":[],"sources":["../../../src/type-generator/mv-registry/config.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n// Plain, zod-free value imports — single source of truth for the UC object-name\n// grammar. The Zod schema (packages/shared/src/schemas/metric-source.ts) imports\n// the SAME UC_FQN_PATTERN for its `source` .regex(...), so the runtime and the\n// canonical schema validate identically without the type-generator pulling the\n// shared Zod schema package into its runtime path (locked dependency-graph\n// ruling — see the comment in ../cache.ts). The relative specifier resolves the\n// shared source directly and drags in no zod.\nimport {\n MAX_UC_OBJECT_NAME_LENGTH,\n METRIC_CONFIG_FILE,\n UC_FQN_PATTERN,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport type {\n MetricConfigResolution,\n MetricLane,\n MetricSourceConfig,\n ResolvedMetricEntry,\n} from \"./types\";\n\n/**\n * Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog\n * limit. Enforced by {@link resolveMetricConfig}.\n */\nconst MAX_METRIC_VIEWS = 200;\n/** Per-segment cap = UC's object-name length limit (255). */\nconst MAX_FQN_SEGMENT_LENGTH = MAX_UC_OBJECT_NAME_LENGTH;\n/** Whole-FQN cap: three max-length segments plus the two separating dots. */\nconst MAX_FQN_LENGTH = MAX_FQN_SEGMENT_LENGTH * 3 + 2;\n/** A metric view FQN is exactly catalog.schema.metric_view. */\nconst FQN_SEGMENT_NAMES = [\"catalog\", \"schema\", \"metric_view\"] as const;\nconst FQN_SEGMENT_COUNT = FQN_SEGMENT_NAMES.length;\n\n/**\n * Locale-independent comparator (UTF-16 code-unit order) for metric-view key\n * ordering. Plain `sort()` is locale-sensitive, so keys could order differently\n * across environments and invalidate the cache hash — this keeps the ordering\n * stable everywhere.\n */\nfunction compareKeys(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Read {@link METRIC_CONFIG_FILE} from a metric-views folder\n * (`config/metric-views/`).\n *\n * Returns `null` if the file does not exist (the metric-view path is\n * additive — apps without definitions.json must not be penalized). There is\n * deliberately no fallback to a legacy filename.\n *\n * Throws on JSON parse errors so misconfiguration surfaces loudly.\n */\nexport async function readMetricConfig(\n metricViewsFolder: string,\n): Promise<MetricSourceConfig | null> {\n const metricPath = path.join(metricViewsFolder, METRIC_CONFIG_FILE);\n let raw: string;\n try {\n raw = await fs.readFile(metricPath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `Failed to parse definitions.json at ${metricPath}: ${(err as Error).message}`,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\n `Invalid definitions.json at ${metricPath}: expected an object with a 'metricViews' map.`,\n );\n }\n\n return parsed as MetricSourceConfig;\n}\n\n/**\n * Validate a key against the JSON Schema's metricKey pattern. Kept\n * lightweight — the shared Zod schema ({@link metricSourceSchema}) is the\n * canonical contract for IDE/CI; this regex is identical to its\n * {@link metricKeySchema}.\n */\nfunction isValidMetricKey(key: string): boolean {\n return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);\n}\n\n// `resolveMetricConfig` re-derives the three-part UC FQN checks inline (rather\n// than calling a shared predicate) so it can emit specific, staged error\n// messages: arity, per-segment charset, per-segment length.\n\n/**\n * Field allowlists enforced by {@link resolveMetricConfig}.\n */\nconst ALLOWED_TOP_LEVEL_FIELDS = new Set([\"$schema\", \"metricViews\"]);\nconst ALLOWED_ENTRY_FIELDS = new Set([\"source\", \"executor\"]);\n\n/**\n * Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.\n */\nexport function resolveMetricConfig(\n config: MetricSourceConfig,\n): MetricConfigResolution {\n for (const field of Object.keys(config)) {\n if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) {\n throw new Error(\n `Invalid top-level field \"${field}\" in definitions.json: only '$schema' and 'metricViews' are allowed.`,\n );\n }\n }\n\n // Default to {} only when metricViews is genuinely absent. A `null` must fall\n // through to the type check below and throw — the canonical Zod schema rejects\n // null.\n const metricViews =\n config.metricViews === undefined ? {} : config.metricViews;\n if (\n typeof metricViews !== \"object\" ||\n metricViews === null ||\n Array.isArray(metricViews)\n ) {\n throw new Error(\n `Invalid 'metricViews' in definitions.json: expected an object map of metric entries.`,\n );\n }\n\n const entries: ResolvedMetricEntry[] = [];\n const sortedKeys = Object.keys(metricViews).sort(compareKeys);\n if (sortedKeys.length > MAX_METRIC_VIEWS) {\n throw new Error(\n `Invalid 'metricViews' in definitions.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`,\n );\n }\n for (const key of sortedKeys) {\n if (!isValidMetricKey(key)) {\n throw new Error(\n `Invalid metric key \"${key}\" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`,\n );\n }\n\n const entry = metricViews[key];\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new Error(\n `Invalid metric entry \"${key}\": expected an object with a 'source' field.`,\n );\n }\n\n for (const field of Object.keys(entry)) {\n if (!ALLOWED_ENTRY_FIELDS.has(field)) {\n throw new Error(\n `Invalid field \"${field}\" on metric entry \"${key}\": only 'source' and 'executor' are allowed at v1.`,\n );\n }\n }\n\n if (typeof entry.source !== \"string\" || entry.source.trim() === \"\") {\n throw new Error(\n `Invalid metric entry \"${key}\": 'source' must be a non-empty string.`,\n );\n }\n\n if (entry.source.length > MAX_FQN_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": FQN is ${entry.source.length} characters, exceeding the maximum of ${MAX_FQN_LENGTH}.`,\n );\n }\n\n // Staged, specific validation against the UC object-name grammar\n // (UC_FQN_PATTERN — shared with the canonical Zod schema). Reported in\n // order of increasing specificity so the message names the exact problem.\n const segments = entry.source.split(\".\");\n\n // Arity: exactly catalog.schema.metric_view. A wrong part count almost\n // always means a name contains a dot — which the dotted `source` cannot\n // express, since every dot is a segment boundary.\n if (segments.length !== FQN_SEGMENT_COUNT) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view> (got ${segments.length} dot-separated part${segments.length === 1 ? \"\" : \"s\"}). A catalog, schema, or metric view name cannot itself contain a dot.`,\n );\n }\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n const segmentName = FQN_SEGMENT_NAMES[i];\n\n // Empty part: a leading/trailing/double dot (e.g. \"a..c\", \".b.c\").\n if (segment.length === 0) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part is empty. A three-part UC FQN needs a non-empty name in each position: <catalog>.<schema>.<metric_view>.`,\n );\n }\n\n // Length cap (UC: object names are at most 255 characters).\n if (segment.length > MAX_FQN_SEGMENT_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": the ${segmentName} segment is ${segment.length} characters, exceeding the maximum of ${MAX_FQN_SEGMENT_LENGTH} per segment.`,\n );\n }\n\n // Character set: must be a valid UC object name (the FQN is always\n // backtick-quoted before it reaches SQL, so UC's *delimited* identifier\n // rules apply — anything but space, '/', and control characters).\n if (!UC_FQN_PATTERN.test(segment)) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part \"${segment}\" contains a character Unity Catalog does not allow in an object name (no spaces, '/', or control characters).`,\n );\n }\n }\n\n const executor = entry.executor;\n if (\n executor !== undefined &&\n executor !== \"app_service_principal\" &&\n executor !== \"user\"\n ) {\n throw new Error(\n `Invalid executor \"${String(executor)}\" on metric entry \"${key}\": must be \"app_service_principal\" or \"user\".`,\n );\n }\n\n const lane: MetricLane = executor === \"user\" ? \"obo\" : \"sp\";\n\n entries.push({ key, source: entry.source, lane });\n }\n\n return { entries };\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { isValidFqn } from "./config.js";
1
+ import { isValidFqn, quoteFqnForSql } from "../../shared/src/schemas/metric-fqn.js";
2
2
  import { describeAdaptive } from "../statement-result.js";
3
3
 
4
4
  //#region src/type-generator/mv-registry/describe.ts
@@ -180,36 +180,6 @@ function inferTimeGrains(type) {
180
180
  ];
181
181
  }
182
182
  /**
183
- * Quote a dot-separated FQN for safe interpolation into a Spark/Databricks SQL
184
- * statement.
185
- *
186
- * Each dot-split segment is wrapped in backtick-quoted-identifier syntax. The
187
- * one character that can break out of a backtick-quoted identifier is the
188
- * backtick itself, escaped by doubling (`` ` `` → `` `` ``) — so every backtick
189
- * inside a segment is doubled before the segment is wrapped. Control characters
190
- * and newlines have no valid escape inside a quoted identifier, so a segment
191
- * containing one is rejected outright.
192
- *
193
- * This is a pure, standalone escaper: it is intentionally independent of FQN
194
- * naming validation ({@link isValidFqn}). Naming validation decides whether an
195
- * FQN is an acceptable metric source; this function only guarantees that
196
- * whatever it is handed cannot break out of the quoted identifier it produces.
197
- *
198
- * An ordinary identifier is unchanged apart from the wrapping backticks:
199
- * `catalog.schema.view` → `` `catalog`.`schema`.`view` ``.
200
- *
201
- * @param fqn - Dot-separated identifier (e.g. `catalog.schema.view`).
202
- * @returns The backtick-quoted, escaped identifier ready for interpolation.
203
- * @throws If any segment contains a control character or newline.
204
- */
205
- function quoteFqnForSql(fqn) {
206
- const CONTROL_OR_NEWLINE = /\p{Cc}/u;
207
- return fqn.split(".").map((segment) => {
208
- if (CONTROL_OR_NEWLINE.test(segment)) throw new Error(`Cannot quote FQN segment "${segment}" for SQL: it contains a control character or newline, which has no valid escape inside a backtick-quoted identifier.`);
209
- return `\`${segment.replace(/`/g, "``")}\``;
210
- }).join(".");
211
- }
212
- /**
213
183
  * Build a DescribeFetcher from a real WorkspaceClient + warehouseId.
214
184
  */
215
185
  function createWorkspaceDescribeFetcher(client, warehouseId) {
@@ -1 +1 @@
1
- {"version":3,"file":"describe.js","names":[],"sources":["../../../src/type-generator/mv-registry/describe.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport { type DescribeFormatMemo, describeAdaptive } from \"../statement-result\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport { isValidFqn } from \"./config\";\nimport type { DescribeFetcher, MetricColumnMetadata } from \"./types\";\n\n/**\n * Parse the JSON payload returned by DESCRIBE TABLE EXTENDED ... AS JSON.\n *\n * The Statement Execution API returns a single string cell — this normalizer\n * unwraps it. Handles both the production (real warehouse) shape and the\n * shape produced by mocked test responses.\n */\nexport function parseDescribeTableExtendedJson(\n response: DatabricksStatementExecutionResponse,\n): unknown {\n if (response.status?.state === \"FAILED\") {\n const msg = response.status.error?.message ?? \"DESCRIBE failed\";\n throw new Error(`DESCRIBE TABLE EXTENDED failed: ${msg}`);\n }\n\n const rows = response.result?.data_array ?? [];\n if (rows.length === 0) {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED returned no rows. Verify the FQN points to a metric view.\",\n );\n }\n\n const cell = rows[0]?.[0];\n if (typeof cell !== \"string\") {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED first cell was not a JSON string. Confirm the AS JSON suffix is supported.\",\n );\n }\n\n try {\n return JSON.parse(cell);\n } catch (err) {\n throw new Error(\n `Failed to parse DESCRIBE TABLE EXTENDED JSON: ${(err as Error).message}`,\n );\n }\n}\n\n/**\n * Pure function: turn the parsed DESCRIBE JSON into structured column metadata.\n *\n * Tolerant of multiple JSON shapes (the field may be `columns` or\n * `schema.fields`, type may be a string or `{ name }` object, the measure\n * marker may be `is_measure` or under `metadata.is_measure`).\n */\nexport function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] {\n if (!parsed || typeof parsed !== \"object\") {\n return [];\n }\n\n const root = parsed as Record<string, unknown>;\n const columnsCandidate = (root.columns ??\n (root.schema && typeof root.schema === \"object\"\n ? (root.schema as Record<string, unknown>).fields\n : undefined)) as unknown;\n\n if (!Array.isArray(columnsCandidate)) {\n return [];\n }\n\n const columns: MetricColumnMetadata[] = [];\n for (const raw of columnsCandidate) {\n if (!raw || typeof raw !== \"object\") continue;\n const obj = raw as Record<string, unknown>;\n const name =\n typeof obj.name === \"string\"\n ? obj.name\n : typeof obj.column_name === \"string\"\n ? obj.column_name\n : undefined;\n if (!name) continue;\n\n const typeRaw = obj.type ?? obj.data_type ?? obj.type_name;\n let type = \"STRING\";\n if (typeof typeRaw === \"string\") {\n type = typeRaw;\n } else if (typeRaw && typeof typeRaw === \"object\") {\n const inner = (typeRaw as Record<string, unknown>).name;\n if (typeof inner === \"string\") type = inner;\n }\n\n let isMeasure = false;\n if (typeof obj.is_measure === \"boolean\") {\n isMeasure = obj.is_measure;\n } else if (\n obj.metadata &&\n typeof obj.metadata === \"object\" &&\n typeof (obj.metadata as Record<string, unknown>).is_measure === \"boolean\"\n ) {\n isMeasure = (obj.metadata as Record<string, unknown>)\n .is_measure as boolean;\n } else if (obj.kind === \"measure\" || obj.role === \"measure\") {\n isMeasure = true;\n }\n\n const description =\n typeof obj.comment === \"string\"\n ? obj.comment\n : typeof obj.description === \"string\"\n ? obj.description\n : undefined;\n\n const displayName = extractStringFromAny(obj, [\n \"display_name\",\n \"displayName\",\n ]);\n const format = extractFormatString(obj);\n\n // Time-grain inference is type-driven, not YAML-attribute-driven.\n const timeGrains = isMeasure ? undefined : inferTimeGrains(type);\n\n columns.push({\n name,\n type,\n isMeasure,\n description,\n ...(displayName ? { displayName } : {}),\n ...(format ? { format } : {}),\n ...(timeGrains ? { timeGrains } : {}),\n });\n }\n\n return columns;\n}\n\n/**\n * Read a non-empty string attribute from a DESCRIBE column entry, tolerating\n * the multiple shapes UC has shipped for this metadata over time.\n */\nfunction extractStringFromAny(\n obj: Record<string, unknown>,\n candidates: readonly string[],\n): string | undefined {\n for (const key of candidates) {\n const direct = obj[key];\n if (typeof direct === \"string\" && direct.trim().length > 0) {\n return direct;\n }\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n if (typeof nested === \"string\" && nested.trim().length > 0) {\n return nested;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Read the column's `format` attribute from a DESCRIBE entry and return a\n * printf-like format string suitable for `formatValue` and `toD3Format`.\n */\nfunction extractFormatString(obj: Record<string, unknown>): string | undefined {\n for (const key of [\"format\", \"format_spec\"]) {\n const direct = obj[key];\n const fromDirect = formatStringFromValue(direct);\n if (fromDirect) return fromDirect;\n\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n const fromMeta = formatStringFromValue(nested);\n if (fromMeta) return fromMeta;\n }\n }\n return undefined;\n}\n\nfunction formatStringFromValue(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return translateStructuredFormat(value as Record<string, unknown>);\n }\n return undefined;\n}\n\n/**\n * Translate the structured `format` object emitted by DESCRIBE TABLE EXTENDED\n * AS JSON into a printf-like format string.\n */\nfunction translateStructuredFormat(\n spec: Record<string, unknown>,\n): string | undefined {\n if (spec.currency && typeof spec.currency === \"object\") {\n return currencyFormatString(spec.currency as Record<string, unknown>);\n }\n if (spec.percent && typeof spec.percent === \"object\") {\n return percentFormatString(spec.percent as Record<string, unknown>);\n }\n if (spec.number && typeof spec.number === \"object\") {\n return numberFormatString(spec.number as Record<string, unknown>);\n }\n return undefined;\n}\n\nfunction currencyFormatString(c: Record<string, unknown>): string {\n const places = readDecimalPlaces(c) ?? 2;\n const codeRaw = c.currency_code;\n const code =\n typeof codeRaw === \"string\" && codeRaw.trim().length > 0\n ? codeRaw.toUpperCase()\n : \"USD\";\n const symbol = currencySymbol(code);\n return `${symbol}#,##0${fractionalSuffix(places)}`;\n}\n\nfunction percentFormatString(p: Record<string, unknown>): string {\n const places = readDecimalPlaces(p) ?? 0;\n return `0${fractionalSuffix(places)}%`;\n}\n\nfunction numberFormatString(n: Record<string, unknown>): string {\n const places = readDecimalPlaces(n) ?? 0;\n return `#,##0${fractionalSuffix(places)}`;\n}\n\nfunction fractionalSuffix(places: number): string {\n return places > 0 ? `.${\"0\".repeat(places)}` : \"\";\n}\n\n/**\n * Maximum decimal places honored from a format spec.\n */\nconst MAX_DECIMAL_PLACES = 100;\n\nfunction readDecimalPlaces(obj: Record<string, unknown>): number | undefined {\n const dp = obj.decimal_places;\n if (typeof dp === \"number\" && Number.isFinite(dp) && dp >= 0) {\n return Math.min(Math.floor(dp), MAX_DECIMAL_PLACES);\n }\n if (dp && typeof dp === \"object\" && !Array.isArray(dp)) {\n const places = (dp as Record<string, unknown>).places;\n if (typeof places === \"number\" && Number.isFinite(places) && places >= 0) {\n return Math.min(Math.floor(places), MAX_DECIMAL_PLACES);\n }\n }\n return undefined;\n}\n\n/**\n * Map ISO currency codes to their conventional prefix symbol. Unknown codes\n * fall back to the literal code + space.\n */\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n USD: \"$\",\n EUR: \"€\",\n GBP: \"£\",\n JPY: \"¥\",\n CNY: \"¥\",\n INR: \"₹\",\n BRL: \"R$\",\n};\n\nfunction currencySymbol(code: string): string {\n return CURRENCY_SYMBOLS[code] ?? `${code} `;\n}\n\n/**\n * Infer the standard set of valid time grains for a dimension based on its\n * SQL data type.\n */\nfunction inferTimeGrains(type: string): string[] | undefined {\n const normalized = type\n .toLowerCase()\n .replace(/\\(.*\\)$/, \"\")\n .trim();\n if (\n normalized === \"timestamp\" ||\n normalized === \"timestamp_ltz\" ||\n normalized === \"timestamp_ntz\"\n ) {\n return [\"day\", \"hour\", \"minute\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n if (normalized === \"date\") {\n return [\"day\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n return undefined;\n}\n\n/**\n * Quote a dot-separated FQN for safe interpolation into a Spark/Databricks SQL\n * statement.\n *\n * Each dot-split segment is wrapped in backtick-quoted-identifier syntax. The\n * one character that can break out of a backtick-quoted identifier is the\n * backtick itself, escaped by doubling (`` ` `` → `` `` ``) — so every backtick\n * inside a segment is doubled before the segment is wrapped. Control characters\n * and newlines have no valid escape inside a quoted identifier, so a segment\n * containing one is rejected outright.\n *\n * This is a pure, standalone escaper: it is intentionally independent of FQN\n * naming validation ({@link isValidFqn}). Naming validation decides whether an\n * FQN is an acceptable metric source; this function only guarantees that\n * whatever it is handed cannot break out of the quoted identifier it produces.\n *\n * An ordinary identifier is unchanged apart from the wrapping backticks:\n * `catalog.schema.view` → `` `catalog`.`schema`.`view` ``.\n *\n * @param fqn - Dot-separated identifier (e.g. `catalog.schema.view`).\n * @returns The backtick-quoted, escaped identifier ready for interpolation.\n * @throws If any segment contains a control character or newline.\n */\nexport function quoteFqnForSql(fqn: string): string {\n // Reject anything that cannot be represented inside a backtick-quoted\n // identifier. \\p{Cc} is the Unicode \"control\" category, which covers C0\n // (incl. \\n, \\r, \\t), DEL, and C1 — i.e. every control character/newline.\n const CONTROL_OR_NEWLINE = /\\p{Cc}/u;\n return fqn\n .split(\".\")\n .map((segment) => {\n if (CONTROL_OR_NEWLINE.test(segment)) {\n throw new Error(\n `Cannot quote FQN segment \"${segment}\" for SQL: it contains a control character or newline, which has no valid escape inside a backtick-quoted identifier.`,\n );\n }\n // Double every backtick — the only break-out from a backtick-quoted\n // identifier — then wrap the whole segment in backticks.\n return `\\`${segment.replace(/`/g, \"``\")}\\``;\n })\n .join(\".\");\n}\n\n/**\n * Build a DescribeFetcher from a real WorkspaceClient + warehouseId.\n */\nexport function createWorkspaceDescribeFetcher(\n client: WorkspaceClient,\n warehouseId: string,\n): DescribeFetcher {\n // One format probe per fetcher (= per typegen run): the first DESCRIBE\n // discovers the warehouse's working format, every later one reuses it.\n const describeFormat: DescribeFormatMemo = {};\n return async (fqn: string) => {\n /**\n * Defense-in-depth: every caller passes a source that already cleared\n * {@link resolveMetricConfig}, but this fetcher is an exported seam — re-check\n * before interpolating into SQL.\n */\n if (!isValidFqn(fqn)) {\n throw new Error(\n `Invalid metric source \"${fqn}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view>.`,\n );\n }\n // Escape + quote every segment before interpolation. isValidFqn already\n // rejects backticks/control chars for metric sources, so this is\n // belt-and-suspenders for the SQL-injection seam — and keeps the quoting\n // independent of the naming rule.\n const quotedFqn = quoteFqnForSql(fqn);\n return describeAdaptive(\n client,\n `DESCRIBE TABLE EXTENDED ${quotedFqn} AS JSON`,\n warehouseId,\n describeFormat,\n );\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"describe.js","names":[],"sources":["../../../src/type-generator/mv-registry/describe.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\n// Grammar + SQL-quoting for metric-view FQNs live together in the shared,\n// zod-free leaf so the type-generator and the analytics runtime validate and\n// escape against one source of truth (see the module doc in metric-fqn.ts).\nimport {\n isValidFqn,\n quoteFqnForSql,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport { type DescribeFormatMemo, describeAdaptive } from \"../statement-result\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport type { DescribeFetcher, MetricColumnMetadata } from \"./types\";\n\n/**\n * Parse the JSON payload returned by DESCRIBE TABLE EXTENDED ... AS JSON.\n *\n * The Statement Execution API returns a single string cell — this normalizer\n * unwraps it. Handles both the production (real warehouse) shape and the\n * shape produced by mocked test responses.\n */\nexport function parseDescribeTableExtendedJson(\n response: DatabricksStatementExecutionResponse,\n): unknown {\n if (response.status?.state === \"FAILED\") {\n const msg = response.status.error?.message ?? \"DESCRIBE failed\";\n throw new Error(`DESCRIBE TABLE EXTENDED failed: ${msg}`);\n }\n\n const rows = response.result?.data_array ?? [];\n if (rows.length === 0) {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED returned no rows. Verify the FQN points to a metric view.\",\n );\n }\n\n const cell = rows[0]?.[0];\n if (typeof cell !== \"string\") {\n throw new Error(\n \"DESCRIBE TABLE EXTENDED first cell was not a JSON string. Confirm the AS JSON suffix is supported.\",\n );\n }\n\n try {\n return JSON.parse(cell);\n } catch (err) {\n throw new Error(\n `Failed to parse DESCRIBE TABLE EXTENDED JSON: ${(err as Error).message}`,\n );\n }\n}\n\n/**\n * Pure function: turn the parsed DESCRIBE JSON into structured column metadata.\n *\n * Tolerant of multiple JSON shapes (the field may be `columns` or\n * `schema.fields`, type may be a string or `{ name }` object, the measure\n * marker may be `is_measure` or under `metadata.is_measure`).\n */\nexport function extractMetricColumns(parsed: unknown): MetricColumnMetadata[] {\n if (!parsed || typeof parsed !== \"object\") {\n return [];\n }\n\n const root = parsed as Record<string, unknown>;\n const columnsCandidate = (root.columns ??\n (root.schema && typeof root.schema === \"object\"\n ? (root.schema as Record<string, unknown>).fields\n : undefined)) as unknown;\n\n if (!Array.isArray(columnsCandidate)) {\n return [];\n }\n\n const columns: MetricColumnMetadata[] = [];\n for (const raw of columnsCandidate) {\n if (!raw || typeof raw !== \"object\") continue;\n const obj = raw as Record<string, unknown>;\n const name =\n typeof obj.name === \"string\"\n ? obj.name\n : typeof obj.column_name === \"string\"\n ? obj.column_name\n : undefined;\n if (!name) continue;\n\n const typeRaw = obj.type ?? obj.data_type ?? obj.type_name;\n let type = \"STRING\";\n if (typeof typeRaw === \"string\") {\n type = typeRaw;\n } else if (typeRaw && typeof typeRaw === \"object\") {\n const inner = (typeRaw as Record<string, unknown>).name;\n if (typeof inner === \"string\") type = inner;\n }\n\n let isMeasure = false;\n if (typeof obj.is_measure === \"boolean\") {\n isMeasure = obj.is_measure;\n } else if (\n obj.metadata &&\n typeof obj.metadata === \"object\" &&\n typeof (obj.metadata as Record<string, unknown>).is_measure === \"boolean\"\n ) {\n isMeasure = (obj.metadata as Record<string, unknown>)\n .is_measure as boolean;\n } else if (obj.kind === \"measure\" || obj.role === \"measure\") {\n isMeasure = true;\n }\n\n const description =\n typeof obj.comment === \"string\"\n ? obj.comment\n : typeof obj.description === \"string\"\n ? obj.description\n : undefined;\n\n const displayName = extractStringFromAny(obj, [\n \"display_name\",\n \"displayName\",\n ]);\n const format = extractFormatString(obj);\n\n // Time-grain inference is type-driven, not YAML-attribute-driven.\n const timeGrains = isMeasure ? undefined : inferTimeGrains(type);\n\n columns.push({\n name,\n type,\n isMeasure,\n description,\n ...(displayName ? { displayName } : {}),\n ...(format ? { format } : {}),\n ...(timeGrains ? { timeGrains } : {}),\n });\n }\n\n return columns;\n}\n\n/**\n * Read a non-empty string attribute from a DESCRIBE column entry, tolerating\n * the multiple shapes UC has shipped for this metadata over time.\n */\nfunction extractStringFromAny(\n obj: Record<string, unknown>,\n candidates: readonly string[],\n): string | undefined {\n for (const key of candidates) {\n const direct = obj[key];\n if (typeof direct === \"string\" && direct.trim().length > 0) {\n return direct;\n }\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n if (typeof nested === \"string\" && nested.trim().length > 0) {\n return nested;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Read the column's `format` attribute from a DESCRIBE entry and return a\n * printf-like format string suitable for `formatValue` and `toD3Format`.\n */\nfunction extractFormatString(obj: Record<string, unknown>): string | undefined {\n for (const key of [\"format\", \"format_spec\"]) {\n const direct = obj[key];\n const fromDirect = formatStringFromValue(direct);\n if (fromDirect) return fromDirect;\n\n const meta = obj.metadata;\n if (meta && typeof meta === \"object\" && !Array.isArray(meta)) {\n const nested = (meta as Record<string, unknown>)[key];\n const fromMeta = formatStringFromValue(nested);\n if (fromMeta) return fromMeta;\n }\n }\n return undefined;\n}\n\nfunction formatStringFromValue(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.trim().length > 0) return value.trim();\n if (value && typeof value === \"object\" && !Array.isArray(value)) {\n return translateStructuredFormat(value as Record<string, unknown>);\n }\n return undefined;\n}\n\n/**\n * Translate the structured `format` object emitted by DESCRIBE TABLE EXTENDED\n * AS JSON into a printf-like format string.\n */\nfunction translateStructuredFormat(\n spec: Record<string, unknown>,\n): string | undefined {\n if (spec.currency && typeof spec.currency === \"object\") {\n return currencyFormatString(spec.currency as Record<string, unknown>);\n }\n if (spec.percent && typeof spec.percent === \"object\") {\n return percentFormatString(spec.percent as Record<string, unknown>);\n }\n if (spec.number && typeof spec.number === \"object\") {\n return numberFormatString(spec.number as Record<string, unknown>);\n }\n return undefined;\n}\n\nfunction currencyFormatString(c: Record<string, unknown>): string {\n const places = readDecimalPlaces(c) ?? 2;\n const codeRaw = c.currency_code;\n const code =\n typeof codeRaw === \"string\" && codeRaw.trim().length > 0\n ? codeRaw.toUpperCase()\n : \"USD\";\n const symbol = currencySymbol(code);\n return `${symbol}#,##0${fractionalSuffix(places)}`;\n}\n\nfunction percentFormatString(p: Record<string, unknown>): string {\n const places = readDecimalPlaces(p) ?? 0;\n return `0${fractionalSuffix(places)}%`;\n}\n\nfunction numberFormatString(n: Record<string, unknown>): string {\n const places = readDecimalPlaces(n) ?? 0;\n return `#,##0${fractionalSuffix(places)}`;\n}\n\nfunction fractionalSuffix(places: number): string {\n return places > 0 ? `.${\"0\".repeat(places)}` : \"\";\n}\n\n/**\n * Maximum decimal places honored from a format spec.\n */\nconst MAX_DECIMAL_PLACES = 100;\n\nfunction readDecimalPlaces(obj: Record<string, unknown>): number | undefined {\n const dp = obj.decimal_places;\n if (typeof dp === \"number\" && Number.isFinite(dp) && dp >= 0) {\n return Math.min(Math.floor(dp), MAX_DECIMAL_PLACES);\n }\n if (dp && typeof dp === \"object\" && !Array.isArray(dp)) {\n const places = (dp as Record<string, unknown>).places;\n if (typeof places === \"number\" && Number.isFinite(places) && places >= 0) {\n return Math.min(Math.floor(places), MAX_DECIMAL_PLACES);\n }\n }\n return undefined;\n}\n\n/**\n * Map ISO currency codes to their conventional prefix symbol. Unknown codes\n * fall back to the literal code + space.\n */\nconst CURRENCY_SYMBOLS: Record<string, string> = {\n USD: \"$\",\n EUR: \"€\",\n GBP: \"£\",\n JPY: \"¥\",\n CNY: \"¥\",\n INR: \"₹\",\n BRL: \"R$\",\n};\n\nfunction currencySymbol(code: string): string {\n return CURRENCY_SYMBOLS[code] ?? `${code} `;\n}\n\n/**\n * Infer the standard set of valid time grains for a dimension based on its\n * SQL data type.\n */\nfunction inferTimeGrains(type: string): string[] | undefined {\n const normalized = type\n .toLowerCase()\n .replace(/\\(.*\\)$/, \"\")\n .trim();\n if (\n normalized === \"timestamp\" ||\n normalized === \"timestamp_ltz\" ||\n normalized === \"timestamp_ntz\"\n ) {\n return [\"day\", \"hour\", \"minute\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n if (normalized === \"date\") {\n return [\"day\", \"month\", \"quarter\", \"week\", \"year\"];\n }\n return undefined;\n}\n\n/**\n * Build a DescribeFetcher from a real WorkspaceClient + warehouseId.\n */\nexport function createWorkspaceDescribeFetcher(\n client: WorkspaceClient,\n warehouseId: string,\n): DescribeFetcher {\n // One format probe per fetcher (= per typegen run): the first DESCRIBE\n // discovers the warehouse's working format, every later one reuses it.\n const describeFormat: DescribeFormatMemo = {};\n return async (fqn: string) => {\n /**\n * Defense-in-depth: every caller passes a source that already cleared\n * {@link resolveMetricConfig}, but this fetcher is an exported seam — re-check\n * before interpolating into SQL.\n */\n if (!isValidFqn(fqn)) {\n throw new Error(\n `Invalid metric source \"${fqn}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view>.`,\n );\n }\n // Escape + quote every segment before interpolation. isValidFqn already\n // rejects backticks/control chars for metric sources, so this is\n // belt-and-suspenders for the SQL-injection seam — and keeps the quoting\n // independent of the naming rule.\n const quotedFqn = quoteFqnForSql(fqn);\n return describeAdaptive(\n client,\n `DESCRIBE TABLE EXTENDED ${quotedFqn} AS JSON`,\n warehouseId,\n describeFormat,\n );\n };\n}\n"],"mappings":"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"}
@@ -19,7 +19,7 @@ function emptyMetricSchema(entry) {
19
19
  }
20
20
  const MV_DESCRIBE_CONCURRENCY = 10;
21
21
  /**
22
- * Run schema synchronization for every entry in `metric-views.json`.
22
+ * Run schema synchronization for every entry in `definitions.json`.
23
23
  */
24
24
  async function syncMetrics(resolution, fetcher) {
25
25
  const { entries } = resolution;
@@ -1 +1 @@
1
- {"version":3,"file":"sync.js","names":[],"sources":["../../../src/type-generator/mv-registry/sync.ts"],"sourcesContent":["import { getErrorDiagnostic, isConnectivityError } from \"../errors\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport {\n extractMetricColumns,\n parseDescribeTableExtendedJson,\n} from \"./describe\";\nimport type {\n DescribeFetcher,\n MetricColumnMetadata,\n MetricConfigResolution,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n ResolvedMetricEntry,\n} from \"./types\";\n\n/**\n * Build the degraded schema emitted when an entry's columns are not\n * available — same key/source/lane as a real schema, with empty\n * measure/dimension allowlists and `degraded: true`.\n */\nexport function emptyMetricSchema(\n entry: Pick<MetricSchema, \"key\" | \"source\" | \"lane\">,\n): MetricSchema {\n return {\n key: entry.key,\n source: entry.source,\n lane: entry.lane,\n measures: [],\n dimensions: [],\n degraded: true,\n };\n}\n\n// Maximum number of in-flight DESCRIBE statements per syncMetrics pass.\nconst MV_DESCRIBE_CONCURRENCY = 10;\n\n// Outcome of describing a single metric entry.\ninterface MetricDescribeOutcome {\n index: number;\n schema: MetricSchema;\n failure?: MetricSyncFailure;\n}\n\n/**\n * Run schema synchronization for every entry in `metric-views.json`.\n */\nexport async function syncMetrics(\n resolution: MetricConfigResolution,\n fetcher: DescribeFetcher,\n): Promise<MetricSyncResult> {\n const { entries } = resolution;\n const schemas = new Array<MetricSchema>(entries.length);\n const failureSlots = new Array<MetricSyncFailure | undefined>(entries.length);\n\n const failedOutcome = (\n index: number,\n entry: ResolvedMetricEntry,\n reason: string,\n transient: boolean,\n ): MetricDescribeOutcome => ({\n index,\n schema: emptyMetricSchema(entry),\n failure: { key: entry.key, source: entry.source, reason, transient },\n });\n\n const describeOne = async (\n entry: ResolvedMetricEntry,\n index: number,\n ): Promise<MetricDescribeOutcome> => {\n let response: DatabricksStatementExecutionResponse;\n try {\n response = await fetcher(entry.source);\n } catch (err) {\n const reason = `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(err)}`;\n // Connectivity blips self-converge (retry next pass); auth, a bad\n // warehouse id, a truncated / multi-chunk result, or a malformed request\n // are deterministic and must surface — the same split the query path makes.\n return failedOutcome(index, entry, reason, isConnectivityError(err));\n }\n\n const state = response.status?.state;\n if (state !== \"SUCCEEDED\" && state !== \"FAILED\") {\n return { index, schema: emptyMetricSchema(entry) };\n }\n\n let columns: MetricColumnMetadata[];\n try {\n const parsed = parseDescribeTableExtendedJson(response);\n columns = extractMetricColumns(parsed);\n } catch (err) {\n const reason = `Failed to extract columns from DESCRIBE response: ${(err as Error).message}`;\n return failedOutcome(index, entry, reason, false);\n }\n\n if (columns.length === 0) {\n const reason =\n \"DESCRIBE response yielded zero columns — check the response shape (top-level `columns` array or `schema.fields`).\";\n return failedOutcome(index, entry, reason, false);\n }\n\n const measures = columns.filter((c) => c.isMeasure);\n const dimensions = columns.filter((c) => !c.isMeasure);\n\n return {\n index,\n schema: {\n key: entry.key,\n source: entry.source,\n lane: entry.lane,\n measures,\n dimensions,\n },\n };\n };\n\n for (\n let offset = 0;\n offset < entries.length;\n offset += MV_DESCRIBE_CONCURRENCY\n ) {\n const slice = entries.slice(offset, offset + MV_DESCRIBE_CONCURRENCY);\n const settled = await Promise.allSettled(\n slice.map((entry, i) => describeOne(entry, offset + i)),\n );\n\n for (let i = 0; i < settled.length; i++) {\n const result = settled[i];\n if (result.status === \"fulfilled\") {\n const { index, schema, failure } = result.value;\n schemas[index] = schema;\n if (failure) {\n failureSlots[index] = failure;\n }\n } else {\n const index = offset + i;\n const entry = entries[index];\n const { schema, failure } = failedOutcome(\n index,\n entry,\n `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(result.reason)}`,\n isConnectivityError(result.reason),\n );\n schemas[index] = schema;\n failureSlots[index] = failure;\n }\n }\n }\n\n const failures = failureSlots.filter(\n (failure): failure is MetricSyncFailure => failure !== undefined,\n );\n\n return { schemas, failures };\n}\n"],"mappings":";;;;;;;;;AAqBA,SAAgB,kBACd,OACc;AACd,QAAO;EACL,KAAK,MAAM;EACX,QAAQ,MAAM;EACd,MAAM,MAAM;EACZ,UAAU,EAAE;EACZ,YAAY,EAAE;EACd,UAAU;EACX;;AAIH,MAAM,0BAA0B;;;;AAYhC,eAAsB,YACpB,YACA,SAC2B;CAC3B,MAAM,EAAE,YAAY;CACpB,MAAM,UAAU,IAAI,MAAoB,QAAQ,OAAO;CACvD,MAAM,eAAe,IAAI,MAAqC,QAAQ,OAAO;CAE7E,MAAM,iBACJ,OACA,OACA,QACA,eAC2B;EAC3B;EACA,QAAQ,kBAAkB,MAAM;EAChC,SAAS;GAAE,KAAK,MAAM;GAAK,QAAQ,MAAM;GAAQ;GAAQ;GAAW;EACrE;CAED,MAAM,cAAc,OAClB,OACA,UACmC;EACnC,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,QAAQ,MAAM,OAAO;WAC/B,KAAK;AAKZ,UAAO,cAAc,OAAO,OAJb,mCAAmC,mBAAmB,IAAI,IAI9B,oBAAoB,IAAI,CAAC;;EAGtE,MAAM,QAAQ,SAAS,QAAQ;AAC/B,MAAI,UAAU,eAAe,UAAU,SACrC,QAAO;GAAE;GAAO,QAAQ,kBAAkB,MAAM;GAAE;EAGpD,IAAI;AACJ,MAAI;AAEF,aAAU,qBADK,+BAA+B,SAAS,CACjB;WAC/B,KAAK;AAEZ,UAAO,cAAc,OAAO,OADb,qDAAsD,IAAc,WACxC,MAAM;;AAGnD,MAAI,QAAQ,WAAW,EAGrB,QAAO,cAAc,OAAO,OAD1B,qHACyC,MAAM;EAGnD,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,UAAU;EACnD,MAAM,aAAa,QAAQ,QAAQ,MAAM,CAAC,EAAE,UAAU;AAEtD,SAAO;GACL;GACA,QAAQ;IACN,KAAK,MAAM;IACX,QAAQ,MAAM;IACd,MAAM,MAAM;IACZ;IACA;IACD;GACF;;AAGH,MACE,IAAI,SAAS,GACb,SAAS,QAAQ,QACjB,UAAU,yBACV;EACA,MAAM,QAAQ,QAAQ,MAAM,QAAQ,SAAS,wBAAwB;EACrE,MAAM,UAAU,MAAM,QAAQ,WAC5B,MAAM,KAAK,OAAO,MAAM,YAAY,OAAO,SAAS,EAAE,CAAC,CACxD;AAED,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACvC,MAAM,SAAS,QAAQ;AACvB,OAAI,OAAO,WAAW,aAAa;IACjC,MAAM,EAAE,OAAO,QAAQ,YAAY,OAAO;AAC1C,YAAQ,SAAS;AACjB,QAAI,QACF,cAAa,SAAS;UAEnB;IACL,MAAM,QAAQ,SAAS;IACvB,MAAM,QAAQ,QAAQ;IACtB,MAAM,EAAE,QAAQ,YAAY,cAC1B,OACA,OACA,mCAAmC,mBAAmB,OAAO,OAAO,IACpE,oBAAoB,OAAO,OAAO,CACnC;AACD,YAAQ,SAAS;AACjB,iBAAa,SAAS;;;;AAS5B,QAAO;EAAE;EAAS,UAJD,aAAa,QAC3B,YAA0C,YAAY,OACxD;EAE2B"}
1
+ {"version":3,"file":"sync.js","names":[],"sources":["../../../src/type-generator/mv-registry/sync.ts"],"sourcesContent":["import { getErrorDiagnostic, isConnectivityError } from \"../errors\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport {\n extractMetricColumns,\n parseDescribeTableExtendedJson,\n} from \"./describe\";\nimport type {\n DescribeFetcher,\n MetricColumnMetadata,\n MetricConfigResolution,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n ResolvedMetricEntry,\n} from \"./types\";\n\n/**\n * Build the degraded schema emitted when an entry's columns are not\n * available — same key/source/lane as a real schema, with empty\n * measure/dimension allowlists and `degraded: true`.\n */\nexport function emptyMetricSchema(\n entry: Pick<MetricSchema, \"key\" | \"source\" | \"lane\">,\n): MetricSchema {\n return {\n key: entry.key,\n source: entry.source,\n lane: entry.lane,\n measures: [],\n dimensions: [],\n degraded: true,\n };\n}\n\n// Maximum number of in-flight DESCRIBE statements per syncMetrics pass.\nconst MV_DESCRIBE_CONCURRENCY = 10;\n\n// Outcome of describing a single metric entry.\ninterface MetricDescribeOutcome {\n index: number;\n schema: MetricSchema;\n failure?: MetricSyncFailure;\n}\n\n/**\n * Run schema synchronization for every entry in `definitions.json`.\n */\nexport async function syncMetrics(\n resolution: MetricConfigResolution,\n fetcher: DescribeFetcher,\n): Promise<MetricSyncResult> {\n const { entries } = resolution;\n const schemas = new Array<MetricSchema>(entries.length);\n const failureSlots = new Array<MetricSyncFailure | undefined>(entries.length);\n\n const failedOutcome = (\n index: number,\n entry: ResolvedMetricEntry,\n reason: string,\n transient: boolean,\n ): MetricDescribeOutcome => ({\n index,\n schema: emptyMetricSchema(entry),\n failure: { key: entry.key, source: entry.source, reason, transient },\n });\n\n const describeOne = async (\n entry: ResolvedMetricEntry,\n index: number,\n ): Promise<MetricDescribeOutcome> => {\n let response: DatabricksStatementExecutionResponse;\n try {\n response = await fetcher(entry.source);\n } catch (err) {\n const reason = `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(err)}`;\n // Connectivity blips self-converge (retry next pass); auth, a bad\n // warehouse id, a truncated / multi-chunk result, or a malformed request\n // are deterministic and must surface — the same split the query path makes.\n return failedOutcome(index, entry, reason, isConnectivityError(err));\n }\n\n const state = response.status?.state;\n if (state !== \"SUCCEEDED\" && state !== \"FAILED\") {\n return { index, schema: emptyMetricSchema(entry) };\n }\n\n let columns: MetricColumnMetadata[];\n try {\n const parsed = parseDescribeTableExtendedJson(response);\n columns = extractMetricColumns(parsed);\n } catch (err) {\n const reason = `Failed to extract columns from DESCRIBE response: ${(err as Error).message}`;\n return failedOutcome(index, entry, reason, false);\n }\n\n if (columns.length === 0) {\n const reason =\n \"DESCRIBE response yielded zero columns — check the response shape (top-level `columns` array or `schema.fields`).\";\n return failedOutcome(index, entry, reason, false);\n }\n\n const measures = columns.filter((c) => c.isMeasure);\n const dimensions = columns.filter((c) => !c.isMeasure);\n\n return {\n index,\n schema: {\n key: entry.key,\n source: entry.source,\n lane: entry.lane,\n measures,\n dimensions,\n },\n };\n };\n\n for (\n let offset = 0;\n offset < entries.length;\n offset += MV_DESCRIBE_CONCURRENCY\n ) {\n const slice = entries.slice(offset, offset + MV_DESCRIBE_CONCURRENCY);\n const settled = await Promise.allSettled(\n slice.map((entry, i) => describeOne(entry, offset + i)),\n );\n\n for (let i = 0; i < settled.length; i++) {\n const result = settled[i];\n if (result.status === \"fulfilled\") {\n const { index, schema, failure } = result.value;\n schemas[index] = schema;\n if (failure) {\n failureSlots[index] = failure;\n }\n } else {\n const index = offset + i;\n const entry = entries[index];\n const { schema, failure } = failedOutcome(\n index,\n entry,\n `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(result.reason)}`,\n isConnectivityError(result.reason),\n );\n schemas[index] = schema;\n failureSlots[index] = failure;\n }\n }\n }\n\n const failures = failureSlots.filter(\n (failure): failure is MetricSyncFailure => failure !== undefined,\n );\n\n return { schemas, failures };\n}\n"],"mappings":";;;;;;;;;AAqBA,SAAgB,kBACd,OACc;AACd,QAAO;EACL,KAAK,MAAM;EACX,QAAQ,MAAM;EACd,MAAM,MAAM;EACZ,UAAU,EAAE;EACZ,YAAY,EAAE;EACd,UAAU;EACX;;AAIH,MAAM,0BAA0B;;;;AAYhC,eAAsB,YACpB,YACA,SAC2B;CAC3B,MAAM,EAAE,YAAY;CACpB,MAAM,UAAU,IAAI,MAAoB,QAAQ,OAAO;CACvD,MAAM,eAAe,IAAI,MAAqC,QAAQ,OAAO;CAE7E,MAAM,iBACJ,OACA,OACA,QACA,eAC2B;EAC3B;EACA,QAAQ,kBAAkB,MAAM;EAChC,SAAS;GAAE,KAAK,MAAM;GAAK,QAAQ,MAAM;GAAQ;GAAQ;GAAW;EACrE;CAED,MAAM,cAAc,OAClB,OACA,UACmC;EACnC,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,QAAQ,MAAM,OAAO;WAC/B,KAAK;AAKZ,UAAO,cAAc,OAAO,OAJb,mCAAmC,mBAAmB,IAAI,IAI9B,oBAAoB,IAAI,CAAC;;EAGtE,MAAM,QAAQ,SAAS,QAAQ;AAC/B,MAAI,UAAU,eAAe,UAAU,SACrC,QAAO;GAAE;GAAO,QAAQ,kBAAkB,MAAM;GAAE;EAGpD,IAAI;AACJ,MAAI;AAEF,aAAU,qBADK,+BAA+B,SAAS,CACjB;WAC/B,KAAK;AAEZ,UAAO,cAAc,OAAO,OADb,qDAAsD,IAAc,WACxC,MAAM;;AAGnD,MAAI,QAAQ,WAAW,EAGrB,QAAO,cAAc,OAAO,OAD1B,qHACyC,MAAM;EAGnD,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,UAAU;EACnD,MAAM,aAAa,QAAQ,QAAQ,MAAM,CAAC,EAAE,UAAU;AAEtD,SAAO;GACL;GACA,QAAQ;IACN,KAAK,MAAM;IACX,QAAQ,MAAM;IACd,MAAM,MAAM;IACZ;IACA;IACD;GACF;;AAGH,MACE,IAAI,SAAS,GACb,SAAS,QAAQ,QACjB,UAAU,yBACV;EACA,MAAM,QAAQ,QAAQ,MAAM,QAAQ,SAAS,wBAAwB;EACrE,MAAM,UAAU,MAAM,QAAQ,WAC5B,MAAM,KAAK,OAAO,MAAM,YAAY,OAAO,SAAS,EAAE,CAAC,CACxD;AAED,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACvC,MAAM,SAAS,QAAQ;AACvB,OAAI,OAAO,WAAW,aAAa;IACjC,MAAM,EAAE,OAAO,QAAQ,YAAY,OAAO;AAC1C,YAAQ,SAAS;AACjB,QAAI,QACF,cAAa,SAAS;UAEnB;IACL,MAAM,QAAQ,SAAS;IACvB,MAAM,QAAQ,QAAQ;IACtB,MAAM,EAAE,QAAQ,YAAY,cAC1B,OACA,OACA,mCAAmC,mBAAmB,OAAO,OAAO,IACpE,oBAAoB,OAAO,OAAO,CACnC;AACD,YAAQ,SAAS;AACjB,iBAAa,SAAS;;;;AAS5B,QAAO;EAAE;EAAS,UAJD,aAAa,QAC3B,YAA0C,YAAY,OACxD;EAE2B"}
@@ -11,7 +11,11 @@ interface AppKitTypesPluginOptions {
11
11
  * Defaults to a sibling of `outFile`, computed by the generator.
12
12
  */
13
13
  mvOutFile?: string;
14
- /** Folders to watch for changes. */
14
+ /**
15
+ * Folders to watch for changes. Defaults to `config/queries` and
16
+ * `config/metric-views`. When overridden, include a `queries` folder and/or a
17
+ * `metric-views` folder — they are resolved by their trailing path segment.
18
+ */
15
19
  watchFolders?: string[];
16
20
  }
17
21
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.d.ts","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"mappings":";;;;;AAGmC;UA6BzB,wBAAA;EAER,OAAA;EAFgC;;;;EAOhC,SAAA;EAEY;EAAZ,YAAA;AAAA;;;;;;;iBASc,iBAAA,CAAkB,OAAA,GAAU,wBAAA,GAA2B,MAAA"}
1
+ {"version":3,"file":"vite-plugin.d.ts","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"mappings":";;;;;AAGmC;UA8BzB,wBAAA;EAER,OAAA;EAFgC;;;;EAOhC,SAAA;EAMY;AASd;;;;EATE,YAAA;AAAA;;;;;;;iBASc,iBAAA,CAAkB,OAAA,GAAU,wBAAA,GAA2B,MAAA"}
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from "../logging/logger.js";
2
+ import { METRIC_CONFIG_FILE } from "../shared/src/schemas/metric-fqn.js";
2
3
  import { getWarehouseState, startWarehouse, waitUntilRunning } from "./warehouse-status.js";
3
4
  import { ANALYTICS_TYPES_FILE, TYPES_DIR, TypegenFatalError, TypegenSyntaxError, generateFromEntryPoint } from "./index.js";
4
5
  import { WorkspaceClient } from "@databricks/sdk-experimental";
@@ -24,6 +25,8 @@ function appKitTypesPlugin(options) {
24
25
  let outFile;
25
26
  let mvOutFile;
26
27
  let watchFolders;
28
+ let queryFolder;
29
+ let metricViewsFolder;
27
30
  let inFlight = null;
28
31
  let queued = false;
29
32
  let pendingMode = "non-blocking";
@@ -48,7 +51,8 @@ function appKitTypesPlugin(options) {
48
51
  }
49
52
  await generateFromEntryPoint({
50
53
  outFile,
51
- queryFolder: watchFolders[0],
54
+ queryFolder,
55
+ metricViewsFolder,
52
56
  warehouseId,
53
57
  noCache: false,
54
58
  mode,
@@ -167,14 +171,25 @@ function appKitTypesPlugin(options) {
167
171
  logger.debug("Warehouse ID not found. Skipping type generation.");
168
172
  return false;
169
173
  }
170
- if (!existsSync(path.join(process.cwd(), "config", "queries"))) return false;
174
+ const hasQueries = existsSync(path.join(process.cwd(), "config", "queries"));
175
+ const hasMetricViews = existsSync(path.join(process.cwd(), "config", "metric-views"));
176
+ if (!hasQueries && !hasMetricViews) return false;
171
177
  return true;
172
178
  },
173
179
  configResolved(config) {
174
180
  const projectRoot = path.resolve(config.root, "..");
175
181
  outFile = path.resolve(projectRoot, options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`);
176
182
  mvOutFile = options?.mvOutFile !== void 0 ? path.resolve(projectRoot, options.mvOutFile) : void 0;
177
- watchFolders = options?.watchFolders ?? [path.join(process.cwd(), "config", "queries")];
183
+ const defaultQueryFolder = path.join(process.cwd(), "config", "queries");
184
+ const defaultMetricViewsFolder = path.join(process.cwd(), "config", "metric-views");
185
+ watchFolders = options?.watchFolders ?? [defaultQueryFolder, defaultMetricViewsFolder];
186
+ if (options?.watchFolders) {
187
+ queryFolder = watchFolders.find((f) => path.basename(f) === "queries");
188
+ metricViewsFolder = watchFolders.find((f) => path.basename(f) === "metric-views");
189
+ } else {
190
+ queryFolder = defaultQueryFolder;
191
+ metricViewsFolder = defaultMetricViewsFolder;
192
+ }
178
193
  },
179
194
  buildStart() {
180
195
  if (process.env.NODE_ENV === "production") return runGenerate("blocking");
@@ -184,7 +199,9 @@ function appKitTypesPlugin(options) {
184
199
  configureServer(server) {
185
200
  server.watcher.add(watchFolders);
186
201
  server.watcher.on("change", (changedFile) => {
187
- if (watchFolders.some((folder) => changedFile.startsWith(folder)) && (changedFile.endsWith(".sql") || path.basename(changedFile) === "metric-views.json")) {
202
+ const isWatchedFile = watchFolders.some((folder) => changedFile.startsWith(folder));
203
+ const isMetricConfig = metricViewsFolder !== void 0 && path.basename(changedFile) === METRIC_CONFIG_FILE && path.dirname(path.resolve(changedFile)) === path.resolve(metricViewsFolder);
204
+ if (isWatchedFile && (changedFile.endsWith(".sql") || isMetricConfig)) {
188
205
  runGenerate("non-blocking");
189
206
  armWarehouseWatch();
190
207
  }