@databricks/appkit 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/llms.txt CHANGED
@@ -1,193 +1,1077 @@
1
- # llms.txt — Guidance for AI systems using the Databricks AppKit (@databricks/appkit)
2
-
1
+ # llms.txt — LLM Guide for Building Great Databricks Apps with AppKit
3
2
  Project: Databricks AppKit
4
- Author: Databricks
5
- Version: 1.0.0
6
-
7
- # =====================
8
- # General Description
9
- # =====================
10
- AppKit is a modular TypeScript SDK for building apps with workflows and plugins.
11
- It provides a single entrypoint (createApp) where you configure and register plugins.
12
- Each plugin is then available under AppKit[pluginName].
13
-
14
- Main concepts:
15
- - createApp(config): initializes the SDK with plugins
16
- - Plugins: extend AppKit with functionality (server, analytics, ai, etc.)
17
- - AppKit[pluginName]: exposes plugin API after initialization
18
- - New plugins can be created by extending the Plugin class.
19
-
20
- # =====================
21
- # Primary Usage Pattern
22
- # =====================
23
- Always use async/await.
24
- Always initialize AppKit before using plugins.
25
- Server and plugins already initialized, no custom endpoints.
26
-
27
- Example:
3
+
4
+ This document is written *for LLMs* generating code in a brand-new project folder that installs AppKit from npm. It is intentionally prescriptive.
5
+
6
+ ## High-level mission
7
+
8
+ Build **full-stack TypeScript apps** on Databricks using:
9
+
10
+ - **Backend**: `@databricks/appkit`
11
+ - **Frontend**: `@databricks/appkit-ui`
12
+ - **Analytics**: SQL files in `config/queries/*.sql` executed via the AppKit analytics plugin
13
+
14
+ This file is designed to work even when you *do not* have access to the AppKit source repo. Prefer only public package APIs and portable project structures.
15
+
16
+ ## Hard rules (LLM guardrails)
17
+
18
+ - **Do not invent APIs**. If unsure, stick to the patterns shown in this file and only documented exports from `@databricks/appkit` and `@databricks/appkit-ui`.
19
+ - **`createApp()` is async**. Prefer **top-level `await createApp(...)`**. If you can’t, use `void createApp(...)` and do not ignore promise rejection.
20
+ - **Always memoize query parameters** passed to `useAnalyticsQuery` / charts to avoid refetch loops.
21
+ - **Always handle loading/error/empty states** in UI (use `Skeleton`, error text, empty state).
22
+ - **Always use `sql.*` helpers** for query parameters (do not pass raw strings/numbers unless the query expects none).
23
+ - **Never construct SQL strings dynamically**. Use parameterized queries with `:paramName`.
24
+ - **Never use `require()`**. Use ESM `import/export`.
25
+
26
+ ## TypeScript import rules (when using `verbatimModuleSyntax`)
27
+
28
+ If your `tsconfig.json` uses `"verbatimModuleSyntax": true`, **always use `import type` for type-only imports** (otherwise builds can fail in strict setups):
28
29
 
29
30
  ```ts
30
- import { createApp, server, analytics } from "@databricks/appkit";
31
+ import type { ReactNode } from "react";
32
+ import { useMemo } from "react";
33
+ ```
34
+
35
+ ## Canonical project layout
36
+
37
+ Recommended structure (client/server split):
38
+
39
+ ```
40
+ my-app/
41
+ ├── server/
42
+ │ ├── index.ts # backend entry point (AppKit)
43
+ │ └── .env # optional local dev env vars (do not commit)
44
+ ├── client/
45
+ │ ├── index.html
46
+ │ ├── vite.config.ts
47
+ │ └── src/
48
+ │ ├── main.tsx
49
+ │ └── App.tsx
50
+ ├── config/
51
+ │ └── queries/
52
+ │ └── my_query.sql
53
+ ├── app.yaml
54
+ ├── package.json
55
+ └── tsconfig.json
56
+ ```
57
+
58
+ Why this layout:
59
+
60
+ - The AppKit `server()` plugin automatically serves:
61
+ - **Dev**: Vite dev server (HMR) from `client/`
62
+ - **Prod**: static files from `client/dist` (built by Vite)
63
+
64
+ ## Project scaffolding (start here)
65
+
66
+ ### `package.json`
67
+
68
+ ```json
69
+ {
70
+ "name": "my-app",
71
+ "private": true,
72
+ "version": "0.0.0",
73
+ "type": "module",
74
+ "scripts": {
75
+ "dev": "NODE_ENV=development tsx watch server/index.ts",
76
+ "build": "npm run build:server && npm run build:client",
77
+ "build:server": "tsdown --out-dir build server/index.ts",
78
+ "build:client": "cd client && npm run build",
79
+ "start": "node build/index.mjs"
80
+ },
81
+ "dependencies": {
82
+ "@databricks/appkit": "^0.0.2"
83
+ },
84
+ "devDependencies": {
85
+ "@types/node": "^20.0.0",
86
+ "tsdown": "^0.15.7",
87
+ "tsx": "^4.19.0",
88
+ "typescript": "~5.6.0"
89
+ }
90
+ }
91
+ ```
92
+
93
+ ### `client/package.json`
94
+
95
+ ```json
96
+ {
97
+ "name": "client",
98
+ "private": true,
99
+ "version": "0.0.0",
100
+ "type": "module",
101
+ "scripts": {
102
+ "dev": "vite",
103
+ "build": "vite build",
104
+ "preview": "vite preview"
105
+ },
106
+ "dependencies": {
107
+ "@databricks/appkit-ui": "^0.0.2",
108
+ "react": "^18.0.0",
109
+ "react-dom": "^18.0.0",
110
+ "recharts": "^3.0.0"
111
+ },
112
+ "devDependencies": {
113
+ "@types/react": "^18.0.0",
114
+ "@types/react-dom": "^18.0.0",
115
+ "@vitejs/plugin-react": "^5.0.0",
116
+ "typescript": "~5.6.0",
117
+ "vite": "^6.0.0"
118
+ }
119
+ }
120
+ ```
121
+
122
+ ### `client/index.html`
123
+
124
+ ```html
125
+ <!doctype html>
126
+ <html lang="en">
127
+ <head>
128
+ <meta charset="UTF-8" />
129
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
130
+ <title>My App</title>
131
+ </head>
132
+ <body>
133
+ <div id="root"></div>
134
+ <script type="module" src="/src/main.tsx"></script>
135
+ </body>
136
+ </html>
137
+ ```
138
+
139
+ ### `client/src/main.tsx`
140
+
141
+ ```tsx
142
+ import { StrictMode } from "react";
143
+ import { createRoot } from "react-dom/client";
144
+ import App from "./App";
145
+
146
+ createRoot(document.getElementById("root")!).render(
147
+ <StrictMode>
148
+ <App />
149
+ </StrictMode>,
150
+ );
151
+ ```
152
+
153
+ ### `client/src/App.tsx` (minimal)
154
+
155
+ ```tsx
156
+ export default function App() {
157
+ return (
158
+ <div className="p-8">
159
+ <h1 className="text-2xl font-bold">My App</h1>
160
+ </div>
161
+ );
162
+ }
163
+ ```
164
+
165
+ ### `client/vite.config.ts`
166
+
167
+ ```ts
168
+ import { defineConfig } from "vite";
169
+ import react from "@vitejs/plugin-react";
170
+
171
+ export default defineConfig({
172
+ plugins: [react()],
173
+ });
174
+ ```
175
+
176
+ ### `tsconfig.json`
177
+
178
+ ```json
179
+ {
180
+ "compilerOptions": {
181
+ "target": "ES2022",
182
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
183
+ "module": "ESNext",
184
+ "moduleResolution": "bundler",
185
+ "jsx": "react-jsx",
186
+ "strict": true,
187
+ "skipLibCheck": true,
188
+ "noEmit": true,
189
+ "allowImportingTsExtensions": true,
190
+ "verbatimModuleSyntax": true
191
+ },
192
+ "include": ["server", "client/src"]
193
+ }
194
+ ```
195
+
196
+ ### `server/index.ts`
197
+
198
+ ```ts
199
+ import { createApp, server } from "@databricks/appkit";
31
200
 
32
201
  await createApp({
33
- plugins: [
34
- server({ port: 8000 }),
35
- analytics(),
36
- ],
202
+ plugins: [server()],
37
203
  });
38
204
  ```
