@databricks/appkit 0.42.0 → 0.43.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +3 -1
- package/dist/agents/databricks.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +2 -1
- package/dist/cache/index.js.map +1 -1
- package/dist/cli/commands/generate-types.js +5 -0
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/connectors/files/client.js +4 -1
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/mcp/client.d.ts.map +1 -1
- package/dist/connectors/mcp/client.js +3 -0
- package/dist/connectors/mcp/client.js.map +1 -1
- package/dist/context/client-options.js +28 -0
- package/dist/context/client-options.js.map +1 -0
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +1 -10
- package/dist/context/service-context.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/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +6 -1
- package/dist/plugins/lakebase/lakebase.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/development/type-generation.md +28 -2
- package/package.json +2 -1
- 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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","names":[],"sources":["../../../src/type-generator/mv-registry/config.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n// Plain, zod-free value imports — single source of truth for the UC object-name\n// grammar. The Zod schema (packages/shared/src/schemas/metric-source.ts) imports\n// the SAME UC_FQN_PATTERN for its `source` .regex(...), so the runtime and the\n// canonical schema validate identically without the type-generator pulling the\n// shared Zod schema package into its runtime path (locked dependency-graph\n// ruling — see the comment in ../cache.ts). The relative specifier resolves the\n// shared source directly and drags in no zod.\nimport {\n MAX_UC_OBJECT_NAME_LENGTH,\n UC_FQN_PATTERN,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport type {\n MetricConfigResolution,\n MetricLane,\n MetricSourceConfig,\n ResolvedMetricEntry,\n} from \"./types\";\n\nconst MV_CONFIG_FILE = \"metric-views.json\";\n\n/**\n * Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog\n * limit. Enforced by {@link resolveMetricConfig}.\n */\nconst MAX_METRIC_VIEWS = 200;\n/** Per-segment cap = UC's object-name length limit (255). */\nconst MAX_FQN_SEGMENT_LENGTH = MAX_UC_OBJECT_NAME_LENGTH;\n/** Whole-FQN cap: three max-length segments plus the two separating dots. */\nconst MAX_FQN_LENGTH = MAX_FQN_SEGMENT_LENGTH * 3 + 2;\n/** A metric view FQN is exactly catalog.schema.metric_view. */\nconst FQN_SEGMENT_NAMES = [\"catalog\", \"schema\", \"metric_view\"] as const;\nconst FQN_SEGMENT_COUNT = FQN_SEGMENT_NAMES.length;\n\n/**\n * Locale-independent comparator (UTF-16 code-unit order) shared by BOTH artifact\n * key orderings. Plain `sort()` is locale-sensitive, so keys could order\n * differently across environments and invalidate the cache hash — this keeps the\n * ordering stable everywhere.\n */\nexport function compareKeys(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Read {@link MV_CONFIG_FILE} from a queries folder.\n *\n * Returns `null` if the file does not exist (the metric-view path is\n * additive — apps without metric-views.json must not be penalized). There is\n * deliberately no fallback to the legacy `metric.json` filename.\n *\n * Throws on JSON parse errors so misconfiguration surfaces loudly.\n */\nexport async function readMetricConfig(\n queryFolder: string,\n): Promise<MetricSourceConfig | null> {\n const metricPath = path.join(queryFolder, MV_CONFIG_FILE);\n let raw: string;\n try {\n raw = await fs.readFile(metricPath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `Failed to parse metric-views.json at ${metricPath}: ${(err as Error).message}`,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\n `Invalid metric-views.json at ${metricPath}: expected an object with a 'metricViews' map.`,\n );\n }\n\n return parsed as MetricSourceConfig;\n}\n\n/**\n * Validate a key against the JSON Schema's metricKey pattern. Kept\n * lightweight — the shared Zod schema ({@link metricSourceSchema}) is the\n * canonical contract for IDE/CI; this regex is identical to its\n * {@link metricKeySchema}.\n */\nfunction isValidMetricKey(key: string): boolean {\n return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);\n}\n\n/**\n * Total predicate: is `fqn` a well-formed three-part UC metric view FQN?\n *\n * Well-formed = exactly three non-empty, dot-separated segments, each a valid\n * Unity Catalog object name per the shared {@link UC_FQN_PATTERN} (the single\n * source of truth, also used by the canonical Zod schema). Used as the\n * defense-in-depth re-check at the describe fetcher seam; {@link resolveMetricConfig}\n * runs the same checks but with specific, staged error messages.\n *\n * @note Segment length ({@link MAX_FQN_SEGMENT_LENGTH}) is NOT checked here —\n * an over-long but otherwise legal name is still \"valid shape\". The length cap\n * is a separate concern enforced (with its own message) in resolveMetricConfig.\n */\nexport function isValidFqn(fqn: string): boolean {\n const segments = fqn.split(\".\");\n if (segments.length !== FQN_SEGMENT_COUNT) {\n return false;\n }\n return segments.every((segment) => UC_FQN_PATTERN.test(segment));\n}\n\n/**\n * Field allowlists enforced by {@link resolveMetricConfig}.\n */\nconst ALLOWED_TOP_LEVEL_FIELDS = new Set([\"$schema\", \"metricViews\"]);\nconst ALLOWED_ENTRY_FIELDS = new Set([\"source\", \"executor\"]);\n\n/**\n * Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.\n */\nexport function resolveMetricConfig(\n config: MetricSourceConfig,\n): MetricConfigResolution {\n for (const field of Object.keys(config)) {\n if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) {\n throw new Error(\n `Invalid top-level field \"${field}\" in metric-views.json: only '$schema' and 'metricViews' are allowed.`,\n );\n }\n }\n\n // Default to {} only when metricViews is genuinely absent. A `null` must fall\n // through to the type check below and throw — the canonical Zod schema rejects\n // null.\n const metricViews =\n config.metricViews === undefined ? {} : config.metricViews;\n if (\n typeof metricViews !== \"object\" ||\n metricViews === null ||\n Array.isArray(metricViews)\n ) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: expected an object map of metric entries.`,\n );\n }\n\n const entries: ResolvedMetricEntry[] = [];\n const sortedKeys = Object.keys(metricViews).sort(compareKeys);\n if (sortedKeys.length > MAX_METRIC_VIEWS) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`,\n );\n }\n for (const key of sortedKeys) {\n if (!isValidMetricKey(key)) {\n throw new Error(\n `Invalid metric key \"${key}\" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`,\n );\n }\n\n const entry = metricViews[key];\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new Error(\n `Invalid metric entry \"${key}\": expected an object with a 'source' field.`,\n );\n }\n\n for (const field of Object.keys(entry)) {\n if (!ALLOWED_ENTRY_FIELDS.has(field)) {\n throw new Error(\n `Invalid field \"${field}\" on metric entry \"${key}\": only 'source' and 'executor' are allowed at v1.`,\n );\n }\n }\n\n if (typeof entry.source !== \"string\" || entry.source.trim() === \"\") {\n throw new Error(\n `Invalid metric entry \"${key}\": 'source' must be a non-empty string.`,\n );\n }\n\n if (entry.source.length > MAX_FQN_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": FQN is ${entry.source.length} characters, exceeding the maximum of ${MAX_FQN_LENGTH}.`,\n );\n }\n\n // Staged, specific validation against the UC object-name grammar\n // (UC_FQN_PATTERN — shared with the canonical Zod schema). Reported in\n // order of increasing specificity so the message names the exact problem.\n const segments = entry.source.split(\".\");\n\n // Arity: exactly catalog.schema.metric_view. A wrong part count almost\n // always means a name contains a dot — which the dotted `source` cannot\n // express, since every dot is a segment boundary.\n if (segments.length !== FQN_SEGMENT_COUNT) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view> (got ${segments.length} dot-separated part${segments.length === 1 ? \"\" : \"s\"}). A catalog, schema, or metric view name cannot itself contain a dot.`,\n );\n }\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n const segmentName = FQN_SEGMENT_NAMES[i];\n\n // Empty part: a leading/trailing/double dot (e.g. \"a..c\", \".b.c\").\n if (segment.length === 0) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part is empty. A three-part UC FQN needs a non-empty name in each position: <catalog>.<schema>.<metric_view>.`,\n );\n }\n\n // Length cap (UC: object names are at most 255 characters).\n if (segment.length > MAX_FQN_SEGMENT_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": the ${segmentName} segment is ${segment.length} characters, exceeding the maximum of ${MAX_FQN_SEGMENT_LENGTH} per segment.`,\n );\n }\n\n // Character set: must be a valid UC object name (the FQN is always\n // backtick-quoted before it reaches SQL, so UC's *delimited* identifier\n // rules apply — anything but space, '/', and control characters).\n if (!UC_FQN_PATTERN.test(segment)) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part \"${segment}\" contains a character Unity Catalog does not allow in an object name (no spaces, '/', or control characters).`,\n );\n }\n }\n\n const executor = entry.executor;\n if (\n executor !== undefined &&\n executor !== \"app_service_principal\" &&\n executor !== \"user\"\n ) {\n throw new Error(\n `Invalid executor \"${String(executor)}\" on metric entry \"${key}\": must be \"app_service_principal\" or \"user\".`,\n );\n }\n\n const lane: MetricLane = executor === \"user\" ? \"obo\" : \"sp\";\n\n entries.push({ key, source: entry.source, lane });\n }\n\n return { entries };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../../../src/type-generator/mv-registry/config.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n// Plain, zod-free value imports — single source of truth for the UC object-name\n// grammar. The Zod schema (packages/shared/src/schemas/metric-source.ts) imports\n// the SAME UC_FQN_PATTERN for its `source` .regex(...), so the runtime and the\n// canonical schema validate identically without the type-generator pulling the\n// shared Zod schema package into its runtime path (locked dependency-graph\n// ruling — see the comment in ../cache.ts). The relative specifier resolves the\n// shared source directly and drags in no zod.\nimport {\n MAX_UC_OBJECT_NAME_LENGTH,\n UC_FQN_PATTERN,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport type {\n MetricConfigResolution,\n MetricLane,\n MetricSourceConfig,\n ResolvedMetricEntry,\n} from \"./types\";\n\nconst MV_CONFIG_FILE = \"metric-views.json\";\n\n/**\n * Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog\n * limit. Enforced by {@link resolveMetricConfig}.\n */\nconst MAX_METRIC_VIEWS = 200;\n/** Per-segment cap = UC's object-name length limit (255). */\nconst MAX_FQN_SEGMENT_LENGTH = MAX_UC_OBJECT_NAME_LENGTH;\n/** Whole-FQN cap: three max-length segments plus the two separating dots. */\nconst MAX_FQN_LENGTH = MAX_FQN_SEGMENT_LENGTH * 3 + 2;\n/** A metric view FQN is exactly catalog.schema.metric_view. */\nconst FQN_SEGMENT_NAMES = [\"catalog\", \"schema\", \"metric_view\"] as const;\nconst FQN_SEGMENT_COUNT = FQN_SEGMENT_NAMES.length;\n\n/**\n * Locale-independent comparator (UTF-16 code-unit order) for metric-view key\n * ordering. Plain `sort()` is locale-sensitive, so keys could order differently\n * across environments and invalidate the cache hash — this keeps the ordering\n * stable everywhere.\n */\nfunction compareKeys(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Read {@link MV_CONFIG_FILE} from a queries folder.\n *\n * Returns `null` if the file does not exist (the metric-view path is\n * additive — apps without metric-views.json must not be penalized). There is\n * deliberately no fallback to the legacy `metric.json` filename.\n *\n * Throws on JSON parse errors so misconfiguration surfaces loudly.\n */\nexport async function readMetricConfig(\n queryFolder: string,\n): Promise<MetricSourceConfig | null> {\n const metricPath = path.join(queryFolder, MV_CONFIG_FILE);\n let raw: string;\n try {\n raw = await fs.readFile(metricPath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `Failed to parse metric-views.json at ${metricPath}: ${(err as Error).message}`,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\n `Invalid metric-views.json at ${metricPath}: expected an object with a 'metricViews' map.`,\n );\n }\n\n return parsed as MetricSourceConfig;\n}\n\n/**\n * Validate a key against the JSON Schema's metricKey pattern. Kept\n * lightweight — the shared Zod schema ({@link metricSourceSchema}) is the\n * canonical contract for IDE/CI; this regex is identical to its\n * {@link metricKeySchema}.\n */\nfunction isValidMetricKey(key: string): boolean {\n return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);\n}\n\n/**\n * Total predicate: is `fqn` a well-formed three-part UC metric view FQN?\n *\n * Well-formed = exactly three non-empty, dot-separated segments, each a valid\n * Unity Catalog object name per the shared {@link UC_FQN_PATTERN} (the single\n * source of truth, also used by the canonical Zod schema). Used as the\n * defense-in-depth re-check at the describe fetcher seam; {@link resolveMetricConfig}\n * runs the same checks but with specific, staged error messages.\n *\n * @note Segment length ({@link MAX_FQN_SEGMENT_LENGTH}) is NOT checked here —\n * an over-long but otherwise legal name is still \"valid shape\". The length cap\n * is a separate concern enforced (with its own message) in resolveMetricConfig.\n */\nexport function isValidFqn(fqn: string): boolean {\n const segments = fqn.split(\".\");\n if (segments.length !== FQN_SEGMENT_COUNT) {\n return false;\n }\n return segments.every((segment) => UC_FQN_PATTERN.test(segment));\n}\n\n/**\n * Field allowlists enforced by {@link resolveMetricConfig}.\n */\nconst ALLOWED_TOP_LEVEL_FIELDS = new Set([\"$schema\", \"metricViews\"]);\nconst ALLOWED_ENTRY_FIELDS = new Set([\"source\", \"executor\"]);\n\n/**\n * Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.\n */\nexport function resolveMetricConfig(\n config: MetricSourceConfig,\n): MetricConfigResolution {\n for (const field of Object.keys(config)) {\n if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) {\n throw new Error(\n `Invalid top-level field \"${field}\" in metric-views.json: only '$schema' and 'metricViews' are allowed.`,\n );\n }\n }\n\n // Default to {} only when metricViews is genuinely absent. A `null` must fall\n // through to the type check below and throw — the canonical Zod schema rejects\n // null.\n const metricViews =\n config.metricViews === undefined ? {} : config.metricViews;\n if (\n typeof metricViews !== \"object\" ||\n metricViews === null ||\n Array.isArray(metricViews)\n ) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: expected an object map of metric entries.`,\n );\n }\n\n const entries: ResolvedMetricEntry[] = [];\n const sortedKeys = Object.keys(metricViews).sort(compareKeys);\n if (sortedKeys.length > MAX_METRIC_VIEWS) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`,\n );\n }\n for (const key of sortedKeys) {\n if (!isValidMetricKey(key)) {\n throw new Error(\n `Invalid metric key \"${key}\" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`,\n );\n }\n\n const entry = metricViews[key];\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new Error(\n `Invalid metric entry \"${key}\": expected an object with a 'source' field.`,\n );\n }\n\n for (const field of Object.keys(entry)) {\n if (!ALLOWED_ENTRY_FIELDS.has(field)) {\n throw new Error(\n `Invalid field \"${field}\" on metric entry \"${key}\": only 'source' and 'executor' are allowed at v1.`,\n );\n }\n }\n\n if (typeof entry.source !== \"string\" || entry.source.trim() === \"\") {\n throw new Error(\n `Invalid metric entry \"${key}\": 'source' must be a non-empty string.`,\n );\n }\n\n if (entry.source.length > MAX_FQN_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": FQN is ${entry.source.length} characters, exceeding the maximum of ${MAX_FQN_LENGTH}.`,\n );\n }\n\n // Staged, specific validation against the UC object-name grammar\n // (UC_FQN_PATTERN — shared with the canonical Zod schema). Reported in\n // order of increasing specificity so the message names the exact problem.\n const segments = entry.source.split(\".\");\n\n // Arity: exactly catalog.schema.metric_view. A wrong part count almost\n // always means a name contains a dot — which the dotted `source` cannot\n // express, since every dot is a segment boundary.\n if (segments.length !== FQN_SEGMENT_COUNT) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view> (got ${segments.length} dot-separated part${segments.length === 1 ? \"\" : \"s\"}). A catalog, schema, or metric view name cannot itself contain a dot.`,\n );\n }\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n const segmentName = FQN_SEGMENT_NAMES[i];\n\n // Empty part: a leading/trailing/double dot (e.g. \"a..c\", \".b.c\").\n if (segment.length === 0) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part is empty. A three-part UC FQN needs a non-empty name in each position: <catalog>.<schema>.<metric_view>.`,\n );\n }\n\n // Length cap (UC: object names are at most 255 characters).\n if (segment.length > MAX_FQN_SEGMENT_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": the ${segmentName} segment is ${segment.length} characters, exceeding the maximum of ${MAX_FQN_SEGMENT_LENGTH} per segment.`,\n );\n }\n\n // Character set: must be a valid UC object name (the FQN is always\n // backtick-quoted before it reaches SQL, so UC's *delimited* identifier\n // rules apply — anything but space, '/', and control characters).\n if (!UC_FQN_PATTERN.test(segment)) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part \"${segment}\" contains a character Unity Catalog does not allow in an object name (no spaces, '/', or control characters).`,\n );\n }\n }\n\n const executor = entry.executor;\n if (\n executor !== undefined &&\n executor !== \"app_service_principal\" &&\n executor !== \"user\"\n ) {\n throw new Error(\n `Invalid executor \"${String(executor)}\" on metric entry \"${key}\": must be \"app_service_principal\" or \"user\".`,\n );\n }\n\n const lane: MetricLane = executor === \"user\" ? \"obo\" : \"sp\";\n\n entries.push({ key, source: entry.source, lane });\n }\n\n return { entries };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render-types.js","names":[],"sources":["../../../src/type-generator/mv-registry/render-types.ts"],"sourcesContent":["import type { MetricColumnMetadata, MetricSchema } from \"./types\";\n\n/**\n * @todo unify with query-registry.ts\n * Map a Databricks SQL type to a TypeScript primitive.\n * Centralized here (not imported from query-registry) so this module\n * stays self-contained.\n */\nfunction tsTypeFor(sqlType: string): string {\n const normalized = sqlType\n .toUpperCase()\n .replace(/\\(.*\\)$/, \"\")\n .replace(/<.*>$/, \"\")\n .split(\" \")[0];\n\n switch (normalized) {\n case \"BOOLEAN\":\n return \"boolean\";\n case \"TINYINT\":\n case \"SMALLINT\":\n case \"INT\":\n case \"INTEGER\":\n case \"BIGINT\":\n case \"FLOAT\":\n case \"DOUBLE\":\n case \"DECIMAL\":\n case \"NUMERIC\":\n return \"number\";\n default:\n return \"string\";\n }\n}\n\n// Render a MetricRegistry interface entry from a MetricSchema.\nfunction renderMetricEntry(schema: MetricSchema): string {\n if (schema.degraded) {\n return renderDegradedMetricEntry(schema);\n }\n const indent = \" \";\n const colsBlock = (cols: MetricColumnMetadata[]): string => {\n if (cols.length === 0) return \"Record<string, never>\";\n const fields = cols\n .map((col) => {\n const grainComment = col.timeGrains?.length\n ? ` @timeGrain ${col.timeGrains.join(\"|\")}`\n : \"\";\n return `${indent}/** @sqlType ${col.type.replace(/\\*\\//g, \"* /\")}${grainComment} */\n${indent}${JSON.stringify(col.name)}: ${tsTypeFor(col.type)}`;\n })\n .join(\";\\n\");\n return `{\n${fields};\n }`;\n };\n const unionOf = (keys: string[]): string =>\n keys.length > 0 ? keys.join(\" | \") : \"never\";\n\n const measuresBlock = colsBlock(schema.measures);\n const dimensionsBlock = colsBlock(schema.dimensions);\n const measureUnion = unionOf(\n schema.measures.map((m) => JSON.stringify(m.name)),\n );\n const dimensionUnion = unionOf(\n schema.dimensions.map((d) => JSON.stringify(d.name)),\n );\n\n const timeGrainSet = new Set<string>();\n for (const d of schema.dimensions) {\n for (const g of d.timeGrains ?? []) {\n timeGrainSet.add(g);\n }\n }\n const timeGrainUnion =\n timeGrainSet.size > 0\n ? [...timeGrainSet]\n .sort()\n .map((g) => JSON.stringify(g))\n .join(\" | \")\n : \"never\";\n\n const measureMetadata = renderMetadataMap(schema.measures, indent);\n const dimensionMetadata = renderMetadataMap(schema.dimensions, indent, true);\n\n return ` ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: ${measuresBlock};\n dimensions: ${dimensionsBlock};\n measureKeys: ${measureUnion};\n dimensionKeys: ${dimensionUnion};\n timeGrains: ${timeGrainUnion};\n metadata: {\n measures: ${measureMetadata};\n dimensions: ${dimensionMetadata};\n };\n }`;\n}\n\n// Render the permissive (\"degraded-open\") entry for a schema the warehouse could not describe.\nfunction renderDegradedMetricEntry(schema: MetricSchema): string {\n return ` /** Degraded: schema unavailable at type-generation time — permissive types until a successful DESCRIBE refreshes them. */\n ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: Record<string, unknown>;\n dimensions: Record<string, unknown>;\n measureKeys: string;\n dimensionKeys: string;\n timeGrains: string;\n metadata: {\n measures: Record<string, never>;\n dimensions: Record<string, never>;\n };\n }`;\n}\n\n// Render the type-level shape of a column's semantic-metadata map\n// for the `metadata` field of a MetricRegistry entry.\nfunction renderMetadataMap(\n cols: MetricColumnMetadata[],\n indent: string,\n includeTimeGrain = false,\n): string {\n if (cols.length === 0) return \"Record<string, never>\";\n\n const inner = cols\n .map((col) => {\n const fields: string[] = [`type: ${JSON.stringify(col.type)}`];\n if (col.displayName) {\n fields.push(`display_name: ${JSON.stringify(col.displayName)}`);\n }\n if (col.format) {\n fields.push(`format: ${JSON.stringify(col.format)}`);\n }\n if (col.description) {\n fields.push(`description: ${JSON.stringify(col.description)}`);\n }\n if (includeTimeGrain && col.timeGrains && col.timeGrains.length > 0) {\n const grainTuple = col.timeGrains\n .map((g) => JSON.stringify(g))\n .join(\", \");\n fields.push(`time_grain: readonly [${grainTuple}]`);\n }\n const fieldsBlock = fields.map((f) => `${indent} ${f}`).join(\";\\n\");\n return `${indent}${JSON.stringify(col.name)}: {\n${fieldsBlock};\n${indent}}`;\n })\n .join(\";\\n\");\n\n return `{\n${inner};\n }`;\n}\n\n// Render the augmentation block for the appkit-ui MetricRegistry interface.\nfunction renderMetricRegistry(schemas: MetricSchema[]): string {\n if (schemas.length === 0) {\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {}\n}\n`;\n }\n const entries = schemas.map(renderMetricEntry).join(\";\\n\");\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {\n${entries};\n }\n}\n`;\n}\n\n// Build the full metric.d.ts file from a list of metric schemas.\nexport function generateMetricTypeDeclarations(\n schemas: MetricSchema[],\n): string {\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\n${renderMetricRegistry(schemas)}`;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"render-types.js","names":[],"sources":["../../../src/type-generator/mv-registry/render-types.ts"],"sourcesContent":["import type { MetricColumnMetadata, MetricSchema } from \"./types\";\n\n/**\n * @todo unify with query-registry.ts\n * Map a Databricks SQL type to a TypeScript primitive.\n * Centralized here (not imported from query-registry) so this module\n * stays self-contained.\n */\nfunction tsTypeFor(sqlType: string): string {\n const normalized = sqlType\n .toUpperCase()\n .replace(/\\(.*\\)$/, \"\")\n .replace(/<.*>$/, \"\")\n .split(\" \")[0];\n\n switch (normalized) {\n case \"BOOLEAN\":\n return \"boolean\";\n case \"TINYINT\":\n case \"SMALLINT\":\n case \"INT\":\n case \"INTEGER\":\n case \"BIGINT\":\n case \"FLOAT\":\n case \"DOUBLE\":\n case \"DECIMAL\":\n case \"NUMERIC\":\n return \"number\";\n default:\n return \"string\";\n }\n}\n\n// Render a MetricRegistry interface entry from a MetricSchema.\nfunction renderMetricEntry(schema: MetricSchema): string {\n if (schema.degraded) {\n return renderDegradedMetricEntry(schema);\n }\n const indent = \" \";\n const colsBlock = (cols: MetricColumnMetadata[]): string => {\n if (cols.length === 0) return \"Record<string, never>\";\n const fields = cols\n .map((col) => {\n const grainComment = col.timeGrains?.length\n ? ` @timeGrain ${col.timeGrains.join(\"|\")}`\n : \"\";\n return `${indent}/** @sqlType ${col.type.replace(/\\*\\//g, \"* /\")}${grainComment} */\n${indent}${JSON.stringify(col.name)}: ${tsTypeFor(col.type)}`;\n })\n .join(\";\\n\");\n return `{\n${fields};\n }`;\n };\n const unionOf = (keys: string[]): string =>\n keys.length > 0 ? keys.join(\" | \") : \"never\";\n\n const measuresBlock = colsBlock(schema.measures);\n const dimensionsBlock = colsBlock(schema.dimensions);\n const measureUnion = unionOf(\n schema.measures.map((m) => JSON.stringify(m.name)),\n );\n const dimensionUnion = unionOf(\n schema.dimensions.map((d) => JSON.stringify(d.name)),\n );\n\n const timeGrainSet = new Set<string>();\n for (const d of schema.dimensions) {\n for (const g of d.timeGrains ?? []) {\n timeGrainSet.add(g);\n }\n }\n const timeGrainUnion =\n timeGrainSet.size > 0\n ? [...timeGrainSet]\n .sort()\n .map((g) => JSON.stringify(g))\n .join(\" | \")\n : \"never\";\n\n const measureMetadata = renderMetadataMap(schema.measures, indent);\n const dimensionMetadata = renderMetadataMap(schema.dimensions, indent, true);\n\n return ` ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: ${measuresBlock};\n dimensions: ${dimensionsBlock};\n measureKeys: ${measureUnion};\n dimensionKeys: ${dimensionUnion};\n timeGrains: ${timeGrainUnion};\n metadata: {\n measures: ${measureMetadata};\n dimensions: ${dimensionMetadata};\n };\n }`;\n}\n\n// Render the permissive (\"degraded-open\") entry for a schema the warehouse could not describe.\nfunction renderDegradedMetricEntry(schema: MetricSchema): string {\n return ` /** Degraded: schema unavailable at type-generation time — permissive types until a successful DESCRIBE refreshes them. */\n ${JSON.stringify(schema.key)}: {\n key: ${JSON.stringify(schema.key)};\n source: ${JSON.stringify(schema.source)};\n lane: ${JSON.stringify(schema.lane)};\n measures: Record<string, unknown>;\n dimensions: Record<string, unknown>;\n measureKeys: string;\n dimensionKeys: string;\n timeGrains: string;\n metadata: {\n measures: Record<string, never>;\n dimensions: Record<string, never>;\n };\n }`;\n}\n\n// Render the type-level shape of a column's semantic-metadata map\n// for the `metadata` field of a MetricRegistry entry.\nfunction renderMetadataMap(\n cols: MetricColumnMetadata[],\n indent: string,\n includeTimeGrain = false,\n): string {\n if (cols.length === 0) return \"Record<string, never>\";\n\n const inner = cols\n .map((col) => {\n const fields: string[] = [`type: ${JSON.stringify(col.type)}`];\n if (col.displayName) {\n fields.push(`display_name: ${JSON.stringify(col.displayName)}`);\n }\n if (col.format) {\n fields.push(`format: ${JSON.stringify(col.format)}`);\n }\n if (col.description) {\n fields.push(`description: ${JSON.stringify(col.description)}`);\n }\n if (includeTimeGrain && col.timeGrains && col.timeGrains.length > 0) {\n const grainTuple = col.timeGrains\n .map((g) => JSON.stringify(g))\n .join(\", \");\n fields.push(`time_grain: readonly [${grainTuple}]`);\n }\n const fieldsBlock = fields.map((f) => `${indent} ${f}`).join(\";\\n\");\n return `${indent}${JSON.stringify(col.name)}: {\n${fieldsBlock};\n${indent}}`;\n })\n .join(\";\\n\");\n\n return `{\n${inner};\n }`;\n}\n\n// Render the augmentation block for the appkit-ui MetricRegistry interface.\nfunction renderMetricRegistry(schemas: MetricSchema[]): string {\n if (schemas.length === 0) {\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {}\n}\n`;\n }\n const entries = schemas.map(renderMetricEntry).join(\";\\n\");\n return `declare module \"@databricks/appkit-ui/react\" {\n interface MetricRegistry {\n${entries};\n }\n}\n`;\n}\n\n// Build the full metric-views.d.ts file from a list of metric schemas.\nexport function generateMetricTypeDeclarations(\n schemas: MetricSchema[],\n): string {\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\n${renderMetricRegistry(schemas)}`;\n}\n"],"mappings":"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"}
|
|
@@ -11,13 +11,6 @@ interface AppKitTypesPluginOptions {
|
|
|
11
11
|
* Defaults to a sibling of `outFile`, computed by the generator.
|
|
12
12
|
*/
|
|
13
13
|
mvOutFile?: string;
|
|
14
|
-
/**
|
|
15
|
-
* Path to the metric semantic-metadata JSON file (relative to client folder).
|
|
16
|
-
* Build-time artifact — defaults to a sibling of {@link mvOutFile}
|
|
17
|
-
* (itself a sibling of `outFile`), computed by the generator. Skipped
|
|
18
|
-
* automatically when `metric-views.json` is absent.
|
|
19
|
-
*/
|
|
20
|
-
mvMetadataOutFile?: string;
|
|
21
14
|
/** Folders to watch for changes. */
|
|
22
15
|
watchFolders?: string[];
|
|
23
16
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin.d.ts","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"mappings":";;;;;AAGmC;UA6BzB,wBAAA;EAER,OAAA;EAFgC;;;;EAOhC,SAAA;
|
|
1
|
+
{"version":3,"file":"vite-plugin.d.ts","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"mappings":";;;;;AAGmC;UA6BzB,wBAAA;EAER,OAAA;EAFgC;;;;EAOhC,SAAA;EAEY;EAAZ,YAAA;AAAA;;;;;;;iBASc,iBAAA,CAAkB,OAAA,GAAU,wBAAA,GAA2B,MAAA"}
|
|
@@ -23,7 +23,6 @@ const DEV_WAREHOUSE_WATCH_MAX_MS = 6e4;
|
|
|
23
23
|
function appKitTypesPlugin(options) {
|
|
24
24
|
let outFile;
|
|
25
25
|
let mvOutFile;
|
|
26
|
-
let mvMetadataOutFile;
|
|
27
26
|
let watchFolders;
|
|
28
27
|
let inFlight = null;
|
|
29
28
|
let queued = false;
|
|
@@ -53,8 +52,7 @@ function appKitTypesPlugin(options) {
|
|
|
53
52
|
warehouseId,
|
|
54
53
|
noCache: false,
|
|
55
54
|
mode,
|
|
56
|
-
mvOutFile
|
|
57
|
-
mvMetadataOutFile
|
|
55
|
+
mvOutFile
|
|
58
56
|
});
|
|
59
57
|
} catch (error) {
|
|
60
58
|
const isTypegenError = error instanceof TypegenSyntaxError || error instanceof TypegenFatalError;
|
|
@@ -176,7 +174,6 @@ function appKitTypesPlugin(options) {
|
|
|
176
174
|
const projectRoot = path.resolve(config.root, "..");
|
|
177
175
|
outFile = path.resolve(projectRoot, options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`);
|
|
178
176
|
mvOutFile = options?.mvOutFile !== void 0 ? path.resolve(projectRoot, options.mvOutFile) : void 0;
|
|
179
|
-
mvMetadataOutFile = options?.mvMetadataOutFile !== void 0 ? path.resolve(projectRoot, options.mvMetadataOutFile) : void 0;
|
|
180
177
|
watchFolders = options?.watchFolders ?? [path.join(process.cwd(), "config", "queries")];
|
|
181
178
|
},
|
|
182
179
|
buildStart() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { Plugin } from \"vite\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n TypegenFatalError,\n TypegenSyntaxError,\n} from \"./index\";\nimport type { PreflightMode } from \"./preflight\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * How long the DEV background watcher waits for a STARTING warehouse to reach\n * RUNNING before giving up. Short relative to the CLI's preflight budget: this\n * is a best-effort \"regenerate once the warehouse warms up\" convenience, not a\n * gate, so we'd rather stop polling than hold a detached task open for minutes.\n */\nconst DEV_WAREHOUSE_WATCH_MAX_MS = 60_000;\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /**\n * Path to the metric registry d.ts file (relative to client folder).\n * Defaults to a sibling of `outFile`, computed by the generator.\n */\n mvOutFile?: string;\n /**\n * Path to the metric semantic-metadata JSON file (relative to client folder).\n * Build-time artifact — defaults to a sibling of {@link mvOutFile}\n * (itself a sibling of `outFile`), computed by the generator. Skipped\n * automatically when `metric-views.json` is absent.\n */\n mvMetadataOutFile?: string;\n /** Folders to watch for changes. */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let mvOutFile: string | undefined;\n let mvMetadataOutFile: string | undefined;\n let watchFolders: string[];\n\n // Single-flight state for runGenerate(). `inFlight` is the promise of the\n // currently-running drain (null when idle); `queued` records that a trigger\n // arrived while a run was active so exactly ONE trailing run fires afterwards\n // (latest-wins — coalesces any number of overlapping triggers into a single\n // rerun). `queued` is read/cleared synchronously inside the drain loop so a\n // trigger landing in any window is caught before the drain exits.\n //\n // `pendingMode` is the mode the next generate should run in (latest-wins, like\n // `queued`): the foreground build runs non-blocking in dev (instant degrade)\n // while the background warehouse watch runs blocking (real DESCRIBEs). A\n // blocking watch trigger that lands while a non-blocking foreground run is in\n // flight therefore still describes when its trailing run fires.\n let inFlight: Promise<void> | null = null;\n let queued = false;\n let pendingMode: PreflightMode = \"non-blocking\";\n\n // The currently-armed DEV background warehouse watch, if any. Aborting it\n // stops a pending waitUntilRunning (server shutdown, or a newer arm replacing\n // an older one).\n let watchController: AbortController | null = null;\n\n /**\n * Generate types once in the given preflight {@link PreflightMode}. Never\n * throws in dev (logs instead); in production it rethrows so the build fails.\n * This is the un-guarded core — callers should go through {@link runGenerate}\n * so concurrent triggers can't race-write the .d.ts.\n *\n * @param mode - preflight policy for this run. The foreground build passes a\n * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it\n * degrades instantly); the background warehouse watch passes \"blocking\" so\n * its regenerate actually DESCRIBEs and lands real (non-degraded) types.\n */\n async function generateOnce(mode: PreflightMode) {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder: watchFolders[0],\n warehouseId,\n noCache: false,\n mode,\n mvOutFile,\n mvMetadataOutFile,\n });\n } catch (error) {\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // report in their message. Their stack frames and attached query arrays\n // point into appkit internals and only add noise, so surface just the\n // message — both when failing the prod build and when logging in dev.\n const isTypegenError =\n error instanceof TypegenSyntaxError ||\n error instanceof TypegenFatalError;\n\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n if (isTypegenError) error.stack = error.message;\n throw error;\n }\n\n if (isTypegenError) {\n logger.error(\"%s\", error.message);\n } else {\n logger.error(\"Error generating types: %O\", error);\n }\n }\n }\n\n /**\n * Single-flight wrapper around {@link generateOnce}. The initial build, the\n * .sql watcher, and the DEV warehouse watch all route through here so they can\n * never run typegen concurrently (which would race-write the .d.ts).\n *\n * If a run is already in flight, this does NOT start a second one — it records\n * the requested mode and sets a trailing flag so exactly one more run fires\n * after the current finishes, coalescing any number of overlapping triggers\n * (latest-wins, including the mode: a blocking watch trigger that arrives mid\n * non-blocking foreground run still describes when its trailing run fires).\n *\n * @param mode - preflight policy for this run. Recorded into `pendingMode`,\n * which the drain reads for each generate (latest trigger wins).\n * @returns A promise that resolves when this trigger's work (including any\n * trailing run it scheduled) has completed.\n */\n function runGenerate(mode: PreflightMode): Promise<void> {\n pendingMode = mode;\n\n if (inFlight) {\n // A run is active: remember that another trigger arrived and ride out the\n // current run. One trailing run then covers all coalesced triggers and\n // runs in the latest requested mode (recorded above).\n queued = true;\n return inFlight;\n }\n\n // Drain in a loop rather than recursing after a single queued-check: a\n // trigger can land in the window between generateOnce() resolving and the\n // check, so we re-test `queued` until it's clear. Critically, `inFlight` is\n // cleared synchronously in the SAME tick as the final `queued === false`\n // observation — never deferred to a .finally microtask — so there's no\n // window where a trigger sees `inFlight` set but the drain has already\n // decided to exit. The guard stays held for the whole drain, so concurrent\n // triggers only ever set the flag; they never start a parallel generate.\n const drain = async (): Promise<void> => {\n while (true) {\n queued = false;\n // Snapshot the mode synchronously alongside clearing `queued` so a\n // trigger landing during this generate is observed (via `queued`) on the\n // next loop with its own mode, not silently dropped.\n const runMode = pendingMode;\n await generateOnce(runMode);\n // Synchronous check + clear, atomic w.r.t. other (synchronous) callers.\n if (!queued) {\n inFlight = null;\n return;\n }\n }\n };\n\n inFlight = drain();\n return inFlight;\n }\n\n /**\n * DEV-only: get the warehouse to RUNNING in the background and regenerate with\n * real (non-degraded) types once it is — without blocking dev startup. The\n * foreground build only ever degrades in dev (instant `unknown`/cached types),\n * so this is what lands actual DESCRIBE results in the editor for EVERY\n * reachable warehouse state, not just one that happens to already be warm.\n *\n * Post-probe behaviour by state:\n * - RUNNING → describe right away (the dev foreground degraded, so a running\n * warehouse would otherwise never get real types). `waitUntilRunning`\n * returns immediately for an already-running warehouse, then the blocking\n * regenerate fires.\n * - STARTING → it's already coming up; just wait for RUNNING, then describe.\n * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.\n * - DELETED / DELETING → return (a deleted warehouse can't be started, and\n * blocking typegen would treat it as fatal); leave the degraded types.\n *\n * No-op in production or without a warehouse id. Replaces any previously-armed\n * watch (aborting it first). Fully self-contained: it never throws into the\n * caller and never re-arms itself. The whole lifecycle is abortable via the\n * shared {@link watchController} — its signal is threaded into\n * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the\n * regenerate routes through {@link runGenerate} so it can't race-write the\n * .d.ts with the foreground degrade or a `.sql` re-trigger.\n *\n * The regenerate runs in \"blocking\" mode (not the foreground's non-blocking)\n * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —\n * the whole point of warming the warehouse in the background.\n */\n function armWarehouseWatch(): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n if (!warehouseId) return;\n\n // Supersede any in-flight watch so we never run two concurrently.\n watchController?.abort();\n const controller = new AbortController();\n watchController = controller;\n const { signal } = controller;\n\n void (async () => {\n try {\n const client = new WorkspaceClient({});\n const state = await getWarehouseState(client, warehouseId);\n\n // A deleted/deleting warehouse can't be started and blocking typegen\n // would treat it as fatal — leave the degraded types and stop. Every\n // other state (including RUNNING) proceeds to wait-then-describe so the\n // dev editor gets real types, not just the foreground's degraded ones.\n if (state === \"DELETED\" || state === \"DELETING\") {\n return;\n }\n\n // Stopped/stopping won't reach RUNNING on its own — nudge it. RUNNING and\n // STARTING need no start (RUNNING is already up; STARTING is coming up),\n // so don't issue a redundant one. A failed start is non-fatal: give up\n // silently rather than throw out of the detached task (the developer\n // still has degraded/cached types).\n let startedByUs = false;\n if (state === \"STOPPED\" || state === \"STOPPING\") {\n try {\n logger.debug(\"Warehouse is %s; starting it.\", state);\n await startWarehouse(client, warehouseId);\n startedByUs = true;\n } catch {\n return;\n }\n }\n\n // Wait for RUNNING. For an already-RUNNING warehouse this returns on the\n // first poll; for STARTING/STOPPED it polls (abortably) until the\n // warehouse warms up, a terminal state, or the deadline.\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,\n signal,\n // We just issued the start, so the first poll(s) often still report\n // STOPPED/STOPPING before the start propagates. Poll through those\n // instead of bailing, or the regenerate would never fire. When we\n // didn't start it (RUNNING/STARTING branch), keep the default terminal\n // states.\n treatStoppedAsTransient: startedByUs,\n });\n\n if (final === \"RUNNING\" && !signal.aborted) {\n logger.debug(\"Warehouse is RUNNING; regenerating types.\");\n // Blocking: the warehouse is RUNNING now, so describe it and emit real\n // (non-degraded) types — unlike the foreground dev run, which degraded.\n // Routed through the single-flight guard so it coalesces with the\n // foreground degrade / any `.sql` re-trigger instead of racing them.\n await runGenerate(\"blocking\");\n }\n } catch {\n // Detached background task: any failure (timeout, abort, connectivity,\n // auth) is non-fatal — the developer still has degraded/cached types.\n }\n })();\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n if (!existsSync(path.join(process.cwd(), \"config\", \"queries\"))) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n // Metric out-paths resolve against projectRoot only when explicitly\n // provided; unset options pass through as undefined so the generator\n // computes its sibling-of-outFile defaults. In the all-defaults case\n // the final paths are identical (the default outFile above lives in\n // shared/<TYPES_DIR>/), and a customized outFile now keeps its metric\n // siblings next to it instead of pinning them under shared/.\n mvOutFile =\n options?.mvOutFile !== undefined\n ? path.resolve(projectRoot, options.mvOutFile)\n : undefined;\n mvMetadataOutFile =\n options?.mvMetadataOutFile !== undefined\n ? path.resolve(projectRoot, options.mvMetadataOutFile)\n : undefined;\n watchFolders = options?.watchFolders ?? [\n path.join(process.cwd(), \"config\", \"queries\"),\n ];\n },\n\n buildStart() {\n // Production: block the build on this generate (and surface failures).\n // The watch is a dev-only no-op, so just run typegen.\n if (process.env.NODE_ENV === \"production\") {\n return runGenerate(\"blocking\");\n }\n\n // Dev: don't block startup waiting on typegen. The foreground generate runs\n // non-blocking — it skips the warehouse entirely and writes degraded\n // (cached/`unknown`) types instantly. Then arm the warehouse watch so the\n // warehouse gets a one-shot BLOCKING regenerate (real types) in the\n // background for EVERY reachable state: RUNNING describes right away, while\n // STARTING/STOPPED are waited (and started) until they reach RUNNING.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n if (\n isWatchedFile &&\n (changedFile.endsWith(\".sql\") ||\n // Basename equality, not endsWith: a sibling like\n // \"legacy-metric-views.json\" must not trigger a regenerate —\n // only the real config file does.\n path.basename(changedFile) === \"metric-views.json\")\n ) {\n // Route through the single-flight runner (was fire-and-forget\n // generate(), which could race the initial build / watch). This is a\n // dev-only hook, so degrade instantly (non-blocking), then re-arm the\n // warehouse watch so the edited query or metric-view source is\n // re-described in the background against the running warehouse (or\n // once a still-starting one warms up), landing fresh\n // blocking-described types.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n }\n });\n\n // Tear down any pending warehouse watch when the dev server closes so a\n // long backoff can't keep the process alive after shutdown.\n server.httpServer?.once(\"close\", () => {\n watchController?.abort();\n });\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"vite-plugin.js","names":[],"sources":["../../src/type-generator/vite-plugin.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { Plugin } from \"vite\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n ANALYTICS_TYPES_FILE,\n generateFromEntryPoint,\n TYPES_DIR,\n TypegenFatalError,\n TypegenSyntaxError,\n} from \"./index\";\nimport type { PreflightMode } from \"./preflight\";\nimport {\n getWarehouseState,\n startWarehouse,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\nconst logger = createLogger(\"type-generator:vite-plugin\");\n\n/**\n * How long the DEV background watcher waits for a STARTING warehouse to reach\n * RUNNING before giving up. Short relative to the CLI's preflight budget: this\n * is a best-effort \"regenerate once the warehouse warms up\" convenience, not a\n * gate, so we'd rather stop polling than hold a detached task open for minutes.\n */\nconst DEV_WAREHOUSE_WATCH_MAX_MS = 60_000;\n\n/**\n * Options for the AppKit types plugin.\n */\ninterface AppKitTypesPluginOptions {\n /* Path to the output d.ts file (relative to client folder). */\n outFile?: string;\n /**\n * Path to the metric registry d.ts file (relative to client folder).\n * Defaults to a sibling of `outFile`, computed by the generator.\n */\n mvOutFile?: string;\n /** Folders to watch for changes. */\n watchFolders?: string[];\n}\n\n/**\n * Vite plugin to generate types for AppKit queries.\n * Calls generateFromEntryPoint under the hood.\n * @param options - Options to override default values.\n * @returns Vite plugin to generate types for AppKit queries.\n */\nexport function appKitTypesPlugin(options?: AppKitTypesPluginOptions): Plugin {\n let outFile: string;\n let mvOutFile: string | undefined;\n let watchFolders: string[];\n\n // Single-flight state for runGenerate(). `inFlight` is the promise of the\n // currently-running drain (null when idle); `queued` records that a trigger\n // arrived while a run was active so exactly ONE trailing run fires afterwards\n // (latest-wins — coalesces any number of overlapping triggers into a single\n // rerun). `queued` is read/cleared synchronously inside the drain loop so a\n // trigger landing in any window is caught before the drain exits.\n //\n // `pendingMode` is the mode the next generate should run in (latest-wins, like\n // `queued`): the foreground build runs non-blocking in dev (instant degrade)\n // while the background warehouse watch runs blocking (real DESCRIBEs). A\n // blocking watch trigger that lands while a non-blocking foreground run is in\n // flight therefore still describes when its trailing run fires.\n let inFlight: Promise<void> | null = null;\n let queued = false;\n let pendingMode: PreflightMode = \"non-blocking\";\n\n // The currently-armed DEV background warehouse watch, if any. Aborting it\n // stops a pending waitUntilRunning (server shutdown, or a newer arm replacing\n // an older one).\n let watchController: AbortController | null = null;\n\n /**\n * Generate types once in the given preflight {@link PreflightMode}. Never\n * throws in dev (logs instead); in production it rethrows so the build fails.\n * This is the un-guarded core — callers should go through {@link runGenerate}\n * so concurrent triggers can't race-write the .d.ts.\n *\n * @param mode - preflight policy for this run. The foreground build passes a\n * NODE_ENV-derived mode (blocking in production, non-blocking in dev so it\n * degrades instantly); the background warehouse watch passes \"blocking\" so\n * its regenerate actually DESCRIBEs and lands real (non-degraded) types.\n */\n async function generateOnce(mode: PreflightMode) {\n try {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return;\n }\n\n await generateFromEntryPoint({\n outFile,\n queryFolder: watchFolders[0],\n warehouseId,\n noCache: false,\n mode,\n mvOutFile,\n });\n } catch (error) {\n // TypegenSyntaxError / TypegenFatalError carry a complete, actionable\n // report in their message. Their stack frames and attached query arrays\n // point into appkit internals and only add noise, so surface just the\n // message — both when failing the prod build and when logging in dev.\n const isTypegenError =\n error instanceof TypegenSyntaxError ||\n error instanceof TypegenFatalError;\n\n // throw in production to fail the build\n if (process.env.NODE_ENV === \"production\") {\n if (isTypegenError) error.stack = error.message;\n throw error;\n }\n\n if (isTypegenError) {\n logger.error(\"%s\", error.message);\n } else {\n logger.error(\"Error generating types: %O\", error);\n }\n }\n }\n\n /**\n * Single-flight wrapper around {@link generateOnce}. The initial build, the\n * .sql watcher, and the DEV warehouse watch all route through here so they can\n * never run typegen concurrently (which would race-write the .d.ts).\n *\n * If a run is already in flight, this does NOT start a second one — it records\n * the requested mode and sets a trailing flag so exactly one more run fires\n * after the current finishes, coalescing any number of overlapping triggers\n * (latest-wins, including the mode: a blocking watch trigger that arrives mid\n * non-blocking foreground run still describes when its trailing run fires).\n *\n * @param mode - preflight policy for this run. Recorded into `pendingMode`,\n * which the drain reads for each generate (latest trigger wins).\n * @returns A promise that resolves when this trigger's work (including any\n * trailing run it scheduled) has completed.\n */\n function runGenerate(mode: PreflightMode): Promise<void> {\n pendingMode = mode;\n\n if (inFlight) {\n // A run is active: remember that another trigger arrived and ride out the\n // current run. One trailing run then covers all coalesced triggers and\n // runs in the latest requested mode (recorded above).\n queued = true;\n return inFlight;\n }\n\n // Drain in a loop rather than recursing after a single queued-check: a\n // trigger can land in the window between generateOnce() resolving and the\n // check, so we re-test `queued` until it's clear. Critically, `inFlight` is\n // cleared synchronously in the SAME tick as the final `queued === false`\n // observation — never deferred to a .finally microtask — so there's no\n // window where a trigger sees `inFlight` set but the drain has already\n // decided to exit. The guard stays held for the whole drain, so concurrent\n // triggers only ever set the flag; they never start a parallel generate.\n const drain = async (): Promise<void> => {\n while (true) {\n queued = false;\n // Snapshot the mode synchronously alongside clearing `queued` so a\n // trigger landing during this generate is observed (via `queued`) on the\n // next loop with its own mode, not silently dropped.\n const runMode = pendingMode;\n await generateOnce(runMode);\n // Synchronous check + clear, atomic w.r.t. other (synchronous) callers.\n if (!queued) {\n inFlight = null;\n return;\n }\n }\n };\n\n inFlight = drain();\n return inFlight;\n }\n\n /**\n * DEV-only: get the warehouse to RUNNING in the background and regenerate with\n * real (non-degraded) types once it is — without blocking dev startup. The\n * foreground build only ever degrades in dev (instant `unknown`/cached types),\n * so this is what lands actual DESCRIBE results in the editor for EVERY\n * reachable warehouse state, not just one that happens to already be warm.\n *\n * Post-probe behaviour by state:\n * - RUNNING → describe right away (the dev foreground degraded, so a running\n * warehouse would otherwise never get real types). `waitUntilRunning`\n * returns immediately for an already-running warehouse, then the blocking\n * regenerate fires.\n * - STARTING → it's already coming up; just wait for RUNNING, then describe.\n * - STOPPED / STOPPING → kick off a start, wait for RUNNING, then describe.\n * - DELETED / DELETING → return (a deleted warehouse can't be started, and\n * blocking typegen would treat it as fatal); leave the degraded types.\n *\n * No-op in production or without a warehouse id. Replaces any previously-armed\n * watch (aborting it first). Fully self-contained: it never throws into the\n * caller and never re-arms itself. The whole lifecycle is abortable via the\n * shared {@link watchController} — its signal is threaded into\n * `waitUntilRunning`, so a dev-server shutdown cancels a pending wait — and the\n * regenerate routes through {@link runGenerate} so it can't race-write the\n * .d.ts with the foreground degrade or a `.sql` re-trigger.\n *\n * The regenerate runs in \"blocking\" mode (not the foreground's non-blocking)\n * so it actually DESCRIBEs the now-RUNNING warehouse and lands real types —\n * the whole point of warming the warehouse in the background.\n */\n function armWarehouseWatch(): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n if (!warehouseId) return;\n\n // Supersede any in-flight watch so we never run two concurrently.\n watchController?.abort();\n const controller = new AbortController();\n watchController = controller;\n const { signal } = controller;\n\n void (async () => {\n try {\n const client = new WorkspaceClient({});\n const state = await getWarehouseState(client, warehouseId);\n\n // A deleted/deleting warehouse can't be started and blocking typegen\n // would treat it as fatal — leave the degraded types and stop. Every\n // other state (including RUNNING) proceeds to wait-then-describe so the\n // dev editor gets real types, not just the foreground's degraded ones.\n if (state === \"DELETED\" || state === \"DELETING\") {\n return;\n }\n\n // Stopped/stopping won't reach RUNNING on its own — nudge it. RUNNING and\n // STARTING need no start (RUNNING is already up; STARTING is coming up),\n // so don't issue a redundant one. A failed start is non-fatal: give up\n // silently rather than throw out of the detached task (the developer\n // still has degraded/cached types).\n let startedByUs = false;\n if (state === \"STOPPED\" || state === \"STOPPING\") {\n try {\n logger.debug(\"Warehouse is %s; starting it.\", state);\n await startWarehouse(client, warehouseId);\n startedByUs = true;\n } catch {\n return;\n }\n }\n\n // Wait for RUNNING. For an already-RUNNING warehouse this returns on the\n // first poll; for STARTING/STOPPED it polls (abortably) until the\n // warehouse warms up, a terminal state, or the deadline.\n const final = await waitUntilRunning(client, warehouseId, {\n maxMs: DEV_WAREHOUSE_WATCH_MAX_MS,\n signal,\n // We just issued the start, so the first poll(s) often still report\n // STOPPED/STOPPING before the start propagates. Poll through those\n // instead of bailing, or the regenerate would never fire. When we\n // didn't start it (RUNNING/STARTING branch), keep the default terminal\n // states.\n treatStoppedAsTransient: startedByUs,\n });\n\n if (final === \"RUNNING\" && !signal.aborted) {\n logger.debug(\"Warehouse is RUNNING; regenerating types.\");\n // Blocking: the warehouse is RUNNING now, so describe it and emit real\n // (non-degraded) types — unlike the foreground dev run, which degraded.\n // Routed through the single-flight guard so it coalesces with the\n // foreground degrade / any `.sql` re-trigger instead of racing them.\n await runGenerate(\"blocking\");\n }\n } catch {\n // Detached background task: any failure (timeout, abort, connectivity,\n // auth) is non-fatal — the developer still has degraded/cached types.\n }\n })();\n }\n\n return {\n name: \"appkit-types\",\n\n apply() {\n const warehouseId = process.env.DATABRICKS_WAREHOUSE_ID || \"\";\n\n if (!warehouseId) {\n logger.debug(\"Warehouse ID not found. Skipping type generation.\");\n return false;\n }\n\n if (!existsSync(path.join(process.cwd(), \"config\", \"queries\"))) {\n return false;\n }\n\n return true;\n },\n\n configResolved(config) {\n const projectRoot = path.resolve(config.root, \"..\");\n outFile = path.resolve(\n projectRoot,\n options?.outFile ?? `shared/${TYPES_DIR}/${ANALYTICS_TYPES_FILE}`,\n );\n // The metric out-path resolves against projectRoot only when explicitly\n // provided; an unset option passes through as undefined so the generator\n // computes its sibling-of-outFile default. In the all-defaults case the\n // final path is identical (the default outFile above lives in\n // shared/<TYPES_DIR>/), and a customized outFile now keeps its metric\n // sibling next to it instead of pinning it under shared/.\n mvOutFile =\n options?.mvOutFile !== undefined\n ? path.resolve(projectRoot, options.mvOutFile)\n : undefined;\n watchFolders = options?.watchFolders ?? [\n path.join(process.cwd(), \"config\", \"queries\"),\n ];\n },\n\n buildStart() {\n // Production: block the build on this generate (and surface failures).\n // The watch is a dev-only no-op, so just run typegen.\n if (process.env.NODE_ENV === \"production\") {\n return runGenerate(\"blocking\");\n }\n\n // Dev: don't block startup waiting on typegen. The foreground generate runs\n // non-blocking — it skips the warehouse entirely and writes degraded\n // (cached/`unknown`) types instantly. Then arm the warehouse watch so the\n // warehouse gets a one-shot BLOCKING regenerate (real types) in the\n // background for EVERY reachable state: RUNNING describes right away, while\n // STARTING/STOPPED are waited (and started) until they reach RUNNING.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n },\n\n configureServer(server) {\n server.watcher.add(watchFolders);\n\n server.watcher.on(\"change\", (changedFile) => {\n const isWatchedFile = watchFolders.some((folder) =>\n changedFile.startsWith(folder),\n );\n\n if (\n isWatchedFile &&\n (changedFile.endsWith(\".sql\") ||\n // Basename equality, not endsWith: a sibling like\n // \"legacy-metric-views.json\" must not trigger a regenerate —\n // only the real config file does.\n path.basename(changedFile) === \"metric-views.json\")\n ) {\n // Route through the single-flight runner (was fire-and-forget\n // generate(), which could race the initial build / watch). This is a\n // dev-only hook, so degrade instantly (non-blocking), then re-arm the\n // warehouse watch so the edited query or metric-view source is\n // re-described in the background against the running warehouse (or\n // once a still-starting one warms up), landing fresh\n // blocking-described types.\n void runGenerate(\"non-blocking\");\n armWarehouseWatch();\n }\n });\n\n // Tear down any pending warehouse watch when the dev server closes so a\n // long backoff can't keep the process alive after shutdown.\n server.httpServer?.once(\"close\", () => {\n watchController?.abort();\n });\n },\n };\n}\n"],"mappings":";;;;;;;;AAmBA,MAAM,SAAS,aAAa,6BAA6B;;;;;;;AAQzD,MAAM,6BAA6B;;;;;;;AAuBnC,SAAgB,kBAAkB,SAA4C;CAC5E,IAAI;CACJ,IAAI;CACJ,IAAI;CAcJ,IAAI,WAAiC;CACrC,IAAI,SAAS;CACb,IAAI,cAA6B;CAKjC,IAAI,kBAA0C;;;;;;;;;;;;CAa9C,eAAe,aAAa,MAAqB;AAC/C,MAAI;GACF,MAAM,cAAc,QAAQ,IAAI,2BAA2B;AAE3D,OAAI,CAAC,aAAa;AAChB,WAAO,MAAM,oDAAoD;AACjE;;AAGF,SAAM,uBAAuB;IAC3B;IACA,aAAa,aAAa;IAC1B;IACA,SAAS;IACT;IACA;IACD,CAAC;WACK,OAAO;GAKd,MAAM,iBACJ,iBAAiB,sBACjB,iBAAiB;AAGnB,OAAI,QAAQ,IAAI,aAAa,cAAc;AACzC,QAAI,eAAgB,OAAM,QAAQ,MAAM;AACxC,UAAM;;AAGR,OAAI,eACF,QAAO,MAAM,MAAM,MAAM,QAAQ;OAEjC,QAAO,MAAM,8BAA8B,MAAM;;;;;;;;;;;;;;;;;;;CAqBvD,SAAS,YAAY,MAAoC;AACvD,gBAAc;AAEd,MAAI,UAAU;AAIZ,YAAS;AACT,UAAO;;EAWT,MAAM,QAAQ,YAA2B;AACvC,UAAO,MAAM;AACX,aAAS;AAKT,UAAM,aADU,YACW;AAE3B,QAAI,CAAC,QAAQ;AACX,gBAAW;AACX;;;;AAKN,aAAW,OAAO;AAClB,SAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCT,SAAS,oBAA0B;AACjC,MAAI,QAAQ,IAAI,aAAa,aAAc;EAE3C,MAAM,cAAc,QAAQ,IAAI,2BAA2B;AAC3D,MAAI,CAAC,YAAa;AAGlB,mBAAiB,OAAO;EACxB,MAAM,aAAa,IAAI,iBAAiB;AACxC,oBAAkB;EAClB,MAAM,EAAE,WAAW;AAEnB,GAAM,YAAY;AAChB,OAAI;IACF,MAAM,SAAS,IAAI,gBAAgB,EAAE,CAAC;IACtC,MAAM,QAAQ,MAAM,kBAAkB,QAAQ,YAAY;AAM1D,QAAI,UAAU,aAAa,UAAU,WACnC;IAQF,IAAI,cAAc;AAClB,QAAI,UAAU,aAAa,UAAU,WACnC,KAAI;AACF,YAAO,MAAM,iCAAiC,MAAM;AACpD,WAAM,eAAe,QAAQ,YAAY;AACzC,mBAAc;YACR;AACN;;AAkBJ,QAXc,MAAM,iBAAiB,QAAQ,aAAa;KACxD,OAAO;KACP;KAMA,yBAAyB;KAC1B,CAAC,KAEY,aAAa,CAAC,OAAO,SAAS;AAC1C,YAAO,MAAM,4CAA4C;AAKzD,WAAM,YAAY,WAAW;;WAEzB;MAIN;;AAGN,QAAO;EACL,MAAM;EAEN,QAAQ;AAGN,OAAI,EAFgB,QAAQ,IAAI,2BAA2B,KAEzC;AAChB,WAAO,MAAM,oDAAoD;AACjE,WAAO;;AAGT,OAAI,CAAC,WAAW,KAAK,KAAK,QAAQ,KAAK,EAAE,UAAU,UAAU,CAAC,CAC5D,QAAO;AAGT,UAAO;;EAGT,eAAe,QAAQ;GACrB,MAAM,cAAc,KAAK,QAAQ,OAAO,MAAM,KAAK;AACnD,aAAU,KAAK,QACb,aACA,SAAS,WAAW,UAAU,UAAU,GAAG,uBAC5C;AAOD,eACE,SAAS,cAAc,SACnB,KAAK,QAAQ,aAAa,QAAQ,UAAU,GAC5C;AACN,kBAAe,SAAS,gBAAgB,CACtC,KAAK,KAAK,QAAQ,KAAK,EAAE,UAAU,UAAU,CAC9C;;EAGH,aAAa;AAGX,OAAI,QAAQ,IAAI,aAAa,aAC3B,QAAO,YAAY,WAAW;AAShC,GAAK,YAAY,eAAe;AAChC,sBAAmB;;EAGrB,gBAAgB,QAAQ;AACtB,UAAO,QAAQ,IAAI,aAAa;AAEhC,UAAO,QAAQ,GAAG,WAAW,gBAAgB;AAK3C,QAJsB,aAAa,MAAM,WACvC,YAAY,WAAW,OAAO,CAC/B,KAIE,YAAY,SAAS,OAAO,IAI3B,KAAK,SAAS,YAAY,KAAK,sBACjC;AAQA,KAAK,YAAY,eAAe;AAChC,wBAAmB;;KAErB;AAIF,UAAO,YAAY,KAAK,eAAe;AACrC,qBAAiB,OAAO;KACxB;;EAEL"}
|
|
@@ -6,7 +6,7 @@ AppKit can automatically generate TypeScript types for your SQL queries, providi
|
|
|
6
6
|
|
|
7
7
|
Generate type-safe TypeScript declarations for query keys, parameters, and result rows.
|
|
8
8
|
|
|
9
|
-
All generated files live in `shared/appkit-types/`, one per
|
|
9
|
+
All generated files live in `shared/appkit-types/`, one per concern: `analytics.d.ts` (SQL query types), `serving.d.ts` (model-serving endpoint types), and `metric-views.d.ts`. A single command (and the Vite plugin) produces them all in one pass; see [Metric-view types](#metric-view-types). The `.d.ts` files use [`declare module`](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to augment existing interfaces, so the types apply globally — you never need to import them. TypeScript auto-discovers them through `"include": ["shared/appkit-types"]` in your tsconfig.
|
|
10
10
|
|
|
11
11
|
## Vite plugin: `appKitTypesPlugin`[](#vite-plugin-appkittypesplugin "Direct link to vite-plugin-appkittypesplugin")
|
|
12
12
|
|
|
@@ -86,6 +86,32 @@ npx @databricks/appkit generate-types --wait
|
|
|
86
86
|
|
|
87
87
|
In blocking mode the generator starts a stopped warehouse, waits (bounded) for it to reach `RUNNING`, and then describes your queries. It fails only when the configured warehouse no longer exists (deleted/deleting), so a transient outage or a cold warehouse degrades gracefully rather than breaking the build. The app template wires this up for you: `postinstall` and `predev` run the non-blocking default, while `prebuild` runs `--wait`.
|
|
88
88
|
|
|
89
|
+
## Metric-view types[](#metric-view-types "Direct link to Metric-view types")
|
|
90
|
+
|
|
91
|
+
`generate-types` (and the Vite plugin) emit metric-view types **additively** — there is no separate command. When a `config/queries/metric-views.json` file is present, the same run that generates your query types also DESCRIBEs each declared [UC Metric View](./docs/plugins/analytics.md) and writes `metric-views.d.ts` into `shared/appkit-types/`:
|
|
92
|
+
|
|
93
|
+
* `metric-views.d.ts` — augments the `MetricRegistry` interface so `useMetricView('<key>', …)` is autocompleted and type-checked. Each view's measures, dimensions, and their semantic metadata (SQL type, display name, format, time grains) are encoded at the type level.
|
|
94
|
+
|
|
95
|
+
If `metric-views.json` is absent the metric path stays dormant (nothing is emitted). When present it follows the **same** warehouse-readiness contract as query types: in the default non-blocking run a view that can't be described yet — a cold warehouse, or a bad/unreachable source — is written with permissive types and a warning, while under `--wait` that same situation fails the build so CI never ships incomplete metric types. A malformed `metric-views.json` (invalid JSON, or a source that isn't a three-part UC FQN) fails fast in every mode.
|
|
96
|
+
|
|
97
|
+
`metric-views.json` is keyed by metric key; each entry names the three-part UC FQN of the view and, optionally, the executor it runs as (`app_service_principal`, the default, or `user`):
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"$schema": "https://databricks.github.io/appkit/schemas/metric-source.schema.json",
|
|
102
|
+
"metricViews": {
|
|
103
|
+
"revenue": { "source": "catalog.schema.revenue_metrics" },
|
|
104
|
+
"customers": {
|
|
105
|
+
"source": "catalog.schema.customer_metrics",
|
|
106
|
+
"executor": "user"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The optional `$schema` line enables editor autocomplete and validation against the published schema.
|
|
114
|
+
|
|
89
115
|
## How it works[](#how-it-works "Direct link to How it works")
|
|
90
116
|
|
|
91
117
|
The type generator:
|
|
@@ -116,7 +142,7 @@ const { data } = useAnalyticsQuery("users_list", {
|
|
|
116
142
|
});
|
|
117
143
|
|
|
118
144
|
// TypeScript knows the shape of the result rows
|
|
119
|
-
data?.forEach(row => {
|
|
145
|
+
data?.forEach((row) => {
|
|
120
146
|
console.log(row.email); // ✓ autocomplete works
|
|
121
147
|
});
|
|
122
148
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@databricks/appkit",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.43.1",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"bin": {
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@types/semver": "7.7.1",
|
|
68
68
|
"apache-arrow": "21.1.0",
|
|
69
69
|
"dotenv": "16.6.1",
|
|
70
|
+
"drizzle-orm": "0.45.1",
|
|
70
71
|
"express": "4.22.2",
|
|
71
72
|
"get-port": "7.2.0",
|
|
72
73
|
"js-yaml": "4.2.0",
|