Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/dev-playground/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
test-results/
playwright-report/

# Auto-generated types (endpoint-specific, varies per developer)
# Auto-generated types (regenerated on `pnpm dev` by appKitTypesPlugin)
shared/appkit-types/serving.d.ts
11 changes: 11 additions & 0 deletions apps/dev-playground/config/queries/metric-views.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"metricViews": {
"revenue": {
"source": "appkit_demo.public.revenue_metrics"
},
"customers": {
"source": "appkit_demo.public.customer_metrics",
"executor": "user"
}
}
}
110 changes: 110 additions & 0 deletions apps/dev-playground/shared/appkit-types/metric-views.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Auto-generated by AppKit - DO NOT EDIT
// Generated by 'npx @databricks/appkit generate-types' or Vite plugin during build
import "@databricks/appkit-ui/react";
declare module "@databricks/appkit-ui/react" {
interface MetricRegistry {
"customers": {
key: "customers";
source: "appkit_demo.public.customer_metrics";
lane: "obo";
measures: {
/** @sqlType bigint */
"active_accounts": number;
/** @sqlType decimal */
"churn_rate": number;
/** @sqlType double */
"avg_ltv": number;
};
dimensions: {
/** @sqlType string */
"segment": string;
/** @sqlType string */
"region": string;
/** @sqlType string */
"csm_email": string;
};
measureKeys: "active_accounts" | "churn_rate" | "avg_ltv";
dimensionKeys: "segment" | "region" | "csm_email";
timeGrains: never;
metadata: {
measures: {
"active_accounts": {
type: "bigint";
};
"churn_rate": {
type: "decimal";
};
"avg_ltv": {
type: "double";
};
};
dimensions: {
"segment": {
type: "string";
};
"region": {
type: "string";
};
"csm_email": {
type: "string";
};
};
};
};
"revenue": {
key: "revenue";
source: "appkit_demo.public.revenue_metrics";
lane: "sp";
measures: {
/** @sqlType double */
"mrr": number;
/** @sqlType double */
"arr": number;
/** @sqlType double */
"new_arr": number;
/** @sqlType double */
"churned_arr": number;
};
dimensions: {
/** @sqlType string */
"region": string;
/** @sqlType string */
"segment": string;
/** @sqlType timestamp_ltz @timeGrain day|hour|minute|month|quarter|week|year */
"created_at": string;
};
measureKeys: "mrr" | "arr" | "new_arr" | "churned_arr";
dimensionKeys: "region" | "segment" | "created_at";
timeGrains: "day" | "hour" | "minute" | "month" | "quarter" | "week" | "year";
metadata: {
measures: {
"mrr": {
type: "double";
};
"arr": {
type: "double";
description: "Annualized contract value across all active subscriptions";
};
"new_arr": {
type: "double";
};
"churned_arr": {
type: "double";
};
};
dimensions: {
"region": {
type: "string";
};
"segment": {
type: "string";
};
"created_at": {
type: "timestamp_ltz";
time_grain: readonly ["day", "hour", "minute", "month", "quarter", "week", "year"];
};
};
};
};
}
}
29 changes: 27 additions & 2 deletions docs/docs/development/type-generation.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ AppKit can automatically generate TypeScript types for your SQL queries, providi

Generate type-safe TypeScript declarations for query keys, parameters, and result rows.