39
205
 
40
- # ==============================================
41
- # Basic Usage Pattern starting the server
42
- # ==============================================
206
+ ### Running the app
207
+
208
+ ```bash
209
+ # Install dependencies
210
+ npm install
211
+ cd client && npm install && cd ..
212
+
213
+ # Development (starts backend + Vite dev server)
214
+ npm run dev
215
+
216
+ # Production build
217
+ npm run build
218
+ npm start
219
+ ```
43
220
 
44
- Example:
221
+ ## Integrating into an existing app
222
+
223
+ If you already have a React/Vite app and want to add AppKit:
224
+
225
+ ### 1. Install dependencies
226
+
227
+ ```bash
228
+ npm install @databricks/appkit
229
+ npm install -D tsx tsdown
230
+
231
+ # If you don't already have a client/ folder, create one and move your Vite app into it:
232
+ # - move index.html -> client/index.html
233
+ # - move vite.config.ts -> client/vite.config.ts
234
+ # - move src/ -> client/src/
235
+ #
236
+ # Then install client deps:
237
+ cd client
238
+ npm install @databricks/appkit-ui react react-dom recharts
239
+ npm install -D vite @vitejs/plugin-react typescript
240
+ cd ..
241
+ ```
242
+
243
+ ### 2. Create `server/index.ts` (new file)
45
244
 
