@databricks/appkit 0.43.0 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CLAUDE.md +0 -1
  2. package/NOTICE.md +1 -0
  3. package/dist/appkit/package.js +1 -1
  4. package/dist/cli/commands/generate-types.js +5 -0
  5. package/dist/cli/commands/generate-types.js.map +1 -1
  6. package/dist/index.js +0 -1
  7. package/dist/plugins/analytics/query.js +0 -1
  8. package/dist/plugins/analytics/query.js.map +1 -1
  9. package/dist/type-generator/cache.js +1 -6
  10. package/dist/type-generator/cache.js.map +1 -1
  11. package/dist/type-generator/index.js +162 -146
  12. package/dist/type-generator/index.js.map +1 -1
  13. package/dist/type-generator/mv-registry/config.js +5 -5
  14. package/dist/type-generator/mv-registry/config.js.map +1 -1
  15. package/dist/type-generator/mv-registry/render-types.js.map +1 -1
  16. package/dist/type-generator/vite-plugin.d.ts +0 -7
  17. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  18. package/dist/type-generator/vite-plugin.js +1 -4
  19. package/dist/type-generator/vite-plugin.js.map +1 -1
  20. package/docs/api/appkit-ui/ui/ContextMenu.md +2 -2
  21. package/docs/api/appkit-ui/ui/Drawer.md +0 -56
  22. package/docs/api/appkit-ui/ui/DropdownMenu.md +3 -3
  23. package/docs/api/appkit-ui/ui/HoverCard.md +2 -2
  24. package/docs/api/appkit-ui/ui/Menubar.md +3 -3
  25. package/docs/api/appkit-ui/ui/Popover.md +2 -2
  26. package/docs/api/appkit-ui/ui/Select.md +2 -2
  27. package/docs/api/appkit-ui/ui/Tooltip.md +2 -2
  28. package/docs/development/type-generation.md +28 -2
  29. package/llms.txt +0 -1
  30. package/package.json +3 -2
  31. package/sbom.cdx.json +1 -1
  32. package/dist/shared/src/index.js +0 -4
  33. package/dist/shared/src/sql/index.js +0 -3
  34. package/dist/type-generator/mv-registry/metadata.js +0 -40
  35. package/dist/type-generator/mv-registry/metadata.js.map +0 -1
  36. package/docs/api/appkit-ui/ui/ChartContainer.md +0 -343
package/CLAUDE.md CHANGED
@@ -211,7 +211,6 @@ npx @databricks/appkit docs <query>
211
211
  - [Calendar](./docs/api/appkit-ui/ui/Calendar.md): Date picker component for selecting single dates or date ranges
212
212
  - [Card](./docs/api/appkit-ui/ui/Card.md): Container for grouping related content with header, body, and footer sections
213
213
  - [Carousel](./docs/api/appkit-ui/ui/Carousel.md): Slideshow component for cycling through content with navigation controls
214
- - [ChartContainer](./docs/api/appkit-ui/ui/ChartContainer.md): Container for rendering data visualizations using Recharts
215
214
  - [Checkbox](./docs/api/appkit-ui/ui/Checkbox.md): Checkbox input for selecting multiple options
216
215
  - [Collapsible](./docs/api/appkit-ui/ui/Collapsible.md): Interactive component that expands and collapses content
217
216
  - [Command](./docs/api/appkit-ui/ui/Command.md): Command palette for keyboard-driven navigation and actions
