@databricks/appkit 0.43.0 → 0.43.1

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.
@@ -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 { generateMetricsMetadataJson } from \"./mv-registry/metadata\";\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 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 // Unique message → keep the compact one-line `tag name message` form.\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 * what state is the warehouse in right now?\n *\n * Uses {@link getWarehouseState} (`warehouses.get`) — a read-only GET that can\n * never start the warehouse — unlike the metric DESCRIBE statements it guards,\n * whose execution auto-starts a stopped warehouse and waits on it.\n *\n * Returns the observed state so the gate can distinguish a transient\n * not-running state (STOPPED/STARTING/... → degraded entries that retry) from a\n * terminal one (DELETED/DELETING → degraded entries pinned sticky). Takes the\n * lazy client *getter* (not a client) so the probe also absorbs client\n * construction failure. A connectivity blip returns `undefined`, which the gate\n * reads as transient not-running; a deterministic failure (auth, bad id) is\n * re-thrown so the gate can classify it 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.d.ts` file under the same\n * directory as `outFile`. Skipped entirely if `metric-views.json` is absent.\n * @param options.mvMetadataOutFile - optional output file for the\n * build-time semantic metadata JSON bundle (`metrics.metadata.json`).\n * Defaults to a sibling of `mvOutFile`. Skipped entirely if\n * `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 mvMetadataOutFile?: string;\n metricFetcher?: DescribeFetcher;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n mvOutFile,\n mvMetadataOutFile,\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. The path is\n // purely additive — apps that never adopt metric views must not produce\n // empty noise.\n if (queryFolder) {\n const mvConfig = await readMetricConfig(queryFolder);\n if (mvConfig) {\n const resolution = resolveMetricConfig(mvConfig);\n\n // Metric schemas persist in the shared typegen cache as a `metrics`\n // section (sibling of `queries`, same file/version), keyed by metric key\n // with md5(\"<source>|<lane>\") as the change detector. Loaded strictly\n // AFTER the query path's own load → mutate → save cycle, so the single\n // metric-side save below can never clobber a query entry.\n const cache = await loadCache();\n\n // The section is consumed through a null-prototype copy: metric keys\n // are user-controlled config input and \"__proto__\" passes the metric\n // key regex — on a plain object, writing it would hit the\n // Object.prototype setter (mutating the object's prototype and silently\n // dropping the entry) instead of storing data. A null prototype also\n // keeps partition reads from resolving inherited names (\"constructor\",\n // \"toString\", ...) as phantom entries.\n const mvCacheSection: Record<string, MetricCacheEntry> =\n 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 (structurally valid\n // entry, hash match, not retry-flagged) is served from cache no matter\n // what the warehouse is doing — a degraded pass falls back to\n // last-known-good schemas, exactly like queries degrade to cached types.\n // Only the remainder (new, edited, retry-flagged, or unrevivable entries)\n // is eligible for DESCRIBE, so a fully-warm pass makes zero warehouse\n // calls and constructs zero clients.\n const hitSchemas = new Map<string, MetricSchema>();\n const describeNeeded: typeof resolution.entries = [];\n // Degraded cached schemas pinned `retry: false` are sticky failures: they\n // serve their permissive schema like any hit, but are collected here for\n // the single notice below so the misconfiguration isn't silently hidden.\n const stickyDegradedHits: string[] = [];\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.retry\n ) {\n hitSchemas.set(entry.key, prior.schema);\n if (prior.schema.degraded === true) {\n stickyDegradedHits.push(entry.key);\n }\n } else {\n describeNeeded.push(entry);\n }\n }\n\n if (stickyDegradedHits.length > 0) {\n logger.warn(\n \"cached failure for %s — fix the entry in metric-views.json or run with --no-cache to retry.\",\n stickyDegradedHits.join(\", \"),\n );\n }\n\n // At most ONE WorkspaceClient per pass for the whole metric path: the\n // status probe, the blocking preflight, and the default DESCRIBE fetcher\n // share this lazily-created instance, so a pass that never contacts the\n // warehouse constructs zero clients.\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\n // DESCRIBE batch (probe → decide → wait / start+wait; only\n // DELETED/DELETING is fatal). Deliberately split from the query path's\n // preflight — metric views may bind a different warehouse in future. Two\n // softenings vs the query preflight: a failed probe and a timed-out wait\n // are NOT fatal here — we fall through to syncMetrics, which classifies a\n // still-not-ready warehouse as degraded rather than failing the build.\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\n // STOPPED/STOPPING 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\n // exactly DELETED/DELETING — the warehouse was deleted while\n // we waited. Fatal, same as catching it at decision time.\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. A STOPPED/STOPPING resolve (this\n // wait runs without treatStoppedAsTransient) stays a soft\n // fall-through — a stopped warehouse is startable, so it\n // degrades and converges rather than failing the build.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n }\n }\n } catch (err) {\n // Connectivity blip: fall through to syncMetrics, whose DESCRIBEs\n // degrade a not-ready / unreachable warehouse rather than throwing. A\n // deterministic failure (auth, bad warehouse id, a timed-out start)\n // is fatal — surface it instead of stalling ~5 min against a\n // not-ready warehouse, mirroring the query path's preflight catch.\n if (!isConnectivityError(err)) {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n }\n }\n\n // Honor the non-blocking preflight contract (#406) for metric DESCRIBEs:\n // a `DESCRIBE TABLE EXTENDED ... AS JSON` waits up to 30s per key and\n // auto-starts a stopped warehouse — exactly what \"non-blocking\" promises\n // not to do. So one status-only probe (which can't start the warehouse)\n // decides whether to DESCRIBE now or emit degraded artifacts for a later\n // blocking run; it keeps the observed state so the skip can tell a\n // transient not-running warehouse from a terminal DELETED/DELETING one.\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 // probeWarehouseState only throws on a deterministic failure (auth,\n // bad warehouse id) — a connectivity blip already returned undefined.\n // Pin it fatal through the same path as a fatal blocking preflight.\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n describeNow = gateState === \"RUNNING\";\n }\n\n let described: MetricSchema[];\n let failures: MetricSyncFailure[] = [];\n // True when this pass skipped DESCRIBE for a reason that can never\n // self-converge — a deleted/deleting warehouse (fatal preflight or gate\n // skip). The write site pins those degraded outcomes sticky.\n let terminalSkip = false;\n if (preflightFatalMessage !== undefined) {\n // Fatal preflight (deleted/deleting warehouse): fail like the query\n // path — skip DESCRIBE, emit degraded schemas so both artifacts are\n // still written, and record one fatal error per describe-needed key\n // (cache hits are unaffected). The end-of-run throw below surfaces them\n // after the writes. Terminal, so these entries are pinned sticky.\n described = describeNeeded.map(emptyMetricSchema);\n terminalSkip = true;\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\n // construct a 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\n // would otherwise silently ship an empty entry that the runtime\n // fail-closed gate 503s in production. syncMetrics is log-free; this\n // caller is the single 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\n // non-terminal state (stopped / cold-starting), so their schemas are\n // unknown — not errors. One summary line, no per-key warns; failed\n // keys are excluded (the warn loop above already reported them).\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 (permissive types) so both\n // artifacts exist; cache hits keep serving last-known-good. A transient\n // state refreshes on a later RUNNING pass; a DELETED/DELETING probe is\n // terminal, so those keys are pinned sticky below.\n described = describeNeeded.map(emptyMetricSchema);\n terminalSkip = gateState === \"DELETED\" || gateState === \"DELETING\";\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 // Persist outcomes for exactly the keys this pass owned (the\n // describe-needed set); hits were partitioned out above and are never\n // rewritten, so a warehouse-down pass keeps last-known-good entries. A\n // successful DESCRIBE caches `retry: false`; a degraded outcome caches\n // `retry: true` only when re-describing could later succeed (non-terminal\n // state or transient failure), else sticky `retry: false`. One save per\n // pass; with `noCache` the section started empty, so it's overwritten.\n const failureByKey = new Map<string, MetricSyncFailure>();\n for (const failure of failures) {\n failureByKey.set(failure.key, failure);\n }\n for (let i = 0; i < describeNeeded.length; i++) {\n // syncMetrics (and both .map(emptyMetricSchema) branches) return\n // one schema per entry in entry order, so described[i] always\n // belongs to describeNeeded[i].\n const entry = describeNeeded[i];\n const failure = failureByKey.get(entry.key);\n mvCacheSection[entry.key] = {\n hash: metricCacheHash(entry.source, entry.lane),\n schema: described[i],\n retry:\n described[i].degraded === true &&\n !terminalSkip &&\n (failure === undefined || failure.transient === true),\n };\n }\n\n // Prune entries whose key is no longer configured, so a removed metric\n // doesn't haunt the cache file forever.\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 // — a warm pass over a shrunk config has nothing to describe but must\n // still shrink the file.\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 // (resolution.entries order — the renderers sort internally where\n // determinism matters).\n const describedByKey = new Map<string, MetricSchema>();\n for (const schema of described) {\n describedByKey.set(schema.key, schema);\n }\n const mvSchemas = resolution.entries.map((entry) => {\n const schema =\n hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);\n if (schema !== undefined) return schema;\n // Defensive: every entry is either a cache hit or describe-needed (and\n // every describe-needed entry yields exactly one schema above), so this\n // should be unreachable. If the invariant ever breaks, warn loudly but\n // still emit a permissive degraded schema — the metric path never\n // crashes a build over a single entry.\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 const mvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n const mvDeclarations = generateMetricTypeDeclarations(mvSchemas);\n await fs.mkdir(path.dirname(mvFile), { recursive: true });\n await fs.writeFile(mvFile, mvDeclarations, \"utf-8\");\n\n // Emit the semantic-metadata JSON bundle alongside the .d.ts. The hook\n // imports this artifact (via a registration call from the consuming\n // app) and exposes the per-metric subset on its return value.\n const mvMetadataFile =\n mvMetadataOutFile ??\n path.join(path.dirname(mvFile), METRIC_METADATA_FILE);\n const metadataJson = generateMetricsMetadataJson(mvSchemas);\n await fs.mkdir(path.dirname(mvMetadataFile), { recursive: true });\n await fs.writeFile(mvMetadataFile, metadataJson, \"utf-8\");\n\n logger.debug(\n \"Wrote MetricRegistry augmentation + metadata bundle for %d metric(s)%s\",\n mvSchemas.length,\n failures.length > 0 ? ` (${failures.length} failure(s))` : \"\",\n );\n }\n }\n\n // One-time migration: remove old generated file and patch project configs\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Types are always written above — including `result: unknown` for any query\n // that could not be described. Connectivity failures pass silently so a\n // transient warehouse outage never blocks a build; genuine SQL errors and\n // non-connectivity fatal request failures surface after the file write.\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// 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\n/** Directory name for generated AppKit type declaration files. */\nexport const TYPES_DIR = \"appkit-types\";\n/** Default filename for analytics query type declarations. */\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\n/** Default filename for serving endpoint type declarations. */\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\n/** Default filename for metric-view registry type declarations. */\nexport const METRIC_TYPES_FILE = \"metric.d.ts\";\n/**\n * Default filename for the build-time semantic-metadata JSON bundle, sibling of\n * {@link METRIC_TYPES_FILE}. Shape is `Record<metricKey, { measures,\n * dimensions }>` (UC FQN and execution lane are server-side concerns, kept out\n * of this client-shipped artifact). The consuming app imports it at build time\n * and registers it via `@databricks/appkit-ui/format`'s\n * `registerMetricsMetadata()`, so the React hook returns per-metric `metadata`\n * without a second network round-trip.\n */\nexport const METRIC_METADATA_FILE = \"metrics.metadata.json\";\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.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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"}
@@ -21,10 +21,10 @@ const FQN_SEGMENT_NAMES = [
21
21
  ];