46
245
  ```ts
246
+ import { createApp, server } from "@databricks/appkit";
247
+
248
+ await createApp({
249
+ plugins: [server()],
250
+ });
251
+ ```
252
+
253
+ ### 3. Update `package.json` scripts
254
+
255
+ ```json
256
+ {
257
+ "scripts": {
258
+ "dev": "NODE_ENV=development tsx watch server/index.ts",
259
+ "build": "npm run build:server && npm run build:client",
260
+ "build:server": "tsdown --out-dir build server/index.ts",
261
+ "build:client": "cd client && npm run build",
262
+ "start": "node build/index.mjs"
263
+ }
264
+ }
265
+ ```
266
+
267
+ ### 4. That's it
268
+
269
+ - AppKit's server plugin will automatically serve your Vite app in dev mode and `client/dist` in production.
270
+ - If your Vite app must stay at the repo root (no `client/` folder), AppKit can still work, but the recommended layout is `client/` + `server/`.
271
+
272
+ ### Adding analytics to an existing app
273
+
274
+ ```ts
275
+ // server/index.ts
47
276
  import { createApp, server, analytics } from "@databricks/appkit";
48
277
 
49
- const AppKit = await createApp({
278
+ await createApp({
279
+ plugins: [server(), analytics({})],
280
+ });
281
+ ```
282
+
283
+ Then create `config/queries/` and add your `.sql` files.
284
+
285
+ ## Environment variables
286
+
287
+ ### Required for Databricks Apps deployment
288
+
289
+ These are typically **provided by Databricks Apps runtime** (exact set can vary by platform/version):
290
+
291
+ | Variable | Description |
292
+ |----------|-------------|
293
+ | `DATABRICKS_HOST` | Workspace URL (e.g. `https://xxx.cloud.databricks.com`) |
294
+ | `DATABRICKS_APP_PORT` | Port to bind (default: `8000`) |
295
+ | `DATABRICKS_APP_NAME` | App name in Databricks |
296
+
297
+ ### Required for SQL queries (analytics plugin)
298
+
299
+ | Variable | Description | How to set |
300
+ |----------|-------------|------------|
301
+ | `DATABRICKS_WAREHOUSE_ID` | SQL warehouse ID | In `app.yaml`: `valueFrom: sql-warehouse` |
302
+
303
+ ### Optional
304
+
305
+ | Variable | Description | Default |
306
+ |----------|-------------|---------|
307
+ | `DATABRICKS_WORKSPACE_ID` | Workspace ID | Auto-fetched from API |
308
+ | `NODE_ENV` | `"development"` or `"production"` | — |
309
+ | `FLASK_RUN_HOST` | Host to bind | `0.0.0.0` |
310
+
311
+ ### Local development
312
+
313
+ For local development, you need to authenticate with Databricks. Options:
314
+
315
+ **Option 1: Databricks CLI profile (recommended)**
316
+
317
+ ```bash
318
+ # Configure once
319
+ databricks configure --profile my-profile
320
+
321
+ # Then run with profile
322
+ DATABRICKS_CONFIG_PROFILE=my-profile npm run dev
323
+ # If your Databricks SDK expects a different variable name, try:
324
+ # DATABRICKS_PROFILE=my-profile npm run dev
325
+ ```
326
+
327
+ **Option 2: Environment variables**
328
+
329
+ ```bash
330
+ export DATABRICKS_HOST="https://xxx.cloud.databricks.com"
331
+ export DATABRICKS_TOKEN="dapi..."
332
+ export DATABRICKS_WAREHOUSE_ID="abc123..."
333
+ npm run dev
334
+ ```
335
+
336
+ **Option 3: `.env` file (auto-loaded by AppKit)**
337
+
338
+ ```bash
339
+ # .env (add to .gitignore!)
340
+ DATABRICKS_HOST=https://xxx.cloud.databricks.com
341
+ DATABRICKS_TOKEN=dapi...
342
+ DATABRICKS_WAREHOUSE_ID=abc123...
343
+ ```
344
+
345
+ ### Telemetry (optional)
346
+
347
+ | Variable | Description |
348
+ |----------|-------------|
349
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | OpenTelemetry collector endpoint |
350
+ | `OTEL_SERVICE_NAME` | Service name for traces |
351
+
352
+ ## Backend: `@databricks/appkit`
353
+
354
+ ### Minimal server (golden template)
355
+
356
+ The smallest valid AppKit server:
357
+
358
+ ```ts
359
+ // server/index.ts
360
+ import { createApp, server } from "@databricks/appkit";
361
+
362
+ await createApp({
363
+ plugins: [server()],
364
+ });
365
+ ```
366
+
367
+ ### Server plugin (`server()`)
368
+
369
+ What it does:
370
+
371
+ - Starts an Express server (default `host=0.0.0.0`, `port=8000`)
372
+ - Mounts plugin routes under `/api/<pluginName>/...`
373
+ - Adds `/health` (returns `{ status: "ok" }`)
374
+ - Serves frontend:
375
+ - **Development** (`NODE_ENV=development`): runs a Vite dev server in middleware mode
376
+ - **Production**: auto-detects static frontend directory (checks `dist`, `client/dist`, `build`, `public`, `out`)
377
+
378
+ Config (real options):
379
+
380
+ ```ts
381
+ import { createApp, server } from "@databricks/appkit";
382
+
383
+ await createApp({
50
384
  plugins: [
51
- server({ port: 8000, autoStart: false }),
52
- analytics(),
385
+ server({
386
+ port: 8000, // default: Number(process.env.DATABRICKS_APP_PORT) || 8000
387
+ host: "0.0.0.0", // default: process.env.FLASK_RUN_HOST || "0.0.0.0"
388
+ autoStart: true, // default: true
389
+ staticPath: "dist", // optional: force a specific static directory
390
+ }),
53
391
  ],
54
392
  });
55
-
56
- const app = await AppKit.server.start();
57
- app.get("/ping", (req, res) => res.send("pong"));
58
393
  ```
59
394
 
60
- # =====================
61
- # Plugin APIs
62
- # =====================
395
+ Manual server start (when you need to `.extend()` Express):
63
396
 
64
- Each plugin exposes a set of endpoints by default.
397
+ ```ts
398
+ import { createApp, server } from "@databricks/appkit";
65
399
 
66
- ## Server Plugin
67
- - AppKit.server.start(): Promise<Express.Application>
68
- - Purpose: Start an Express server with configured port, only use if { autoStart: false } is provided in the config of the server plugin
69
- - Usage: Add routes via the returned app
70
- - Config - When setting the plugin, the following options can be provided:
71
- server({
72
- port?: number;
73
- staticPath?: string; // This provides the path where the frontend assets are.
74
- autoStart?: boolean;
75
- })
400
+ const appkit = await createApp({
401
+ plugins: [server({ autoStart: false })],
402
+ });
403
+
404
+ appkit.server.extend((app) => {
405
+ app.get("/custom", (_req, res) => res.json({ ok: true }));
406
+ });
76
407
 
77
- ## Analytics Plugin
78
- - AppKit.analytics.query.executeQuery({ query, parameters }: { query: string; parameters?: Record<string, any> }, options?: ExecuteOptions): Promise<ExecuteStatementOutput>;
79
- - Purpose: Provide SQL by key interface.
80
- - Usage: Only for structured query + insert examples. SQL never goes into the call to the function. Any SQL that needs to be written,
81
- will be written into config/queries/<query_key>.sql. All queries should be parameterized (use placeholders).
82
- - Default endpoints:
83
- - POST /api/analytics/:query_key -> `query_key` will be the key to the file that contains the query. Expects a body with the shape { parameters?: Record<string, any>; }. parameters will be bound into the query.
408
+ await appkit.server.start();
409
+ ```
84
410
 
85
- # =====================
86
- # Custom Plugins
87
- # =====================
411
+ ### Analytics plugin (`analytics()`)
88
412
 
89
- Databricks AppKit Might not cover all the cases needed, so for those cases a plugin can be created.
90
- Here is an example:
413
+ Add SQL query execution backed by Databricks SQL Warehouses.
91
414
 