package/NOTICE.md CHANGED
@@ -59,6 +59,7 @@ This Software contains code from the following open source projects:
59
59
  | [commander](https://www.npmjs.com/package/commander) | 2.20.3, 5.1.0, 7.2.0, 8.3.0, 10.0.1, 12.1.0 | MIT | https://github.com/tj/commander.js#readme |
60
60
  | [dompurify](https://www.npmjs.com/package/dompurify) | 3.4.11 | (MPL-2.0 OR Apache-2.0) | https://github.com/cure53/DOMPurify |
61
61
  | [dotenv](https://www.npmjs.com/package/dotenv) | 16.6.1 | BSD-2-Clause | https://github.com/motdotla/dotenv#readme |
62
+ | [drizzle-orm](https://www.npmjs.com/package/drizzle-orm) | 0.45.1 | Apache-2.0 | https://orm.drizzle.team |
62
63
  | [echarts](https://www.npmjs.com/package/echarts) | 6.0.0 | Apache-2.0 | https://echarts.apache.org |
63
64
  | [echarts-for-react](https://www.npmjs.com/package/echarts-for-react) | 3.0.5 | MIT | https://github.com/hustcc/echarts-for-react |
64
65
  | [embla-carousel-react](https://www.npmjs.com/package/embla-carousel-react) | 8.6.0 | MIT | https://www.embla-carousel.com |
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.43.0";
3
+ var version = "0.44.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -42,6 +42,11 @@ async function runGenerateTypes(rootDir, outFile, warehouseId, options) {
42
42
  mode
43
43
  });
44
44
  console.log(`Generated query types: ${resolvedOutFile}`);
45
+ const metricConfig = path.join(queryFolder, "metric-views.json");
46
+ if (fs.existsSync(metricConfig)) {
47
+ const typesDir = path.dirname(resolvedOutFile);
48
+ console.log(`Generated metric types: ${path.join(typesDir, "metric-views.d.ts")}`);
49
+ }
45
50
  }
46
51
  } else console.error("Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.");
47
52
  const servingOutFile = path.join(process.cwd(), "shared/appkit-types/serving.d.ts");
@@ -1 +1 @@
1
- {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n if (fs.existsSync(queryFolder)) {\n await typeGen.generateFromEntryPoint({\n queryFolder,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n mode,\n });\n console.log(`Generated query types: ${resolvedOutFile}`);\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":"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"}
1
+ {"version":3,"file":"generate-types.js","names":[],"sources":["../../../src/cli/commands/generate-types.ts"],"sourcesContent":["import { spawn } from \"node:child_process\";\nimport fs from \"node:fs\";\nimport path from \"node:path\";\nimport { Command, Option } from \"commander\";\nimport {\n acquireSpawnLock,\n getSpawnLockPath,\n releaseSpawnLock,\n} from \"./spawn-lock.js\";\n\n/**\n * Resolve the typegen pre-flight mode for the CLI. Defaults to \"non-blocking\" —\n * a one-shot CLI can't describe in the background, so by default it never\n * describes at all: it skips the warehouse probe AND every DESCRIBE, emits\n * best-available types (cache where the SQL hash matches, else `result: unknown`)\n * and returns immediately, never blocking on — or failing because of — a\n * warehouse, even a RUNNING one. Pass `--wait` (commander sets `wait: true`)\n * for a deliberate/CI invocation that should wait for a starting warehouse and\n * fail fast on a stopped one.\n */\nexport function resolveTypegenMode(options?: {\n wait?: boolean;\n}): \"non-blocking\" | \"blocking\" {\n return options?.wait ? \"blocking\" : \"non-blocking\";\n}\n\n/** Options parsed by commander for the generate-types command. */\ninterface GenerateTypesOptions {\n noCache?: boolean;\n wait?: boolean;\n /**\n * Internal: present only on the detached worker invocation. Carries the path\n * of the single-flight lock this worker must release when it finishes. Its\n * presence is what marks an invocation as \"the worker\" — workers always run\n * with `--wait`, so they never spawn another worker (only non-blocking runs\n * spawn), which terminates the recursion.\n */\n workerLock?: string;\n}\n\n/**\n * Generate types command implementation. Runs the library generate (which, in\n * non-blocking mode, writes degraded types and returns immediately). This is the\n * SAME work the worker performs in blocking mode in the background.\n */\nasync function runGenerateTypes(\n rootDir?: string,\n outFile?: string,\n warehouseId?: string,\n options?: GenerateTypesOptions,\n) {\n try {\n const resolvedRootDir = rootDir || process.cwd();\n const noCache = options?.noCache || false;\n const mode = resolveTypegenMode(options);\n\n const typeGen = await import(\"@databricks/appkit/type-generator\");\n\n // Generate analytics query types (requires warehouse ID)\n const resolvedWarehouseId =\n warehouseId || process.env.DATABRICKS_WAREHOUSE_ID;\n\n if (resolvedWarehouseId) {\n const resolvedOutFile =\n outFile ||\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\");\n\n const queryFolder = path.join(resolvedRootDir, \"config/queries\");\n if (fs.existsSync(queryFolder)) {\n await typeGen.generateFromEntryPoint({\n queryFolder,\n outFile: resolvedOutFile,\n warehouseId: resolvedWarehouseId,\n noCache,\n mode,\n });\n console.log(`Generated query types: ${resolvedOutFile}`);\n\n const metricConfig = path.join(queryFolder, \"metric-views.json\");\n if (fs.existsSync(metricConfig)) {\n const typesDir = path.dirname(resolvedOutFile);\n console.log(\n `Generated metric types: ${path.join(typesDir, \"metric-views.d.ts\")}`,\n );\n }\n }\n } else {\n console.error(\n \"Skipping query type generation: no warehouse ID. Set DATABRICKS_WAREHOUSE_ID or pass as argument.\",\n );\n }\n\n // Generate serving endpoint types (no warehouse required)\n const servingOutFile = path.join(\n process.cwd(),\n \"shared/appkit-types/serving.d.ts\",\n );\n await typeGen.generateServingTypes({\n outFile: servingOutFile,\n noCache,\n });\n console.log(`Generated serving types: ${servingOutFile}`);\n } catch (error) {\n if (\n error instanceof Error &&\n error.message.includes(\"Cannot find module\")\n ) {\n console.error(\n \"Error: The 'generate-types' command is only available in @databricks/appkit.\",\n );\n console.error(\"Please install @databricks/appkit to use this command.\");\n process.exit(1);\n }\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // message (which queries failed and how to debug them). The stack trace\n // points into appkit internals and is noise for app developers, so print\n // only the message and exit non-zero instead of letting it bubble up.\n if (\n error instanceof Error &&\n (error.name === \"TypegenSyntaxError\" ||\n error.name === \"TypegenFatalError\")\n ) {\n console.error(error.message);\n process.exit(1);\n }\n throw error;\n }\n}\n\n/**\n * Spawn the detached blocking worker that refreshes real types in the background\n * after the foreground non-blocking generate has already written degraded types.\n *\n * Re-invokes THIS CLI (`process.execPath` + `process.argv[1]` — the bin entry\n * that launched us) with `generate-types --wait --worker-lock <lockPath>` plus\n * the same positional target options the foreground used, so the worker writes\n * to the same out file / reads the same query folder. The worker is:\n * - `detached: true` + `.unref()` so it outlives this process (install/dev-setup\n * can finish and exit while the worker keeps warming the warehouse).\n * - `stdio: \"ignore\"` so it never holds the parent's pipes open or interleaves\n * output into the install/dev log.\n *\n * Spawning is wrapped so any failure is non-fatal: the caller still has degraded\n * types and exits 0.\n *\n * @param lockPath - the acquired single-flight lock; passed to the worker so it\n * releases the SAME lock when it finishes.\n * @param targets - the foreground's positional args, forwarded verbatim.\n * @returns true if the worker was spawned, false if spawning threw.\n */\nexport function spawnTypegenWorker(\n lockPath: string,\n targets: { rootDir?: string; outFile?: string; warehouseId?: string },\n): boolean {\n // The script the runtime launched us with (the `appkit` bin shim). Re-running\n // it under the same node binary reproduces this exact CLI in the worker.\n const cliEntry = process.argv[1];\n\n // Forward the positionals in declaration order (rootDir, outFile,\n // warehouseId). Stop at the first undefined so we never pass a literal\n // \"undefined\" — commander would treat it as a positional value. (rootDir is\n // effectively always set by commander's default, but guard anyway.)\n const positionals: string[] = [];\n for (const value of [targets.rootDir, targets.outFile, targets.warehouseId]) {\n if (value === undefined) break;\n positionals.push(value);\n }\n\n const args = [\n // Forward the parent's node/loader flags so the worker runs under the same\n // runtime. Critically this carries tsx's `--require`/`--import` when the CLI\n // is run from source (`tsx index.ts …`); without them the worker would be\n // `node index.ts …`, which can't parse TypeScript and dies silently — the\n // degraded types would then never refresh. Empty for the built bin (plain\n // `node bin/appkit.js`), so production behaviour is unchanged.\n ...process.execArgv,\n cliEntry,\n \"generate-types\",\n \"--wait\",\n \"--worker-lock\",\n lockPath,\n ...positionals,\n ];\n\n try {\n const child = spawn(process.execPath, args, {\n detached: true,\n stdio: \"ignore\",\n });\n child.unref();\n return true;\n } catch (error) {\n // Non-fatal: the foreground already wrote degraded types. Log and move on.\n console.error(\n `Could not start background type refresh: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n return false;\n }\n}\n\n/**\n * The command action. Orchestrates the non-blocking foreground contract:\n * 1. Run the library generate (writes degraded types immediately in non-blocking\n * mode; does the full blocking lifecycle when this is the worker).\n * 2. If this is a non-blocking, non-worker invocation, try to spawn the detached\n * blocking worker behind the single-flight lock. If the lock is already held\n * by a live worker, skip (single-flight) with a one-line note. Either way the\n * foreground returns normally (exit 0).\n * 3. If this IS the worker (`--worker-lock` present), it ran blocking above and\n * releases the lock here (and via a process-exit guard, so a hard failure /\n * process.exit still frees it).\n */\nasync function generateTypesAction(\n rootDir: string | undefined,\n outFile: string | undefined,\n warehouseId: string | undefined,\n options: GenerateTypesOptions,\n) {\n const isWorker = typeof options.workerLock === \"string\";\n\n // A worker must always free its lock, even if the blocking generate throws or\n // calls process.exit (TypegenFatalError → exit 1). The exit handler covers the\n // process.exit / uncaught paths; the finally covers the normal return.\n if (isWorker && options.workerLock) {\n const lockPath = options.workerLock;\n process.once(\"exit\", () => releaseSpawnLock(lockPath));\n }\n\n try {\n await runGenerateTypes(rootDir, outFile, warehouseId, options);\n } finally {\n if (isWorker && options.workerLock) {\n releaseSpawnLock(options.workerLock);\n }\n }\n\n // Only a non-blocking, non-worker invocation spawns. A worker is always\n // --wait (so resolveTypegenMode → \"blocking\"), which both prevents recursion\n // and means we never get here for a worker.\n if (!isWorker && resolveTypegenMode(options) === \"non-blocking\") {\n const resolvedRootDir = rootDir || process.cwd();\n const lockPath = getSpawnLockPath(resolvedRootDir);\n\n if (acquireSpawnLock(lockPath)) {\n spawnTypegenWorker(lockPath, { rootDir, outFile, warehouseId });\n } else {\n console.log(\"Type refresh already in progress, skipping.\");\n }\n }\n}\n\nexport const generateTypesCommand = new Command(\"generate-types\")\n .description(\"Generate TypeScript types from SQL queries\")\n .argument(\"[rootDir]\", \"Root directory of the project\", process.cwd())\n .argument(\n \"[outFile]\",\n \"Output file path\",\n path.join(process.cwd(), \"shared/appkit-types/analytics.d.ts\"),\n )\n .argument(\"[warehouseId]\", \"Databricks warehouse ID\")\n .option(\"--no-cache\", \"Disable caching for type generation\")\n .option(\n \"--wait\",\n \"Wait for warehouse readiness instead of degrading (use for CI)\",\n )\n // Internal: marks the detached background worker and carries the lock it must\n // release. Hidden from --help; users should never pass it.\n .addOption(\n new Option(\n \"--worker-lock <path>\",\n \"Internal: detached worker lock path\",\n ).hideHelp(),\n )\n .addHelpText(\n \"after\",\n `\nExamples:\n $ appkit generate-types\n $ appkit generate-types . shared/appkit-types/analytics.d.ts\n $ appkit generate-types . shared/appkit-types/analytics.d.ts my-warehouse-id\n $ appkit generate-types --no-cache\n $ appkit generate-types --wait # CI: wait for the warehouse and fail on a cold one`,\n )\n .action(generateTypesAction);\n"],"mappings":";;;;;;;;;;;;;;;;;AAoBA,SAAgB,mBAAmB,SAEH;AAC9B,QAAO,SAAS,OAAO,aAAa;;;;;;;AAsBtC,eAAe,iBACb,SACA,SACA,aACA,SACA;AACA,KAAI;EACF,MAAM,kBAAkB,WAAW,QAAQ,KAAK;EAChD,MAAM,UAAU,SAAS,WAAW;EACpC,MAAM,OAAO,mBAAmB,QAAQ;EAExC,MAAM,UAAU,MAAM,OAAO;EAG7B,MAAM,sBACJ,eAAe,QAAQ,IAAI;AAE7B,MAAI,qBAAqB;GACvB,MAAM,kBACJ,WACA,KAAK,KAAK,QAAQ,KAAK,EAAE,qCAAqC;GAEhE,MAAM,cAAc,KAAK,KAAK,iBAAiB,iBAAiB;AAChE,OAAI,GAAG,WAAW,YAAY,EAAE;AAC9B,UAAM,QAAQ,uBAAuB;KACnC;KACA,SAAS;KACT,aAAa;KACb;KACA;KACD,CAAC;AACF,YAAQ,IAAI,0BAA0B,kBAAkB;IAExD,MAAM,eAAe,KAAK,KAAK,aAAa,oBAAoB;AAChE,QAAI,GAAG,WAAW,aAAa,EAAE;KAC/B,MAAM,WAAW,KAAK,QAAQ,gBAAgB;AAC9C,aAAQ,IACN,2BAA2B,KAAK,KAAK,UAAU,oBAAoB,GACpE;;;QAIL,SAAQ,MACN,oGACD;EAIH,MAAM,iBAAiB,KAAK,KAC1B,QAAQ,KAAK,EACb,mCACD;AACD,QAAM,QAAQ,qBAAqB;GACjC,SAAS;GACT;GACD,CAAC;AACF,UAAQ,IAAI,4BAA4B,iBAAiB;UAClD,OAAO;AACd,MACE,iBAAiB,SACjB,MAAM,QAAQ,SAAS,qBAAqB,EAC5C;AACA,WAAQ,MACN,+EACD;AACD,WAAQ,MAAM,yDAAyD;AACvE,WAAQ,KAAK,EAAE;;AAMjB,MACE,iBAAiB,UAChB,MAAM,SAAS,wBACd,MAAM,SAAS,sBACjB;AACA,WAAQ,MAAM,MAAM,QAAQ;AAC5B,WAAQ,KAAK,EAAE;;AAEjB,QAAM;;;;;;;;;;;;;;;;;;;;;;;;AAyBV,SAAgB,mBACd,UACA,SACS;CAGT,MAAM,WAAW,QAAQ,KAAK;CAM9B,MAAM,cAAwB,EAAE;AAChC,MAAK,MAAM,SAAS;EAAC,QAAQ;EAAS,QAAQ;EAAS,QAAQ;EAAY,EAAE;AAC3E,MAAI,UAAU,OAAW;AACzB,cAAY,KAAK,MAAM;;CAGzB,MAAM,OAAO;EAOX,GAAG,QAAQ;EACX;EACA;EACA;EACA;EACA;EACA,GAAG;EACJ;AAED,KAAI;AAKF,EAJc,MAAM,QAAQ,UAAU,MAAM;GAC1C,UAAU;GACV,OAAO;GACR,CAAC,CACI,OAAO;AACb,SAAO;UACA,OAAO;AAEd,UAAQ,MACN,4CACE,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM,GAEzD;AACD,SAAO;;;;;;;;;;;;;;;AAgBX,eAAe,oBACb,SACA,SACA,aACA,SACA;CACA,MAAM,WAAW,OAAO,QAAQ,eAAe;AAK/C,KAAI,YAAY,QAAQ,YAAY;EAClC,MAAM,WAAW,QAAQ;AACzB,UAAQ,KAAK,cAAc,iBAAiB,SAAS,CAAC;;AAGxD,KAAI;AACF,QAAM,iBAAiB,SAAS,SAAS,aAAa,QAAQ;WACtD;AACR,MAAI,YAAY,QAAQ,WACtB,kBAAiB,QAAQ,WAAW;;AAOxC,KAAI,CAAC,YAAY,mBAAmB,QAAQ,KAAK,gBAAgB;EAE/D,MAAM,WAAW,iBADO,WAAW,QAAQ,KAAK,CACE;AAElD,MAAI,iBAAiB,SAAS,CAC5B,oBAAmB,UAAU;GAAE;GAAS;GAAS;GAAa,CAAC;MAE/D,SAAQ,IAAI,8CAA8C;;;AAKhE,MAAa,uBAAuB,IAAI,QAAQ,iBAAiB,CAC9D,YAAY,6CAA6C,CACzD,SAAS,aAAa,iCAAiC,QAAQ,KAAK,CAAC,CACrE,SACC,aACA,oBACA,KAAK,KAAK,QAAQ,KAAK,EAAE,qCAAqC,CAC/D,CACA,SAAS,iBAAiB,0BAA0B,CACpD,OAAO,cAAc,sCAAsC,CAC3D,OACC,UACA,iEACD,CAGA,UACC,IAAI,OACF,wBACA,sCACD,CAAC,UAAU,CACb,CACA,YACC,SACA;;;;;;wFAOD,CACA,OAAO,oBAAoB"}
package/dist/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { isSQLTypeMarker, sql } from "./shared/src/sql/helpers.js";
2
- import "./shared/src/index.js";
3
2
  import { createLakebasePoolManager } from "./connectors/lakebase/pool-manager.js";
4
3
  import { AppKitError } from "./errors/base.js";
5
4
  import { AuthenticationError } from "./errors/authentication.js";
@@ -1,5 +1,4 @@
1
1
  import { isSQLTypeMarker, sql } from "../../shared/src/sql/helpers.js";
2
- import "../../shared/src/index.js";
3
2
  import { ValidationError } from "../../errors/validation.js";
4
3
  import "../../errors/index.js";
5
4
  import { getWorkspaceId } from "../../context/execution-context.js";
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../../src/plugins/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { sql } from \"@databricks/sdk-experimental\";\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport { getWorkspaceId } from \"../../context\";\nimport { ValidationError } from \"../../errors\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n const validParams = Array.from(queryParams).join(\", \") || \"none\";\n throw ValidationError.invalidValue(\n key,\n parameters[key],\n `a parameter defined in the query (valid: ${validParams})`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw ValidationError.invalidValue(\n key,\n value,\n \"SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())\",\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;;;;AAQA,IAAa,iBAAb,MAA4B;CAC1B,MAAM,mBACJ,OACA,YAC4C;EAC5C,MAAM,YAAY,EAAE,GAAG,YAAY;EAGnC,MAAM,eAAe,MAAM,SAAS,mBAAmB;AAIvD,MAHoB,IAAI,IAAI,MAAM,KAAK,eAAe,MAAM,EAAE,GAAG,CAAC,CAGlD,IAAI,cAAc,IAAI,CAAC,UAAU,aAAa;GAC5D,MAAM,cAAc,MAAM,gBAAgB;AAC1C,OAAI,YACF,WAAU,cAAcA,IAAW,OAAO,YAAY;;AAI1D,SAAO;;CAGT,UAAU,OAAuB;AAC/B,SAAO,WAAW,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;CAGtD,uBACE,OACA,YACqE;EACrE,MAAM,gBAAkD,EAAE;AAE1D,MAAI,YAAY;GAEd,MAAM,oBAAoB,MAAM,SAAS,mBAAmB;GAC5D,MAAM,cAAc,IAAI,IAAI,MAAM,KAAK,oBAAoB,MAAM,EAAE,GAAG,CAAC;AAGvE,QAAK,MAAM,OAAO,OAAO,KAAK,WAAW,CACvC,KAAI,CAAC,YAAY,IAAI,IAAI,EAAE;IACzB,MAAM,cAAc,MAAM,KAAK,YAAY,CAAC,KAAK,KAAK,IAAI;AAC1D,UAAM,gBAAgB,aACpB,KACA,WAAW,MACX,4CAA4C,YAAY,GACzD;;AAKL,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;IACrD,MAAM,YAAY,KAAK,iBAAiB,KAAK,MAAM;AACnD,QAAI,UACF,eAAc,KAAK,UAAU;;;AAKnC,SAAO;GAAE,WAAW;GAAO,YAAY;GAAe;;CAGxD,AAAQ,iBACN,KACA,OACuC;AACvC,MAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO;AAGT,MAAI,CAAC,gBAAgB,MAAM,CACzB,OAAM,gBAAgB,aACpB,KACA,OACA,2FACD;AAGH,SAAO;GACL,MAAM;GACN,OAAO,MAAM;GACb,MAAM,MAAM;GACb"}
1
+ {"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../../src/plugins/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { sql } from \"@databricks/sdk-experimental\";\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport { getWorkspaceId } from \"../../context\";\nimport { ValidationError } from \"../../errors\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n const validParams = Array.from(queryParams).join(\", \") || \"none\";\n throw ValidationError.invalidValue(\n key,\n parameters[key],\n `a parameter defined in the query (valid: ${validParams})`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw ValidationError.invalidValue(\n key,\n value,\n \"SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())\",\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;;;AAQA,IAAa,iBAAb,MAA4B;CAC1B,MAAM,mBACJ,OACA,YAC4C;EAC5C,MAAM,YAAY,EAAE,GAAG,YAAY;EAGnC,MAAM,eAAe,MAAM,SAAS,mBAAmB;AAIvD,MAHoB,IAAI,IAAI,MAAM,KAAK,eAAe,MAAM,EAAE,GAAG,CAAC,CAGlD,IAAI,cAAc,IAAI,CAAC,UAAU,aAAa;GAC5D,MAAM,cAAc,MAAM,gBAAgB;AAC1C,OAAI,YACF,WAAU,cAAcA,IAAW,OAAO,YAAY;;AAI1D,SAAO;;CAGT,UAAU,OAAuB;AAC/B,SAAO,WAAW,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;CAGtD,uBACE,OACA,YACqE;EACrE,MAAM,gBAAkD,EAAE;AAE1D,MAAI,YAAY;GAEd,MAAM,oBAAoB,MAAM,SAAS,mBAAmB;GAC5D,MAAM,cAAc,IAAI,IAAI,MAAM,KAAK,oBAAoB,MAAM,EAAE,GAAG,CAAC;AAGvE,QAAK,MAAM,OAAO,OAAO,KAAK,WAAW,CACvC,KAAI,CAAC,YAAY,IAAI,IAAI,EAAE;IACzB,MAAM,cAAc,MAAM,KAAK,YAAY,CAAC,KAAK,KAAK,IAAI;AAC1D,UAAM,gBAAgB,aACpB,KACA,WAAW,MACX,4CAA4C,YAAY,GACzD;;AAKL,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;IACrD,MAAM,YAAY,KAAK,iBAAiB,KAAK,MAAM;AACnD,QAAI,UACF,eAAc,KAAK,UAAU;;;AAKnC,SAAO;GAAE,WAAW;GAAO,YAAY;GAAe;;CAGxD,AAAQ,iBACN,KACA,OACuC;AACvC,MAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO;AAGT,MAAI,CAAC,gBAAgB,MAAM,CACzB,OAAM,gBAAgB,aACpB,KACA,OACA,2FACD;AAGH,SAAO;GACL,MAAM;GACN,OAAO,MAAM;GACb,MAAM,MAAM;GACb"}
@@ -12,12 +12,7 @@ const logger = createLogger("type-generator:cache");
12
12
  * hand-edits, truncation, or a stale writer can leave entries whose shape no
13
13
  * longer matches {@link MetricCacheEntry}. A malformed entry must read as a
14
14
  * cache MISS (re-describe) rather than crash the pass or render revived
15
- * garbage into the artifacts. Checks exactly what the renderers and the
16
- * metadata bundle consume: `hash` string, `retry` boolean, and a schema with
17
- * `key`/`source` strings, a valid lane, an optional boolean `degraded`, and
18
- * measure/dimension arrays whose elements carry `name`/`type` strings
19
- * (other column fields are optional). Deliberately inline — the shared Zod
20
- * schemas must not enter the type-generator's runtime path.
15
+ * garbage into the artifacts.
21
16
  */
22
17
  function isRevivableMetricCacheEntry(entry) {
23
18
  if (typeof entry !== "object" || entry === null) return false;
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","names":[],"sources":["../../src/type-generator/cache.ts"],"sourcesContent":["import crypto from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createLogger } from \"../logging/logger\";\nimport type { MetricSchema } from \"./mv-registry/types\";\n\nconst logger = createLogger(\"type-generator:cache\");\n\n/**\n * Cache types\n * @property hash - the hash of the SQL query\n * @property type - the type of the query\n * @property retry - when true the entry never satisfies a cache hit, so the\n * query is re-described on the next pass; fresh successful describes\n * persist `retry: false`\n */\ninterface CacheEntry {\n hash: string;\n type: string;\n retry: boolean;\n}\n\n/**\n * One cached metric-view DESCRIBE outcome.\n *\n * `hash` is md5 over `\"<source>|<lane>\"` — the two config inputs that\n * determine a DESCRIBE — so editing either invalidates the entry. `schema`\n * is the full {@link MetricSchema} persisted verbatim (it is JSON-safe by\n * design), letting a warm pass regenerate both metric artifacts without a\n * single warehouse call. `retry: true` marks a SELF-CONVERGING degraded\n * outcome (DESCRIBE skipped behind a not-running warehouse, unanswered, or\n * transiently failed): the cached schema still renders artifacts, but the\n * next eligible pass re-describes exactly these keys so degraded schemas\n * converge to real ones. A degraded schema with `retry: false` is a STICKY\n * failure — a deterministic DESCRIBE failure (bad FQN, unparseable\n * response, zero columns) or a deleted warehouse — that re-describing the\n * unchanged entry cannot fix; it hits like any cached entry until the\n * config hash changes or the cache is bypassed, and the type generator\n * warns about it on every pass that serves it.\n */\nexport interface MetricCacheEntry {\n hash: string;\n schema: MetricSchema;\n retry: boolean;\n}\n\n/**\n * Structural gate for reviving a cached metric entry at partition time.\n *\n * The cache file lives in `node_modules/.databricks` and is plain JSON —\n * hand-edits, truncation, or a stale writer can leave entries whose shape no\n * longer matches {@link MetricCacheEntry}. A malformed entry must read as a\n * cache MISS (re-describe) rather than crash the pass or render revived\n * garbage into the artifacts. Checks exactly what the renderers and the\n * metadata bundle consume: `hash` string, `retry` boolean, and a schema with\n * `key`/`source` strings, a valid lane, an optional boolean `degraded`, and\n * measure/dimension arrays whose elements carry `name`/`type` strings\n * (other column fields are optional). Deliberately inline — the shared Zod\n * schemas must not enter the type-generator's runtime path.\n */\nexport function isRevivableMetricCacheEntry(entry: unknown): boolean {\n if (typeof entry !== \"object\" || entry === null) return false;\n const e = entry as Record<string, unknown>;\n if (typeof e.hash !== \"string\" || typeof e.retry !== \"boolean\") {\n return false;\n }\n const schema = e.schema;\n if (typeof schema !== \"object\" || schema === null || Array.isArray(schema)) {\n return false;\n }\n const s = schema as Record<string, unknown>;\n const isColumnArray = (value: unknown): boolean =>\n Array.isArray(value) &&\n value.every(\n (col) =>\n typeof col === \"object\" &&\n col !== null &&\n typeof (col as Record<string, unknown>).name === \"string\" &&\n typeof (col as Record<string, unknown>).type === \"string\",\n );\n return (\n typeof s.key === \"string\" &&\n typeof s.source === \"string\" &&\n (s.lane === \"sp\" || s.lane === \"obo\") &&\n (s.degraded === undefined || typeof s.degraded === \"boolean\") &&\n isColumnArray(s.measures) &&\n isColumnArray(s.dimensions)\n );\n}\n\n/**\n * Cache interface\n * @property version - the version of the cache\n * @property queries - the queries in the cache\n * @property metrics - cached metric-view schemas keyed by metric key.\n * OPTIONAL on purpose: version \"3\" files written before this section\n * existed load unchanged (absent ⇒ treated as empty by the metric path),\n * and the query path's `noCache` reinit literal stays valid as-is. The\n * section rides through the query path's load → mutate → save cycle as a\n * plain sibling key, so query-side saves preserve it byte-for-byte.\n */\ninterface Cache {\n version: string;\n queries: Record<string, CacheEntry>;\n metrics?: Record<string, MetricCacheEntry>;\n}\n\nexport const CACHE_VERSION = \"3\";\nconst CACHE_FILE = \".appkit-types-cache.json\";\nconst CACHE_DIR = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n);\n\n/**\n * Hash the SQL query\n * Uses MD5 to hash the SQL query\n * @param sql - the SQL query to hash\n * @returns - the hash of the SQL query\n */\nexport function hashSQL(sql: string): string {\n return crypto.createHash(\"md5\").update(sql).digest(\"hex\");\n}\n\n/**\n * Change detector stored on {@link MetricCacheEntry.hash}: md5 over\n * `\"<source>|<lane>\"` — the two config inputs that determine a DESCRIBE —\n * so editing either invalidates the entry.\n */\nexport function metricCacheHash(source: string, lane: string): string {\n return hashSQL(`${source}|${lane}`);\n}\n\n/**\n * Load the cache from the file system\n * If the cache is not found, run the query explain\n * @returns - the cache\n */\nexport async function loadCache(): Promise<Cache> {\n const cachePath = path.join(CACHE_DIR, CACHE_FILE);\n try {\n await fs.mkdir(CACHE_DIR, { recursive: true });\n\n const raw = await fs.readFile(cachePath, \"utf8\");\n const cache = JSON.parse(raw) as Cache;\n if (cache.version === CACHE_VERSION) {\n return cache;\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n logger.warn(\"Cache file is corrupted, flushing cache completely.\");\n }\n }\n return { version: CACHE_VERSION, queries: {} };\n}\n\n/**\n * Save the cache to the file system\n * @param cache - cache object to save\n */\nexport async function saveCache(cache: Cache): Promise<void> {\n const cachePath = path.join(CACHE_DIR, CACHE_FILE);\n await fs.writeFile(cachePath, JSON.stringify(cache, null, 2), \"utf8\");\n}\n"],"mappings":";;;;;;AAMA,MAAM,SAAS,aAAa,uBAAuB;;;;;;;;;;;;;;;AAsDnD,SAAgB,4BAA4B,OAAyB;AACnE,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;CACxD,MAAM,IAAI;AACV,KAAI,OAAO,EAAE,SAAS,YAAY,OAAO,EAAE,UAAU,UACnD,QAAO;CAET,MAAM,SAAS,EAAE;AACjB,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,QAAO;CAET,MAAM,IAAI;CACV,MAAM,iBAAiB,UACrB,MAAM,QAAQ,MAAM,IACpB,MAAM,OACH,QACC,OAAO,QAAQ,YACf,QAAQ,QACR,OAAQ,IAAgC,SAAS,YACjD,OAAQ,IAAgC,SAAS,SACpD;AACH,QACE,OAAO,EAAE,QAAQ,YACjB,OAAO,EAAE,WAAW,aACnB,EAAE,SAAS,QAAQ,EAAE,SAAS,WAC9B,EAAE,aAAa,UAAa,OAAO,EAAE,aAAa,cACnD,cAAc,EAAE,SAAS,IACzB,cAAc,EAAE,WAAW;;AAqB/B,MAAa,gBAAgB;AAC7B,MAAM,aAAa;AACnB,MAAM,YAAY,KAAK,KACrB,QAAQ,KAAK,EACb,gBACA,eACA,SACD;;;;;;;AAQD,SAAgB,QAAQ,KAAqB;AAC3C,QAAO,OAAO,WAAW,MAAM,CAAC,OAAO,IAAI,CAAC,OAAO,MAAM;;;;;;;AAQ3D,SAAgB,gBAAgB,QAAgB,MAAsB;AACpE,QAAO,QAAQ,GAAG,OAAO,GAAG,OAAO;;;;;;;AAQrC,eAAsB,YAA4B;CAChD,MAAM,YAAY,KAAK,KAAK,WAAW,WAAW;AAClD,KAAI;AACF,QAAM,GAAG,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;EAE9C,MAAM,MAAM,MAAM,GAAG,SAAS,WAAW,OAAO;EAChD,MAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,MAAI,MAAM,YAAY,cACpB,QAAO;UAEF,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,QAAO,KAAK,sDAAsD;;AAGtE,QAAO;EAAE,SAAS;EAAe,SAAS,EAAE;EAAE;;;;;;AAOhD,eAAsB,UAAU,OAA6B;CAC3D,MAAM,YAAY,KAAK,KAAK,WAAW,WAAW;AAClD,OAAM,GAAG,UAAU,WAAW,KAAK,UAAU,OAAO,MAAM,EAAE,EAAE,OAAO"}
1
+ {"version":3,"file":"cache.js","names":[],"sources":["../../src/type-generator/cache.ts"],"sourcesContent":["import crypto from \"node:crypto\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createLogger } from \"../logging/logger\";\nimport type { MetricSchema } from \"./mv-registry/types\";\n\nconst logger = createLogger(\"type-generator:cache\");\n\n/**\n * Cache types\n * @property hash - the hash of the SQL query\n * @property type - the type of the query\n * @property retry - when true the entry never satisfies a cache hit, so the\n * query is re-described on the next pass; fresh successful describes\n * persist `retry: false`\n */\ninterface CacheEntry {\n hash: string;\n type: string;\n retry: boolean;\n}\n\n/**\n * One cached metric-view DESCRIBE outcome.\n *\n * `hash` is md5 over `\"<source>|<lane>\"` — the two config inputs that\n * determine a DESCRIBE — so editing either invalidates the entry. `schema`\n * is the full {@link MetricSchema} persisted verbatim (it is JSON-safe by\n * design), letting a warm pass regenerate the metric artifact without a\n * single warehouse call.\n */\nexport interface MetricCacheEntry {\n hash: string;\n schema: MetricSchema;\n retry: boolean;\n}\n\n/**\n * Structural gate for reviving a cached metric entry at partition time.\n *\n * The cache file lives in `node_modules/.databricks` and is plain JSON —\n * hand-edits, truncation, or a stale writer can leave entries whose shape no\n * longer matches {@link MetricCacheEntry}. A malformed entry must read as a\n * cache MISS (re-describe) rather than crash the pass or render revived\n * garbage into the artifacts.\n */\nexport function isRevivableMetricCacheEntry(entry: unknown): boolean {\n if (typeof entry !== \"object\" || entry === null) return false;\n const e = entry as Record<string, unknown>;\n if (typeof e.hash !== \"string\" || typeof e.retry !== \"boolean\") {\n return false;\n }\n const schema = e.schema;\n if (typeof schema !== \"object\" || schema === null || Array.isArray(schema)) {\n return false;\n }\n const s = schema as Record<string, unknown>;\n const isColumnArray = (value: unknown): boolean =>\n Array.isArray(value) &&\n value.every(\n (col) =>\n typeof col === \"object\" &&\n col !== null &&\n typeof (col as Record<string, unknown>).name === \"string\" &&\n typeof (col as Record<string, unknown>).type === \"string\",\n );\n return (\n typeof s.key === \"string\" &&\n typeof s.source === \"string\" &&\n (s.lane === \"sp\" || s.lane === \"obo\") &&\n (s.degraded === undefined || typeof s.degraded === \"boolean\") &&\n isColumnArray(s.measures) &&\n isColumnArray(s.dimensions)\n );\n}\n\n/**\n * Cache interface\n * @property version - the version of the cache\n * @property queries - the queries in the cache\n * @property metrics - cached metric-view schemas keyed by metric key.\n */\ninterface Cache {\n version: string;\n queries: Record<string, CacheEntry>;\n metrics?: Record<string, MetricCacheEntry>;\n}\n\nexport const CACHE_VERSION = \"3\";\nconst CACHE_FILE = \".appkit-types-cache.json\";\nconst CACHE_DIR = path.join(\n process.cwd(),\n \"node_modules\",\n \".databricks\",\n \"appkit\",\n);\n\n/**\n * Hash the SQL query\n * Uses MD5 to hash the SQL query\n * @param sql - the SQL query to hash\n * @returns - the hash of the SQL query\n */\nexport function hashSQL(sql: string): string {\n return crypto.createHash(\"md5\").update(sql).digest(\"hex\");\n}\n\n/**\n * Change detector stored on {@link MetricCacheEntry.hash}: md5 over\n * `\"<source>|<lane>\"` — the two config inputs that determine a DESCRIBE —\n * so editing either invalidates the entry.\n */\nexport function metricCacheHash(source: string, lane: string): string {\n return hashSQL(`${source}|${lane}`);\n}\n\n/**\n * Load the cache from the file system\n * If the cache is not found, run the query explain\n * @returns - the cache\n */\nexport async function loadCache(): Promise<Cache> {\n const cachePath = path.join(CACHE_DIR, CACHE_FILE);\n try {\n await fs.mkdir(CACHE_DIR, { recursive: true });\n\n const raw = await fs.readFile(cachePath, \"utf8\");\n const cache = JSON.parse(raw) as Cache;\n if (cache.version === CACHE_VERSION) {\n return cache;\n }\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n logger.warn(\"Cache file is corrupted, flushing cache completely.\");\n }\n }\n return { version: CACHE_VERSION, queries: {} };\n}\n\n/**\n * Save the cache to the file system\n * @param cache - cache object to save\n */\nexport async function saveCache(cache: Cache): Promise<void> {\n const cachePath = path.join(CACHE_DIR, CACHE_FILE);\n await fs.writeFile(cachePath, JSON.stringify(cache, null, 2), \"utf8\");\n}\n"],"mappings":";;;;;;AAMA,MAAM,SAAS,aAAa,uBAAuB;;;;;;;;;;AAwCnD,SAAgB,4BAA4B,OAAyB;AACnE,KAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;CACxD,MAAM,IAAI;AACV,KAAI,OAAO,EAAE,SAAS,YAAY,OAAO,EAAE,UAAU,UACnD,QAAO;CAET,MAAM,SAAS,EAAE;AACjB,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,QAAO;CAET,MAAM,IAAI;CACV,MAAM,iBAAiB,UACrB,MAAM,QAAQ,MAAM,IACpB,MAAM,OACH,QACC,OAAO,QAAQ,YACf,QAAQ,QACR,OAAQ,IAAgC,SAAS,YACjD,OAAQ,IAAgC,SAAS,SACpD;AACH,QACE,OAAO,EAAE,QAAQ,YACjB,OAAO,EAAE,WAAW,aACnB,EAAE,SAAS,QAAQ,EAAE,SAAS,WAC9B,EAAE,aAAa,UAAa,OAAO,EAAE,aAAa,cACnD,cAAc,EAAE,SAAS,IACzB,cAAc,EAAE,WAAW;;AAgB/B,MAAa,gBAAgB;AAC7B,MAAM,aAAa;AACnB,MAAM,YAAY,KAAK,KACrB,QAAQ,KAAK,EACb,gBACA,eACA,SACD;;;;;;;AAQD,SAAgB,QAAQ,KAAqB;AAC3C,QAAO,OAAO,WAAW,MAAM,CAAC,OAAO,IAAI,CAAC,OAAO,MAAM;;;;;;;AAQ3D,SAAgB,gBAAgB,QAAgB,MAAsB;AACpE,QAAO,QAAQ,GAAG,OAAO,GAAG,OAAO;;;;;;;AAQrC,eAAsB,YAA4B;CAChD,MAAM,YAAY,KAAK,KAAK,WAAW,WAAW;AAClD,KAAI;AACF,QAAM,GAAG,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;EAE9C,MAAM,MAAM,MAAM,GAAG,SAAS,WAAW,OAAO;EAChD,MAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,MAAI,MAAM,YAAY,cACpB,QAAO;UAEF,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,QAAO,KAAK,sDAAsD;;AAGtE,QAAO;EAAE,SAAS;EAAe,SAAS,EAAE;EAAE;;;;;;AAOhD,eAAsB,UAAU,OAA6B;CAC3D,MAAM,YAAY,KAAK,KAAK,WAAW,WAAW;AAClD,OAAM,GAAG,UAAU,WAAW,KAAK,UAAU,OAAO,MAAM,EAAE,EAAE,OAAO"}
@@ -4,7 +4,6 @@ import { getErrorDiagnostic, isConnectivityError } from "./errors.js";
4
4
  import { migrateProjectConfig, removeOldGeneratedTypes, resolveProjectRoot } from "./migration.js";
5
5
  import { readMetricConfig, resolveMetricConfig } from "./mv-registry/config.js";
6
6
  import { createWorkspaceDescribeFetcher } from "./mv-registry/describe.js";
7
- import { generateMetricsMetadataJson } from "./mv-registry/metadata.js";
8
7
  import { generateMetricTypeDeclarations } from "./mv-registry/render-types.js";
9
8
  import { emptyMetricSchema, syncMetrics } from "./mv-registry/sync.js";
10
9
  import { decidePreflight } from "./preflight.js";
@@ -21,7 +20,7 @@ import dotenv from "dotenv";
21
20
  dotenv.config();
22
21
  const logger = createLogger("type-generator");
23
22
  /**
24
- * Upper bound (~5 min) on how long the metric path's `blocking`-mode preflight
23
+ * Upper bound (~5 min) on how long the Metric Views path's `blocking`-mode preflight
25
24
  * waits for a warehouse to reach RUNNING. Mirrors the query path's (unexported)
26
25
  * `PREFLIGHT_WAIT_MAX_MS` in query-registry.ts.
27
26
  */
@@ -147,20 +146,15 @@ declare module "@databricks/appkit-ui/react" {
147
146
  `;
148
147
  }
149
148
  /**
150
- * Status-only probe for the metric-view gate in {@link generateFromEntryPoint}:
151
- * what state is the warehouse in right now?
149
+ * Status-only probe for the metric-view gate in {@link generateFromEntryPoint}
152
150
  *
153
- * Uses {@link getWarehouseState} (`warehouses.get`) — a read-only GET that can
154
- * never start the warehouse — unlike the metric DESCRIBE statements it guards,
155
- * whose execution auto-starts a stopped warehouse and waits on it.
151
+ * Uses {@link getWarehouseState} (`warehouses.get`) —
152
+ * a read-only GET that can never start the warehouse
156
153
  *
157
- * Returns the observed state so the gate can distinguish a transient
158
- * not-running state (STOPPED/STARTING/... → degraded entries that retry) from a
159
- * terminal one (DELETED/DELETING → degraded entries pinned sticky). Takes the
160
- * lazy client *getter* (not a client) so the probe also absorbs client
161
- * construction failure. A connectivity blip returns `undefined`, which the gate
162
- * reads as transient not-running; a deterministic failure (auth, bad id) is
163
- * re-thrown so the gate can classify it fatal rather than silently degrading.
154
+ * Takes the lazy client *getter* so the probe also absorbs client construction failure.
155
+ * A connectivity blip returns `undefined`, which the gate reads as transient not-running;
156
+ * a deterministic failure (auth, bad id) is re-thrown so the gate can classify it
157
+ * fatal rather than silently degrading.
164
158
  */
165
159
  async function probeWarehouseState(getClient, warehouseId) {
166
160
  try {
@@ -186,12 +180,8 @@ async function probeWarehouseState(getClient, warehouseId) {
186
180
  * degraded types immediately. `"blocking"` waits for / starts the warehouse
187
181
  * first, failing the build only for a deleted/deleting one.
188
182
  * @param options.mvOutFile - optional output file for the MetricRegistry
189
- * augmentation. Defaults to a sibling `metric.d.ts` file under the same
183
+ * augmentation. Defaults to a sibling `metric-views.d.ts` file under the same
190
184
  * directory as `outFile`. Skipped entirely if `metric-views.json` is absent.
191
- * @param options.mvMetadataOutFile - optional output file for the
192
- * build-time semantic metadata JSON bundle (`metrics.metadata.json`).
193
- * Defaults to a sibling of `mvOutFile`. Skipped entirely if
194
- * `metric-views.json` is absent.
195
185
  * @param options.metricFetcher - optional DescribeFetcher used by
196
186
  * {@link syncMetrics} (tests inject a mock; production lazily builds a
197
187
  * default WorkspaceClient-backed one). An injected fetcher always runs: it
@@ -199,7 +189,7 @@ async function probeWarehouseState(getClient, warehouseId) {
199
189
  * blocking preflight.
200
190
  */
201
191
  async function generateFromEntryPoint(options) {
202
- const { outFile, queryFolder, warehouseId, noCache, mode = "non-blocking", mvOutFile, mvMetadataOutFile, metricFetcher } = options;
192
+ const { outFile, queryFolder, warehouseId, noCache, mode = "non-blocking", mvOutFile, metricFetcher } = options;
203
193
  const projectRoot = resolveProjectRoot(outFile);
204
194
  logger.debug("Starting type generation...");
205
195
  let queryRegistry = [];
@@ -218,119 +208,28 @@ async function generateFromEntryPoint(options) {
218
208
  await fs.mkdir(path.dirname(outFile), { recursive: true });
219
209
  await fs.writeFile(outFile, typeDeclarations, "utf-8");
220
210
  if (queryFolder) {
221
- const mvConfig = await readMetricConfig(queryFolder);
222
- if (mvConfig) {
223
- const resolution = resolveMetricConfig(mvConfig);
224
- const cache = await loadCache();
225
- const mvCacheSection = Object.create(null);
226
- if (!noCache && cache.metrics) for (const key of Object.keys(cache.metrics)) mvCacheSection[key] = cache.metrics[key];
227
- const hitSchemas = /* @__PURE__ */ new Map();
228
- const describeNeeded = [];
229
- const stickyDegradedHits = [];
230
- for (const entry of resolution.entries) {
231
- const prior = mvCacheSection[entry.key];
232
- if (prior !== void 0 && isRevivableMetricCacheEntry(prior) && prior.hash === metricCacheHash(entry.source, entry.lane) && !prior.retry) {
233
- hitSchemas.set(entry.key, prior.schema);
234
- if (prior.schema.degraded === true) stickyDegradedHits.push(entry.key);
235
- } else describeNeeded.push(entry);
236
- }
237
- if (stickyDegradedHits.length > 0) logger.warn("cached failure for %s — fix the entry in metric-views.json or run with --no-cache to retry.", stickyDegradedHits.join(", "));
238
- let mvClient;
239
- const getMvClient = () => {
240
- mvClient ??= new WorkspaceClient({});
241
- return mvClient;
242
- };
243
- let preflightFatalMessage;
244
- if (mode === "blocking" && metricFetcher === void 0 && describeNeeded.length > 0) try {
245
- const state = await getWarehouseState(getMvClient(), warehouseId);
246
- const decision = decidePreflight(state, mode);
247
- if (decision === "fatal") preflightFatalMessage = `warehouse ${warehouseId} is ${state}`;
248
- else if (decision === "startWaitProceed") {
249
- await startWarehouse(getMvClient(), warehouseId);
250
- const settled = await waitUntilRunning(getMvClient(), warehouseId, {
251
- maxMs: MV_PREFLIGHT_WAIT_MAX_MS,
252
- treatStoppedAsTransient: true
253
- });
254
- if (settled !== "RUNNING") preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;
255
- } else if (decision === "waitThenProceed") {
256
- const settled = await waitUntilRunning(getMvClient(), warehouseId, { maxMs: MV_PREFLIGHT_WAIT_MAX_MS });
257
- if (settled === "DELETED" || settled === "DELETING") preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;
258
- }
259
- } catch (err) {
260
- if (!isConnectivityError(err)) preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;
261
- }
262
- let gateState;
263
- let describeNow = metricFetcher !== void 0 || mode !== "non-blocking" || describeNeeded.length === 0;
264
- if (!describeNow) {
265
- try {
266
- gateState = await probeWarehouseState(getMvClient, warehouseId);
267
- } catch (err) {
268
- preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;
269
- }
270
- describeNow = gateState === "RUNNING";
271
- }
272
- let described;
273
- let failures = [];
274
- let terminalSkip = false;
275
- if (preflightFatalMessage !== void 0) {
276
- described = describeNeeded.map(emptyMetricSchema);
277
- terminalSkip = true;
278
- for (const entry of describeNeeded) fatalErrors.push({
279
- name: entry.key,
280
- message: preflightFatalMessage
281
- });
282
- } else if (describeNeeded.length === 0) described = [];
283
- else if (describeNow) {
284
- const fetcher = metricFetcher ?? createWorkspaceDescribeFetcher(getMvClient(), warehouseId);
285
- ({schemas: described, failures} = await syncMetrics({ entries: describeNeeded }, fetcher));
286
- if (failures.length > 0) for (const f of failures) logger.warn("metric sync failed for %s (%s): %s", f.key, f.source, f.reason);
287
- const failedKeys = new Set(failures.map((f) => f.key));
288
- const degradedKeys = described.filter((s) => s.degraded && !failedKeys.has(s.key)).map((s) => s.key);
289
- if (degradedKeys.length > 0) logger.info("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.", warehouseId, degradedKeys.length, degradedKeys.join(", "));
290
- } else {
291
- described = describeNeeded.map(emptyMetricSchema);
292
- terminalSkip = gateState === "DELETED" || gateState === "DELETING";
293
- logger.info("Warehouse %s is not running — wrote degraded metric types (permissive) for %d metric view(s) (%s); they will refresh once the warehouse is available.", warehouseId, describeNeeded.length, describeNeeded.map((e) => e.key).join(", "));
294
- }
295
- const failureByKey = /* @__PURE__ */ new Map();
296
- for (const failure of failures) failureByKey.set(failure.key, failure);
297
- for (let i = 0; i < describeNeeded.length; i++) {
298
- const entry = describeNeeded[i];
299
- const failure = failureByKey.get(entry.key);
300
- mvCacheSection[entry.key] = {
301
- hash: metricCacheHash(entry.source, entry.lane),
302
- schema: described[i],
303
- retry: described[i].degraded === true && !terminalSkip && (failure === void 0 || failure.transient === true)
304
- };
305
- }
306
- const configuredKeys = new Set(resolution.entries.map((e) => e.key));
307
- let prunedCount = 0;
308
- for (const key of Object.keys(mvCacheSection)) if (!configuredKeys.has(key)) {
309
- delete mvCacheSection[key];
310
- prunedCount++;
311
- }
312
- if (describeNeeded.length > 0 || noCache || prunedCount > 0) {
313
- cache.metrics = mvCacheSection;
314
- await saveCache(cache);
315
- }
316
- const describedByKey = /* @__PURE__ */ new Map();
317
- for (const schema of described) describedByKey.set(schema.key, schema);
318
- const mvSchemas = resolution.entries.map((entry) => {
319
- const schema = hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);
320
- if (schema !== void 0) return schema;
321
- logger.warn("no schema resolved for metric key %s — emitting degraded types (should not happen)", entry.key);
322
- return emptyMetricSchema(entry);
211
+ const mvFile = mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);
212
+ let mvResult;
213
+ try {
214
+ mvResult = await syncMetricViewsTypes({
215
+ queryFolder,
216
+ warehouseId,
217
+ metricOutFile: mvFile,
218
+ cache: !noCache,
219
+ metricFetcher,
220
+ mode
323
221
  });
324
- const mvFile = mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);
325
- const mvDeclarations = generateMetricTypeDeclarations(mvSchemas);
326
- await fs.mkdir(path.dirname(mvFile), { recursive: true });
327
- await fs.writeFile(mvFile, mvDeclarations, "utf-8");
328
- const mvMetadataFile = mvMetadataOutFile ?? path.join(path.dirname(mvFile), METRIC_METADATA_FILE);
329
- const metadataJson = generateMetricsMetadataJson(mvSchemas);
330
- await fs.mkdir(path.dirname(mvMetadataFile), { recursive: true });
331
- await fs.writeFile(mvMetadataFile, metadataJson, "utf-8");
332
- logger.debug("Wrote MetricRegistry augmentation + metadata bundle for %d metric(s)%s", mvSchemas.length, failures.length > 0 ? ` (${failures.length} failure(s))` : "");
222
+ } catch (configError) {
223
+ throw new TypegenFatalError([{
224
+ name: "metric-views.json",
225
+ message: getErrorDiagnostic(configError)
226
+ }], warehouseId);
333
227
  }
228
+ for (const fe of mvResult.fatalErrors) fatalErrors.push(fe);
229
+ if (mode === "blocking") for (const failure of mvResult.failures) fatalErrors.push({
230
+ name: failure.key,
231
+ message: `metric view ${failure.key} (${failure.source}) could not be described: ${failure.reason}`
232
+ });
334
233
  }
335
234
  await removeOldGeneratedTypes(projectRoot, "appKitTypes.d.ts");
336
235
  await migrateProjectConfig(projectRoot);
@@ -338,25 +237,142 @@ async function generateFromEntryPoint(options) {
338
237
  if (fatalErrors.length > 0) throw new TypegenFatalError(fatalErrors, warehouseId);
339
238
  logger.debug("Type generation complete!");
340
239
  }
240
+ /**
241
+ * Unified metric-view type-generation pipeline behind {@link
242
+ * generateFromEntryPoint}'s metric section (which forwards its
243
+ * `"non-blocking"`/`"blocking"` mode). Also directly callable with the default
244
+ * `"describe-now"` mode for a focused, always-converge metric refresh.
245
+ *
246
+ *
247
+ * @param options.queryFolder - folder that holds `metric-views.json` (`<root>/config/queries`).
248
+ * @param options.warehouseId - SQL warehouse used for `DESCRIBE TABLE EXTENDED`.
249
+ * @param options.metricOutFile - output path for the MetricRegistry `.d.ts`.
250
+ * @param options.cache - cache toggle, default ON. Only `cache === false` disables it (so `undefined`/`true` keep caching).
251
+ * @param options.metricFetcher - optional injected {@link DescribeFetcher}
252
+ * @param options.mode - preflight/gate policy, default `"describe-now"`.
253
+ */
254
+ async function syncMetricViewsTypes(options) {
255
+ const { queryFolder, warehouseId, metricOutFile, cache: cacheEnabled, metricFetcher, mode = "describe-now" } = options;
256
+ const noCache = cacheEnabled === false;
257
+ const mvConfig = await readMetricConfig(queryFolder);
258
+ if (!mvConfig) return {
259
+ schemas: [],
260
+ failures: [],
261
+ fatalErrors: [],
262
+ noConfig: true
263
+ };
264
+ const resolution = resolveMetricConfig(mvConfig);
265
+ const fatalErrors = [];
266
+ const cache = await loadCache();
267
+ const mvCacheSection = Object.create(null);
268
+ if (!noCache && cache.metrics) for (const key of Object.keys(cache.metrics)) mvCacheSection[key] = cache.metrics[key];
269
+ const hitSchemas = /* @__PURE__ */ new Map();
270
+ const describeNeeded = [];
271
+ for (const entry of resolution.entries) {
272
+ const prior = mvCacheSection[entry.key];
273
+ if (prior !== void 0 && isRevivableMetricCacheEntry(prior) && prior.hash === metricCacheHash(entry.source, entry.lane) && prior.schema.degraded !== true) hitSchemas.set(entry.key, prior.schema);
274
+ else describeNeeded.push(entry);
275
+ }
276
+ let mvClient;
277
+ const getMvClient = () => {
278
+ mvClient ??= new WorkspaceClient({});
279
+ return mvClient;
280
+ };
281
+ let preflightFatalMessage;
282
+ if (mode === "blocking" && metricFetcher === void 0 && describeNeeded.length > 0) try {
283
+ const state = await getWarehouseState(getMvClient(), warehouseId);
284
+ const decision = decidePreflight(state, mode);
285
+ if (decision === "fatal") preflightFatalMessage = `warehouse ${warehouseId} is ${state}`;
286
+ else if (decision === "startWaitProceed") {
287
+ await startWarehouse(getMvClient(), warehouseId);
288
+ const settled = await waitUntilRunning(getMvClient(), warehouseId, {
289
+ maxMs: MV_PREFLIGHT_WAIT_MAX_MS,
290
+ treatStoppedAsTransient: true
291
+ });
292
+ if (settled !== "RUNNING") preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;
293
+ } else if (decision === "waitThenProceed") {
294
+ const settled = await waitUntilRunning(getMvClient(), warehouseId, { maxMs: MV_PREFLIGHT_WAIT_MAX_MS });
295
+ if (settled === "DELETED" || settled === "DELETING") preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;
296
+ }
297
+ } catch (err) {
298
+ if (!isConnectivityError(err)) preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;
299
+ }
300
+ let gateState;
301
+ let describeNow = metricFetcher !== void 0 || mode !== "non-blocking" || describeNeeded.length === 0;
302
+ if (!describeNow) {
303
+ try {
304
+ gateState = await probeWarehouseState(getMvClient, warehouseId);
305
+ } catch (err) {
306
+ preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;
307
+ }
308
+ describeNow = gateState === "RUNNING";
309
+ }
310
+ let described;
311
+ let failures = [];
312
+ if (preflightFatalMessage !== void 0) {
313
+ described = describeNeeded.map(emptyMetricSchema);
314
+ for (const entry of describeNeeded) fatalErrors.push({
315
+ name: entry.key,
316
+ message: preflightFatalMessage
317
+ });
318
+ } else if (describeNeeded.length === 0) described = [];
319
+ else if (describeNow) {
320
+ const fetcher = metricFetcher ?? createWorkspaceDescribeFetcher(getMvClient(), warehouseId);
321
+ ({schemas: described, failures} = await syncMetrics({ entries: describeNeeded }, fetcher));
322
+ if (failures.length > 0) for (const f of failures) logger.warn("metric sync failed for %s (%s): %s", f.key, f.source, f.reason);
323
+ const failedKeys = new Set(failures.map((f) => f.key));
324
+ const degradedKeys = described.filter((s) => s.degraded && !failedKeys.has(s.key)).map((s) => s.key);
325
+ if (degradedKeys.length > 0) logger.info("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.", warehouseId, degradedKeys.length, degradedKeys.join(", "));
326
+ } else {
327
+ described = describeNeeded.map(emptyMetricSchema);
328
+ logger.info("Warehouse %s is not running — wrote degraded metric types (permissive) for %d metric view(s) (%s); they will refresh once the warehouse is available.", warehouseId, describeNeeded.length, describeNeeded.map((e) => e.key).join(", "));
329
+ }
330
+ for (let i = 0; i < describeNeeded.length; i++) {
331
+ const entry = describeNeeded[i];
332
+ if (described[i].degraded === true) {
333
+ delete mvCacheSection[entry.key];
334
+ continue;
335
+ }
336
+ mvCacheSection[entry.key] = {
337
+ hash: metricCacheHash(entry.source, entry.lane),
338
+ schema: described[i],
339
+ retry: false
340
+ };
341
+ }
342
+ const configuredKeys = new Set(resolution.entries.map((e) => e.key));
343
+ let prunedCount = 0;
344
+ for (const key of Object.keys(mvCacheSection)) if (!configuredKeys.has(key)) {
345
+ delete mvCacheSection[key];
346
+ prunedCount++;
347
+ }
348
+ if (describeNeeded.length > 0 || noCache || prunedCount > 0) {
349
+ cache.metrics = mvCacheSection;
350
+ await saveCache(cache);
351
+ }
352
+ const describedByKey = /* @__PURE__ */ new Map();
353
+ for (const schema of described) describedByKey.set(schema.key, schema);
354
+ const schemas = resolution.entries.map((entry) => {
355
+ const schema = hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);
356
+ if (schema !== void 0) return schema;
357
+ logger.warn("no schema resolved for metric key %s — emitting degraded types (should not happen)", entry.key);
358
+ return emptyMetricSchema(entry);
359
+ });
360
+ await fs.mkdir(path.dirname(metricOutFile), { recursive: true });
361
+ await fs.writeFile(metricOutFile, generateMetricTypeDeclarations(schemas), "utf-8");
362
+ logger.debug("Wrote MetricRegistry augmentation for %d metric(s)%s", schemas.length, failures.length > 0 ? ` (${failures.length} failure(s))` : "");
363
+ return {
364
+ metricOutFile,
365
+ schemas,
366
+ failures,
367
+ fatalErrors,
368
+ noConfig: false
369
+ };
370
+ }
341
371
  const generateServingTypes = generateServingTypes$1;
342
- /** Directory name for generated AppKit type declaration files. */
343
372
  const TYPES_DIR = "appkit-types";
344
- /** Default filename for analytics query type declarations. */
345
373
  const ANALYTICS_TYPES_FILE = "analytics.d.ts";
346
- /** Default filename for serving endpoint type declarations. */
347
374
  const SERVING_TYPES_FILE = "serving.d.ts";
348
- /** Default filename for metric-view registry type declarations. */
349
- const METRIC_TYPES_FILE = "metric.d.ts";
350
- /**
351
- * Default filename for the build-time semantic-metadata JSON bundle, sibling of
352
- * {@link METRIC_TYPES_FILE}. Shape is `Record<metricKey, { measures,
353
- * dimensions }>` (UC FQN and execution lane are server-side concerns, kept out
354
- * of this client-shipped artifact). The consuming app imports it at build time
355
- * and registers it via `@databricks/appkit-ui/format`'s
356
- * `registerMetricsMetadata()`, so the React hook returns per-metric `metadata`
357
- * without a second network round-trip.
358
- */
359
- const METRIC_METADATA_FILE = "metrics.metadata.json";
375
+ const METRIC_TYPES_FILE = "metric-views.d.ts";
360
376
 
361
377
  //#endregion
362
378
  export { ANALYTICS_TYPES_FILE, SERVING_TYPES_FILE, TYPES_DIR, TypegenFatalError, TypegenSyntaxError, generateFromEntryPoint, generateServingTypes };