All generated files live in `shared/appkit-types/`, one per plugin (e.g. `analytics.d.ts`). They use [`declare module`](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to augment existing interfaces, so the types apply globally — you never need to import them. TypeScript auto-discovers them through `"include": ["shared/appkit-types"]` in your tsconfig.
All generated files live in `shared/appkit-types/`, one per concern: `analytics.d.ts` (SQL query types), `serving.d.ts` (model-serving endpoint types), and `metric-views.d.ts`. A single command (and the Vite plugin) produces them all in one pass; see [Metric-view types](#metric-view-types). The `.d.ts` files use [`declare module`](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation) to augment existing interfaces, so the types apply globally — you never need to import them. TypeScript auto-discovers them through `"include": ["shared/appkit-types"]` in your tsconfig.

## Vite plugin: `appKitTypesPlugin`

Expand Down Expand Up @@ -84,6 +84,31 @@ npx @databricks/appkit generate-types --wait

In blocking mode the generator starts a stopped warehouse, waits (bounded) for it to reach `RUNNING`, and then describes your queries. It fails only when the configured warehouse no longer exists (deleted/deleting), so a transient outage or a cold warehouse degrades gracefully rather than breaking the build. The app template wires this up for you: `postinstall` and `predev` run the non-blocking default, while `prebuild` runs `--wait`.

## Metric-view types

`generate-types` (and the Vite plugin) emit metric-view types **additively** — there is no separate command. When a `config/queries/metric-views.json` file is present, the same run that generates your query types also DESCRIBEs each declared [UC Metric View](../plugins/analytics.md) and writes `metric-views.d.ts` into `shared/appkit-types/`:

- `metric-views.d.ts` — augments the `MetricRegistry` interface so `useMetricView('<key>', …)` is autocompleted and type-checked. Each view's measures, dimensions, and their semantic metadata (SQL type, display name, format, time grains) are encoded at the type level.

If `metric-views.json` is absent the metric path stays dormant (nothing is emitted). When present it follows the **same** warehouse-readiness contract as query types: in the default non-blocking run a view that can't be described yet — a cold warehouse, or a bad/unreachable source — is written with permissive types and a warning, while under `--wait` that same situation fails the build so CI never ships incomplete metric types. A malformed `metric-views.json` (invalid JSON, or a source that isn't a three-part UC FQN) fails fast in every mode.

`metric-views.json` is keyed by metric key; each entry names the three-part UC FQN of the view and, optionally, the executor it runs as (`app_service_principal`, the default, or `user`):

```json
{
"$schema": "https://databricks.github.io/appkit/schemas/metric-source.schema.json",
"metricViews": {
"revenue": { "source": "catalog.schema.revenue_metrics" },
"customers": {
"source": "catalog.schema.customer_metrics",
"executor": "user"
}
}
}
```

The optional `$schema` line enables editor autocomplete and validation against the published schema.

## How it works

The type generator:
Expand Down Expand Up @@ -121,7 +146,7 @@ const { data } = useAnalyticsQuery("users_list", {
});

// TypeScript knows the shape of the result rows
data?.forEach(row => {
data?.forEach((row) => {
console.log(row.email); // ✓ autocomplete works
});
```
Expand Down
24 changes: 13 additions & 11 deletions packages/appkit/src/type-generator/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@ interface CacheEntry {
* `hash` is md5 over `"<source>|<lane>"` — the two config inputs that
* determine a DESCRIBE — so editing either invalidates the entry. `schema`
* is the full {@link MetricSchema} persisted verbatim (it is JSON-safe by
* design), letting a warm pass regenerate both metric artifacts without a
* single warehouse call. `retry: true` marks a SELF-CONVERGING degraded
* outcome (DESCRIBE skipped behind a not-running warehouse, unanswered, or
* transiently failed): the cached schema still renders artifacts, but the
* next eligible pass re-describes exactly these keys so degraded schemas
* converge to real ones. A degraded schema with `retry: false` is a STICKY
* failure — a deterministic DESCRIBE failure (bad FQN, unparseable
* response, zero columns) or a deleted warehouse — that re-describing the
* unchanged entry cannot fix; it hits like any cached entry until the
* config hash changes or the cache is bypassed, and the type generator
* warns about it on every pass that serves it.
* design), letting a warm pass regenerate the metric artifact without a
* single warehouse call.
*
* The cache holds ONLY successful (non-degraded) describes. A degraded
* outcome — DESCRIBE skipped behind a not-running or deleted warehouse,
* unanswered, or a per-key failure — is rendered into the emitted artifact
* but never written here (mirroring the query path, which "Never persists
* `result: unknown`"). A key that degraded on one pass is therefore left
* uncached and simply re-described on the next eligible pass; there is no
* sticky degraded entry to serve. `retry` is vestigial — always `false`,
* mirroring the query path's only cache write — and is retained solely for
* on-disk shape compatibility with existing version-3 caches (the revival
* gate still checks it is a boolean).
*/
export interface MetricCacheEntry {
hash: string;
Expand Down
Loading
Loading