92
415
  ```ts
93
- import { Plugin, toPlugin } from '@databricks/appkit';
416
+ import { analytics, createApp, server } from "@databricks/appkit";
94
417
 
95
- class OpenWeatherPlugin extends Plugin {
96
- name: string = "open-weather";
97
- private apiKey: string;
98
- private url: string;
418
+ await createApp({
419
+ plugins: [server(), analytics({})],
420
+ });
421
+ ```
99
422
 
100
- constructor(config: any, auth: IAuthManager, telemetry: ITelemetryManager) {
101
- super(config, auth, telemetry);
423
+ Where queries live:
102
424
 
103
- this.apiKey = process.env.OPEN_WEATHER_API_KEY!;
104
- this.url = process.env.OPEN_WEATHER_URL || "https://api.openweathermap.org/data/3.0/onecall";
425
+ - Put `.sql` files in `config/queries/`.
426
+ - Query key is the filename without `.sql` (e.g. `spend_summary.sql` → `"spend_summary"`).
105
427
 
106
- // ...
107
- }
428
+ SQL parameters:
108
429
 
109
- async getWeather(lat: number, lon: number): Promise<any | null> {
110
- const url = `${this.url}?lat=${lat}&lon=${lon}&appid=${this.apiKey}`;
111
-
112
- try {
113
- const response = await fetch(url);
114
- if (!response.ok) {
115
- console.error("Error fetching weather data:", response.statusText);
116
- return null;
117
- }
118
-
119
- const data = await response.json();
120
- return data;
121
- } catch (error) {
122
- console.error("Fetch error:", error);
123
- return null;
124
- }
125
- }
430
+ - Use `:paramName` placeholders.
431
+ - Optionally annotate parameter types using SQL comments:
432
+
433
+ ```sql
434
+ -- @param startDate DATE
435
+ -- @param endDate DATE
436
+ -- @param limit NUMERIC
437
+ SELECT ...
438
+ WHERE usage_date BETWEEN :startDate AND :endDate
439
+ LIMIT :limit
440
+ ```
441
+
442
+ Supported `-- @param` types (case-insensitive):
443
+
444
+ - `STRING`, `NUMERIC`, `BOOLEAN`, `DATE`, `TIMESTAMP`, `BINARY`
445
+
446
+ Server-injected params (important):
447
+
448
+ - `:workspaceId` is **injected by the server** and **must not** be annotated.
449
+ - Example:
450
+
451
+ ```sql
452
+ WHERE workspace_id = :workspaceId
453
+ ```
454
+
455
+ HTTP endpoints exposed (mounted under `/api/analytics`):
456
+
457
+ - `POST /api/analytics/query/:query_key`
458
+ - `POST /api/analytics/users/me/query/:query_key`
459
+ - `GET /api/analytics/arrow-result/:jobId`
460
+ - `GET /api/analytics/users/me/arrow-result/:jobId`
461
+
462
+ Formats:
463
+
464
+ - `format: "JSON"` (default) returns JSON rows
465
+ - `format: "ARROW"` returns an Arrow “external links” payload over SSE, then the client fetches binary Arrow from `/api/analytics/arrow-result/:jobId`
466
+
467
+ ### Request context (`getRequestContext()`)
468
+
469
+ If a plugin sets `requiresDatabricksClient = true`, AppKit adds middleware that provides request context.
470
+
471
+ Headers used:
472
+
473
+ - `x-forwarded-user`: required in production; identifies the user
474
+ - `x-forwarded-access-token`: optional; enables **user token passthrough** if `DATABRICKS_HOST` is set
475
+
476
+ Context fields (real behavior):
477
+
478
+ - `userId`: derived from `x-forwarded-user` (in development it falls back to `serviceUserId`)
479
+ - `serviceUserId`: service principal/user ID
480
+ - `warehouseId`: `Promise<string>` (from `DATABRICKS_WAREHOUSE_ID`, or auto-selected in development)
481
+ - `workspaceId`: `Promise<string>` (from `DATABRICKS_WORKSPACE_ID` or fetched)
482
+ - `userDatabricksClient`: present only when passthrough is available (or in dev it equals service client)
483
+ - `serviceDatabricksClient`: always present
484
+
485
+ ### Custom plugins (backend)
486
+
487
+ If you need custom API routes or background logic, implement an AppKit plugin.
488
+
489
+ ```ts
490
+ import { Plugin, toPlugin } from "@databricks/appkit";
491
+ import type express from "express";
492
+
493
+ class MyPlugin extends Plugin {
494
+ name = "my-plugin";
495
+ envVars = []; // list required env vars here
496
+ requiresDatabricksClient = false; // set true if you need getRequestContext()
126
497
 
127
- /**
128
- * Optionally the plugin can inject its own routes to the router
129
- */
130
498
  injectRoutes(router: express.Router) {
131
- /**
132
- * Each route is scoped to the plugin name. So in this case the route will be end up being
133
- * /api/open-weather/weather
134
- *
135
- * and an example request would be:
136
- * GET /api/open-weather/weather?lat=40.7128&lon=-74.0060
137
- */
138
- router.get("/weather", async (req: any, res: any) => {
139
- const { lat, lon } = req.query;
140
- const data = await this.getWeather(lat, lon);
141
- res.send(data);
499
+ this.route(router, {
500
+ name: "hello",
501
+ method: "get",
502
+ path: "/hello",
503
+ handler: async (_req, res) => {
504
+ res.json({ ok: true });
505
+ },
142
506
  });
143
507
  }
144
508
  }
145
509
 
146
- export const openWeather = toPlugin<typeof OpenWeatherPlugin, OpenWeatherConfig, "openWeather">(OpenWeatherPlugin, "openWeather");
510
+ export const myPlugin = toPlugin<typeof MyPlugin, Record<string, never>, "my-plugin">(
511
+ MyPlugin,
512
+ "my-plugin",
513
+ );
147
514
  ```
148
515
 
149
- Then it would be used as the rest of the plugins
516
+ ### Caching (global + plugin-level)
517
+
518
+ Global:
150
519
 