22
22
  const FQN_SEGMENT_COUNT = FQN_SEGMENT_NAMES.length;
23
23
  /**
24
- * Locale-independent comparator (UTF-16 code-unit order) shared by BOTH artifact
25
- * key orderings. Plain `sort()` is locale-sensitive, so keys could order
26
- * differently across environments and invalidate the cache hash — this keeps the
27
- * ordering stable everywhere.
24
+ * Locale-independent comparator (UTF-16 code-unit order) for metric-view key
25
+ * ordering. Plain `sort()` is locale-sensitive, so keys could order differently
26
+ * across environments and invalidate the cache hash — this keeps the ordering
27
+ * stable everywhere.
28
28
  */
29
29
  function compareKeys(a, b) {
30
30
  return a < b ? -1 : a > b ? 1 : 0;
@@ -127,5 +127,5 @@ function resolveMetricConfig(config) {
127
127
  }
128
128
 
129
129
  //#endregion
130
- export { compareKeys, isValidFqn, readMetricConfig, resolveMetricConfig };
130
+ export { isValidFqn, readMetricConfig, resolveMetricConfig };
131
131
  //# 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) shared by BOTH artifact\n * key orderings. Plain `sort()` is locale-sensitive, so keys could order\n * differently across environments and invalidate the cache hash — this keeps the\n * ordering stable everywhere.\n */\nexport function 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 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 +1 @@
1
- {"version":3,"file":"render-types.js","names":[],"sources":["../../../src/type-generator/mv-registry/render-types.ts"],"sourcesContent":["import type { MetricColumnMetadata, MetricSchema } from \"./types\";\n\n/**\n * @todo unify with query-registry.ts\n * Map a Databricks SQL type to a TypeScript primitive.\n * Centralized here (not imported from query-registry) so this module\n * stays self-contained.\n */\nfunction tsTypeFor(sqlType: string): string {\n const normalized = sqlType\n .toUpperCase()\n .replace(/\\(.*\\)$/, \"\")\n .replace(/<.*>$/, \"\")\n .split(\" \")[0];\n\n switch (normalized) {\n case \"BOOLEAN\":\n return \"boolean\";\n case \"TINYINT\":\n case \"SMALLINT\":\n case \"INT\":\n case \"INTEGER\":\n case \"BIGINT\":\n case \"FLOAT\":\n case \"DOUBLE\":\n case \"DECIMAL\":\n case \"NUMERIC\":\n return \"number\";\n default:\n return \"string\";\n }\n}\n\n// Render a MetricRegistry interface entry from a MetricSchema.\nfunction renderMetricEntry(schema: MetricSchema): string {\n if (schema.degraded) {\n return renderDegradedMetricEntry(schema);\n }\n const indent = \" \";\n const colsBlock = (cols: MetricColumnMetadata[]): string => {\n if (cols.length === 0) return \"Record<string, never>\";\n const fields = cols\n .map((col) => {\n const grainComment = col.timeGrains?.length\n ? ` @timeGrain ${col.timeGrains.join(\"|\")}`\n : \"\";\n return `${indent}/** @sqlType ${col.type.replace(/\\*\\//g, \"* /\")}${grainComment} */\n${indent}${JSON.stringify(col.name)}: ${tsTypeFor(col.type)}`;\n })\n .join(\";\\n\");\n return `{\n${fields};\n }`;\n };\n const unionOf = (keys: string[]): string =>\n keys.length > 0 ? keys.join(\" | \") : \"never\";\n\n const measuresBlock = colsBlock(schema.measures);\n const dimensionsBlock = colsBlock(schema.dimensions);\n const measureUnion = unionOf(\n schema.measures.map((m) => JSON.stringify(m.name)),\n );\n const dimensionUnion = unionOf(\n schema.dimensions.map((d) => JSON.stringify(d.name)),\n );\n\n const timeGrainSet = new Set<string>();\n for (const d of schema.dimensions) {\n for (const g of d.timeGrains ?? []) {\n timeGrainSet.add(g);\n }\n }\n const timeGrainUnion =\n timeGrainSet.size > 0\n ? [...timeGrainSet]\n .sort()\n .map((g) => JSON.stringify(g))\n .join(\" | \")\n : \"never\";\n\n const measureMetadata = renderMetadataMap(schema.measures, indent);\n const dimensionMetadata = renderMetadataMap(schema.dimensions, indent, true);\n\n return ` ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: ${measuresBlock};\n dimensions: ${dimensionsBlock};\n measureKeys: ${measureUnion};\n dimensionKeys: ${dimensionUnion};\n timeGrains: ${timeGrainUnion};\n metadata: {\n measures: ${measureMetadata};\n dimensions: ${dimensionMetadata};\n };\n }`;\n}\n\n// Render the permissive (\"degraded-open\") entry for a schema the warehouse could not describe.\nfunction renderDegradedMetricEntry(schema: MetricSchema): string {\n return ` /** Degraded: schema unavailable at type-generation time — permissive types until a successful DESCRIBE refreshes them. */\n ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: Record<string, unknown>;\n dimensions: Record<string, unknown>;\n measureKeys: string;\n dimensionKeys: string;\n timeGrains: string;\n metadata: {\n measures: Record<string, never>;\n dimensions: Record<string, never>;\n };\n }`;\n}\n\n// Render the type-level shape of a column's semantic-metadata map\n// for the `metadata` field of a MetricRegistry entry.\nfunction renderMetadataMap(\n cols: MetricColumnMetadata[],\n indent: string,\n includeTimeGrain = false,\n): string {\n if (cols.length === 0) return \"Record<string, never>\";\n\n const inner = cols\n .map((col) => {\n const fields: string[] = [`type: ${JSON.stringify(col.type)}`];\n if (col.displayName) {\n fields.push(`display_name: ${JSON.stringify(col.displayName)}`);\n }\n if (col.format) {\n fields.push(`format: ${JSON.stringify(col.format)}`);\n }\n if (col.description) {\n fields.push(`description: ${JSON.stringify(col.description)}`);\n }\n if (includeTimeGrain && col.timeGrains && col.timeGrains.length > 0) {\n const grainTuple = col.timeGrains\n .map((g) => JSON.stringify(g))\n .join(\", \");\n fields.push(`time_grain: readonly [${grainTuple}]`);\n }\n const fieldsBlock = fields.map((f) => `${indent} ${f}`).join(\";\\n\");\n return `${indent}${JSON.stringify(col.name)}: {\n${fieldsBlock};\n${indent}}`;\n })\n .join(\";\\n\");\n\n return `{\n${inner};\n }`;\n}\n\n// Render the augmentation block for the appkit-ui MetricRegistry interface.\nfunction renderMetricRegistry(schemas: MetricSchema[]): string {\n if (schemas.length === 0) {\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {}\n}\n`;\n }\n const entries = schemas.map(renderMetricEntry).join(\";\\n\");\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {\n${entries};\n }\n}\n`;\n}\n\n// Build the full metric.d.ts file from a list of metric schemas.\nexport function generateMetricTypeDeclarations(\n schemas: MetricSchema[],\n): string {\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\n${renderMetricRegistry(schemas)}`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"render-types.js","names":[],"sources":["../../../src/type-generator/mv-registry/render-types.ts"],"sourcesContent":["import type { MetricColumnMetadata, MetricSchema } from \"./types\";\n\n/**\n * @todo unify with query-registry.ts\n * Map a Databricks SQL type to a TypeScript primitive.\n * Centralized here (not imported from query-registry) so this module\n * stays self-contained.\n */\nfunction tsTypeFor(sqlType: string): string {\n const normalized = sqlType\n .toUpperCase()\n .replace(/\\(.*\\)$/, \"\")\n .replace(/<.*>$/, \"\")\n .split(\" \")[0];\n\n switch (normalized) {\n case \"BOOLEAN\":\n return \"boolean\";\n case \"TINYINT\":\n case \"SMALLINT\":\n case \"INT\":\n case \"INTEGER\":\n case \"BIGINT\":\n case \"FLOAT\":\n case \"DOUBLE\":\n case \"DECIMAL\":\n case \"NUMERIC\":\n return \"number\";\n default:\n return \"string\";\n }\n}\n\n// Render a MetricRegistry interface entry from a MetricSchema.\nfunction renderMetricEntry(schema: MetricSchema): string {\n if (schema.degraded) {\n return renderDegradedMetricEntry(schema);\n }\n const indent = \" \";\n const colsBlock = (cols: MetricColumnMetadata[]): string => {\n if (cols.length === 0) return \"Record<string, never>\";\n const fields = cols\n .map((col) => {\n const grainComment = col.timeGrains?.length\n ? ` @timeGrain ${col.timeGrains.join(\"|\")}`\n : \"\";\n return `${indent}/** @sqlType ${col.type.replace(/\\*\\//g, \"* /\")}${grainComment} */\n${indent}${JSON.stringify(col.name)}: ${tsTypeFor(col.type)}`;\n })\n .join(\";\\n\");\n return `{\n${fields};\n }`;\n };\n const unionOf = (keys: string[]): string =>\n keys.length > 0 ? keys.join(\" | \") : \"never\";\n\n const measuresBlock = colsBlock(schema.measures);\n const dimensionsBlock = colsBlock(schema.dimensions);\n const measureUnion = unionOf(\n schema.measures.map((m) => JSON.stringify(m.name)),\n );\n const dimensionUnion = unionOf(\n schema.dimensions.map((d) => JSON.stringify(d.name)),\n );\n\n const timeGrainSet = new Set<string>();\n for (const d of schema.dimensions) {\n for (const g of d.timeGrains ?? []) {\n timeGrainSet.add(g);\n }\n }\n const timeGrainUnion =\n timeGrainSet.size > 0\n ? [...timeGrainSet]\n .sort()\n .map((g) => JSON.stringify(g))\n .join(\" | \")\n : \"never\";\n\n const measureMetadata = renderMetadataMap(schema.measures, indent);\n const dimensionMetadata = renderMetadataMap(schema.dimensions, indent, true);\n\n return ` ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: ${measuresBlock};\n dimensions: ${dimensionsBlock};\n measureKeys: ${measureUnion};\n dimensionKeys: ${dimensionUnion};\n timeGrains: ${timeGrainUnion};\n metadata: {\n measures: ${measureMetadata};\n dimensions: ${dimensionMetadata};\n };\n }`;\n}\n\n// Render the permissive (\"degraded-open\") entry for a schema the warehouse could not describe.\nfunction renderDegradedMetricEntry(schema: MetricSchema): string {\n return ` /** Degraded: schema unavailable at type-generation time — permissive types until a successful DESCRIBE refreshes them. */\n ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: Record<string, unknown>;\n dimensions: Record<string, unknown>;\n measureKeys: string;\n dimensionKeys: string;\n timeGrains: string;\n metadata: {\n measures: Record<string, never>;\n dimensions: Record<string, never>;\n };\n }`;\n}\n\n// Render the type-level shape of a column's semantic-metadata map\n// for the `metadata` field of a MetricRegistry entry.\nfunction renderMetadataMap(\n cols: MetricColumnMetadata[],\n indent: string,\n includeTimeGrain = false,\n): string {\n if (cols.length === 0) return \"Record<string, never>\";\n\n const inner = cols\n .map((col) => {\n const fields: string[] = [`type: ${JSON.stringify(col.type)}`];\n if (col.displayName) {\n fields.push(`display_name: ${JSON.stringify(col.displayName)}`);\n }\n if (col.format) {\n fields.push(`format: ${JSON.stringify(col.format)}`);\n }\n if (col.description) {\n fields.push(`description: ${JSON.stringify(col.description)}`);\n }\n if (includeTimeGrain && col.timeGrains && col.timeGrains.length > 0) {\n const grainTuple = col.timeGrains\n .map((g) => JSON.stringify(g))\n .join(\", \");\n fields.push(`time_grain: readonly [${grainTuple}]`);\n }\n const fieldsBlock = fields.map((f) => `${indent} ${f}`).join(\";\\n\");\n return `${indent}${JSON.stringify(col.name)}: {\n${fieldsBlock};\n${indent}}`;\n })\n .join(\";\\n\");\n\n return `{\n${inner};\n }`;\n}\n\n// Render the augmentation block for the appkit-ui MetricRegistry interface.\nfunction renderMetricRegistry(schemas: MetricSchema[]): string {\n if (schemas.length === 0) {\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {}\n}\n`;\n }\n const entries = schemas.map(renderMetricEntry).join(\";\\n\");\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {\n${entries};\n }\n}\n`;\n}\n\n// Build the full metric-views.d.ts file from a list of metric schemas.\nexport function generateMetricTypeDeclarations(\n schemas: MetricSchema[],\n): string {\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\n${renderMetricRegistry(schemas)}`;\n}\n"],"mappings":";;;;;;;AAQA,SAAS,UAAU,SAAyB;AAO1C,SANmB,QAChB,aAAa,CACb,QAAQ,WAAW,GAAG,CACtB,QAAQ,SAAS,GAAG,CACpB,MAAM,IAAI,CAAC,IAEd;EACE,KAAK,UACH,QAAO;EACT,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK;EACL,KAAK,UACH,QAAO;EACT,QACE,QAAO;;;AAKb,SAAS,kBAAkB,QAA8B;AACvD,KAAI,OAAO,SACT,QAAO,0BAA0B,OAAO;CAE1C,MAAM,SAAS;CACf,MAAM,aAAa,SAAyC;AAC1D,MAAI,KAAK,WAAW,EAAG,QAAO;AAU9B,SAAO;EATQ,KACZ,KAAK,QAAQ;GACZ,MAAM,eAAe,IAAI,YAAY,SACjC,eAAe,IAAI,WAAW,KAAK,IAAI,KACvC;AACJ,UAAO,GAAG,OAAO,eAAe,IAAI,KAAK,QAAQ,SAAS,MAAM,GAAG,aAAa;EACtF,SAAS,KAAK,UAAU,IAAI,KAAK,CAAC,IAAI,UAAU,IAAI,KAAK;IACnD,CACD,KAAK,MAAM,CAET;;;CAGP,MAAM,WAAW,SACf,KAAK,SAAS,IAAI,KAAK,KAAK,MAAM,GAAG;CAEvC,MAAM,gBAAgB,UAAU,OAAO,SAAS;CAChD,MAAM,kBAAkB,UAAU,OAAO,WAAW;CACpD,MAAM,eAAe,QACnB,OAAO,SAAS,KAAK,MAAM,KAAK,UAAU,EAAE,KAAK,CAAC,CACnD;CACD,MAAM,iBAAiB,QACrB,OAAO,WAAW,KAAK,MAAM,KAAK,UAAU,EAAE,KAAK,CAAC,CACrD;CAED,MAAM,+BAAe,IAAI,KAAa;AACtC,MAAK,MAAM,KAAK,OAAO,WACrB,MAAK,MAAM,KAAK,EAAE,cAAc,EAAE,CAChC,cAAa,IAAI,EAAE;CAGvB,MAAM,iBACJ,aAAa,OAAO,IAChB,CAAC,GAAG,aAAa,CACd,MAAM,CACN,KAAK,MAAM,KAAK,UAAU,EAAE,CAAC,CAC7B,KAAK,MAAM,GACd;CAEN,MAAM,kBAAkB,kBAAkB,OAAO,UAAU,OAAO;CAClE,MAAM,oBAAoB,kBAAkB,OAAO,YAAY,QAAQ,KAAK;AAE5E,QAAO,OAAO,KAAK,UAAU,OAAO,IAAI,CAAC;aAC9B,KAAK,UAAU,OAAO,IAAI,CAAC;gBACxB,KAAK,UAAU,OAAO,OAAO,CAAC;cAChC,KAAK,UAAU,OAAO,KAAK,CAAC;kBACxB,cAAc;oBACZ,gBAAgB;qBACf,aAAa;uBACX,eAAe;oBAClB,eAAe;;oBAEf,gBAAgB;sBACd,kBAAkB;;;;AAMxC,SAAS,0BAA0B,QAA8B;AAC/D,QAAO;MACH,KAAK,UAAU,OAAO,IAAI,CAAC;aACpB,KAAK,UAAU,OAAO,IAAI,CAAC;gBACxB,KAAK,UAAU,OAAO,OAAO,CAAC;cAChC,KAAK,UAAU,OAAO,KAAK,CAAC;;;;;;;;;;;;AAe1C,SAAS,kBACP,MACA,QACA,mBAAmB,OACX;AACR,KAAI,KAAK,WAAW,EAAG,QAAO;AA2B9B,QAAO;EAzBO,KACX,KAAK,QAAQ;EACZ,MAAM,SAAmB,CAAC,SAAS,KAAK,UAAU,IAAI,KAAK,GAAG;AAC9D,MAAI,IAAI,YACN,QAAO,KAAK,iBAAiB,KAAK,UAAU,IAAI,YAAY,GAAG;AAEjE,MAAI,IAAI,OACN,QAAO,KAAK,WAAW,KAAK,UAAU,IAAI,OAAO,GAAG;AAEtD,MAAI,IAAI,YACN,QAAO,KAAK,gBAAgB,KAAK,UAAU,IAAI,YAAY,GAAG;AAEhE,MAAI,oBAAoB,IAAI,cAAc,IAAI,WAAW,SAAS,GAAG;GACnE,MAAM,aAAa,IAAI,WACpB,KAAK,MAAM,KAAK,UAAU,EAAE,CAAC,CAC7B,KAAK,KAAK;AACb,UAAO,KAAK,yBAAyB,WAAW,GAAG;;EAErD,MAAM,cAAc,OAAO,KAAK,MAAM,GAAG,OAAO,IAAI,IAAI,CAAC,KAAK,MAAM;AACpE,SAAO,GAAG,SAAS,KAAK,UAAU,IAAI,KAAK,CAAC;EAChD,YAAY;EACZ,OAAO;GACH,CACD,KAAK,MAAM,CAGR;;;AAKR,SAAS,qBAAqB,SAAiC;AAC7D,KAAI,QAAQ,WAAW,EACrB,QAAO;;;;AAMT,QAAO;;EADS,QAAQ,IAAI,kBAAkB,CAAC,KAAK,MAAM,CAGlD;;;;;AAOV,SAAgB,+BACd,SACQ;AACR,QAAO;;;EAGP,qBAAqB,QAAQ"}
@@ -11,13 +11,6 @@ interface AppKitTypesPluginOptions {
11
11
  * Defaults to a sibling of `outFile`, computed by the generator.
12
12
  */
13
13
  mvOutFile?: string;
14
- /**
15
- * Path to the metric semantic-metadata JSON file (relative to client folder).
16
- * Build-time artifact — defaults to a sibling of {@link mvOutFile}
17
- * (itself a sibling of `outFile`), computed by the generator. Skipped
18
- * automatically when `metric-views.json` is absent.
19
- */
20
- mvMetadataOutFile?: string;
21
14
  /** Folders to watch for changes. */
22
15
  watchFolders?: string[];
23
16
  }
@@ -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;EASY;;AASd;;;;EAXE,iBAAA;EAWgC;EAThC,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;UA6BzB,wBAAA;EAER,OAAA;EAFgC;;;;EAOhC,SAAA;EAEY;EAAZ,YAAA;AAAA;;;;;;;iBASc,iBAAA,CAAkB,OAAA,GAAU,wBAAA,GAA2B,MAAA"}
@@ -23,7 +23,6 @@ const DEV_WAREHOUSE_WATCH_MAX_MS = 6e4;
23
23
  function appKitTypesPlugin(options) {
24
24
  let outFile;
25
25
  let mvOutFile;
26
- let mvMetadataOutFile;
27
26
  let watchFolders;
28
27
  let inFlight = null;
29
28
  let queued = false;
@@ -53,8 +52,7 @@ function appKitTypesPlugin(options) {
53
52
  warehouseId,
54
53
  noCache: false,
55
54
  mode,
56
- mvOutFile,
57
- mvMetadataOutFile
55
+ mvOutFile
58
56
  });
59
57
  } catch (error) {
60
58
  const isTypegenError = error instanceof TypegenSyntaxError || error instanceof TypegenFatalError;
@@ -176,7 +174,6 @@ function appKitTypesPlugin(options) {
176
174
  const projectRoot = path.resolve(config.root, "..");
177
175
  outFile = path.resolve(projectRoot, options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`);
178
176
  mvOutFile = options?.mvOutFile !== void 0 ? path.resolve(projectRoot, options.mvOutFile) : void 0;
179
- mvMetadataOutFile = options?.mvMetadataOutFile !== void 0 ? path.resolve(projectRoot, options.mvMetadataOutFile) : void 0;
180
177
  watchFolders = options?.watchFolders ?? [path.join(process.cwd(), "config", "queries")];
181
178
  },
182
179
  buildStart() {
@@ -1 +1 @@
1
- {"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { Plugin } from \"vite\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n TypegenFatalError,\n TypegenSyntaxError,\n} from \"./index\";\nimport type { PreflightMode } from \"./preflight\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * How long the DEV background watcher waits for a STARTING warehouse to reach\n * RUNNING before giving up. Short relative to the CLI's preflight budget: this\n * is a best-effort \"regenerate once the warehouse warms up\" convenience, not a\n * gate, so we'd rather stop polling than hold a detached task open for minutes.\n */\nconst DEV_WAREHOUSE_WATCH_MAX_MS = 60_000;\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /**\n * Path to the metric registry d.ts file (relative to client folder).\n * Defaults to a sibling of `outFile`, computed by the generator.\n */\n mvOutFile?: string;\n /**\n * Path to the metric semantic-metadata JSON file (relative to client folder).\n * Build-time artifact — defaults to a sibling of {@link mvOutFile}\n * (itself a sibling of `outFile`), computed by the generator. Skipped\n * automatically when `metric-views.json` is absent.\n */\n mvMetadataOutFile?: string;\n /** Folders to watch for changes. */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let mvOutFile: string | undefined;\n let mvMetadataOutFile: string | undefined;\n let watchFolders: string[];\n\n // Single-flight state for runGenerate(). `inFlight` is the promise of the\n // currently-running drain (null when idle); `queued` records that a trigger\n // arrived while a run was active so exactly ONE trailing run fires afterwards\n // (latest-wins — coalesces any number of overlapping triggers into a single\n // rerun). `queued` is read/cleared synchronously inside the drain loop so a\n // trigger landing in any window is caught before the drain exits.\n //\n // `pendingMode` is the mode the next generate should run in (latest-wins, like\n // `queued`): the foreground build runs non-blocking in dev (instant degrade)\n // while the background warehouse watch runs blocking (real DESCRIBEs). A\n // blocking watch trigger that lands while a non-blocking foreground run is in\n // flight therefore still describes when its trailing run fires.\n let inFlight: Promise<void> | null = null;\n let queued = false;\n let pendingMode: PreflightMode = \"non-blocking\";\n\n // The currently-armed DEV background warehouse watch, if any. Aborting it\n // stops a pending waitUntilRunning (server shutdown, or a newer arm replacing\n // an older one).\n let watchController: AbortController | null = null;\n\n /**\n * Generate types once in the given preflight {@link PreflightMode}. Never\n * throws in dev (logs instead); in production it rethrows so the build fails.\n * This is the un-guarded core — callers should go through {@link runGenerate}\n * so concurrent triggers can't race-write the .d.ts.\n *\n * @param mode - preflight policy for this run. The foreground build passes a\n * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it\n * degrades instantly); the background warehouse watch passes \"blocking\" so\n * its regenerate actually DESCRIBEs and lands real (non-degraded) types.\n */\n async function generateOnce(mode: PreflightMode) {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder: watchFolders[0],\n warehouseId,\n noCache: false,\n mode,\n mvOutFile,\n mvMetadataOutFile,\n });\n } catch (error) {\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // report in their message. Their stack frames and attached query arrays\n // point into appkit internals and only add noise, so surface just the\n // message — both when failing the prod build and when logging in dev.\n const isTypegenError =\n error instanceof TypegenSyntaxError ||\n error instanceof TypegenFatalError;\n\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n if (isTypegenError) error.stack = error.message;\n throw error;\n }\n\n if (isTypegenError) {\n logger.error(\"%s\", error.message);\n } else {\n logger.error(\"Error generating types: %O\", error);\n }\n }\n }\n\n /**\n * Single-flight wrapper around {@link generateOnce}. The initial build, the\n * .sql watcher, and the DEV warehouse watch all route through here so they can\n * never run typegen concurrently (which would race-write the .d.ts).\n *\n * If a run is already in flight, this does NOT start a second one — it records\n * the requested mode and sets a trailing flag so exactly one more run fires\n * after the current finishes, coalescing any number of overlapping triggers\n * (latest-wins, including the mode: a blocking watch trigger that arrives mid\n * non-blocking foreground run still describes when its trailing run fires).\n *\n * @param mode - preflight policy for this run. Recorded into `pendingMode`,\n * which the drain reads for each generate (latest trigger wins).\n * @returns A promise that resolves when this trigger's work (including any\n * trailing run it scheduled) has completed.\n */\n function runGenerate(mode: PreflightMode): Promise<void> {\n pendingMode = mode;\n\n if (inFlight) {\n // A run is active: remember that another trigger arrived and ride out the\n // current run. One trailing run then covers all coalesced triggers and\n // runs in the latest requested mode (recorded above).\n queued = true;\n return inFlight;\n }\n\n // Drain in a loop rather than recursing after a single queued-check: a\n // trigger can land in the window between generateOnce() resolving and the\n // check, so we re-test `queued` until it's clear. Critically, `inFlight` is\n // cleared synchronously in the SAME tick as the final `queued === false`\n // observation — never deferred to a .finally microtask — so there's no\n // window where a trigger sees `inFlight` set but the drain has already\n // decided to exit. The guard stays held for the whole drain, so concurrent\n // triggers only ever set the flag; they never start a parallel generate.\n const drain = async (): Promise<void> => {\n while (true) {\n queued = false;\n // Snapshot the mode synchronously alongside clearing `queued` so a\n // trigger landing during this generate is observed (via `queued`) on the\n // next loop with its own mode, not silently dropped.\n const runMode = pendingMode;\n await generateOnce(runMode);\n // Synchronous check + clear, atomic w.r.t. other (synchronous) callers.\n if (!queued) {\n inFlight = null;\n return;\n }\n }\n };\n\n inFlight = drain();\n return inFlight;\n }\n\n /**\n * DEV-only: get the warehouse to RUNNING in the background and regenerate with\n * real (non-degraded) types once it is — without blocking dev startup. The\n * foreground build only ever degrades in dev (instant `unknown`/cached types),\n * so this is what lands actual DESCRIBE results in the editor for EVERY\n * reachable warehouse state, not just one that happens to already be warm.\n *\n * Post-probe behaviour by state:\n * - RUNNING → describe right away (the dev foreground degraded, so a running\n * warehouse would otherwise never get real types). `waitUntilRunning`\n * returns immediately for an already-running warehouse, then the blocking\n * regenerate fires.\n * - STARTING → it's already coming up; just wait for RUNNING, then describe.\n * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.\n * - DELETED / DELETING → return (a deleted warehouse can't be started, and\n * blocking typegen would treat it as fatal); leave the degraded types.\n *\n * No-op in production or without a warehouse id. Replaces any previously-armed\n * watch (aborting it first). Fully self-contained: it never throws into the\n * caller and never re-arms itself. The whole lifecycle is abortable via the\n * shared {@link watchController} — its signal is threaded into\n * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the\n * regenerate routes through {@link runGenerate} so it can't race-write the\n * .d.ts with the foreground degrade or a `.sql` re-trigger.\n *\n * The regenerate runs in \"blocking\" mode (not the foreground's non-blocking)\n * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —\n * the whole point of warming the warehouse in the background.\n */\n function armWarehouseWatch(): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n if (!warehouseId) return;\n\n // Supersede any in-flight watch so we never run two concurrently.\n watchController?.abort();\n const controller = new AbortController();\n watchController = controller;\n const { signal } = controller;\n\n void (async () => {\n try {\n const client = new WorkspaceClient({});\n const state = await getWarehouseState(client, warehouseId);\n\n // A deleted/deleting warehouse can't be started and blocking typegen\n // would treat it as fatal — leave the degraded types and stop. Every\n // other state (including RUNNING) proceeds to wait-then-describe so the\n // dev editor gets real types, not just the foreground's degraded ones.\n if (state === \"DELETED\" || state === \"DELETING\") {\n return;\n }\n\n // Stopped/stopping won't reach RUNNING on its own — nudge it. RUNNING and\n // STARTING need no start (RUNNING is already up; STARTING is coming up),\n // so don't issue a redundant one. A failed start is non-fatal: give up\n // silently rather than throw out of the detached task (the developer\n // still has degraded/cached types).\n let startedByUs = false;\n if (state === \"STOPPED\" || state === \"STOPPING\") {\n try {\n logger.debug(\"Warehouse is %s; starting it.\", state);\n await startWarehouse(client, warehouseId);\n startedByUs = true;\n } catch {\n return;\n }\n }\n\n // Wait for RUNNING. For an already-RUNNING warehouse this returns on the\n // first poll; for STARTING/STOPPED it polls (abortably) until the\n // warehouse warms up, a terminal state, or the deadline.\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,\n signal,\n // We just issued the start, so the first poll(s) often still report\n // STOPPED/STOPPING before the start propagates. Poll through those\n // instead of bailing, or the regenerate would never fire. When we\n // didn't start it (RUNNING/STARTING branch), keep the default terminal\n // states.\n treatStoppedAsTransient: startedByUs,\n });\n\n if (final === \"RUNNING\" && !signal.aborted) {\n logger.debug(\"Warehouse is RUNNING; regenerating types.\");\n // Blocking: the warehouse is RUNNING now, so describe it and emit real\n // (non-degraded) types — unlike the foreground dev run, which degraded.\n // Routed through the single-flight guard so it coalesces with the\n // foreground degrade / any `.sql` re-trigger instead of racing them.\n await runGenerate(\"blocking\");\n }\n } catch {\n // Detached background task: any failure (timeout, abort, connectivity,\n // auth) is non-fatal — the developer still has degraded/cached types.\n }\n })();\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n if (!existsSync(path.join(process.cwd(), \"config\", \"queries\"))) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n // Metric out-paths resolve against projectRoot only when explicitly\n // provided; unset options pass through as undefined so the generator\n // computes its sibling-of-outFile defaults. In the all-defaults case\n // the final paths are identical (the default outFile above lives in\n // shared/<TYPES_DIR>/), and a customized outFile now keeps its metric\n // siblings next to it instead of pinning them under shared/.\n mvOutFile =\n options?.mvOutFile !== undefined\n ? path.resolve(projectRoot, options.mvOutFile)\n : undefined;\n mvMetadataOutFile =\n options?.mvMetadataOutFile !== undefined\n ? path.resolve(projectRoot, options.mvMetadataOutFile)\n : undefined;\n watchFolders = options?.watchFolders ?? [\n path.join(process.cwd(), \"config\", \"queries\"),\n ];\n },\n\n buildStart() {\n // Production: block the build on this generate (and surface failures).\n // The watch is a dev-only no-op, so just run typegen.\n if (process.env.NODE_ENV === \"production\") {\n return runGenerate(\"blocking\");\n }\n\n // Dev: don't block startup waiting on typegen. The foreground generate runs\n // non-blocking — it skips the warehouse entirely and writes degraded\n // (cached/`unknown`) types instantly. Then arm the warehouse watch so the\n // warehouse gets a one-shot BLOCKING regenerate (real types) in the\n // background for EVERY reachable state: RUNNING describes right away, while\n // STARTING/STOPPED are waited (and started) until they reach RUNNING.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n if (\n isWatchedFile &&\n (changedFile.endsWith(\".sql\") ||\n // Basename equality, not endsWith: a sibling like\n // \"legacy-metric-views.json\" must not trigger a regenerate —\n // only the real config file does.\n path.basename(changedFile) === \"metric-views.json\")\n ) {\n // Route through the single-flight runner (was fire-and-forget\n // generate(), which could race the initial build / watch). This is a\n // dev-only hook, so degrade instantly (non-blocking), then re-arm the\n // warehouse watch so the edited query or metric-view source is\n // re-described in the background against the running warehouse (or\n // once a still-starting one warms up), landing fresh\n // blocking-described types.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n }\n });\n\n // Tear down any pending warehouse watch when the dev server closes so a\n // long backoff can't keep the process alive after shutdown.\n server.httpServer?.once(\"close\", () => {\n watchController?.abort();\n });\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { Plugin } from \"vite\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n TypegenFatalError,\n TypegenSyntaxError,\n} from \"./index\";\nimport type { PreflightMode } from \"./preflight\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * How long the DEV background watcher waits for a STARTING warehouse to reach\n * RUNNING before giving up. Short relative to the CLI's preflight budget: this\n * is a best-effort \"regenerate once the warehouse warms up\" convenience, not a\n * gate, so we'd rather stop polling than hold a detached task open for minutes.\n */\nconst DEV_WAREHOUSE_WATCH_MAX_MS = 60_000;\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /**\n * Path to the metric registry d.ts file (relative to client folder).\n * Defaults to a sibling of `outFile`, computed by the generator.\n */\n mvOutFile?: string;\n /** Folders to watch for changes. */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let mvOutFile: string | undefined;\n let watchFolders: string[];\n\n // Single-flight state for runGenerate(). `inFlight` is the promise of the\n // currently-running drain (null when idle); `queued` records that a trigger\n // arrived while a run was active so exactly ONE trailing run fires afterwards\n // (latest-wins — coalesces any number of overlapping triggers into a single\n // rerun). `queued` is read/cleared synchronously inside the drain loop so a\n // trigger landing in any window is caught before the drain exits.\n //\n // `pendingMode` is the mode the next generate should run in (latest-wins, like\n // `queued`): the foreground build runs non-blocking in dev (instant degrade)\n // while the background warehouse watch runs blocking (real DESCRIBEs). A\n // blocking watch trigger that lands while a non-blocking foreground run is in\n // flight therefore still describes when its trailing run fires.\n let inFlight: Promise<void> | null = null;\n let queued = false;\n let pendingMode: PreflightMode = \"non-blocking\";\n\n // The currently-armed DEV background warehouse watch, if any. Aborting it\n // stops a pending waitUntilRunning (server shutdown, or a newer arm replacing\n // an older one).\n let watchController: AbortController | null = null;\n\n /**\n * Generate types once in the given preflight {@link PreflightMode}. Never\n * throws in dev (logs instead); in production it rethrows so the build fails.\n * This is the un-guarded core — callers should go through {@link runGenerate}\n * so concurrent triggers can't race-write the .d.ts.\n *\n * @param mode - preflight policy for this run. The foreground build passes a\n * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it\n * degrades instantly); the background warehouse watch passes \"blocking\" so\n * its regenerate actually DESCRIBEs and lands real (non-degraded) types.\n */\n async function generateOnce(mode: PreflightMode) {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder: watchFolders[0],\n warehouseId,\n noCache: false,\n mode,\n mvOutFile,\n });\n } catch (error) {\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // report in their message. Their stack frames and attached query arrays\n // point into appkit internals and only add noise, so surface just the\n // message — both when failing the prod build and when logging in dev.\n const isTypegenError =\n error instanceof TypegenSyntaxError ||\n error instanceof TypegenFatalError;\n\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n if (isTypegenError) error.stack = error.message;\n throw error;\n }\n\n if (isTypegenError) {\n logger.error(\"%s\", error.message);\n } else {\n logger.error(\"Error generating types: %O\", error);\n }\n }\n }\n\n /**\n * Single-flight wrapper around {@link generateOnce}. The initial build, the\n * .sql watcher, and the DEV warehouse watch all route through here so they can\n * never run typegen concurrently (which would race-write the .d.ts).\n *\n * If a run is already in flight, this does NOT start a second one — it records\n * the requested mode and sets a trailing flag so exactly one more run fires\n * after the current finishes, coalescing any number of overlapping triggers\n * (latest-wins, including the mode: a blocking watch trigger that arrives mid\n * non-blocking foreground run still describes when its trailing run fires).\n *\n * @param mode - preflight policy for this run. Recorded into `pendingMode`,\n * which the drain reads for each generate (latest trigger wins).\n * @returns A promise that resolves when this trigger's work (including any\n * trailing run it scheduled) has completed.\n */\n function runGenerate(mode: PreflightMode): Promise<void> {\n pendingMode = mode;\n\n if (inFlight) {\n // A run is active: remember that another trigger arrived and ride out the\n // current run. One trailing run then covers all coalesced triggers and\n // runs in the latest requested mode (recorded above).\n queued = true;\n return inFlight;\n }\n\n // Drain in a loop rather than recursing after a single queued-check: a\n // trigger can land in the window between generateOnce() resolving and the\n // check, so we re-test `queued` until it's clear. Critically, `inFlight` is\n // cleared synchronously in the SAME tick as the final `queued === false`\n // observation — never deferred to a .finally microtask — so there's no\n // window where a trigger sees `inFlight` set but the drain has already\n // decided to exit. The guard stays held for the whole drain, so concurrent\n // triggers only ever set the flag; they never start a parallel generate.\n const drain = async (): Promise<void> => {\n while (true) {\n queued = false;\n // Snapshot the mode synchronously alongside clearing `queued` so a\n // trigger landing during this generate is observed (via `queued`) on the\n // next loop with its own mode, not silently dropped.\n const runMode = pendingMode;\n await generateOnce(runMode);\n // Synchronous check + clear, atomic w.r.t. other (synchronous) callers.\n if (!queued) {\n inFlight = null;\n return;\n }\n }\n };\n\n inFlight = drain();\n return inFlight;\n }\n\n /**\n * DEV-only: get the warehouse to RUNNING in the background and regenerate with\n * real (non-degraded) types once it is — without blocking dev startup. The\n * foreground build only ever degrades in dev (instant `unknown`/cached types),\n * so this is what lands actual DESCRIBE results in the editor for EVERY\n * reachable warehouse state, not just one that happens to already be warm.\n *\n * Post-probe behaviour by state:\n * - RUNNING → describe right away (the dev foreground degraded, so a running\n * warehouse would otherwise never get real types). `waitUntilRunning`\n * returns immediately for an already-running warehouse, then the blocking\n * regenerate fires.\n * - STARTING → it's already coming up; just wait for RUNNING, then describe.\n * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.\n * - DELETED / DELETING → return (a deleted warehouse can't be started, and\n * blocking typegen would treat it as fatal); leave the degraded types.\n *\n * No-op in production or without a warehouse id. Replaces any previously-armed\n * watch (aborting it first). Fully self-contained: it never throws into the\n * caller and never re-arms itself. The whole lifecycle is abortable via the\n * shared {@link watchController} — its signal is threaded into\n * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the\n * regenerate routes through {@link runGenerate} so it can't race-write the\n * .d.ts with the foreground degrade or a `.sql` re-trigger.\n *\n * The regenerate runs in \"blocking\" mode (not the foreground's non-blocking)\n * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —\n * the whole point of warming the warehouse in the background.\n */\n function armWarehouseWatch(): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n if (!warehouseId) return;\n\n // Supersede any in-flight watch so we never run two concurrently.\n watchController?.abort();\n const controller = new AbortController();\n watchController = controller;\n const { signal } = controller;\n\n void (async () => {\n try {\n const client = new WorkspaceClient({});\n const state = await getWarehouseState(client, warehouseId);\n\n // A deleted/deleting warehouse can't be started and blocking typegen\n // would treat it as fatal — leave the degraded types and stop. Every\n // other state (including RUNNING) proceeds to wait-then-describe so the\n // dev editor gets real types, not just the foreground's degraded ones.\n if (state === \"DELETED\" || state === \"DELETING\") {\n return;\n }\n\n // Stopped/stopping won't reach RUNNING on its own — nudge it. RUNNING and\n // STARTING need no start (RUNNING is already up; STARTING is coming up),\n // so don't issue a redundant one. A failed start is non-fatal: give up\n // silently rather than throw out of the detached task (the developer\n // still has degraded/cached types).\n let startedByUs = false;\n if (state === \"STOPPED\" || state === \"STOPPING\") {\n try {\n logger.debug(\"Warehouse is %s; starting it.\", state);\n await startWarehouse(client, warehouseId);\n startedByUs = true;\n } catch {\n return;\n }\n }\n\n // Wait for RUNNING. For an already-RUNNING warehouse this returns on the\n // first poll; for STARTING/STOPPED it polls (abortably) until the\n // warehouse warms up, a terminal state, or the deadline.\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,\n signal,\n // We just issued the start, so the first poll(s) often still report\n // STOPPED/STOPPING before the start propagates. Poll through those\n // instead of bailing, or the regenerate would never fire. When we\n // didn't start it (RUNNING/STARTING branch), keep the default terminal\n // states.\n treatStoppedAsTransient: startedByUs,\n });\n\n if (final === \"RUNNING\" && !signal.aborted) {\n logger.debug(\"Warehouse is RUNNING; regenerating types.\");\n // Blocking: the warehouse is RUNNING now, so describe it and emit real\n // (non-degraded) types — unlike the foreground dev run, which degraded.\n // Routed through the single-flight guard so it coalesces with the\n // foreground degrade / any `.sql` re-trigger instead of racing them.\n await runGenerate(\"blocking\");\n }\n } catch {\n // Detached background task: any failure (timeout, abort, connectivity,\n // auth) is non-fatal — the developer still has degraded/cached types.\n }\n })();\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n if (!existsSync(path.join(process.cwd(), \"config\", \"queries\"))) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n // The metric out-path resolves against projectRoot only when explicitly\n // provided; an unset option passes through as undefined so the generator\n // computes its sibling-of-outFile default. In the all-defaults case the\n // final path is identical (the default outFile above lives in\n // shared/<TYPES_DIR>/), and a customized outFile now keeps its metric\n // sibling next to it instead of pinning it under shared/.\n mvOutFile =\n options?.mvOutFile !== undefined\n ? path.resolve(projectRoot, options.mvOutFile)\n : undefined;\n watchFolders = options?.watchFolders ?? [\n path.join(process.cwd(), \"config\", \"queries\"),\n ];\n },\n\n buildStart() {\n // Production: block the build on this generate (and surface failures).\n // The watch is a dev-only no-op, so just run typegen.\n if (process.env.NODE_ENV === \"production\") {\n return runGenerate(\"blocking\");\n }\n\n // Dev: don't block startup waiting on typegen. The foreground generate runs\n // non-blocking — it skips the warehouse entirely and writes degraded\n // (cached/`unknown`) types instantly. Then arm the warehouse watch so the\n // warehouse gets a one-shot BLOCKING regenerate (real types) in the\n // background for EVERY reachable state: RUNNING describes right away, while\n // STARTING/STOPPED are waited (and started) until they reach RUNNING.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n if (\n isWatchedFile &&\n (changedFile.endsWith(\".sql\") ||\n // Basename equality, not endsWith: a sibling like\n // \"legacy-metric-views.json\" must not trigger a regenerate —\n // only the real config file does.\n path.basename(changedFile) === \"metric-views.json\")\n ) {\n // Route through the single-flight runner (was fire-and-forget\n // generate(), which could race the initial build / watch). This is a\n // dev-only hook, so degrade instantly (non-blocking), then re-arm the\n // warehouse watch so the edited query or metric-view source is\n // re-described in the background against the running warehouse (or\n // once a still-starting one warms up), landing fresh\n // blocking-described types.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n }\n });\n\n // Tear down any pending warehouse watch when the dev server closes so a\n // long backoff can't keep the process alive after shutdown.\n server.httpServer?.once(\"close\", () => {\n watchController?.abort();\n });\n },\n };\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ AppKit can automatically generate TypeScript types for your SQL queries, providi
6
6
 
7
7
  Generate type-safe TypeScript declarations for query keys, parameters, and result rows.
8
8
 
9
- All generated files live in `shared/appkit-types/`, one per plugin (e.g. `analytics.d.ts`). They use [`declare module`](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to augment existing interfaces, so the types apply globally — you never need to import them. TypeScript auto-discovers them through `"include": ["shared/appkit-types"]` in your tsconfig.
9
+ All generated files live in `shared/appkit-types/`, one per concern: `analytics.d.ts` (SQL query types), `serving.d.ts` (model-serving endpoint types), and `metric-views.d.ts`. A single command (and the Vite plugin) produces them all in one pass; see [Metric-view types](#metric-view-types). The `.d.ts` files use [`declare module`](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to augment existing interfaces, so the types apply globally — you never need to import them. TypeScript auto-discovers them through `"include": ["shared/appkit-types"]` in your tsconfig.
10
10
 
11
11
  ## Vite plugin: `appKitTypesPlugin`[​](#vite-plugin-appkittypesplugin "Direct link to vite-plugin-appkittypesplugin")
12
12
 
@@ -86,6 +86,32 @@ npx @databricks/appkit generate-types --wait
86
86
 
87
87
  In blocking mode the generator starts a stopped warehouse, waits (bounded) for it to reach `RUNNING`, and then describes your queries. It fails only when the configured warehouse no longer exists (deleted/deleting), so a transient outage or a cold warehouse degrades gracefully rather than breaking the build. The app template wires this up for you: `postinstall` and `predev` run the non-blocking default, while `prebuild` runs `--wait`.
88
88
 
89
+ ## Metric-view types[​](#metric-view-types "Direct link to Metric-view types")
90
+
91
+ `generate-types` (and the Vite plugin) emit metric-view types **additively** — there is no separate command. When a `config/queries/metric-views.json` file is present, the same run that generates your query types also DESCRIBEs each declared [UC Metric View](./docs/plugins/analytics.md) and writes `metric-views.d.ts` into `shared/appkit-types/`:
92
+
93
+ * `metric-views.d.ts` — augments the `MetricRegistry` interface so `useMetricView('<key>', …)` is autocompleted and type-checked. Each view's measures, dimensions, and their semantic metadata (SQL type, display name, format, time grains) are encoded at the type level.
94
+
95
+ If `metric-views.json` is absent the metric path stays dormant (nothing is emitted). When present it follows the **same** warehouse-readiness contract as query types: in the default non-blocking run a view that can't be described yet — a cold warehouse, or a bad/unreachable source — is written with permissive types and a warning, while under `--wait` that same situation fails the build so CI never ships incomplete metric types. A malformed `metric-views.json` (invalid JSON, or a source that isn't a three-part UC FQN) fails fast in every mode.
96
+
97
+ `metric-views.json` is keyed by metric key; each entry names the three-part UC FQN of the view and, optionally, the executor it runs as (`app_service_principal`, the default, or `user`):
98
+
99
+ ```json
100
+ {
101
+ "$schema": "https://databricks.github.io/appkit/schemas/metric-source.schema.json",
102
+ "metricViews": {
103
+ "revenue": { "source": "catalog.schema.revenue_metrics" },
104
+ "customers": {
105
+ "source": "catalog.schema.customer_metrics",
106
+ "executor": "user"
107
+ }
108
+ }
109
+ }
110
+
111
+ ```
112
+
113
+ The optional `$schema` line enables editor autocomplete and validation against the published schema.
114
+
89
115
  ## How it works[​](#how-it-works "Direct link to How it works")
90
116
 
91
117
  The type generator:
@@ -116,7 +142,7 @@ const { data } = useAnalyticsQuery("users_list", {
116
142
  });
117
143
 
118
144
  // TypeScript knows the shape of the result rows
119
- data?.forEach(row => {
145
+ data?.forEach((row) => {
120
146
  console.log(row.email); // ✓ autocomplete works
121
147
  });
122
148
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@databricks/appkit",
3
3
  "type": "module",
4
- "version": "0.43.0",
4
+ "version": "0.43.1",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
7
7
  "bin": {
@@ -67,6 +67,7 @@
67
67
  "@types/semver": "7.7.1",
68
68
  "apache-arrow": "21.1.0",
69
69
  "dotenv": "16.6.1",
70
+ "drizzle-orm": "0.45.1",
70
71
  "express": "4.22.2",
71
72
  "get-port": "7.2.0",
72
73
  "js-yaml": "4.2.0",