Skip to content
Merged
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
25 changes: 3 additions & 22 deletions packages/appkit/src/type-generator/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,8 @@ 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.
*/
export interface MetricCacheEntry {
hash: string;
Expand All @@ -51,12 +42,7 @@ export interface MetricCacheEntry {
* hand-edits, truncation, or a stale writer can leave entries whose shape no
* longer matches {@link MetricCacheEntry}. A malformed entry must read as a
* cache MISS (re-describe) rather than crash the pass or render revived
* garbage into the artifacts. Checks exactly what the renderers and the
* metadata bundle consume: `hash` string, `retry` boolean, and a schema with
* `key`/`source` strings, a valid lane, an optional boolean `degraded`, and
* measure/dimension arrays whose elements carry `name`/`type` strings
* (other column fields are optional). Deliberately inline — the shared Zod
* schemas must not enter the type-generator's runtime path.
* garbage into the artifacts.
*/
export function isRevivableMetricCacheEntry(entry: unknown): boolean {
if (typeof entry !== "object" || entry === null) return false;
Expand Down Expand Up @@ -93,11 +79,6 @@ export function isRevivableMetricCacheEntry(entry: unknown): boolean {
* @property version - the version of the cache
* @property queries - the queries in the cache
* @property metrics - cached metric-view schemas keyed by metric key.
* OPTIONAL on purpose: version "3" files written before this section
* existed load unchanged (absent ⇒ treated as empty by the metric path),
* and the query path's `noCache` reinit literal stays valid as-is. The
* section rides through the query path's load → mutate → save cycle as a
* plain sibling key, so query-side saves preserve it byte-for-byte.
*/
interface Cache {
version: string;
Expand Down
Loading
Loading