151
520
  ```ts
152
- import { createApp, server, analytics } from "@databricks/appkit";
153
- import { openWeather } from './open-weather';
521
+ await createApp({
522
+ plugins: [server(), analytics({})],
523
+ cache: {
524
+ enabled: true,
525
+ ttl: 3600, // seconds
526
+ strictPersistence: false,
527
+ },
528
+ });
529
+ ```
530
+
531
+ - Storage auto-selects **Lakebase persistent cache when healthy**, otherwise falls back to in-memory.
532
+
533
+ Plugin-level:
534
+
535
+ ```ts
536
+ // inside a Plugin subclass:
537
+ const value = await this.cache.getOrExecute(
538
+ ["my-plugin", "data", userId],
539
+ async () => expensiveWork(),
540
+ userKey,
541
+ { ttl: 300 },
542
+ );
543
+ ```
544
+
545
+ ## Frontend: `@databricks/appkit-ui`
546
+
547
+ ### Imports
548
+
549
+ - React-facing APIs: `@databricks/appkit-ui/react`
550
+ - Non-React utilities (sql markers, arrow, SSE): `@databricks/appkit-ui/js`
551
+
552
+ ```tsx
553
+ import { useAnalyticsQuery, Card, Skeleton } from "@databricks/appkit-ui/react";
554
+ import { sql } from "@databricks/appkit-ui/js";
555
+ ```
556
+
557
+ ### `useAnalyticsQuery(queryKey, parameters, options?)`
558
+
559
+ Facts:
560
+
561
+ - Uses **SSE** under the hood (not `fetch()` polling).
562
+ - By default it hits `POST /api/analytics/query/:queryKey`.
563
+ - Returns `{ data, loading, error }` where `data` is `null` until loaded.
564
+ - `format` is `"JSON"` or `"ARROW"` (uppercase).
565
+
566
+ When to use it:
567
+
568
+ - Use `useAnalyticsQuery` **only** when you need a custom UI (cards/KPIs/forms/conditional rendering).
569
+ - If you just need a standard chart or table, prefer the built-in components (`BarChart`, `LineChart`, `DataTable`, etc.) so you don’t re-implement loading/error/empty states.
570
+
571
+ Limitations (common LLM pitfall):
572
+
573
+ - There is **no `enabled` option**. Use conditional rendering to mount/unmount the component.
574
+ - There is **no `refetch()`**. Change `parameters` (memoized) or re-mount to re-run the query.
575
+
576
+ Recommended usage pattern (memoized params + explicit states):
577
+
578
+ ```tsx
579
+ import { useMemo } from "react";
580
+ import { useAnalyticsQuery, Skeleton } from "@databricks/appkit-ui/react";
581
+ import { sql } from "@databricks/appkit-ui/js";
582
+
583
+ export function Users() {
584
+ const params = useMemo(
585
+ () => ({
586
+ status: sql.string("active"),
587
+ limit: sql.number(50),
588
+ }),
589
+ [],
590
+ );
591
+
592
+ const { data, loading, error } = useAnalyticsQuery("users_list", params);
593
+
594
+ if (loading) return <Skeleton className="h-24 w-full" />;
595
+ if (error) return <div className="text-destructive">Error: {error}</div>;
596
+ if (!data || data.length === 0) return <div>No results</div>;
597
+
598
+ return <pre>{JSON.stringify(data[0], null, 2)}</pre>;
599
+ }
600
+ ```
601
+
602
+ Options:
603
+
604
+ - `format?: "JSON" | "ARROW"` (default `"JSON"`)
605
+ - `autoStart?: boolean` (default `true`)
606
+ - `maxParametersSize?: number` (default `100 * 1024` bytes)
607
+
608
+ ### `useChartData({ queryKey, parameters, format, transformer })`
609
+
610
+ - `format` here is **lowercase**: `"json" | "arrow" | "auto"` (default `"auto"`)
611
+ - Auto-selection heuristics:
612
+ - If `parameters._preferArrow === true` → Arrow
613
+ - If `parameters._preferJson === true` → JSON
614
+ - If `parameters.limit` is a number > 500 → Arrow
615
+ - If `parameters.startDate` and `parameters.endDate` exist → Arrow
616
+
617
+ ### Charts (unified query/data API)
618
+
619
+ All charts support:
620
+
621
+ - **Query mode**: `queryKey` + `parameters`
622
+ - **Data mode**: `data` (inline JSON, no server)
623
+
624
+ Available chart components:
625
+
626
+ - `BarChart`, `LineChart`, `AreaChart`, `PieChart`, `DonutChart`, `HeatmapChart`, `ScatterChart`, `RadarChart`
627
+
628
+ Avoid double-fetching:
629
+
630
+ ```tsx
631
+ // ❌ Wrong: fetches the same query twice
632
+ // const { data } = useAnalyticsQuery("spend_data", params);
633
+ // return <LineChart queryKey="spend_data" parameters={params} />;
634
+
635
+ // ✅ Correct: let the chart fetch
636
+ return <LineChart queryKey="spend_data" parameters={params} />;
637
+ ```
638
+
639
+ Query mode (recommended for Databricks-backed analytics):
640
+
641
+ ```tsx
642
+ import { LineChart } from "@databricks/appkit-ui/react";
643
+ import { sql } from "@databricks/appkit-ui/js";
644
+ import { useMemo } from "react";
645
+
646
+ export function SpendChart() {
647
+ const params = useMemo(
648
+ () => ({
649
+ startDate: sql.date("2024-01-01"),
650
+ endDate: sql.date("2024-12-31"),
651
+ aggregationLevel: sql.string("day"),
652
+ }),
653
+ [],
654
+ );
655
+
656
+ return (
657
+ <LineChart
658
+ queryKey="spend_data"
659
+ parameters={params}
660
+ format="auto" // "auto" | "json" | "arrow"
661
+ xKey="period"
662
+ yKey="cost_usd"
663
+ smooth
664
+ showSymbol={false}
665
+ />
666
+ );
667
+ }
668
+ ```
669
+
670
+ ### SQL helpers (`sql.*`)
671
+
672
+ Use these to build typed parameters (they return marker objects: `{ __sql_type, value }`):
673
+
674
+ - `sql.string(value)` → STRING (accepts string|number|boolean)
675
+ - `sql.number(value)` → NUMERIC (accepts number|string)
676
+ - `sql.boolean(value)` → BOOLEAN (accepts boolean|string("true"/"false")|number(1/0))
677
+ - `sql.date(value)` → DATE (accepts Date or `"YYYY-MM-DD"`)
678
+ - `sql.timestamp(value)` → TIMESTAMP (accepts Date, ISO string, or unix time)
679
+
680
+ Binary parameters (important):
681
+
682
+ - Databricks SQL Warehouse doesn't support `BINARY` as a parameter type.
683
+ - `sql.binary(value)` returns a **STRING marker containing hex**, so use `UNHEX(:param)` in SQL.
684
+ - `sql.binary` accepts `Uint8Array`, `ArrayBuffer`, or a hex string.
685
+
686
+ ### SQL result types (important)
687
+
688
+ Databricks SQL JSON results can return some numeric-like fields (especially `DECIMAL`) as strings. If a field behaves like a string at runtime, convert explicitly:
689
+
690
+ ```ts
691
+ const value = Number(row.amount);
692
+ ```
693
+
694
+ If you need more reliable numeric fidelity for large datasets, prefer `format: "ARROW"` and process Arrow on the client.
695
+
696
+ ### `connectSSE` (custom SSE connections)
697
+
698
+ For custom streaming endpoints (not analytics), use the `connectSSE` utility:
699
+
700
+ ```tsx
701
+ import { connectSSE } from "@databricks/appkit-ui/js";
702
+ import { useEffect, useState } from "react";
703
+
704
+ function useCustomStream(endpoint: string) {
705
+ const [messages, setMessages] = useState<string[]>([]);
706
+ const [connected, setConnected] = useState(false);
707
+
708
+ useEffect(() => {
709
+ const controller = new AbortController();
710
+
711
+ connectSSE({
712
+ url: endpoint,
713
+ payload: { key: "value" }, // optional: makes it a POST
714
+ onMessage: async ({ data }) => {
715
+ setConnected(true);
716
+ setMessages((prev) => [...prev, data]);
717
+ },
718
+ onError: (error) => {
719
+ console.error("SSE error:", error);
720
+ setConnected(false);
721
+ },
722
+ signal: controller.signal,
723
+ maxRetries: 3, // default: 3
724
+ retryDelay: 2000, // default: 2000ms (exponential backoff)
725
+ timeout: 300000, // default: 5 minutes
726
+ maxBufferSize: 1048576, // default: 1MB
727
+ });
728
+
729
+ return () => controller.abort();
730
+ }, [endpoint]);
731
+
732
+ return { messages, connected };
733
+ }
734
+ ```
735
+
736
+ Options:
737
+
738
+ - `url`: SSE endpoint URL (required)
739
+ - `payload`: Optional request body (if provided, uses POST; otherwise GET)
740
+ - `onMessage({ id, data })`: Called for each SSE message
741
+ - `onError(error)`: Called on connection errors
742
+ - `signal`: AbortSignal to cancel the connection
743
+ - `lastEventId`: Resume from a specific event ID
744
+ - `maxRetries`: Max retry attempts (default: 3)
745
+ - `retryDelay`: Base delay between retries in ms (default: 2000)
746
+ - `timeout`: Connection timeout in ms (default: 300000)
747
+ - `maxBufferSize`: Max buffer size in bytes (default: 1MB)
748
+
749
+ ### `ArrowClient` (advanced Arrow processing)
750
+
751
+ For low-level Arrow data handling:
752
+
753
+ ```tsx
754
+ import { ArrowClient } from "@databricks/appkit-ui/js";
755
+
756
+ // Process Arrow buffer
757
+ const table = await ArrowClient.processArrowBuffer(buffer);
758
+
759
+ // Fetch and process Arrow data in one call
760
+ const table = await ArrowClient.fetchAndProcessArrow(url, headers);
154
761
 
155
- const AppKit = await createApp({
762
+ // Extract fields from table
763
+ const fields = ArrowClient.extractArrowFields(table);
764
+ // → [{ name: "date", type: ... }, { name: "value", type: ... }]
765
+
766
+ // Extract columns as arrays
767
+ const columns = ArrowClient.extractArrowColumns(table);
768
+ // → { date: [...], value: [...] }
769
+
770
+ // Extract chart data
771
+ const { xData, yDataMap } = ArrowClient.extractChartData(table, "date", ["value", "count"]);
772
+ // → { xData: [...], yDataMap: { value: [...], count: [...] } }
773
+
774
+ // Auto-detect chart fields from Arrow table
775
+ const detected = ArrowClient.detectFieldsFromArrow(table);
776
+ // → { xField: "date", yFields: ["value"], chartType: "timeseries" }
777
+ ```
778
+
779
+ ### DataTable
780
+
781
+ `DataTable` is a production-ready table integrated with `useAnalyticsQuery`.
782
+
783
+ Key behaviors:
784
+
785
+ - `parameters` is required (use `{}` if none)
786
+ - Supports opinionated mode (auto columns) and full-control mode (`children(table)`)
787
+
788
+ ```tsx
789
+ import { DataTable } from "@databricks/appkit-ui/react";
790
+
791
+ export function UsersTable() {
792
+ return (
793
+ <DataTable
794
+ queryKey="users_list"
795
+ parameters={{}}
796
+ filterColumn="email"
797
+ filterPlaceholder="Filter by email..."
798
+ pageSize={25}
799
+ pageSizeOptions={[10, 25, 50, 100]}
800
+ />
801
+ );
802
+ }
803
+ ```
804
+
805
+ ### UI components (primitives)
806
+
807
+ AppKit-UI ships shadcn-style primitives. Import from `@databricks/appkit-ui/react`.
808
+
809
+ Note: Exact exports can vary by AppKit-UI version. Prefer using IDE auto-import/autocomplete to confirm what your installed version exports.
810
+
811
+ Radix constraint (common bug):
812
+
813
+ - `SelectItem` cannot have `value=""`. Use a sentinel value like `"all"` or `"none"`.
814
+
815
+ **Available components:**
816
+
817
+ `Accordion`, `Alert`, `AlertDialog`, `AspectRatio`, `Avatar`, `Badge`, `Breadcrumb`, `Button`, `ButtonGroup`, `Calendar`, `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter`, `Carousel`, `Checkbox`, `Collapsible`, `Command`, `ContextMenu`, `Dialog`, `DialogTrigger`, `DialogContent`, `DialogHeader`, `DialogTitle`, `DialogDescription`, `DialogFooter`, `Drawer`, `DropdownMenu`, `Empty`, `Field`, `Form`, `HoverCard`, `Input`, `InputGroup`, `InputOtp`, `Item`, `Kbd`, `Label`, `Menubar`, `NavigationMenu`, `Pagination`, `Popover`, `Progress`, `RadioGroup`, `Resizable`, `ScrollArea`, `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`, `Separator`, `Sheet`, `Sidebar`, `Skeleton`, `Slider`, `Sonner`, `Spinner`, `Switch`, `Table`, `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent`, `Textarea`, `Toggle`, `ToggleGroup`, `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider`
818
+
819
+ ### Card pattern
820
+
821
+ ```tsx
822
+ import {
823
+ Card,
824
+ CardHeader,
825
+ CardTitle,
826
+ CardDescription,
827
+ CardContent,
828
+ CardFooter,
829
+ } from "@databricks/appkit-ui/react";
830
+
831
+ function MetricCard({ title, value, description }: Props) {
832
+ return (
833
+ <Card>
834
+ <CardHeader>
835
+ <CardDescription>{description}</CardDescription>
836
+ <CardTitle className="text-3xl">{value}</CardTitle>
837
+ </CardHeader>
838
+ <CardContent>
839
+ {/* Optional content */}
840
+ </CardContent>
841
+ <CardFooter>
842
+ {/* Optional footer */}
843
+ </CardFooter>
844
+ </Card>
845
+ );
846
+ }
847
+ ```
848
+
849
+ ### Select pattern
850
+
851
+ ```tsx
852
+ import {
853
+ Select,
854
+ SelectTrigger,
855
+ SelectValue,
856
+ SelectContent,
857
+ SelectItem,
858
+ } from "@databricks/appkit-ui/react";
859
+
860
+ function DateRangeSelect({ value, onChange }: Props) {
861
+ return (
862
+ <Select value={value} onValueChange={onChange}>
863
+ <SelectTrigger className="w-40">
864
+ <SelectValue placeholder="Select range" />
865
+ </SelectTrigger>
866
+ <SelectContent>
867
+ <SelectItem value="7d">Last 7 days</SelectItem>
868
+ <SelectItem value="30d">Last 30 days</SelectItem>
869
+ <SelectItem value="90d">Last 90 days</SelectItem>
870
+ </SelectContent>
871
+ </Select>
872
+ );
873
+ }
874
+ ```
875
+
876
+ ### Tabs pattern
877
+
878
+ ```tsx
879
+ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@databricks/appkit-ui/react";
880
+
881
+ function Dashboard() {
882
+ return (
883
+ <Tabs defaultValue="overview">
884
+ <TabsList>
885
+ <TabsTrigger value="overview">Overview</TabsTrigger>
886
+ <TabsTrigger value="analytics">Analytics</TabsTrigger>
887
+ </TabsList>
888
+ <TabsContent value="overview">
889
+ <p>Overview content</p>
890
+ </TabsContent>
891
+ <TabsContent value="analytics">
892
+ <p>Analytics content</p>
893
+ </TabsContent>
894
+ </Tabs>
895
+ );
896
+ }
897
+ ```
898
+
899
+ ### Dialog pattern
900
+
901
+ ```tsx
902
+ import {
903
+ Dialog,
904
+ DialogTrigger,
905
+ DialogContent,
906
+ DialogHeader,
907
+ DialogTitle,
908
+ DialogDescription,
909
+ DialogFooter,
910
+ Button,
911
+ } from "@databricks/appkit-ui/react";
912
+
913
+ function ConfirmDialog() {
914
+ return (
915
+ <Dialog>
916
+ <DialogTrigger asChild>
917
+ <Button variant="destructive">Delete</Button>
918
+ </DialogTrigger>
919
+ <DialogContent>
920
+ <DialogHeader>
921
+ <DialogTitle>Confirm deletion</DialogTitle>
922
+ <DialogDescription>
923
+ This action cannot be undone.
924
+ </DialogDescription>
925
+ </DialogHeader>
926
+ <DialogFooter>
927
+ <Button variant="outline">Cancel</Button>
928
+ <Button variant="destructive">Delete</Button>
929
+ </DialogFooter>
930
+ </DialogContent>
931
+ </Dialog>
932
+ );
933
+ }
934
+ ```
935
+
936
+ ### TooltipProvider requirement
937
+
938
+ If using tooltips anywhere in your app, wrap your root component with `TooltipProvider`:
939
+
940
+ ```tsx
941
+ import { TooltipProvider } from "@databricks/appkit-ui/react";
942
+
943
+ function App() {
944
+ return (
945
+ <TooltipProvider>
946
+ {/* Your app content */}
947
+ </TooltipProvider>
948
+ );
949
+ }
950
+ ```
951
+
952
+ ### Button variants
953
+
954
+ ```tsx
955
+ import { Button } from "@databricks/appkit-ui/react";
956
+
957
+ <Button variant="default">Primary</Button>
958
+ <Button variant="secondary">Secondary</Button>
959
+ <Button variant="outline">Outline</Button>
960
+ <Button variant="ghost">Ghost</Button>
961
+ <Button variant="destructive">Destructive</Button>
962
+ <Button variant="link">Link</Button>
963
+ ```
964
+
965
+ ### Loading skeleton pattern
966
+
967
+ ```tsx
968
+ import { Card, CardHeader, Skeleton } from "@databricks/appkit-ui/react";
969
+
970
+ function LoadingCard() {
971
+ return (
972
+ <Card>
973
+ <CardHeader>
974
+ <Skeleton className="h-4 w-24 mb-2" />
975
+ <Skeleton className="h-8 w-20 mb-2" />
976
+ <Skeleton className="h-4 w-28" />
977
+ </CardHeader>
978
+ </Card>
979
+ );
980
+ }
981
+ ```
982
+
983
+ ## Type generation (QueryRegistry + IntelliSense)
984
+
985
+ Goal: generate `client/src/appKitTypes.d.ts` so query keys, params, and result rows are type-safe.
986
+
987
+ ### Vite plugin: `appKitTypesPlugin`
988
+
989
+ Correct option names:
990
+
991
+ - `outFile?: string` (default `src/appKitTypes.d.ts`)
992
+ - `watchFolders?: string[]` (default `["../config/queries"]`)
993
+
994
+ ```ts
995
+ // client/vite.config.ts
996
+ import { defineConfig } from "vite";
997
+ import react from "@vitejs/plugin-react";
998
+ import { appKitTypesPlugin } from "@databricks/appkit";
999
+
1000
+ export default defineConfig({
156
1001
  plugins: [
157
- server({ port: 8000 }),
158
- analytics(),
159
- openWeather(),
1002
+ react(),
1003
+ appKitTypesPlugin({
1004
+ outFile: "src/appKitTypes.d.ts",
1005
+ watchFolders: ["../config/queries"],
1006
+ }),
160
1007
  ],
161
1008
  });
1009
+ ```
162
1010
 
