@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.
- package/CLAUDE.md +0 -1
- package/NOTICE.md +1 -0
- package/dist/appkit/package.js +1 -1
- package/dist/cli/commands/generate-types.js +5 -0
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/index.js +0 -1
- package/dist/plugins/analytics/query.js +0 -1
- package/dist/plugins/analytics/query.js.map +1 -1
- package/dist/type-generator/cache.js +1 -6
- package/dist/type-generator/cache.js.map +1 -1
- package/dist/type-generator/index.js +162 -146
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/mv-registry/config.js +5 -5
- package/dist/type-generator/mv-registry/config.js.map +1 -1
- package/dist/type-generator/mv-registry/render-types.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts +0 -7
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +1 -4
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/docs/api/appkit-ui/ui/ContextMenu.md +2 -2
- package/docs/api/appkit-ui/ui/Drawer.md +0 -56
- package/docs/api/appkit-ui/ui/DropdownMenu.md +3 -3
- package/docs/api/appkit-ui/ui/HoverCard.md +2 -2
- package/docs/api/appkit-ui/ui/Menubar.md +3 -3
- package/docs/api/appkit-ui/ui/Popover.md +2 -2
- package/docs/api/appkit-ui/ui/Select.md +2 -2
- package/docs/api/appkit-ui/ui/Tooltip.md +2 -2
- package/docs/development/type-generation.md +28 -2
- package/llms.txt +0 -1
- package/package.json +3 -2
- package/sbom.cdx.json +1 -1
- package/dist/shared/src/index.js +0 -4
- package/dist/shared/src/sql/index.js +0 -3
- package/dist/type-generator/mv-registry/metadata.js +0 -40
- package/dist/type-generator/mv-registry/metadata.js.map +0 -1
- 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 |
|
package/dist/appkit/package.js
CHANGED
|
@@ -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":"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"}
|
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":"
|
|
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.
|
|
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
|
|
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
|
|
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`) —
|
|
154
|
-
*
|
|
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
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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,
|
|
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
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
-
|
|
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 };
|