@databricks/appkit 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/index.d.ts +49 -2
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +87 -10
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cli/commands/generate-types.js +9 -4
- package/dist/cli/commands/generate-types.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts +16 -0
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js +162 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/metric.js +7 -0
- package/dist/plugins/analytics/mv/cache.js +51 -0
- package/dist/plugins/analytics/mv/cache.js.map +1 -0
- package/dist/plugins/analytics/mv/constants.js +72 -0
- package/dist/plugins/analytics/mv/constants.js.map +1 -0
- package/dist/plugins/analytics/mv/formatters.js +151 -0
- package/dist/plugins/analytics/mv/formatters.js.map +1 -0
- package/dist/plugins/analytics/mv/index.js +6 -0
- package/dist/plugins/analytics/mv/registry.js +55 -0
- package/dist/plugins/analytics/mv/registry.js.map +1 -0
- package/dist/plugins/analytics/mv/schemas.js +178 -0
- package/dist/plugins/analytics/mv/schemas.js.map +1 -0
- package/dist/plugins/analytics/types.js.map +1 -1
- package/dist/schemas/metric-fqn.js +14 -0
- package/dist/schemas/metric-fqn.js.map +1 -0
- package/dist/shared/src/schemas/metric-fqn.js +78 -46
- package/dist/shared/src/schemas/metric-fqn.js.map +1 -1
- package/dist/shared/src/schemas/metric-source.js +90 -0
- package/dist/shared/src/schemas/metric-source.js.map +1 -0
- package/dist/type-generator/index.js +14 -7
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/mv-registry/config.js +13 -31
- package/dist/type-generator/mv-registry/config.js.map +1 -1
- package/dist/type-generator/mv-registry/describe.js +1 -31
- package/dist/type-generator/mv-registry/describe.js.map +1 -1
- package/dist/type-generator/mv-registry/sync.js +1 -1
- package/dist/type-generator/mv-registry/sync.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts +5 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +21 -4
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/docs/development/type-generation.md +3 -3
- package/docs/plugins/analytics.md +172 -23
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { MAX_UC_OBJECT_NAME_LENGTH, UC_FQN_PATTERN } from "./metric-fqn.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region ../shared/src/schemas/metric-source.ts
|
|
5
|
+
/**
|
|
6
|
+
* AppKit metric-source schema.
|
|
7
|
+
*
|
|
8
|
+
* Single source of truth for `config/metric-views/definitions.json`
|
|
9
|
+
* the config that activates the Analytics' metric-view path.
|
|
10
|
+
*
|
|
11
|
+
* `definitions.json` declares UC Metric Views under a single `metricViews` map.
|
|
12
|
+
* Each entry binds a metric key to a UC metric view FQN plus the executor
|
|
13
|
+
* the query runs as:
|
|
14
|
+
* - `executor: "app_service_principal"` (default) — queried as the app service
|
|
15
|
+
* principal (cache scope shared across all users).
|
|
16
|
+
* - `executor: "user"` — queried as the requesting user (on-behalf-of;
|
|
17
|
+
* cache scope per-user).
|
|
18
|
+
*
|
|
19
|
+
* A single map (rather than per-executor sections) makes metric keys unique
|
|
20
|
+
* by construction — the same key cannot be declared twice with different
|
|
21
|
+
* executors.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Safety cap on the number of declared metric views — a typo / DoS guard, NOT
|
|
25
|
+
* a Unity Catalog limit. Mirrors the type-generator's `MAX_METRIC_VIEWS` so
|
|
26
|
+
* runtime config validation and type generation accept exactly the same
|
|
27
|
+
* configs (a config that fails generation must not silently pass at runtime).
|
|
28
|
+
*/
|
|
29
|
+
const MAX_METRIC_VIEWS = 200;
|
|
30
|
+
/**
|
|
31
|
+
* Whole-FQN length cap: three max-length UC object names plus the two dots.
|
|
32
|
+
* The per-segment cap ({@link MAX_UC_OBJECT_NAME_LENGTH}) is enforced in the
|
|
33
|
+
* `superRefine` below; this whole-string bound is the declarative half (it
|
|
34
|
+
* serializes to a JSON-schema `maxLength`, whereas the per-segment check — like
|
|
35
|
+
* the entry-count cap — cannot be expressed declaratively and lives in the
|
|
36
|
+
* refinement, so it is a runtime/type-generator gate only).
|
|
37
|
+
*/
|
|
38
|
+
const MAX_FQN_LENGTH = MAX_UC_OBJECT_NAME_LENGTH * 3 + 2;
|
|
39
|
+
/**
|
|
40
|
+
* Three-part Unity Catalog FQN matcher, composed from the single-segment
|
|
41
|
+
* {@link UC_FQN_PATTERN} so the per-segment grammar has exactly one source of
|
|
42
|
+
* truth (shared by the type-generator runtime, which imports the zod-free
|
|
43
|
+
* {@link UC_FQN_PATTERN} directly — see `./metric-fqn.ts`).
|
|
44
|
+
*
|
|
45
|
+
* `UC_FQN_PATTERN` is `^<segment>+$`; stripping its `^`/`$` anchors yields the
|
|
46
|
+
* per-segment sub-pattern, which is joined with literal dots into
|
|
47
|
+
* `^<segment>\.<segment>\.<segment>$`. Exactly three dot-separated segments,
|
|
48
|
+
* each a valid UC object name. Arity (and the per-segment length cap) is also
|
|
49
|
+
* enforced structurally by the type-generator's `resolveMetricConfig`.
|
|
50
|
+
*/
|
|
51
|
+
const UC_FQN_SEGMENT_SOURCE = UC_FQN_PATTERN.source.replace(/^\^/, "").replace(/\$$/, "");
|
|
52
|
+
const UC_THREE_PART_FQN_PATTERN = new RegExp(`^${UC_FQN_SEGMENT_SOURCE}\\.${UC_FQN_SEGMENT_SOURCE}\\.${UC_FQN_SEGMENT_SOURCE}$`);
|
|
53
|
+
const metricKeySchema = z.string().regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/).describe("Metric key. Must be a valid identifier (letters, digits, underscores; cannot start with a digit). Becomes the route key in POST /api/analytics/metric/:key, the hook argument in useMetricView('<key>', ...), and the MetricRegistry augmentation key.");
|
|
54
|
+
const metricExecutorSchema = z.enum(["app_service_principal", "user"]).describe("Who the metric view is queried as. 'app_service_principal' (default) runs as the app service principal with a cache shared across all users; 'user' runs on-behalf-of the requesting user with a per-user cache.");
|
|
55
|
+
/**
|
|
56
|
+
* @note Entries are objects (rather than bare strings) at v1 so future per-entry
|
|
57
|
+
* options (cacheTtl, defaultFilter, allowlists) can ship as additive
|
|
58
|
+
* properties without a breaking change. `executor` is the first such option.
|
|
59
|
+
*/
|
|
60
|
+
const metricEntrySchema = z.object({
|
|
61
|
+
source: z.string().regex(UC_THREE_PART_FQN_PATTERN).max(MAX_FQN_LENGTH).describe("Three-part Unity Catalog FQN of the metric view: <catalog>.<schema>.<metric_view>").meta({ examples: ["appkit_demo.public.revenue_metrics", "main.analytics.customer_metrics"] }),
|
|
62
|
+
executor: metricExecutorSchema.default("app_service_principal")
|
|
63
|
+
}).strict().describe("A single metric view source declaration: the UC FQN to query and the executor to query it as. Future per-entry options (cacheTtl, defaultFilter, allowlists) ship as additive properties.");
|
|
64
|
+
const metricSourceSchema = z.object({
|
|
65
|
+
$schema: z.string().optional().describe("Reference to the JSON Schema for validation"),
|
|
66
|
+
metricViews: z.record(metricKeySchema, metricEntrySchema).optional().describe("Metric view declarations, keyed by metric key. Each entry names the UC metric view to query and the executor it runs as.")
|
|
67
|
+
}).strict().describe("Schema for AppKit config/metric-views/definitions.json — declares Unity Catalog Metric View sources for the analytics plugin's metric-view path. Each entry under 'metricViews' binds a metric key to a UC metric view FQN and an executor ('app_service_principal' shared cache, or 'user' per-user cache). Object form (rather than bare string) at v1 enables future per-entry option growth without breaking changes.").superRefine((value, ctx) => {
|
|
68
|
+
const entries = value.metricViews ? Object.entries(value.metricViews) : [];
|
|
69
|
+
if (entries.length > MAX_METRIC_VIEWS) ctx.addIssue({
|
|
70
|
+
code: "custom",
|
|
71
|
+
message: `too many metric views: ${entries.length} declared, exceeding the maximum of ${MAX_METRIC_VIEWS}`,
|
|
72
|
+
path: ["metricViews"]
|
|
73
|
+
});
|
|
74
|
+
for (const [key, entry] of entries) {
|
|
75
|
+
const segments = entry.source.split(".");
|
|
76
|
+
for (let i = 0; i < segments.length; i++) if (segments[i].length > MAX_UC_OBJECT_NAME_LENGTH) ctx.addIssue({
|
|
77
|
+
code: "custom",
|
|
78
|
+
message: `metric source segment ${i + 1} is ${segments[i].length} characters, exceeding the per-segment maximum of ${MAX_UC_OBJECT_NAME_LENGTH}`,
|
|
79
|
+
path: [
|
|
80
|
+
"metricViews",
|
|
81
|
+
key,
|
|
82
|
+
"source"
|
|
83
|
+
]
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
//#endregion
|
|
89
|
+
export { metricSourceSchema };
|
|
90
|
+
//# sourceMappingURL=metric-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-source.js","names":[],"sources":["../../../../../shared/src/schemas/metric-source.ts"],"sourcesContent":["/**\n * AppKit metric-source schema.\n *\n * Single source of truth for `config/metric-views/definitions.json`\n * the config that activates the Analytics' metric-view path.\n *\n * `definitions.json` declares UC Metric Views under a single `metricViews` map.\n * Each entry binds a metric key to a UC metric view FQN plus the executor\n * the query runs as:\n * - `executor: \"app_service_principal\"` (default) — queried as the app service\n * principal (cache scope shared across all users).\n * - `executor: \"user\"` — queried as the requesting user (on-behalf-of;\n * cache scope per-user).\n *\n * A single map (rather than per-executor sections) makes metric keys unique\n * by construction — the same key cannot be declared twice with different\n * executors.\n */\n\nimport { z } from \"zod\";\nimport { MAX_UC_OBJECT_NAME_LENGTH, UC_FQN_PATTERN } from \"./metric-fqn\";\n\n/**\n * Safety cap on the number of declared metric views — a typo / DoS guard, NOT\n * a Unity Catalog limit. Mirrors the type-generator's `MAX_METRIC_VIEWS` so\n * runtime config validation and type generation accept exactly the same\n * configs (a config that fails generation must not silently pass at runtime).\n */\nconst MAX_METRIC_VIEWS = 200;\n\n/**\n * Whole-FQN length cap: three max-length UC object names plus the two dots.\n * The per-segment cap ({@link MAX_UC_OBJECT_NAME_LENGTH}) is enforced in the\n * `superRefine` below; this whole-string bound is the declarative half (it\n * serializes to a JSON-schema `maxLength`, whereas the per-segment check — like\n * the entry-count cap — cannot be expressed declaratively and lives in the\n * refinement, so it is a runtime/type-generator gate only).\n */\nconst MAX_FQN_LENGTH = MAX_UC_OBJECT_NAME_LENGTH * 3 + 2;\n\n/**\n * Three-part Unity Catalog FQN matcher, composed from the single-segment\n * {@link UC_FQN_PATTERN} so the per-segment grammar has exactly one source of\n * truth (shared by the type-generator runtime, which imports the zod-free\n * {@link UC_FQN_PATTERN} directly — see `./metric-fqn.ts`).\n *\n * `UC_FQN_PATTERN` is `^<segment>+$`; stripping its `^`/`$` anchors yields the\n * per-segment sub-pattern, which is joined with literal dots into\n * `^<segment>\\.<segment>\\.<segment>$`. Exactly three dot-separated segments,\n * each a valid UC object name. Arity (and the per-segment length cap) is also\n * enforced structurally by the type-generator's `resolveMetricConfig`.\n */\nconst UC_FQN_SEGMENT_SOURCE = UC_FQN_PATTERN.source\n .replace(/^\\^/, \"\")\n .replace(/\\$$/, \"\");\nconst UC_THREE_PART_FQN_PATTERN = new RegExp(\n `^${UC_FQN_SEGMENT_SOURCE}\\\\.${UC_FQN_SEGMENT_SOURCE}\\\\.${UC_FQN_SEGMENT_SOURCE}$`,\n);\n\nexport const metricKeySchema = z\n .string()\n .regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/)\n .describe(\n \"Metric key. Must be a valid identifier (letters, digits, underscores; cannot start with a digit). Becomes the route key in POST /api/analytics/metric/:key, the hook argument in useMetricView('<key>', ...), and the MetricRegistry augmentation key.\",\n );\n\nexport const metricExecutorSchema = z\n .enum([\"app_service_principal\", \"user\"])\n .describe(\n \"Who the metric view is queried as. 'app_service_principal' (default) runs as the app service principal with a cache shared across all users; 'user' runs on-behalf-of the requesting user with a per-user cache.\",\n );\n\n/**\n * @note Entries are objects (rather than bare strings) at v1 so future per-entry\n * options (cacheTtl, defaultFilter, allowlists) can ship as additive\n * properties without a breaking change. `executor` is the first such option.\n */\nexport const metricEntrySchema = z\n .object({\n source: z\n .string()\n .regex(UC_THREE_PART_FQN_PATTERN)\n .max(MAX_FQN_LENGTH)\n .describe(\n \"Three-part Unity Catalog FQN of the metric view: <catalog>.<schema>.<metric_view>\",\n )\n .meta({\n examples: [\n \"appkit_demo.public.revenue_metrics\",\n \"main.analytics.customer_metrics\",\n ],\n }),\n executor: metricExecutorSchema.default(\"app_service_principal\"),\n })\n .strict()\n .describe(\n \"A single metric view source declaration: the UC FQN to query and the executor to query it as. Future per-entry options (cacheTtl, defaultFilter, allowlists) ship as additive properties.\",\n );\n\nexport const metricSourceSchema = z\n .object({\n $schema: z\n .string()\n .optional()\n .describe(\"Reference to the JSON Schema for validation\"),\n metricViews: z\n .record(metricKeySchema, metricEntrySchema)\n .optional()\n .describe(\n \"Metric view declarations, keyed by metric key. Each entry names the UC metric view to query and the executor it runs as.\",\n ),\n })\n .strict()\n .describe(\n \"Schema for AppKit config/metric-views/definitions.json — declares Unity Catalog Metric View sources for the analytics plugin's metric-view path. Each entry under 'metricViews' binds a metric key to a UC metric view FQN and an executor ('app_service_principal' shared cache, or 'user' per-user cache). Object form (rather than bare string) at v1 enables future per-entry option growth without breaking changes.\",\n )\n // Caps that cannot be expressed declaratively (zod 4's `z.record` has no\n // `.max`, and a per-dot-segment length bound isn't a whole-string\n // `maxLength`). Enforced here so runtime config validation matches the\n // type-generator's `resolveMetricConfig` exactly — a config that fails type\n // generation must not silently pass at runtime. These refinements are\n // invisible to `z.toJSONSchema`, so the generated JSON schema carries only\n // the declarative `maxLength` on `source`; runtime + type-generator remain\n // the authoritative gates for the entry-count and per-segment caps.\n .superRefine((value, ctx) => {\n const entries = value.metricViews ? Object.entries(value.metricViews) : [];\n\n if (entries.length > MAX_METRIC_VIEWS) {\n ctx.addIssue({\n code: \"custom\",\n message: `too many metric views: ${entries.length} declared, exceeding the maximum of ${MAX_METRIC_VIEWS}`,\n path: [\"metricViews\"],\n });\n }\n\n for (const [key, entry] of entries) {\n const segments = entry.source.split(\".\");\n for (let i = 0; i < segments.length; i++) {\n if (segments[i].length > MAX_UC_OBJECT_NAME_LENGTH) {\n ctx.addIssue({\n code: \"custom\",\n message: `metric source segment ${i + 1} is ${segments[i].length} characters, exceeding the per-segment maximum of ${MAX_UC_OBJECT_NAME_LENGTH}`,\n path: [\"metricViews\", key, \"source\"],\n });\n }\n }\n }\n });\n\nexport type MetricKey = z.infer<typeof metricKeySchema>;\nexport type MetricExecutor = z.infer<typeof metricExecutorSchema>;\nexport type MetricEntry = z.infer<typeof metricEntrySchema>;\nexport type MetricSource = z.infer<typeof metricSourceSchema>;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAM,mBAAmB;;;;;;;;;AAUzB,MAAM,iBAAiB,4BAA4B,IAAI;;;;;;;;;;;;;AAcvD,MAAM,wBAAwB,eAAe,OAC1C,QAAQ,OAAO,GAAG,CAClB,QAAQ,OAAO,GAAG;AACrB,MAAM,4BAA4B,IAAI,OACpC,IAAI,sBAAsB,KAAK,sBAAsB,KAAK,sBAAsB,GACjF;AAED,MAAa,kBAAkB,EAC5B,QAAQ,CACR,MAAM,2BAA2B,CACjC,SACC,yPACD;AAEH,MAAa,uBAAuB,EACjC,KAAK,CAAC,yBAAyB,OAAO,CAAC,CACvC,SACC,mNACD;;;;;;AAOH,MAAa,oBAAoB,EAC9B,OAAO;CACN,QAAQ,EACL,QAAQ,CACR,MAAM,0BAA0B,CAChC,IAAI,eAAe,CACnB,SACC,oFACD,CACA,KAAK,EACJ,UAAU,CACR,sCACA,kCACD,EACF,CAAC;CACJ,UAAU,qBAAqB,QAAQ,wBAAwB;CAChE,CAAC,CACD,QAAQ,CACR,SACC,4LACD;AAEH,MAAa,qBAAqB,EAC/B,OAAO;CACN,SAAS,EACN,QAAQ,CACR,UAAU,CACV,SAAS,8CAA8C;CAC1D,aAAa,EACV,OAAO,iBAAiB,kBAAkB,CAC1C,UAAU,CACV,SACC,2HACD;CACJ,CAAC,CACD,QAAQ,CACR,SACC,4ZACD,CASA,aAAa,OAAO,QAAQ;CAC3B,MAAM,UAAU,MAAM,cAAc,OAAO,QAAQ,MAAM,YAAY,GAAG,EAAE;AAE1E,KAAI,QAAQ,SAAS,iBACnB,KAAI,SAAS;EACX,MAAM;EACN,SAAS,0BAA0B,QAAQ,OAAO,sCAAsC;EACxF,MAAM,CAAC,cAAc;EACtB,CAAC;AAGJ,MAAK,MAAM,CAAC,KAAK,UAAU,SAAS;EAClC,MAAM,WAAW,MAAM,OAAO,MAAM,IAAI;AACxC,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,IACnC,KAAI,SAAS,GAAG,SAAS,0BACvB,KAAI,SAAS;GACX,MAAM;GACN,SAAS,yBAAyB,IAAI,EAAE,MAAM,SAAS,GAAG,OAAO,oDAAoD;GACrH,MAAM;IAAC;IAAe;IAAK;IAAS;GACrC,CAAC;;EAIR"}
|
|
@@ -179,9 +179,15 @@ async function probeWarehouseState(getClient, warehouseId) {
|
|
|
179
179
|
* only when the warehouse is already RUNNING, otherwise emits permissive
|
|
180
180
|
* degraded types immediately. `"blocking"` waits for / starts the warehouse
|
|
181
181
|
* first, failing the build only for a deleted/deleting one.
|
|
182
|
+
* @param options.metricViewsFolder - folder that holds `definitions.json`
|
|
183
|
+
* (`<root>/config/metric-views`). Optional and independent of `queryFolder`:
|
|
184
|
+
* metric-view types generate whenever this folder holds a config, even if the
|
|
185
|
+
* app has no `config/queries`. When omitted it defaults to a sibling
|
|
186
|
+
* `metric-views` directory of `queryFolder` (so query-only callers keep
|
|
187
|
+
* working); when neither is given, the metric path is skipped.
|
|
182
188
|
* @param options.mvOutFile - optional output file for the MetricRegistry
|
|
183
189
|
* augmentation. Defaults to a sibling `metric-views.d.ts` file under the same
|
|
184
|
-
* directory as `outFile`. Skipped entirely if `
|
|
190
|
+
* directory as `outFile`. Skipped entirely if `definitions.json` is absent.
|
|
185
191
|
* @param options.metricFetcher - optional DescribeFetcher used by
|
|
186
192
|
* {@link syncMetrics} (tests inject a mock; production lazily builds a
|
|
187
193
|
* default WorkspaceClient-backed one). An injected fetcher always runs: it
|
|
@@ -190,6 +196,7 @@ async function probeWarehouseState(getClient, warehouseId) {
|
|
|
190
196
|
*/
|
|
191
197
|
async function generateFromEntryPoint(options) {
|
|
192
198
|
const { outFile, queryFolder, warehouseId, noCache, mode = "non-blocking", mvOutFile, metricFetcher } = options;
|
|
199
|
+
const metricViewsFolder = options.metricViewsFolder ?? (queryFolder ? path.resolve(queryFolder, "..", "metric-views") : void 0);
|
|
193
200
|
const projectRoot = resolveProjectRoot(outFile);
|
|
194
201
|
logger.debug("Starting type generation...");
|
|
195
202
|
let queryRegistry = [];
|
|
@@ -207,12 +214,12 @@ async function generateFromEntryPoint(options) {
|
|
|
207
214
|
const typeDeclarations = generateTypeDeclarations(queryRegistry);
|
|
208
215
|
await fs.mkdir(path.dirname(outFile), { recursive: true });
|
|
209
216
|
await fs.writeFile(outFile, typeDeclarations, "utf-8");
|
|
210
|
-
if (
|
|
217
|
+
if (metricViewsFolder) {
|
|
211
218
|
const mvFile = mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);
|
|
212
219
|
let mvResult;
|
|
213
220
|
try {
|
|
214
221
|
mvResult = await syncMetricViewsTypes({
|
|
215
|
-
|
|
222
|
+
metricViewsFolder,
|
|
216
223
|
warehouseId,
|
|
217
224
|
metricOutFile: mvFile,
|
|
218
225
|
cache: !noCache,
|
|
@@ -221,7 +228,7 @@ async function generateFromEntryPoint(options) {
|
|
|
221
228
|
});
|
|
222
229
|
} catch (configError) {
|
|
223
230
|
throw new TypegenFatalError([{
|
|
224
|
-
name: "metric-views.json",
|
|
231
|
+
name: "config/metric-views/definitions.json",
|
|
225
232
|
message: getErrorDiagnostic(configError)
|
|
226
233
|
}], warehouseId);
|
|
227
234
|
}
|
|
@@ -244,7 +251,7 @@ async function generateFromEntryPoint(options) {
|
|
|
244
251
|
* `"describe-now"` mode for a focused, always-converge metric refresh.
|
|
245
252
|
*
|
|
246
253
|
*
|
|
247
|
-
* @param options.
|
|
254
|
+
* @param options.metricViewsFolder - folder that holds `definitions.json` (`<root>/config/metric-views`).
|
|
248
255
|
* @param options.warehouseId - SQL warehouse used for `DESCRIBE TABLE EXTENDED`.
|
|
249
256
|
* @param options.metricOutFile - output path for the MetricRegistry `.d.ts`.
|
|
250
257
|
* @param options.cache - cache toggle, default ON. Only `cache === false` disables it (so `undefined`/`true` keep caching).
|
|
@@ -252,9 +259,9 @@ async function generateFromEntryPoint(options) {
|
|
|
252
259
|
* @param options.mode - preflight/gate policy, default `"describe-now"`.
|
|
253
260
|
*/
|
|
254
261
|
async function syncMetricViewsTypes(options) {
|
|
255
|
-
const {
|
|
262
|
+
const { metricViewsFolder, warehouseId, metricOutFile, cache: cacheEnabled, metricFetcher, mode = "describe-now" } = options;
|
|
256
263
|
const noCache = cacheEnabled === false;
|
|
257
|
-
const mvConfig = await readMetricConfig(
|
|
264
|
+
const mvConfig = await readMetricConfig(metricViewsFolder);
|
|
258
265
|
if (!mvConfig) return {
|
|
259
266
|
schemas: [],
|
|
260
267
|
failures: [],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport dotenv from \"dotenv\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n isRevivableMetricCacheEntry,\n loadCache,\n type MetricCacheEntry,\n metricCacheHash,\n saveCache,\n} from \"./cache\";\nimport { getErrorDiagnostic, isConnectivityError } from \"./errors\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"./migration\";\nimport { readMetricConfig, resolveMetricConfig } from \"./mv-registry/config\";\nimport { createWorkspaceDescribeFetcher } from \"./mv-registry/describe\";\nimport { generateMetricTypeDeclarations } from \"./mv-registry/render-types\";\nimport { emptyMetricSchema, syncMetrics } from \"./mv-registry/sync\";\nimport type {\n DescribeFetcher,\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n} from \"./mv-registry/types\";\nimport { decidePreflight, type PreflightMode } from \"./preflight\";\nimport { generateQueriesFromDescribe } from \"./query-registry\";\nimport { generateServingTypes as generateServingTypesImpl } from \"./serving/generator\";\nimport type { QueryFatalError, QuerySchema, QuerySyntaxError } from \"./types\";\nimport {\n getWarehouseState,\n startWarehouse,\n type WarehouseState,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\ndotenv.config();\n\nconst logger = createLogger(\"type-generator\");\n\n/**\n * Upper bound (~5 min) on how long the Metric Views path's `blocking`-mode preflight\n * waits for a warehouse to reach RUNNING. Mirrors the query path's (unexported)\n * `PREFLIGHT_WAIT_MAX_MS` in query-registry.ts.\n */\nconst MV_PREFLIGHT_WAIT_MAX_MS = 300_000;\n\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\n}\n\nfunction formatFailureRows(\n label: string,\n queries: TypegenFailure[],\n color: (value: string) => string,\n) {\n if (queries.length === 0) return [];\n\n // Group by message so a shared failure — e.g. a warehouse-level fatal that\n // hits every query identically — prints once instead of repeating per row.\n const byMessage = new Map<string, string[]>();\n for (const { name, message } of queries) {\n const names = byMessage.get(message);\n if (names) names.push(name);\n else byMessage.set(message, [name]);\n }\n\n const maxNameLen = Math.max(...queries.map((query) => query.name.length));\n const tag = color(label.padEnd(7));\n const rows: string[] = [];\n for (const [message, names] of byMessage) {\n if (names.length === 1) {\n rows.push(\n ` ${tag} ${pc.bold(names[0].padEnd(maxNameLen))} ${pc.dim(message)}`,\n );\n continue;\n }\n // Shared message → print it once, then list the affected query names.\n rows.push(\n ` ${tag} ${pc.dim(message)} ${pc.dim(`(${names.length} ${plural(names.length, \"query\", \"queries\")})`)}`,\n );\n rows.push(\n ` ${names.map((name) => pc.bold(name)).join(pc.dim(\", \"))}`,\n );\n }\n return rows;\n}\n\nfunction formatTypegenFailureMessage(options: {\n syntaxErrors: QuerySyntaxError[];\n fatalErrors?: QueryFatalError[];\n warehouseId?: string;\n title: string;\n causes: string[];\n nextStep: string;\n}) {\n const { syntaxErrors, fatalErrors = [], warehouseId, title } = options;\n const total = syntaxErrors.length + fatalErrors.length;\n const separator = pc.dim(\"─\".repeat(60));\n const warehouse = warehouseId\n ? ` against ${pc.dim(`warehouse ${warehouseId}`)}`\n : \"\";\n\n return [\n ` ${pc.bold(pc.red(\"Type generation failed\"))}`,\n ` ${separator}`,\n ` ${title}: ${total} ${plural(total, \"query\", \"queries\")} could not be described${warehouse}.`,\n ` AppKit wrote generated types with ${pc.bold(\"result: unknown\")} for the failed ${plural(total, \"query\", \"queries\")}.`,\n \"\",\n ...formatFailureRows(\"SQL ERR\", syntaxErrors, pc.red),\n ...(syntaxErrors.length > 0 && fatalErrors.length > 0 ? [\"\"] : []),\n ...formatFailureRows(\"FATAL\", fatalErrors, pc.red),\n \"\",\n ` ${pc.bold(\"Common causes\")}`,\n ...options.causes.map((cause) => ` - ${cause}`),\n \"\",\n ` ${pc.bold(\"Next step\")}`,\n ` ${options.nextStep}`,\n ].join(\"\\n\");\n}\n\n/**\n * Thrown when one or more queries fail `DESCRIBE QUERY` against a *reachable*\n * warehouse — i.e. genuine SQL errors (bad table, syntax, incompatible type),\n * as opposed to a connectivity failure (warehouse unreachable), which degrades\n * silently. Whether this is fatal is the caller's decision: the Vite plugin and\n * CLI fail the build in production and warn-only in development.\n */\nexport class TypegenSyntaxError extends Error {\n readonly queries: QuerySyntaxError[];\n readonly fatalQueries: QueryFatalError[];\n\n constructor(\n queries: QuerySyntaxError[],\n warehouseId?: string,\n fatalQueries: QueryFatalError[] = [],\n ) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: queries,\n fatalErrors: fatalQueries,\n warehouseId,\n title: \"DESCRIBE QUERY failed\",\n causes: [\n \"SQL syntax errors\",\n \"missing tables or views\",\n \"warehouse format incompatibilities\",\n ],\n nextStep: warehouseId\n ? `Run each SQL ERR query directly in a Databricks SQL editor against warehouse ${pc.bold(warehouseId)}.`\n : \"Run each SQL ERR query directly in a Databricks SQL editor.\",\n }),\n );\n this.name = \"TypegenSyntaxError\";\n this.queries = queries;\n this.fatalQueries = fatalQueries;\n }\n}\n\n/**\n * Thrown when DESCRIBE QUERY could not be requested because of a non-SQL fatal\n * setup/request problem, such as missing permissions, invalid warehouse IDs, or\n * malformed SDK configuration. Like TypegenSyntaxError, this is thrown only\n * after the declaration file has been written with `result: unknown` entries.\n */\nexport class TypegenFatalError extends Error {\n readonly queries: QueryFatalError[];\n\n constructor(queries: QueryFatalError[], warehouseId?: string) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: [],\n fatalErrors: queries,\n warehouseId,\n title: \"DESCRIBE QUERY could not be requested\",\n causes: [\n \"missing warehouse permissions\",\n \"invalid warehouse ID\",\n \"authentication failure\",\n \"SDK configuration errors\",\n ],\n nextStep: warehouseId\n ? `Verify access to warehouse ${pc.bold(warehouseId)} and rerun type generation.`\n : \"Verify warehouse access and rerun type generation.\",\n }),\n );\n this.name = \"TypegenFatalError\";\n this.queries = queries;\n }\n}\n\n/**\n * Generate type declarations for QueryRegistry\n * Create the d.ts file from the plugin routes and query schemas\n * @param querySchemas - the list of query schemas\n * @returns - the type declarations as a string\n */\nfunction generateTypeDeclarations(querySchemas: QuerySchema[] = []): string {\n const queryEntries = querySchemas\n .map(({ name, type }) => {\n const indentedType = type\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : ` ${line}`))\n .join(\"\\n\");\n return ` ${name}: ${indentedType}`;\n })\n .join(\";\\n\");\n\n const querySection = queryEntries ? `\\n${queryEntries};\\n ` : \"\";\n\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\nimport type { SQLTypeMarker, SQLStringMarker, SQLNumberMarker, SQLBooleanMarker, SQLBinaryMarker, SQLDateMarker, SQLTimestampMarker } from \"@databricks/appkit-ui/js\";\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface QueryRegistry {${querySection}}\n}\n`;\n}\n\n/**\n * Status-only probe for the metric-view gate in {@link generateFromEntryPoint}\n *\n * Uses {@link getWarehouseState} (`warehouses.get`) —\n * a read-only GET that can never start the warehouse\n *\n * Takes the lazy client *getter* so the probe also absorbs client construction failure.\n * A connectivity blip returns `undefined`, which the gate reads as transient not-running;\n * a deterministic failure (auth, bad id) is re-thrown so the gate can classify it\n * fatal rather than silently degrading.\n */\nasync function probeWarehouseState(\n getClient: () => WorkspaceClient,\n warehouseId: string,\n): Promise<WarehouseState | undefined> {\n try {\n return await getWarehouseState(getClient(), warehouseId);\n } catch (err) {\n // Connectivity blip → undefined (gate degrades, retries next pass). A\n // deterministic failure (auth, bad warehouse id, client construction) must\n // not masquerade as not-running — re-throw so the gate pins it fatal, the\n // same split the query path's preflight makes.\n if (isConnectivityError(err)) return undefined;\n throw err;\n }\n}\n\n/**\n * Entry point for generating type declarations from all imported files\n * @param options - the options for the generation\n * @param options.entryPoint - the entry point file\n * @param options.outFile - the output file\n * @param options.noCache - skip the typegen cache entirely: every query is\n * re-described, and the metric path ignores its cached schemas (every\n * configured key becomes describe-needed) and overwrites the cache's\n * `metrics` section with this pass's results.\n * @param options.mode - preflight policy (see {@link PreflightMode}), default\n * `\"non-blocking\"`. For queries, `\"non-blocking\"` never touches the\n * warehouse. For metric views it makes one status-only probe and DESCRIBEs\n * only when the warehouse is already RUNNING, otherwise emits permissive\n * degraded types immediately. `\"blocking\"` waits for / starts the warehouse\n * first, failing the build only for a deleted/deleting one.\n * @param options.mvOutFile - optional output file for the MetricRegistry\n * augmentation. Defaults to a sibling `metric-views.d.ts` file under the same\n * directory as `outFile`. Skipped entirely if `metric-views.json` is absent.\n * @param options.metricFetcher - optional DescribeFetcher used by\n * {@link syncMetrics} (tests inject a mock; production lazily builds a\n * default WorkspaceClient-backed one). An injected fetcher always runs: it\n * hits no warehouse, so it bypasses both the non-blocking gate and the\n * blocking preflight.\n */\nexport async function generateFromEntryPoint(options: {\n outFile: string;\n queryFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n mode?: PreflightMode;\n mvOutFile?: string;\n metricFetcher?: DescribeFetcher;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n mvOutFile,\n metricFetcher,\n } = options;\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n let fatalErrors: QueryFatalError[] = [];\n if (queryFolder) {\n const result = await generateQueriesFromDescribe(queryFolder, warehouseId, {\n noCache,\n mode,\n });\n queryRegistry = result.schemas;\n syntaxErrors = result.syntaxErrors ?? [];\n fatalErrors = result.fatalErrors ?? [];\n }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n\n // Metric-view types: only emit when metric-views.json exists.\n if (queryFolder) {\n const mvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n\n let mvResult: SyncMetricViewsTypesResult;\n try {\n mvResult = await syncMetricViewsTypes({\n queryFolder,\n warehouseId,\n metricOutFile: mvFile,\n cache: !noCache,\n metricFetcher,\n mode,\n });\n } catch (configError) {\n // syncMetricViewsTypes only throws for a malformed metric-views.json — re-throw as a message-only TypegenFatalError.\n throw new TypegenFatalError(\n [\n {\n name: \"metric-views.json\",\n message: getErrorDiagnostic(configError),\n },\n ],\n warehouseId,\n );\n }\n\n // Deleted/deleting-warehouse fatal preflight (blocking mode only);\n // empty (no-op) when metric-views.json is absent or in non-blocking mode.\n for (const fe of mvResult.fatalErrors) {\n fatalErrors.push(fe);\n }\n\n // Blocking (`--wait` / prod Vite) escalates per-key DESCRIBE failures — a bad or unreachable source, i.e. a config error\n // to build failures so the end-of-run throw fails after the writes.\n if (mode === \"blocking\") {\n for (const failure of mvResult.failures) {\n fatalErrors.push({\n name: failure.key,\n message: `metric view ${failure.key} (${failure.source}) could not be described: ${failure.reason}`,\n });\n }\n }\n }\n\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Types are always written above — including `result: unknown` for any Metric View that could not be described.\n if (syntaxErrors.length > 0) {\n throw new TypegenSyntaxError(syntaxErrors, warehouseId, fatalErrors);\n }\n if (fatalErrors.length > 0) {\n throw new TypegenFatalError(fatalErrors, warehouseId);\n }\n\n logger.debug(\"Type generation complete!\");\n}\n\n/**\n * Result of a {@link syncMetricViewsTypes} run, returned to the caller (the CLI\n * directly, or {@link generateFromEntryPoint} which delegates to it) so it can\n * report what happened and decide its exit code.\n */\nexport interface SyncMetricViewsTypesResult {\n metricOutFile?: string;\n schemas: MetricSchema[];\n failures: MetricSyncFailure[];\n /**\n * `true` when no `metric-views.json` was found in the query folder, so nothing\n * was synced.\n */\n noConfig: boolean;\n /**\n * Per-key fatal preflight errors (empty except in the `blocking`-mode\n * deleted/deleting-warehouse and deterministic-preflight-failure cases). The\n * artifacts are still written; {@link generateFromEntryPoint} surfaces these\n * by throwing {@link TypegenFatalError} after the writes. A `\"describe-now\"`\n * run sets no blocking preflight, so for that mode this is always empty.\n */\n fatalErrors: Array<{ name: string; message: string }>;\n}\n\n/**\n * Unified metric-view type-generation pipeline behind {@link\n * generateFromEntryPoint}'s metric section (which forwards its\n * `\"non-blocking\"`/`\"blocking\"` mode). Also directly callable with the default\n * `\"describe-now\"` mode for a focused, always-converge metric refresh.\n *\n *\n * @param options.queryFolder - folder that holds `metric-views.json` (`<root>/config/queries`).\n * @param options.warehouseId - SQL warehouse used for `DESCRIBE TABLE EXTENDED`.\n * @param options.metricOutFile - output path for the MetricRegistry `.d.ts`.\n * @param options.cache - cache toggle, default ON. Only `cache === false` disables it (so `undefined`/`true` keep caching).\n * @param options.metricFetcher - optional injected {@link DescribeFetcher}\n * @param options.mode - preflight/gate policy, default `\"describe-now\"`.\n */\nexport async function syncMetricViewsTypes(options: {\n queryFolder: string;\n warehouseId: string;\n metricOutFile: string;\n cache?: boolean;\n metricFetcher?: DescribeFetcher;\n mode?: \"describe-now\" | \"non-blocking\" | \"blocking\";\n}): Promise<SyncMetricViewsTypesResult> {\n const {\n queryFolder,\n warehouseId,\n metricOutFile,\n cache: cacheEnabled,\n metricFetcher,\n mode = \"describe-now\",\n } = options;\n\n // Only `cache === false` disables caching; `undefined`/`true` keep it on.\n const noCache = cacheEnabled === false;\n\n const mvConfig = await readMetricConfig(queryFolder);\n if (!mvConfig) {\n // No metric-views.json — additive path stays dormant. The CLI turns this\n // into a friendly \"nothing to sync\" message and exits 0;\n // generateFromEntryPoint simply ignores `noConfig`.\n return { schemas: [], failures: [], fatalErrors: [], noConfig: true };\n }\n\n const resolution = resolveMetricConfig(mvConfig);\n\n const fatalErrors: Array<{ name: string; message: string }> = [];\n\n // Load the shared typegen cache and copy its `metrics` section into a null-prototype map.\n const cache = await loadCache();\n const mvCacheSection: Record<string, MetricCacheEntry> = Object.create(null);\n if (!noCache && cache.metrics) {\n for (const key of Object.keys(cache.metrics)) {\n mvCacheSection[key] = cache.metrics[key];\n }\n }\n\n // Partition BEFORE any gate/preflight decision: a hit (a structurally valid,\n // hash-matching, NON-degraded cached entry) is served from cache no matter\n // what the warehouse is doing. The cache only ever holds successful describes\n // (a degraded outcome is never persisted — see the write block below), so the\n // `degraded !== true` guard is normally moot; it also defends against a stale\n // degraded entry left by an older writer, which re-describes instead of\n // serving. Everything else (new, edited, unrevivable, or degraded) is eligible\n // for DESCRIBE, so a fully-warm pass makes zero warehouse calls and constructs\n // zero clients. Mirrors the query path: only a good result is cache-servable.\n const hitSchemas = new Map<string, MetricSchema>();\n const describeNeeded: typeof resolution.entries = [];\n for (const entry of resolution.entries) {\n const prior = mvCacheSection[entry.key];\n if (\n prior !== undefined &&\n isRevivableMetricCacheEntry(prior) &&\n prior.hash === metricCacheHash(entry.source, entry.lane) &&\n prior.schema.degraded !== true\n ) {\n hitSchemas.set(entry.key, prior.schema);\n } else {\n describeNeeded.push(entry);\n }\n }\n\n let mvClient: WorkspaceClient | undefined;\n const getMvClient = (): WorkspaceClient => {\n mvClient ??= new WorkspaceClient({});\n return mvClient;\n };\n\n // Blocking-mode preflight: ensure the warehouse is running before the MV DESCRIBE\n // batch (probe → decide → wait / start+wait; only DELETED/DELETING is fatal). Two softenings vs the query preflight: a failed probe and a timed-out wait are NOT fatal here — we fall through to syncMetrics, which classifies a still-not-ready warehouse as degraded rather than failing the build. Skipped for `describe-now`/`non-blocking` (only `mode === \"blocking\"` enters here).\n let preflightFatalMessage: string | undefined;\n if (\n mode === \"blocking\" &&\n metricFetcher === undefined &&\n describeNeeded.length > 0\n ) {\n try {\n const state = await getWarehouseState(getMvClient(), warehouseId);\n const decision = decidePreflight(state, mode);\n if (decision === \"fatal\") {\n preflightFatalMessage = `warehouse ${warehouseId} is ${state}`;\n } else if (decision === \"startWaitProceed\") {\n // treatStoppedAsTransient rides out the stale pre-start STOPPED/STOPPING\n // reading, same as the query preflight.\n await startWarehouse(getMvClient(), warehouseId);\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n treatStoppedAsTransient: true,\n });\n if (settled !== \"RUNNING\") {\n // With treatStoppedAsTransient, a non-RUNNING resolve is exactly\n // DELETED/DELETING — the warehouse was deleted while we waited.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n }\n } else if (decision === \"waitThenProceed\") {\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n });\n if (settled === \"DELETED\" || settled === \"DELETING\") {\n // Deleted mid-wait: fatal.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n }\n }\n } catch (err) {\n // Connectivity blip: fall through to syncMetrics, whose DESCRIBEs degrade\n // a not-ready / unreachable warehouse rather than throwing.\n if (!isConnectivityError(err)) {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n }\n }\n\n let gateState: WarehouseState | undefined;\n let describeNow =\n metricFetcher !== undefined ||\n mode !== \"non-blocking\" ||\n describeNeeded.length === 0;\n if (!describeNow) {\n try {\n gateState = await probeWarehouseState(getMvClient, warehouseId);\n } catch (err) {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n describeNow = gateState === \"RUNNING\";\n }\n\n let described: MetricSchema[];\n let failures: MetricSyncFailure[] = [];\n if (preflightFatalMessage !== undefined) {\n // Fatal preflight (deleted/deleting warehouse): fail like the query path —\n // skip DESCRIBE, emit degraded schemas so both artifacts are still written,\n // and record one fatal error per describe-needed key (cache hits are\n // unaffected). The caller surfaces them after the writes. The degraded\n // schemas are not cached (see the write block), so a later pass re-probes.\n described = describeNeeded.map(emptyMetricSchema);\n for (const entry of describeNeeded) {\n fatalErrors.push({ name: entry.key, message: preflightFatalMessage });\n }\n } else if (describeNeeded.length === 0) {\n // Nothing left to describe — every configured key was a cache hit.\n // syncMetrics would be a no-op (and building its fetcher would construct a\n // client for nothing); artifacts regenerate from cache.\n described = [];\n } else if (describeNow) {\n const fetcher =\n metricFetcher ??\n createWorkspaceDescribeFetcher(getMvClient(), warehouseId);\n ({ schemas: described, failures } = await syncMetrics(\n { entries: describeNeeded },\n fetcher,\n ));\n\n // Surface DESCRIBE failures loudly: a misconfigured metric-views.json would\n // otherwise silently ship an empty entry that the runtime fail-closed gate\n // 503s in production. syncMetrics is log-free; this caller is the single\n // owner of failure logging.\n if (failures.length > 0) {\n for (const f of failures) {\n logger.warn(\n \"metric sync failed for %s (%s): %s\",\n f.key,\n f.source,\n f.reason,\n );\n }\n }\n\n // Degraded-but-not-failed keys: the warehouse answered with a non-terminal\n // state (stopped / cold-starting), so their schemas are unknown.\n const failedKeys = new Set(failures.map((f) => f.key));\n const degradedKeys = described\n .filter((s) => s.degraded && !failedKeys.has(s.key))\n .map((s) => s.key);\n if (degradedKeys.length > 0) {\n logger.info(\n \"Warehouse %s did not return schemas for %d metric view(s) (%s) — wrote degraded metric types (permissive); they will refresh once the warehouse is available.\",\n warehouseId,\n degradedKeys.length,\n degradedKeys.join(\", \"),\n );\n }\n } else {\n // Un-probed DESCRIBEs deliberately skipped, not failures: emit each\n // describe-needed key as a degraded schema so both artifacts exist; cache\n // hits keep serving last-known-good.\n described = describeNeeded.map(emptyMetricSchema);\n logger.info(\n \"Warehouse %s is not running — wrote degraded metric types (permissive) for %d metric view(s) (%s); they will refresh once the warehouse is available.\",\n warehouseId,\n describeNeeded.length,\n describeNeeded.map((e) => e.key).join(\", \"),\n );\n }\n\n // Cache only successful schema results for describe-needed keys; remove stale cache for degraded ones.\n for (let i = 0; i < describeNeeded.length; i++) {\n // syncMetrics return one schema per entry in entry order, so described[i] always belongs to describeNeeded[i].\n const entry = describeNeeded[i];\n if (described[i].degraded === true) {\n delete mvCacheSection[entry.key];\n continue;\n }\n mvCacheSection[entry.key] = {\n hash: metricCacheHash(entry.source, entry.lane),\n schema: described[i],\n // Vestigial, mirrors the query path's only cache write (always false): a\n // persisted entry is by construction a good result, so it never needs a\n // re-describe flag. Kept for on-disk shape compatibility with existing\n // version-3 caches (isRevivableMetricCacheEntry gates on a boolean).\n retry: false,\n };\n }\n\n // Prune entries whose key is no longer configured\n const configuredKeys = new Set(resolution.entries.map((e) => e.key));\n let prunedCount = 0;\n for (const key of Object.keys(mvCacheSection)) {\n if (!configuredKeys.has(key)) {\n delete mvCacheSection[key];\n prunedCount++;\n }\n }\n\n // Save when this pass produced outcomes, bypassed the cache, or pruned.\n if (describeNeeded.length > 0 || noCache || prunedCount > 0) {\n cache.metrics = mvCacheSection;\n await saveCache(cache);\n }\n\n // Merge cached hits with fresh results back into config order.\n const describedByKey = new Map<string, MetricSchema>();\n for (const schema of described) {\n describedByKey.set(schema.key, schema);\n }\n const schemas = resolution.entries.map((entry) => {\n const schema = hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);\n if (schema !== undefined) return schema;\n logger.warn(\n \"no schema resolved for metric key %s — emitting degraded types (should not happen)\",\n entry.key,\n );\n return emptyMetricSchema(entry);\n });\n\n await fs.mkdir(path.dirname(metricOutFile), { recursive: true });\n await fs.writeFile(\n metricOutFile,\n generateMetricTypeDeclarations(schemas),\n \"utf-8\",\n );\n\n logger.debug(\n \"Wrote MetricRegistry augmentation for %d metric(s)%s\",\n schemas.length,\n failures.length > 0 ? ` (${failures.length} failure(s))` : \"\",\n );\n\n return {\n metricOutFile,\n schemas,\n failures,\n fatalErrors,\n noConfig: false,\n };\n}\n\n// Rolldown tree-shaking only preserves \"own exports\" (locally defined) — not re-exports.\n// A local binding ensures the serving vite plugin's import keeps this in the dependency graph,\n// mirroring how generateFromEntryPoint (also defined here) is preserved via the analytics vite plugin.\nexport const generateServingTypes = generateServingTypesImpl;\n\n// Re-export the mv-registry types so consumers (CLI, the type-generator\n// .d.ts shim in `packages/shared`) can pick them up from this entry point —\n// the .d.ts shim documents these as part of the package's public surface.\nexport type {\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n};\n\nexport const TYPES_DIR = \"appkit-types\";\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\nexport const METRIC_TYPES_FILE = \"metric-views.d.ts\";\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["generateServingTypesImpl"],"sources":["../../src/type-generator/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport dotenv from \"dotenv\";\nimport pc from \"picocolors\";\nimport { createLogger } from \"../logging/logger\";\nimport {\n isRevivableMetricCacheEntry,\n loadCache,\n type MetricCacheEntry,\n metricCacheHash,\n saveCache,\n} from \"./cache\";\nimport { getErrorDiagnostic, isConnectivityError } from \"./errors\";\nimport {\n migrateProjectConfig,\n removeOldGeneratedTypes,\n resolveProjectRoot,\n} from \"./migration\";\nimport { readMetricConfig, resolveMetricConfig } from \"./mv-registry/config\";\nimport { createWorkspaceDescribeFetcher } from \"./mv-registry/describe\";\nimport { generateMetricTypeDeclarations } from \"./mv-registry/render-types\";\nimport { emptyMetricSchema, syncMetrics } from \"./mv-registry/sync\";\nimport type {\n DescribeFetcher,\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n} from \"./mv-registry/types\";\nimport { decidePreflight, type PreflightMode } from \"./preflight\";\nimport { generateQueriesFromDescribe } from \"./query-registry\";\nimport { generateServingTypes as generateServingTypesImpl } from \"./serving/generator\";\nimport type { QueryFatalError, QuerySchema, QuerySyntaxError } from \"./types\";\nimport {\n getWarehouseState,\n startWarehouse,\n type WarehouseState,\n waitUntilRunning,\n} from \"./warehouse-status\";\n\ndotenv.config();\n\nconst logger = createLogger(\"type-generator\");\n\n/**\n * Upper bound (~5 min) on how long the Metric Views path's `blocking`-mode preflight\n * waits for a warehouse to reach RUNNING. Mirrors the query path's (unexported)\n * `PREFLIGHT_WAIT_MAX_MS` in query-registry.ts.\n */\nconst MV_PREFLIGHT_WAIT_MAX_MS = 300_000;\n\ntype TypegenFailure = QuerySyntaxError | QueryFatalError;\n\nfunction plural(count: number, singular: string, pluralForm = `${singular}s`) {\n return count === 1 ? singular : pluralForm;\n}\n\nfunction formatFailureRows(\n label: string,\n queries: TypegenFailure[],\n color: (value: string) => string,\n) {\n if (queries.length === 0) return [];\n\n // Group by message so a shared failure — e.g. a warehouse-level fatal that\n // hits every query identically — prints once instead of repeating per row.\n const byMessage = new Map<string, string[]>();\n for (const { name, message } of queries) {\n const names = byMessage.get(message);\n if (names) names.push(name);\n else byMessage.set(message, [name]);\n }\n\n const maxNameLen = Math.max(...queries.map((query) => query.name.length));\n const tag = color(label.padEnd(7));\n const rows: string[] = [];\n for (const [message, names] of byMessage) {\n if (names.length === 1) {\n rows.push(\n ` ${tag} ${pc.bold(names[0].padEnd(maxNameLen))} ${pc.dim(message)}`,\n );\n continue;\n }\n // Shared message → print it once, then list the affected query names.\n rows.push(\n ` ${tag} ${pc.dim(message)} ${pc.dim(`(${names.length} ${plural(names.length, \"query\", \"queries\")})`)}`,\n );\n rows.push(\n ` ${names.map((name) => pc.bold(name)).join(pc.dim(\", \"))}`,\n );\n }\n return rows;\n}\n\nfunction formatTypegenFailureMessage(options: {\n syntaxErrors: QuerySyntaxError[];\n fatalErrors?: QueryFatalError[];\n warehouseId?: string;\n title: string;\n causes: string[];\n nextStep: string;\n}) {\n const { syntaxErrors, fatalErrors = [], warehouseId, title } = options;\n const total = syntaxErrors.length + fatalErrors.length;\n const separator = pc.dim(\"─\".repeat(60));\n const warehouse = warehouseId\n ? ` against ${pc.dim(`warehouse ${warehouseId}`)}`\n : \"\";\n\n return [\n ` ${pc.bold(pc.red(\"Type generation failed\"))}`,\n ` ${separator}`,\n ` ${title}: ${total} ${plural(total, \"query\", \"queries\")} could not be described${warehouse}.`,\n ` AppKit wrote generated types with ${pc.bold(\"result: unknown\")} for the failed ${plural(total, \"query\", \"queries\")}.`,\n \"\",\n ...formatFailureRows(\"SQL ERR\", syntaxErrors, pc.red),\n ...(syntaxErrors.length > 0 && fatalErrors.length > 0 ? [\"\"] : []),\n ...formatFailureRows(\"FATAL\", fatalErrors, pc.red),\n \"\",\n ` ${pc.bold(\"Common causes\")}`,\n ...options.causes.map((cause) => ` - ${cause}`),\n \"\",\n ` ${pc.bold(\"Next step\")}`,\n ` ${options.nextStep}`,\n ].join(\"\\n\");\n}\n\n/**\n * Thrown when one or more queries fail `DESCRIBE QUERY` against a *reachable*\n * warehouse — i.e. genuine SQL errors (bad table, syntax, incompatible type),\n * as opposed to a connectivity failure (warehouse unreachable), which degrades\n * silently. Whether this is fatal is the caller's decision: the Vite plugin and\n * CLI fail the build in production and warn-only in development.\n */\nexport class TypegenSyntaxError extends Error {\n readonly queries: QuerySyntaxError[];\n readonly fatalQueries: QueryFatalError[];\n\n constructor(\n queries: QuerySyntaxError[],\n warehouseId?: string,\n fatalQueries: QueryFatalError[] = [],\n ) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: queries,\n fatalErrors: fatalQueries,\n warehouseId,\n title: \"DESCRIBE QUERY failed\",\n causes: [\n \"SQL syntax errors\",\n \"missing tables or views\",\n \"warehouse format incompatibilities\",\n ],\n nextStep: warehouseId\n ? `Run each SQL ERR query directly in a Databricks SQL editor against warehouse ${pc.bold(warehouseId)}.`\n : \"Run each SQL ERR query directly in a Databricks SQL editor.\",\n }),\n );\n this.name = \"TypegenSyntaxError\";\n this.queries = queries;\n this.fatalQueries = fatalQueries;\n }\n}\n\n/**\n * Thrown when DESCRIBE QUERY could not be requested because of a non-SQL fatal\n * setup/request problem, such as missing permissions, invalid warehouse IDs, or\n * malformed SDK configuration. Like TypegenSyntaxError, this is thrown only\n * after the declaration file has been written with `result: unknown` entries.\n */\nexport class TypegenFatalError extends Error {\n readonly queries: QueryFatalError[];\n\n constructor(queries: QueryFatalError[], warehouseId?: string) {\n super(\n formatTypegenFailureMessage({\n syntaxErrors: [],\n fatalErrors: queries,\n warehouseId,\n title: \"DESCRIBE QUERY could not be requested\",\n causes: [\n \"missing warehouse permissions\",\n \"invalid warehouse ID\",\n \"authentication failure\",\n \"SDK configuration errors\",\n ],\n nextStep: warehouseId\n ? `Verify access to warehouse ${pc.bold(warehouseId)} and rerun type generation.`\n : \"Verify warehouse access and rerun type generation.\",\n }),\n );\n this.name = \"TypegenFatalError\";\n this.queries = queries;\n }\n}\n\n/**\n * Generate type declarations for QueryRegistry\n * Create the d.ts file from the plugin routes and query schemas\n * @param querySchemas - the list of query schemas\n * @returns - the type declarations as a string\n */\nfunction generateTypeDeclarations(querySchemas: QuerySchema[] = []): string {\n const queryEntries = querySchemas\n .map(({ name, type }) => {\n const indentedType = type\n .split(\"\\n\")\n .map((line, i) => (i === 0 ? line : ` ${line}`))\n .join(\"\\n\");\n return ` ${name}: ${indentedType}`;\n })\n .join(\";\\n\");\n\n const querySection = queryEntries ? `\\n${queryEntries};\\n ` : \"\";\n\n return `// Auto-generated by AppKit - DO NOT EDIT\n// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build\nimport \"@databricks/appkit-ui/react\";\nimport type { SQLTypeMarker, SQLStringMarker, SQLNumberMarker, SQLBooleanMarker, SQLBinaryMarker, SQLDateMarker, SQLTimestampMarker } from \"@databricks/appkit-ui/js\";\n\ndeclare module \"@databricks/appkit-ui/react\" {\n interface QueryRegistry {${querySection}}\n}\n`;\n}\n\n/**\n * Status-only probe for the metric-view gate in {@link generateFromEntryPoint}\n *\n * Uses {@link getWarehouseState} (`warehouses.get`) —\n * a read-only GET that can never start the warehouse\n *\n * Takes the lazy client *getter* so the probe also absorbs client construction failure.\n * A connectivity blip returns `undefined`, which the gate reads as transient not-running;\n * a deterministic failure (auth, bad id) is re-thrown so the gate can classify it\n * fatal rather than silently degrading.\n */\nasync function probeWarehouseState(\n getClient: () => WorkspaceClient,\n warehouseId: string,\n): Promise<WarehouseState | undefined> {\n try {\n return await getWarehouseState(getClient(), warehouseId);\n } catch (err) {\n // Connectivity blip → undefined (gate degrades, retries next pass). A\n // deterministic failure (auth, bad warehouse id, client construction) must\n // not masquerade as not-running — re-throw so the gate pins it fatal, the\n // same split the query path's preflight makes.\n if (isConnectivityError(err)) return undefined;\n throw err;\n }\n}\n\n/**\n * Entry point for generating type declarations from all imported files\n * @param options - the options for the generation\n * @param options.entryPoint - the entry point file\n * @param options.outFile - the output file\n * @param options.noCache - skip the typegen cache entirely: every query is\n * re-described, and the metric path ignores its cached schemas (every\n * configured key becomes describe-needed) and overwrites the cache's\n * `metrics` section with this pass's results.\n * @param options.mode - preflight policy (see {@link PreflightMode}), default\n * `\"non-blocking\"`. For queries, `\"non-blocking\"` never touches the\n * warehouse. For metric views it makes one status-only probe and DESCRIBEs\n * only when the warehouse is already RUNNING, otherwise emits permissive\n * degraded types immediately. `\"blocking\"` waits for / starts the warehouse\n * first, failing the build only for a deleted/deleting one.\n * @param options.metricViewsFolder - folder that holds `definitions.json`\n * (`<root>/config/metric-views`). Optional and independent of `queryFolder`:\n * metric-view types generate whenever this folder holds a config, even if the\n * app has no `config/queries`. When omitted it defaults to a sibling\n * `metric-views` directory of `queryFolder` (so query-only callers keep\n * working); when neither is given, the metric path is skipped.\n * @param options.mvOutFile - optional output file for the MetricRegistry\n * augmentation. Defaults to a sibling `metric-views.d.ts` file under the same\n * directory as `outFile`. Skipped entirely if `definitions.json` is absent.\n * @param options.metricFetcher - optional DescribeFetcher used by\n * {@link syncMetrics} (tests inject a mock; production lazily builds a\n * default WorkspaceClient-backed one). An injected fetcher always runs: it\n * hits no warehouse, so it bypasses both the non-blocking gate and the\n * blocking preflight.\n */\nexport async function generateFromEntryPoint(options: {\n outFile: string;\n queryFolder?: string;\n metricViewsFolder?: string;\n warehouseId: string;\n noCache?: boolean;\n mode?: PreflightMode;\n mvOutFile?: string;\n metricFetcher?: DescribeFetcher;\n}) {\n const {\n outFile,\n queryFolder,\n warehouseId,\n noCache,\n mode = \"non-blocking\",\n mvOutFile,\n metricFetcher,\n } = options;\n\n // Metric config lives in `config/metric-views/`, a sibling of the queries\n // folder. Prefer the explicit option; otherwise derive the sibling of\n // `queryFolder` so callers that pass only `queryFolder` keep emitting metric\n // types. Undefined when neither is given → the metric path stays dormant.\n const metricViewsFolder =\n options.metricViewsFolder ??\n (queryFolder ? path.resolve(queryFolder, \"..\", \"metric-views\") : undefined);\n\n const projectRoot = resolveProjectRoot(outFile);\n\n logger.debug(\"Starting type generation...\");\n\n let queryRegistry: QuerySchema[] = [];\n let syntaxErrors: QuerySyntaxError[] = [];\n let fatalErrors: QueryFatalError[] = [];\n if (queryFolder) {\n const result = await generateQueriesFromDescribe(queryFolder, warehouseId, {\n noCache,\n mode,\n });\n queryRegistry = result.schemas;\n syntaxErrors = result.syntaxErrors ?? [];\n fatalErrors = result.fatalErrors ?? [];\n }\n\n const typeDeclarations = generateTypeDeclarations(queryRegistry);\n\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n await fs.writeFile(outFile, typeDeclarations, \"utf-8\");\n\n // Metric-view types: emit whenever a metric-views folder is resolved (gated\n // on the metric config's own dir, NOT the queries folder — an app can declare\n // metric views without any `.sql` queries). `syncMetricViewsTypes` still\n // returns `noConfig` when the folder holds no `definitions.json`.\n if (metricViewsFolder) {\n const mvFile =\n mvOutFile ?? path.join(path.dirname(outFile), METRIC_TYPES_FILE);\n\n let mvResult: SyncMetricViewsTypesResult;\n try {\n mvResult = await syncMetricViewsTypes({\n metricViewsFolder,\n warehouseId,\n metricOutFile: mvFile,\n cache: !noCache,\n metricFetcher,\n mode,\n });\n } catch (configError) {\n // syncMetricViewsTypes only throws for a malformed definitions.json — re-throw as a message-only TypegenFatalError.\n throw new TypegenFatalError(\n [\n {\n name: \"config/metric-views/definitions.json\",\n message: getErrorDiagnostic(configError),\n },\n ],\n warehouseId,\n );\n }\n\n // Deleted/deleting-warehouse fatal preflight (blocking mode only);\n // empty (no-op) when definitions.json is absent or in non-blocking mode.\n for (const fe of mvResult.fatalErrors) {\n fatalErrors.push(fe);\n }\n\n // Blocking (`--wait` / prod Vite) escalates per-key DESCRIBE failures — a bad or unreachable source, i.e. a config error\n // to build failures so the end-of-run throw fails after the writes.\n if (mode === \"blocking\") {\n for (const failure of mvResult.failures) {\n fatalErrors.push({\n name: failure.key,\n message: `metric view ${failure.key} (${failure.source}) could not be described: ${failure.reason}`,\n });\n }\n }\n }\n\n await removeOldGeneratedTypes(projectRoot, \"appKitTypes.d.ts\");\n await migrateProjectConfig(projectRoot);\n\n // Types are always written above — including `result: unknown` for any Metric View that could not be described.\n if (syntaxErrors.length > 0) {\n throw new TypegenSyntaxError(syntaxErrors, warehouseId, fatalErrors);\n }\n if (fatalErrors.length > 0) {\n throw new TypegenFatalError(fatalErrors, warehouseId);\n }\n\n logger.debug(\"Type generation complete!\");\n}\n\n/**\n * Result of a {@link syncMetricViewsTypes} run, returned to the caller (the CLI\n * directly, or {@link generateFromEntryPoint} which delegates to it) so it can\n * report what happened and decide its exit code.\n */\nexport interface SyncMetricViewsTypesResult {\n metricOutFile?: string;\n schemas: MetricSchema[];\n failures: MetricSyncFailure[];\n /**\n * `true` when no `definitions.json` was found in the metric-views folder, so\n * nothing was synced.\n */\n noConfig: boolean;\n /**\n * Per-key fatal preflight errors (empty except in the `blocking`-mode\n * deleted/deleting-warehouse and deterministic-preflight-failure cases). The\n * artifacts are still written; {@link generateFromEntryPoint} surfaces these\n * by throwing {@link TypegenFatalError} after the writes. A `\"describe-now\"`\n * run sets no blocking preflight, so for that mode this is always empty.\n */\n fatalErrors: Array<{ name: string; message: string }>;\n}\n\n/**\n * Unified metric-view type-generation pipeline behind {@link\n * generateFromEntryPoint}'s metric section (which forwards its\n * `\"non-blocking\"`/`\"blocking\"` mode). Also directly callable with the default\n * `\"describe-now\"` mode for a focused, always-converge metric refresh.\n *\n *\n * @param options.metricViewsFolder - folder that holds `definitions.json` (`<root>/config/metric-views`).\n * @param options.warehouseId - SQL warehouse used for `DESCRIBE TABLE EXTENDED`.\n * @param options.metricOutFile - output path for the MetricRegistry `.d.ts`.\n * @param options.cache - cache toggle, default ON. Only `cache === false` disables it (so `undefined`/`true` keep caching).\n * @param options.metricFetcher - optional injected {@link DescribeFetcher}\n * @param options.mode - preflight/gate policy, default `\"describe-now\"`.\n */\nexport async function syncMetricViewsTypes(options: {\n metricViewsFolder: string;\n warehouseId: string;\n metricOutFile: string;\n cache?: boolean;\n metricFetcher?: DescribeFetcher;\n mode?: \"describe-now\" | \"non-blocking\" | \"blocking\";\n}): Promise<SyncMetricViewsTypesResult> {\n const {\n metricViewsFolder,\n warehouseId,\n metricOutFile,\n cache: cacheEnabled,\n metricFetcher,\n mode = \"describe-now\",\n } = options;\n\n // Only `cache === false` disables caching; `undefined`/`true` keep it on.\n const noCache = cacheEnabled === false;\n\n const mvConfig = await readMetricConfig(metricViewsFolder);\n if (!mvConfig) {\n // No definitions.json — additive path stays dormant. The CLI turns this\n // into a friendly \"nothing to sync\" message and exits 0;\n // generateFromEntryPoint simply ignores `noConfig`.\n return { schemas: [], failures: [], fatalErrors: [], noConfig: true };\n }\n\n const resolution = resolveMetricConfig(mvConfig);\n\n const fatalErrors: Array<{ name: string; message: string }> = [];\n\n // Load the shared typegen cache and copy its `metrics` section into a null-prototype map.\n const cache = await loadCache();\n const mvCacheSection: Record<string, MetricCacheEntry> = Object.create(null);\n if (!noCache && cache.metrics) {\n for (const key of Object.keys(cache.metrics)) {\n mvCacheSection[key] = cache.metrics[key];\n }\n }\n\n // Partition BEFORE any gate/preflight decision: a hit (a structurally valid,\n // hash-matching, NON-degraded cached entry) is served from cache no matter\n // what the warehouse is doing. The cache only ever holds successful describes\n // (a degraded outcome is never persisted — see the write block below), so the\n // `degraded !== true` guard is normally moot; it also defends against a stale\n // degraded entry left by an older writer, which re-describes instead of\n // serving. Everything else (new, edited, unrevivable, or degraded) is eligible\n // for DESCRIBE, so a fully-warm pass makes zero warehouse calls and constructs\n // zero clients. Mirrors the query path: only a good result is cache-servable.\n const hitSchemas = new Map<string, MetricSchema>();\n const describeNeeded: typeof resolution.entries = [];\n for (const entry of resolution.entries) {\n const prior = mvCacheSection[entry.key];\n if (\n prior !== undefined &&\n isRevivableMetricCacheEntry(prior) &&\n prior.hash === metricCacheHash(entry.source, entry.lane) &&\n prior.schema.degraded !== true\n ) {\n hitSchemas.set(entry.key, prior.schema);\n } else {\n describeNeeded.push(entry);\n }\n }\n\n let mvClient: WorkspaceClient | undefined;\n const getMvClient = (): WorkspaceClient => {\n mvClient ??= new WorkspaceClient({});\n return mvClient;\n };\n\n // Blocking-mode preflight: ensure the warehouse is running before the MV DESCRIBE\n // batch (probe → decide → wait / start+wait; only DELETED/DELETING is fatal). Two softenings vs the query preflight: a failed probe and a timed-out wait are NOT fatal here — we fall through to syncMetrics, which classifies a still-not-ready warehouse as degraded rather than failing the build. Skipped for `describe-now`/`non-blocking` (only `mode === \"blocking\"` enters here).\n let preflightFatalMessage: string | undefined;\n if (\n mode === \"blocking\" &&\n metricFetcher === undefined &&\n describeNeeded.length > 0\n ) {\n try {\n const state = await getWarehouseState(getMvClient(), warehouseId);\n const decision = decidePreflight(state, mode);\n if (decision === \"fatal\") {\n preflightFatalMessage = `warehouse ${warehouseId} is ${state}`;\n } else if (decision === \"startWaitProceed\") {\n // treatStoppedAsTransient rides out the stale pre-start STOPPED/STOPPING\n // reading, same as the query preflight.\n await startWarehouse(getMvClient(), warehouseId);\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n treatStoppedAsTransient: true,\n });\n if (settled !== \"RUNNING\") {\n // With treatStoppedAsTransient, a non-RUNNING resolve is exactly\n // DELETED/DELETING — the warehouse was deleted while we waited.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n }\n } else if (decision === \"waitThenProceed\") {\n const settled = await waitUntilRunning(getMvClient(), warehouseId, {\n maxMs: MV_PREFLIGHT_WAIT_MAX_MS,\n });\n if (settled === \"DELETED\" || settled === \"DELETING\") {\n // Deleted mid-wait: fatal.\n preflightFatalMessage = `warehouse ${warehouseId} is ${settled}`;\n }\n }\n } catch (err) {\n // Connectivity blip: fall through to syncMetrics, whose DESCRIBEs degrade\n // a not-ready / unreachable warehouse rather than throwing.\n if (!isConnectivityError(err)) {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n }\n }\n\n let gateState: WarehouseState | undefined;\n let describeNow =\n metricFetcher !== undefined ||\n mode !== \"non-blocking\" ||\n describeNeeded.length === 0;\n if (!describeNow) {\n try {\n gateState = await probeWarehouseState(getMvClient, warehouseId);\n } catch (err) {\n preflightFatalMessage = `warehouse ${warehouseId}: ${getErrorDiagnostic(err)}`;\n }\n describeNow = gateState === \"RUNNING\";\n }\n\n let described: MetricSchema[];\n let failures: MetricSyncFailure[] = [];\n if (preflightFatalMessage !== undefined) {\n // Fatal preflight (deleted/deleting warehouse): fail like the query path —\n // skip DESCRIBE, emit degraded schemas so both artifacts are still written,\n // and record one fatal error per describe-needed key (cache hits are\n // unaffected). The caller surfaces them after the writes. The degraded\n // schemas are not cached (see the write block), so a later pass re-probes.\n described = describeNeeded.map(emptyMetricSchema);\n for (const entry of describeNeeded) {\n fatalErrors.push({ name: entry.key, message: preflightFatalMessage });\n }\n } else if (describeNeeded.length === 0) {\n // Nothing left to describe — every configured key was a cache hit.\n // syncMetrics would be a no-op (and building its fetcher would construct a\n // client for nothing); artifacts regenerate from cache.\n described = [];\n } else if (describeNow) {\n const fetcher =\n metricFetcher ??\n createWorkspaceDescribeFetcher(getMvClient(), warehouseId);\n ({ schemas: described, failures } = await syncMetrics(\n { entries: describeNeeded },\n fetcher,\n ));\n\n // Surface DESCRIBE failures loudly: a misconfigured definitions.json would\n // otherwise silently ship an empty entry that the runtime fail-closed gate\n // 503s in production. syncMetrics is log-free; this caller is the single\n // owner of failure logging.\n if (failures.length > 0) {\n for (const f of failures) {\n logger.warn(\n \"metric sync failed for %s (%s): %s\",\n f.key,\n f.source,\n f.reason,\n );\n }\n }\n\n // Degraded-but-not-failed keys: the warehouse answered with a non-terminal\n // state (stopped / cold-starting), so their schemas are unknown.\n const failedKeys = new Set(failures.map((f) => f.key));\n const degradedKeys = described\n .filter((s) => s.degraded && !failedKeys.has(s.key))\n .map((s) => s.key);\n if (degradedKeys.length > 0) {\n logger.info(\n \"Warehouse %s did not return schemas for %d metric view(s) (%s) — wrote degraded metric types (permissive); they will refresh once the warehouse is available.\",\n warehouseId,\n degradedKeys.length,\n degradedKeys.join(\", \"),\n );\n }\n } else {\n // Un-probed DESCRIBEs deliberately skipped, not failures: emit each\n // describe-needed key as a degraded schema so both artifacts exist; cache\n // hits keep serving last-known-good.\n described = describeNeeded.map(emptyMetricSchema);\n logger.info(\n \"Warehouse %s is not running — wrote degraded metric types (permissive) for %d metric view(s) (%s); they will refresh once the warehouse is available.\",\n warehouseId,\n describeNeeded.length,\n describeNeeded.map((e) => e.key).join(\", \"),\n );\n }\n\n // Cache only successful schema results for describe-needed keys; remove stale cache for degraded ones.\n for (let i = 0; i < describeNeeded.length; i++) {\n // syncMetrics return one schema per entry in entry order, so described[i] always belongs to describeNeeded[i].\n const entry = describeNeeded[i];\n if (described[i].degraded === true) {\n delete mvCacheSection[entry.key];\n continue;\n }\n mvCacheSection[entry.key] = {\n hash: metricCacheHash(entry.source, entry.lane),\n schema: described[i],\n // Vestigial, mirrors the query path's only cache write (always false): a\n // persisted entry is by construction a good result, so it never needs a\n // re-describe flag. Kept for on-disk shape compatibility with existing\n // version-3 caches (isRevivableMetricCacheEntry gates on a boolean).\n retry: false,\n };\n }\n\n // Prune entries whose key is no longer configured\n const configuredKeys = new Set(resolution.entries.map((e) => e.key));\n let prunedCount = 0;\n for (const key of Object.keys(mvCacheSection)) {\n if (!configuredKeys.has(key)) {\n delete mvCacheSection[key];\n prunedCount++;\n }\n }\n\n // Save when this pass produced outcomes, bypassed the cache, or pruned.\n if (describeNeeded.length > 0 || noCache || prunedCount > 0) {\n cache.metrics = mvCacheSection;\n await saveCache(cache);\n }\n\n // Merge cached hits with fresh results back into config order.\n const describedByKey = new Map<string, MetricSchema>();\n for (const schema of described) {\n describedByKey.set(schema.key, schema);\n }\n const schemas = resolution.entries.map((entry) => {\n const schema = hitSchemas.get(entry.key) ?? describedByKey.get(entry.key);\n if (schema !== undefined) return schema;\n logger.warn(\n \"no schema resolved for metric key %s — emitting degraded types (should not happen)\",\n entry.key,\n );\n return emptyMetricSchema(entry);\n });\n\n await fs.mkdir(path.dirname(metricOutFile), { recursive: true });\n await fs.writeFile(\n metricOutFile,\n generateMetricTypeDeclarations(schemas),\n \"utf-8\",\n );\n\n logger.debug(\n \"Wrote MetricRegistry augmentation for %d metric(s)%s\",\n schemas.length,\n failures.length > 0 ? ` (${failures.length} failure(s))` : \"\",\n );\n\n return {\n metricOutFile,\n schemas,\n failures,\n fatalErrors,\n noConfig: false,\n };\n}\n\n// Rolldown tree-shaking only preserves \"own exports\" (locally defined) — not re-exports.\n// A local binding ensures the serving vite plugin's import keeps this in the dependency graph,\n// mirroring how generateFromEntryPoint (also defined here) is preserved via the analytics vite plugin.\nexport const generateServingTypes = generateServingTypesImpl;\n\n// Re-export the mv-registry types so consumers (CLI, the type-generator\n// .d.ts shim in `packages/shared`) can pick them up from this entry point —\n// the .d.ts shim documents these as part of the package's public surface.\nexport type {\n MetricColumnMetadata,\n MetricLane,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n};\n\nexport const TYPES_DIR = \"appkit-types\";\nexport const ANALYTICS_TYPES_FILE = \"analytics.d.ts\";\nexport const SERVING_TYPES_FILE = \"serving.d.ts\";\nexport const METRIC_TYPES_FILE = \"metric-views.d.ts\";\n"],"mappings":"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"}
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { MAX_UC_OBJECT_NAME_LENGTH, UC_FQN_PATTERN } from "../../shared/src/schemas/metric-fqn.js";
|
|
1
|
+
import { MAX_UC_OBJECT_NAME_LENGTH, METRIC_CONFIG_FILE, UC_FQN_PATTERN } from "../../shared/src/schemas/metric-fqn.js";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
5
|
//#region src/type-generator/mv-registry/config.ts
|
|
6
|
-
const MV_CONFIG_FILE = "metric-views.json";
|
|
7
6
|
/**
|
|
8
7
|
* Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog
|
|
9
8
|
* limit. Enforced by {@link resolveMetricConfig}.
|
|
@@ -30,16 +29,17 @@ function compareKeys(a, b) {
|
|
|
30
29
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
31
30
|
}
|
|
32
31
|
/**
|
|
33
|
-
* Read {@link
|
|
32
|
+
* Read {@link METRIC_CONFIG_FILE} from a metric-views folder
|
|
33
|
+
* (`config/metric-views/`).
|
|
34
34
|
*
|
|
35
35
|
* Returns `null` if the file does not exist (the metric-view path is
|
|
36
|
-
* additive — apps without
|
|
37
|
-
* deliberately no fallback to
|
|
36
|
+
* additive — apps without definitions.json must not be penalized). There is
|
|
37
|
+
* deliberately no fallback to a legacy filename.
|
|
38
38
|
*
|
|
39
39
|
* Throws on JSON parse errors so misconfiguration surfaces loudly.
|
|
40
40
|
*/
|
|
41
|
-
async function readMetricConfig(
|
|
42
|
-
const metricPath = path.join(
|
|
41
|
+
async function readMetricConfig(metricViewsFolder) {
|
|
42
|
+
const metricPath = path.join(metricViewsFolder, METRIC_CONFIG_FILE);
|
|
43
43
|
let raw;
|
|
44
44
|
try {
|
|
45
45
|
raw = await fs.readFile(metricPath, "utf8");
|
|
@@ -51,9 +51,9 @@ async function readMetricConfig(queryFolder) {
|
|
|
51
51
|
try {
|
|
52
52
|
parsed = JSON.parse(raw);
|
|
53
53
|
} catch (err) {
|
|
54
|
-
throw new Error(`Failed to parse
|
|
54
|
+
throw new Error(`Failed to parse definitions.json at ${metricPath}: ${err.message}`);
|
|
55
55
|
}
|
|
56
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`Invalid
|
|
56
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(`Invalid definitions.json at ${metricPath}: expected an object with a 'metricViews' map.`);
|
|
57
57
|
return parsed;
|
|
58
58
|
}
|
|
59
59
|
/**
|
|
@@ -66,24 +66,6 @@ function isValidMetricKey(key) {
|
|
|
66
66
|
return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);
|
|
67
67
|
}
|
|
68
68
|
/**
|
|
69
|
-
* Total predicate: is `fqn` a well-formed three-part UC metric view FQN?
|
|
70
|
-
*
|
|
71
|
-
* Well-formed = exactly three non-empty, dot-separated segments, each a valid
|
|
72
|
-
* Unity Catalog object name per the shared {@link UC_FQN_PATTERN} (the single
|
|
73
|
-
* source of truth, also used by the canonical Zod schema). Used as the
|
|
74
|
-
* defense-in-depth re-check at the describe fetcher seam; {@link resolveMetricConfig}
|
|
75
|
-
* runs the same checks but with specific, staged error messages.
|
|
76
|
-
*
|
|
77
|
-
* @note Segment length ({@link MAX_FQN_SEGMENT_LENGTH}) is NOT checked here —
|
|
78
|
-
* an over-long but otherwise legal name is still "valid shape". The length cap
|
|
79
|
-
* is a separate concern enforced (with its own message) in resolveMetricConfig.
|
|
80
|
-
*/
|
|
81
|
-
function isValidFqn(fqn) {
|
|
82
|
-
const segments = fqn.split(".");
|
|
83
|
-
if (segments.length !== FQN_SEGMENT_COUNT) return false;
|
|
84
|
-
return segments.every((segment) => UC_FQN_PATTERN.test(segment));
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
69
|
* Field allowlists enforced by {@link resolveMetricConfig}.
|
|
88
70
|
*/
|
|
89
71
|
const ALLOWED_TOP_LEVEL_FIELDS = new Set(["$schema", "metricViews"]);
|
|
@@ -92,12 +74,12 @@ const ALLOWED_ENTRY_FIELDS = new Set(["source", "executor"]);
|
|
|
92
74
|
* Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.
|
|
93
75
|
*/
|
|
94
76
|
function resolveMetricConfig(config) {
|
|
95
|
-
for (const field of Object.keys(config)) if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) throw new Error(`Invalid top-level field "${field}" in
|
|
77
|
+
for (const field of Object.keys(config)) if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) throw new Error(`Invalid top-level field "${field}" in definitions.json: only '$schema' and 'metricViews' are allowed.`);
|
|
96
78
|
const metricViews = config.metricViews === void 0 ? {} : config.metricViews;
|
|
97
|
-
if (typeof metricViews !== "object" || metricViews === null || Array.isArray(metricViews)) throw new Error(`Invalid 'metricViews' in
|
|
79
|
+
if (typeof metricViews !== "object" || metricViews === null || Array.isArray(metricViews)) throw new Error(`Invalid 'metricViews' in definitions.json: expected an object map of metric entries.`);
|
|
98
80
|
const entries = [];
|
|
99
81
|
const sortedKeys = Object.keys(metricViews).sort(compareKeys);
|
|
100
|
-
if (sortedKeys.length > MAX_METRIC_VIEWS) throw new Error(`Invalid 'metricViews' in
|
|
82
|
+
if (sortedKeys.length > MAX_METRIC_VIEWS) throw new Error(`Invalid 'metricViews' in definitions.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`);
|
|
101
83
|
for (const key of sortedKeys) {
|
|
102
84
|
if (!isValidMetricKey(key)) throw new Error(`Invalid metric key "${key}" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`);
|
|
103
85
|
const entry = metricViews[key];
|
|
@@ -127,5 +109,5 @@ function resolveMetricConfig(config) {
|
|
|
127
109
|
}
|
|
128
110
|
|
|
129
111
|
//#endregion
|
|
130
|
-
export {
|
|
112
|
+
export { readMetricConfig, resolveMetricConfig };
|
|
131
113
|
//# sourceMappingURL=config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","names":[],"sources":["../../../src/type-generator/mv-registry/config.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n// Plain, zod-free value imports — single source of truth for the UC object-name\n// grammar. The Zod schema (packages/shared/src/schemas/metric-source.ts) imports\n// the SAME UC_FQN_PATTERN for its `source` .regex(...), so the runtime and the\n// canonical schema validate identically without the type-generator pulling the\n// shared Zod schema package into its runtime path (locked dependency-graph\n// ruling — see the comment in ../cache.ts). The relative specifier resolves the\n// shared source directly and drags in no zod.\nimport {\n MAX_UC_OBJECT_NAME_LENGTH,\n UC_FQN_PATTERN,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport type {\n MetricConfigResolution,\n MetricLane,\n MetricSourceConfig,\n ResolvedMetricEntry,\n} from \"./types\";\n\nconst MV_CONFIG_FILE = \"metric-views.json\";\n\n/**\n * Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog\n * limit. Enforced by {@link resolveMetricConfig}.\n */\nconst MAX_METRIC_VIEWS = 200;\n/** Per-segment cap = UC's object-name length limit (255). */\nconst MAX_FQN_SEGMENT_LENGTH = MAX_UC_OBJECT_NAME_LENGTH;\n/** Whole-FQN cap: three max-length segments plus the two separating dots. */\nconst MAX_FQN_LENGTH = MAX_FQN_SEGMENT_LENGTH * 3 + 2;\n/** A metric view FQN is exactly catalog.schema.metric_view. */\nconst FQN_SEGMENT_NAMES = [\"catalog\", \"schema\", \"metric_view\"] as const;\nconst FQN_SEGMENT_COUNT = FQN_SEGMENT_NAMES.length;\n\n/**\n * Locale-independent comparator (UTF-16 code-unit order) for metric-view key\n * ordering. Plain `sort()` is locale-sensitive, so keys could order differently\n * across environments and invalidate the cache hash — this keeps the ordering\n * stable everywhere.\n */\nfunction compareKeys(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Read {@link MV_CONFIG_FILE} from a queries folder.\n *\n * Returns `null` if the file does not exist (the metric-view path is\n * additive — apps without metric-views.json must not be penalized). There is\n * deliberately no fallback to the legacy `metric.json` filename.\n *\n * Throws on JSON parse errors so misconfiguration surfaces loudly.\n */\nexport async function readMetricConfig(\n queryFolder: string,\n): Promise<MetricSourceConfig | null> {\n const metricPath = path.join(queryFolder, MV_CONFIG_FILE);\n let raw: string;\n try {\n raw = await fs.readFile(metricPath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `Failed to parse metric-views.json at ${metricPath}: ${(err as Error).message}`,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\n `Invalid metric-views.json at ${metricPath}: expected an object with a 'metricViews' map.`,\n );\n }\n\n return parsed as MetricSourceConfig;\n}\n\n/**\n * Validate a key against the JSON Schema's metricKey pattern. Kept\n * lightweight — the shared Zod schema ({@link metricSourceSchema}) is the\n * canonical contract for IDE/CI; this regex is identical to its\n * {@link metricKeySchema}.\n */\nfunction isValidMetricKey(key: string): boolean {\n return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);\n}\n\n/**\n * Total predicate: is `fqn` a well-formed three-part UC metric view FQN?\n *\n * Well-formed = exactly three non-empty, dot-separated segments, each a valid\n * Unity Catalog object name per the shared {@link UC_FQN_PATTERN} (the single\n * source of truth, also used by the canonical Zod schema). Used as the\n * defense-in-depth re-check at the describe fetcher seam; {@link resolveMetricConfig}\n * runs the same checks but with specific, staged error messages.\n *\n * @note Segment length ({@link MAX_FQN_SEGMENT_LENGTH}) is NOT checked here —\n * an over-long but otherwise legal name is still \"valid shape\". The length cap\n * is a separate concern enforced (with its own message) in resolveMetricConfig.\n */\nexport function isValidFqn(fqn: string): boolean {\n const segments = fqn.split(\".\");\n if (segments.length !== FQN_SEGMENT_COUNT) {\n return false;\n }\n return segments.every((segment) => UC_FQN_PATTERN.test(segment));\n}\n\n/**\n * Field allowlists enforced by {@link resolveMetricConfig}.\n */\nconst ALLOWED_TOP_LEVEL_FIELDS = new Set([\"$schema\", \"metricViews\"]);\nconst ALLOWED_ENTRY_FIELDS = new Set([\"source\", \"executor\"]);\n\n/**\n * Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.\n */\nexport function resolveMetricConfig(\n config: MetricSourceConfig,\n): MetricConfigResolution {\n for (const field of Object.keys(config)) {\n if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) {\n throw new Error(\n `Invalid top-level field \"${field}\" in metric-views.json: only '$schema' and 'metricViews' are allowed.`,\n );\n }\n }\n\n // Default to {} only when metricViews is genuinely absent. A `null` must fall\n // through to the type check below and throw — the canonical Zod schema rejects\n // null.\n const metricViews =\n config.metricViews === undefined ? {} : config.metricViews;\n if (\n typeof metricViews !== \"object\" ||\n metricViews === null ||\n Array.isArray(metricViews)\n ) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: expected an object map of metric entries.`,\n );\n }\n\n const entries: ResolvedMetricEntry[] = [];\n const sortedKeys = Object.keys(metricViews).sort(compareKeys);\n if (sortedKeys.length > MAX_METRIC_VIEWS) {\n throw new Error(\n `Invalid 'metricViews' in metric-views.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`,\n );\n }\n for (const key of sortedKeys) {\n if (!isValidMetricKey(key)) {\n throw new Error(\n `Invalid metric key \"${key}\" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`,\n );\n }\n\n const entry = metricViews[key];\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new Error(\n `Invalid metric entry \"${key}\": expected an object with a 'source' field.`,\n );\n }\n\n for (const field of Object.keys(entry)) {\n if (!ALLOWED_ENTRY_FIELDS.has(field)) {\n throw new Error(\n `Invalid field \"${field}\" on metric entry \"${key}\": only 'source' and 'executor' are allowed at v1.`,\n );\n }\n }\n\n if (typeof entry.source !== \"string\" || entry.source.trim() === \"\") {\n throw new Error(\n `Invalid metric entry \"${key}\": 'source' must be a non-empty string.`,\n );\n }\n\n if (entry.source.length > MAX_FQN_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": FQN is ${entry.source.length} characters, exceeding the maximum of ${MAX_FQN_LENGTH}.`,\n );\n }\n\n // Staged, specific validation against the UC object-name grammar\n // (UC_FQN_PATTERN — shared with the canonical Zod schema). Reported in\n // order of increasing specificity so the message names the exact problem.\n const segments = entry.source.split(\".\");\n\n // Arity: exactly catalog.schema.metric_view. A wrong part count almost\n // always means a name contains a dot — which the dotted `source` cannot\n // express, since every dot is a segment boundary.\n if (segments.length !== FQN_SEGMENT_COUNT) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view> (got ${segments.length} dot-separated part${segments.length === 1 ? \"\" : \"s\"}). A catalog, schema, or metric view name cannot itself contain a dot.`,\n );\n }\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n const segmentName = FQN_SEGMENT_NAMES[i];\n\n // Empty part: a leading/trailing/double dot (e.g. \"a..c\", \".b.c\").\n if (segment.length === 0) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part is empty. A three-part UC FQN needs a non-empty name in each position: <catalog>.<schema>.<metric_view>.`,\n );\n }\n\n // Length cap (UC: object names are at most 255 characters).\n if (segment.length > MAX_FQN_SEGMENT_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": the ${segmentName} segment is ${segment.length} characters, exceeding the maximum of ${MAX_FQN_SEGMENT_LENGTH} per segment.`,\n );\n }\n\n // Character set: must be a valid UC object name (the FQN is always\n // backtick-quoted before it reaches SQL, so UC's *delimited* identifier\n // rules apply — anything but space, '/', and control characters).\n if (!UC_FQN_PATTERN.test(segment)) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part \"${segment}\" contains a character Unity Catalog does not allow in an object name (no spaces, '/', or control characters).`,\n );\n }\n }\n\n const executor = entry.executor;\n if (\n executor !== undefined &&\n executor !== \"app_service_principal\" &&\n executor !== \"user\"\n ) {\n throw new Error(\n `Invalid executor \"${String(executor)}\" on metric entry \"${key}\": must be \"app_service_principal\" or \"user\".`,\n );\n }\n\n const lane: MetricLane = executor === \"user\" ? \"obo\" : \"sp\";\n\n entries.push({ key, source: entry.source, lane });\n }\n\n return { entries };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"config.js","names":[],"sources":["../../../src/type-generator/mv-registry/config.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n// Plain, zod-free value imports — single source of truth for the UC object-name\n// grammar. The Zod schema (packages/shared/src/schemas/metric-source.ts) imports\n// the SAME UC_FQN_PATTERN for its `source` .regex(...), so the runtime and the\n// canonical schema validate identically without the type-generator pulling the\n// shared Zod schema package into its runtime path (locked dependency-graph\n// ruling — see the comment in ../cache.ts). The relative specifier resolves the\n// shared source directly and drags in no zod.\nimport {\n MAX_UC_OBJECT_NAME_LENGTH,\n METRIC_CONFIG_FILE,\n UC_FQN_PATTERN,\n} from \"../../../../shared/src/schemas/metric-fqn\";\nimport type {\n MetricConfigResolution,\n MetricLane,\n MetricSourceConfig,\n ResolvedMetricEntry,\n} from \"./types\";\n\n/**\n * Safety cap on declared metric views — a typo / DoS guard, NOT a Unity Catalog\n * limit. Enforced by {@link resolveMetricConfig}.\n */\nconst MAX_METRIC_VIEWS = 200;\n/** Per-segment cap = UC's object-name length limit (255). */\nconst MAX_FQN_SEGMENT_LENGTH = MAX_UC_OBJECT_NAME_LENGTH;\n/** Whole-FQN cap: three max-length segments plus the two separating dots. */\nconst MAX_FQN_LENGTH = MAX_FQN_SEGMENT_LENGTH * 3 + 2;\n/** A metric view FQN is exactly catalog.schema.metric_view. */\nconst FQN_SEGMENT_NAMES = [\"catalog\", \"schema\", \"metric_view\"] as const;\nconst FQN_SEGMENT_COUNT = FQN_SEGMENT_NAMES.length;\n\n/**\n * Locale-independent comparator (UTF-16 code-unit order) for metric-view key\n * ordering. Plain `sort()` is locale-sensitive, so keys could order differently\n * across environments and invalidate the cache hash — this keeps the ordering\n * stable everywhere.\n */\nfunction compareKeys(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n\n/**\n * Read {@link METRIC_CONFIG_FILE} from a metric-views folder\n * (`config/metric-views/`).\n *\n * Returns `null` if the file does not exist (the metric-view path is\n * additive — apps without definitions.json must not be penalized). There is\n * deliberately no fallback to a legacy filename.\n *\n * Throws on JSON parse errors so misconfiguration surfaces loudly.\n */\nexport async function readMetricConfig(\n metricViewsFolder: string,\n): Promise<MetricSourceConfig | null> {\n const metricPath = path.join(metricViewsFolder, METRIC_CONFIG_FILE);\n let raw: string;\n try {\n raw = await fs.readFile(metricPath, \"utf8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n return null;\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new Error(\n `Failed to parse definitions.json at ${metricPath}: ${(err as Error).message}`,\n );\n }\n\n if (typeof parsed !== \"object\" || parsed === null || Array.isArray(parsed)) {\n throw new Error(\n `Invalid definitions.json at ${metricPath}: expected an object with a 'metricViews' map.`,\n );\n }\n\n return parsed as MetricSourceConfig;\n}\n\n/**\n * Validate a key against the JSON Schema's metricKey pattern. Kept\n * lightweight — the shared Zod schema ({@link metricSourceSchema}) is the\n * canonical contract for IDE/CI; this regex is identical to its\n * {@link metricKeySchema}.\n */\nfunction isValidMetricKey(key: string): boolean {\n return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key);\n}\n\n// `resolveMetricConfig` re-derives the three-part UC FQN checks inline (rather\n// than calling a shared predicate) so it can emit specific, staged error\n// messages: arity, per-segment charset, per-segment length.\n\n/**\n * Field allowlists enforced by {@link resolveMetricConfig}.\n */\nconst ALLOWED_TOP_LEVEL_FIELDS = new Set([\"$schema\", \"metricViews\"]);\nconst ALLOWED_ENTRY_FIELDS = new Set([\"source\", \"executor\"]);\n\n/**\n * Resolve the {@link MetricSourceConfig.metricViews} map into a flat list of entries.\n */\nexport function resolveMetricConfig(\n config: MetricSourceConfig,\n): MetricConfigResolution {\n for (const field of Object.keys(config)) {\n if (!ALLOWED_TOP_LEVEL_FIELDS.has(field)) {\n throw new Error(\n `Invalid top-level field \"${field}\" in definitions.json: only '$schema' and 'metricViews' are allowed.`,\n );\n }\n }\n\n // Default to {} only when metricViews is genuinely absent. A `null` must fall\n // through to the type check below and throw — the canonical Zod schema rejects\n // null.\n const metricViews =\n config.metricViews === undefined ? {} : config.metricViews;\n if (\n typeof metricViews !== \"object\" ||\n metricViews === null ||\n Array.isArray(metricViews)\n ) {\n throw new Error(\n `Invalid 'metricViews' in definitions.json: expected an object map of metric entries.`,\n );\n }\n\n const entries: ResolvedMetricEntry[] = [];\n const sortedKeys = Object.keys(metricViews).sort(compareKeys);\n if (sortedKeys.length > MAX_METRIC_VIEWS) {\n throw new Error(\n `Invalid 'metricViews' in definitions.json: ${sortedKeys.length} metric views exceed the maximum of ${MAX_METRIC_VIEWS}.`,\n );\n }\n for (const key of sortedKeys) {\n if (!isValidMetricKey(key)) {\n throw new Error(\n `Invalid metric key \"${key}\" in metricViews: must match /^[a-zA-Z_][a-zA-Z0-9_]*$/.`,\n );\n }\n\n const entry = metricViews[key];\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) {\n throw new Error(\n `Invalid metric entry \"${key}\": expected an object with a 'source' field.`,\n );\n }\n\n for (const field of Object.keys(entry)) {\n if (!ALLOWED_ENTRY_FIELDS.has(field)) {\n throw new Error(\n `Invalid field \"${field}\" on metric entry \"${key}\": only 'source' and 'executor' are allowed at v1.`,\n );\n }\n }\n\n if (typeof entry.source !== \"string\" || entry.source.trim() === \"\") {\n throw new Error(\n `Invalid metric entry \"${key}\": 'source' must be a non-empty string.`,\n );\n }\n\n if (entry.source.length > MAX_FQN_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": FQN is ${entry.source.length} characters, exceeding the maximum of ${MAX_FQN_LENGTH}.`,\n );\n }\n\n // Staged, specific validation against the UC object-name grammar\n // (UC_FQN_PATTERN — shared with the canonical Zod schema). Reported in\n // order of increasing specificity so the message names the exact problem.\n const segments = entry.source.split(\".\");\n\n // Arity: exactly catalog.schema.metric_view. A wrong part count almost\n // always means a name contains a dot — which the dotted `source` cannot\n // express, since every dot is a segment boundary.\n if (segments.length !== FQN_SEGMENT_COUNT) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": expected a three-part UC FQN <catalog>.<schema>.<metric_view> (got ${segments.length} dot-separated part${segments.length === 1 ? \"\" : \"s\"}). A catalog, schema, or metric view name cannot itself contain a dot.`,\n );\n }\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n const segmentName = FQN_SEGMENT_NAMES[i];\n\n // Empty part: a leading/trailing/double dot (e.g. \"a..c\", \".b.c\").\n if (segment.length === 0) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part is empty. A three-part UC FQN needs a non-empty name in each position: <catalog>.<schema>.<metric_view>.`,\n );\n }\n\n // Length cap (UC: object names are at most 255 characters).\n if (segment.length > MAX_FQN_SEGMENT_LENGTH) {\n throw new Error(\n `Invalid metric source for \"${key}\": the ${segmentName} segment is ${segment.length} characters, exceeding the maximum of ${MAX_FQN_SEGMENT_LENGTH} per segment.`,\n );\n }\n\n // Character set: must be a valid UC object name (the FQN is always\n // backtick-quoted before it reaches SQL, so UC's *delimited* identifier\n // rules apply — anything but space, '/', and control characters).\n if (!UC_FQN_PATTERN.test(segment)) {\n throw new Error(\n `Invalid metric source \"${entry.source}\" for \"${key}\": the ${segmentName} part \"${segment}\" contains a character Unity Catalog does not allow in an object name (no spaces, '/', or control characters).`,\n );\n }\n }\n\n const executor = entry.executor;\n if (\n executor !== undefined &&\n executor !== \"app_service_principal\" &&\n executor !== \"user\"\n ) {\n throw new Error(\n `Invalid executor \"${String(executor)}\" on metric entry \"${key}\": must be \"app_service_principal\" or \"user\".`,\n );\n }\n\n const lane: MetricLane = executor === \"user\" ? \"obo\" : \"sp\";\n\n entries.push({ key, source: entry.source, lane });\n }\n\n return { entries };\n}\n"],"mappings":";;;;;;;;;AAyBA,MAAM,mBAAmB;;AAEzB,MAAM,yBAAyB;;AAE/B,MAAM,iBAAiB,yBAAyB,IAAI;;AAEpD,MAAM,oBAAoB;CAAC;CAAW;CAAU;CAAc;AAC9D,MAAM,oBAAoB,kBAAkB;;;;;;;AAQ5C,SAAS,YAAY,GAAW,GAAmB;AACjD,QAAO,IAAI,IAAI,KAAK,IAAI,IAAI,IAAI;;;;;;;;;;;;AAalC,eAAsB,iBACpB,mBACoC;CACpC,MAAM,aAAa,KAAK,KAAK,mBAAmB,mBAAmB;CACnE,IAAI;AACJ,KAAI;AACF,QAAM,MAAM,GAAG,SAAS,YAAY,OAAO;UACpC,KAAK;AACZ,MAAK,IAA8B,SAAS,SAC1C,QAAO;AAET,QAAM;;CAGR,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,IAAI;UACjB,KAAK;AACZ,QAAM,IAAI,MACR,uCAAuC,WAAW,IAAK,IAAc,UACtE;;AAGH,KAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,OAAO,CACxE,OAAM,IAAI,MACR,+BAA+B,WAAW,gDAC3C;AAGH,QAAO;;;;;;;;AAST,SAAS,iBAAiB,KAAsB;AAC9C,QAAO,2BAA2B,KAAK,IAAI;;;;;AAU7C,MAAM,2BAA2B,IAAI,IAAI,CAAC,WAAW,cAAc,CAAC;AACpE,MAAM,uBAAuB,IAAI,IAAI,CAAC,UAAU,WAAW,CAAC;;;;AAK5D,SAAgB,oBACd,QACwB;AACxB,MAAK,MAAM,SAAS,OAAO,KAAK,OAAO,CACrC,KAAI,CAAC,yBAAyB,IAAI,MAAM,CACtC,OAAM,IAAI,MACR,4BAA4B,MAAM,sEACnC;CAOL,MAAM,cACJ,OAAO,gBAAgB,SAAY,EAAE,GAAG,OAAO;AACjD,KACE,OAAO,gBAAgB,YACvB,gBAAgB,QAChB,MAAM,QAAQ,YAAY,CAE1B,OAAM,IAAI,MACR,uFACD;CAGH,MAAM,UAAiC,EAAE;CACzC,MAAM,aAAa,OAAO,KAAK,YAAY,CAAC,KAAK,YAAY;AAC7D,KAAI,WAAW,SAAS,iBACtB,OAAM,IAAI,MACR,8CAA8C,WAAW,OAAO,sCAAsC,iBAAiB,GACxH;AAEH,MAAK,MAAM,OAAO,YAAY;AAC5B,MAAI,CAAC,iBAAiB,IAAI,CACxB,OAAM,IAAI,MACR,uBAAuB,IAAI,0DAC5B;EAGH,MAAM,QAAQ,YAAY;AAC1B,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,MAAM,CAC7D,OAAM,IAAI,MACR,yBAAyB,IAAI,8CAC9B;AAGH,OAAK,MAAM,SAAS,OAAO,KAAK,MAAM,CACpC,KAAI,CAAC,qBAAqB,IAAI,MAAM,CAClC,OAAM,IAAI,MACR,kBAAkB,MAAM,qBAAqB,IAAI,oDAClD;AAIL,MAAI,OAAO,MAAM,WAAW,YAAY,MAAM,OAAO,MAAM,KAAK,GAC9D,OAAM,IAAI,MACR,yBAAyB,IAAI,yCAC9B;AAGH,MAAI,MAAM,OAAO,SAAS,eACxB,OAAM,IAAI,MACR,8BAA8B,IAAI,YAAY,MAAM,OAAO,OAAO,wCAAwC,eAAe,GAC1H;EAMH,MAAM,WAAW,MAAM,OAAO,MAAM,IAAI;AAKxC,MAAI,SAAS,WAAW,kBACtB,OAAM,IAAI,MACR,0BAA0B,MAAM,OAAO,SAAS,IAAI,wEAAwE,SAAS,OAAO,qBAAqB,SAAS,WAAW,IAAI,KAAK,IAAI,wEACnM;AAGH,OAAK,IAAI,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;GACxC,MAAM,UAAU,SAAS;GACzB,MAAM,cAAc,kBAAkB;AAGtC,OAAI,QAAQ,WAAW,EACrB,OAAM,IAAI,MACR,0BAA0B,MAAM,OAAO,SAAS,IAAI,SAAS,YAAY,gHAC1E;AAIH,OAAI,QAAQ,SAAS,uBACnB,OAAM,IAAI,MACR,8BAA8B,IAAI,SAAS,YAAY,cAAc,QAAQ,OAAO,wCAAwC,uBAAuB,eACpJ;AAMH,OAAI,CAAC,eAAe,KAAK,QAAQ,CAC/B,OAAM,IAAI,MACR,0BAA0B,MAAM,OAAO,SAAS,IAAI,SAAS,YAAY,SAAS,QAAQ,gHAC3F;;EAIL,MAAM,WAAW,MAAM;AACvB,MACE,aAAa,UACb,aAAa,2BACb,aAAa,OAEb,OAAM,IAAI,MACR,qBAAqB,OAAO,SAAS,CAAC,qBAAqB,IAAI,+CAChE;EAGH,MAAM,OAAmB,aAAa,SAAS,QAAQ;AAEvD,UAAQ,KAAK;GAAE;GAAK,QAAQ,MAAM;GAAQ;GAAM,CAAC;;AAGnD,QAAO,EAAE,SAAS"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isValidFqn } from "
|
|
1
|
+
import { isValidFqn, quoteFqnForSql } from "../../shared/src/schemas/metric-fqn.js";
|
|
2
2
|
import { describeAdaptive } from "../statement-result.js";
|
|
3
3
|
|
|
4
4
|
//#region src/type-generator/mv-registry/describe.ts
|
|
@@ -180,36 +180,6 @@ function inferTimeGrains(type) {
|
|
|
180
180
|
];
|
|
181
181
|
}
|
|
182
182
|
/**
|
|
183
|
-
* Quote a dot-separated FQN for safe interpolation into a Spark/Databricks SQL
|
|
184
|
-
* statement.
|
|
185
|
-
*
|
|
186
|
-
* Each dot-split segment is wrapped in backtick-quoted-identifier syntax. The
|
|
187
|
-
* one character that can break out of a backtick-quoted identifier is the
|
|
188
|
-
* backtick itself, escaped by doubling (`` ` `` → `` `` ``) — so every backtick
|
|
189
|
-
* inside a segment is doubled before the segment is wrapped. Control characters
|
|
190
|
-
* and newlines have no valid escape inside a quoted identifier, so a segment
|
|
191
|
-
* containing one is rejected outright.
|
|
192
|
-
*
|
|
193
|
-
* This is a pure, standalone escaper: it is intentionally independent of FQN
|
|
194
|
-
* naming validation ({@link isValidFqn}). Naming validation decides whether an
|
|
195
|
-
* FQN is an acceptable metric source; this function only guarantees that
|
|
196
|
-
* whatever it is handed cannot break out of the quoted identifier it produces.
|
|
197
|
-
*
|
|
198
|
-
* An ordinary identifier is unchanged apart from the wrapping backticks:
|
|
199
|
-
* `catalog.schema.view` → `` `catalog`.`schema`.`view` ``.
|
|
200
|
-
*
|
|
201
|
-
* @param fqn - Dot-separated identifier (e.g. `catalog.schema.view`).
|
|
202
|
-
* @returns The backtick-quoted, escaped identifier ready for interpolation.
|
|
203
|
-
* @throws If any segment contains a control character or newline.
|
|
204
|
-
*/
|
|
205
|
-
function quoteFqnForSql(fqn) {
|
|
206
|
-
const CONTROL_OR_NEWLINE = /\p{Cc}/u;
|
|
207
|
-
return fqn.split(".").map((segment) => {
|
|
208
|
-
if (CONTROL_OR_NEWLINE.test(segment)) throw new Error(`Cannot quote FQN segment "${segment}" for SQL: it contains a control character or newline, which has no valid escape inside a backtick-quoted identifier.`);
|
|
209
|
-
return `\`${segment.replace(/`/g, "``")}\``;
|
|
210
|
-
}).join(".");
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
183
|
* Build a DescribeFetcher from a real WorkspaceClient + warehouseId.
|
|
214
184
|
*/
|
|
215
185
|
function createWorkspaceDescribeFetcher(client, warehouseId) {
|