163
- const app = await AppKit.server.start();
164
- /**
165
- * A route could also be added here
166
- */
167
- app.get("/api/open-weather/weather", async (req, res) => {
168
- const data = await AppKit.openWeather.getWeather(40.7128, -74.0060);
169
- res.send(data);
170
- });
1011
+ Important nuance:
1012
+
1013
+ - When the frontend is served through AppKit in dev mode, AppKit’s dev server already includes `appKitTypesPlugin()` internally.
1014
+ - You still want it in your client build pipeline if you run `vite build` separately.
1015
+
1016
+ ### CLI: `appkit-generate-types`
1017
+
1018
+ ```bash
1019
+ # Requires DATABRICKS_WAREHOUSE_ID (or pass as 3rd arg)
1020
+ npx appkit-generate-types [rootDir] [outFile] [warehouseId]
1021
+
1022
+ # Example:
1023
+ npx appkit-generate-types . client/src/appKitTypes.d.ts
1024
+
1025
+ # Force regeneration (skip cache):
1026
+ npx appkit-generate-types --no-cache
171
1027
  ```
172
1028
 
1029
+ ## Databricks Apps config: `app.yaml`
1030
+
1031
+ Bind a SQL warehouse for Apps runtime:
1032
+
1033
+ ```yaml
1034
+ env:
1035
+ - name: DATABRICKS_WAREHOUSE_ID
1036
+ valueFrom: sql-warehouse
1037
+ ```
1038
+
1039
+ Full example with command:
1040
+
1041
+ ```yaml
1042
+ command:
1043
+ - node
1044
+ - build/index.mjs
1045
+ env:
1046
+ - name: DATABRICKS_WAREHOUSE_ID
1047
+ valueFrom: sql-warehouse
1048
+ ```
1049
+
1050
+ ## LLM checklist (before you "finalize" code)
1051
+
1052
+ - **Project setup**
1053
+ - `package.json` has `"type": "module"`
1054
+ - `tsx` is in devDependencies for dev server
1055
+ - `dev` script uses `NODE_ENV=development tsx watch server/index.ts`
1056
+ - `client/index.html` exists with `<div id="root"></div>` and script pointing to `client/src/main.tsx`
1057
+ - `client/package.json` exists and includes `@databricks/appkit-ui`
1058
+
1059
+ - **Backend**
1060
+ - `await createApp({ plugins: [...] })` is used (or `void createApp` with intent)
1061
+ - `server()` is included (always)
1062
+ - If using SQL: `analytics({})` included + `config/queries/*.sql` present
1063
+ - Queries use `:param` placeholders, and params are passed from UI using `sql.*`
1064
+ - If query needs workspace scoping: uses `:workspaceId`
173
1065
 
174
- # =====================
175
- # Style Guidelines for AI
176
- # =====================
177
- - Always prefer async/await (never .then chaining in examples).
178
- - Always show explicit plugin config (no hidden defaults).
179
- - Use ESModules (import/export), not require().
180
- - Use TypeScript typings in advanced examples if helpful.
1066
+ - **Frontend**
1067
+ - `useMemo` wraps parameters objects
1068
+ - Loading/error/empty states are explicit
1069
+ - Charts use `format="auto"` unless you have a reason to force `"json"`/`"arrow"`
1070
+ - If using tooltips: root is wrapped with `<TooltipProvider>`
181
1071
 
182
- # =====================
183
- # Anti-Patterns (avoid in examples)
184
- # =====================
185
- - Do not access AppKit internals (only use AppKit[pluginName]).
186
- - Do not assume SQL queries hit a real DB (they return demo data unless configured).
187
- - ❌ Do not show usage without createApp first.
1072
+ - **Never**
1073
+ - Don't build SQL strings manually
1074
+ - Don't pass untyped raw params for annotated queries
1075
+ - Don't ignore `createApp()`'s promise
1076
+ - Don't invent UI components not listed in this file
188
1077
 
189
- # =====================
190
- # Attribution
191
- # =====================
192
- If AI-generated code uses this SDK, attribute:
193
- "Powered by Databricks AppKit (https://github.com/...)".