Skip to content

feat(appkit-ui): add typed databaseApi client for DatabasePlugin routes - #608

Open
ditadi wants to merge 5 commits into
mainfrom
stack/database-hooks/01-client
Open

ditadi wants to merge 5 commits into
mainfrom
stack/database-hooks/01-client

Conversation

@ditadi

@ditadi ditadi commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Why the change

Browser code that calls the /api/database routes DatabasePlugin generates had to hand-write URLs, query encoding, and row types, so this adds databaseApi, a typed client (beta) that gets its types from the same generated database.d.ts the server uses.

Special things to note

  • The generated database.d.ts now also augments @databricks/appkit-ui/js/beta, even in apps that don't install appkit-ui. Those apps rely on skipLibCheck, as they already do for analytics.d.ts, and a generator test covers that case. Apps pick up the new api facet the next time they regenerate types.
  • CI's playground job gains a typecheck:database step. It type-checks the playground's database components against appkit-ui's built dist declarations, which proves the generated types reach them; the playground client has no other typecheck.
  • feat(appkit-ui): add database React hooks with read invalidation #609, stacked on this PR, adds the React hooks. Like feat(appkit): report schema drift and Lakebase host mismatches at startup #607, this PR commits the regenerated Interface.PluginManifest.md that CI's generated-docs check needs. The change is identical in both PRs, so they merge cleanly in either order.

Change outline

There is still one schema and one generated file. Each entry gains an api facet that describes what the HTTP routes accept, and the same entries are bound into both packages (playground notes shown):

 // shared/appkit-types/database.d.ts (generated)
 import "@databricks/appkit";
+import "@databricks/appkit-ui/js/beta";

-declare module "@databricks/appkit" {
-  interface DatabaseRegistry {
+interface GeneratedDatabaseRegistry {
   "notes": {
     row; publicRow; insert; update; filters; includes; hasPrimaryKey;   // trusted facets, unchanged
+    api: {
+      insert: { board_id: number; author: string; body: string; created_at?: string };
+      update: { board_id?: number; author?: string; body?: string };
+      filters: DatabaseLogicalFilter<{ id?; board_id?; author?; body?; created_at? }>;
+      orderable: "id" | "board_id" | "author" | "body" | "created_at";
+      key: "id";   // `never` when the key is private or missing
+    };
   };
-  }
 }
+declare module "@databricks/appkit"           { interface DatabaseRegistry extends GeneratedDatabaseRegistry {} }
+declare module "@databricks/appkit-ui/js/beta" { interface DatabaseRegistry extends GeneratedDatabaseRegistry {} }

author_email is private, so it appears in the trusted facets but in no api facet. The route compiler and the type generator now read that rule from one function, so the browser types can't drift from what the server accepts:

columnHttpCapabilities(meta) → { selectable, queryable, creatable, updatable, publicKey }
  ├── compileTable()   → columns the CRUD routes accept (behavior unchanged)
  └── walkSchema()     → the `api` facet in database.d.ts

shared holds what both sides must agree on, and appkit-ui adds the client on top of it:

 packages/
 ├── shared/src/database/
+│   ├── api-types.ts          # generic param/row types over any registry; bigint travels as a string
+│   ├── query-codec.ts        # list/detail query encoder, round-trip tested against the server decoder
+│   └── errors.ts             # status → category table, moved out of appkit
 ├── appkit/src/
+│   ├── plugins/database/crud/capabilities.ts
 │   ├── plugins/database/crud/contract.ts       # compileTable delegates to capabilities
 │   └── type-generator/database/                # emits `api`, binds into both modules
 └── appkit-ui/src/js/
     ├── beta.ts                                 # exports databaseApi, DatabaseApiError, types
+    └── database/                               # client, errors, registry binding target, types

The new public surface. Private columns, undeclared keys, keyed calls on keyless tables, and limit on to-one includes are all compile errors:

// @databricks/appkit-ui/js/beta — each call rejects with DatabaseApiError { code, status, details }
databaseApi.list(entity, params?, { signal? })        // → { items, limit, offset }
databaseApi.get(entity, id, params?, { signal? })     // → row; keyed entities only
databaseApi.create(entity, values, { signal? })       // → created public row
databaseApi.update(entity, id, values, { signal? })   // → updated public row
databaseApi.remove(entity, id, { signal? })           // → void (204)

Every call goes through the same path:

databaseApi.list("notes", params)
  resolveDatabaseUrl("notes", "list", query)   # route from the boot payload; unpublished → NOT_EXPOSED, no request
  requestDatabase(url, init, accept)           # fetch, then decode the body or { error, details } → DatabaseApiError

The playground's BoardExplorer loads its board and note lists through databaseApi.list. The database plugin docs gain a "Browser client (beta)" section.

This pull request and its description were written by Isaac.

ditadi and others added 4 commits September 26, 2026 22:04
Generate an HTTP-safe `api` facet per entity from the same column
capability predicates the server uses to compile its CRUD contract, and
bind the generated entries into both @databricks/appkit and
@databricks/appkit-ui/js/beta. The status-to-category table and the
list/record query encoder move to `shared` so both sides agree on them.

`databaseApi.list` resolves routes from the boot payload, fails locally
with NOT_EXPOSED for routes the server has not published, and throws a
typed DatabaseApiError. The playground's BoardExplorer lists boards and
notes through it, and CI type-checks the database components against the
built appkit-ui declarations.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: ditadi <victordperd@gmail.com>
Complete the typed browser client with the detail route and the three
write routes. `get`, `update`, and `remove` substitute the encoded id into
the published `:id` route and exist only for entities with a public key.
Writes send a JSON body with bigint values as decimal strings and resolve
with the public row the server answers with; `remove` expects a 204.

Create and update values reject private, generated, and undeclared fields
at compile time, including fields a spread carries in, and JSON columns
accept any object value.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: ditadi <victordperd@gmail.com>
Add a "Browser client (beta)" section to the database plugin page: setup
through the generated database.d.ts, the five databaseApi calls and their
routes, the accepted parameters and values, and the DatabaseApiError codes.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: ditadi <victordperd@gmail.com>
The serving deprecation added `PluginManifest.deprecated` without
regenerating docs/docs/api, so the docs build left a diff that fails CI's
generated-docs check on every PR that touches packages/.

Co-authored-by: Isaac <no-reply@databricks.com>
Signed-off-by: ditadi <victordperd@gmail.com>
@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

📦 Bundle size report

Compared against bundle-size-baseline.json (main).

@databricks/appkit

npm tarball (packed): 1.2 MB (+3.0 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 1.2 MB (+3.0 KB) 426 KB (+1.4 KB)
Type declarations 443 KB 161 KB (+3 B)
Source maps 2.3 MB (+6.1 KB) 800 KB (+2.4 KB)
Other 11 KB 3.7 KB
Total 4.0 MB (+9.0 KB) 1.4 MB (+3.8 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 96 KB (+376 B) 2.5 KB 99 KB (+376 B) external 315 KB (+988 B)
./beta 93 KB (+100 B) 457 B 94 KB (+100 B) external 281 KB (+279 B)
./testing 38 KB (-1 B) 31 KB (+347 B) 69 KB (+346 B) external 202 KB (+988 B)
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 23 KB (+387 B) 0 B 23 KB (+387 B) external 66 KB (+984 B)

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 92 KB
. utils.js initial 4.0 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 77 KB
./beta stream-manager.js initial 5.8 KB
./beta wide-event-emitter.js initial 3.2 KB
./beta databricks.js initial 3.2 KB
./beta configuration.js initial 2.3 KB
./beta service-context.js initial 1.3 KB
./beta client.js initial 434 B
./beta client-options.js initial 219 B
./beta supervisor-api.js lazy 192 B
./beta databricks.js lazy 142 B
./beta index.js lazy 123 B
./testing manifest.js initial 26 KB
./testing index.js initial 10.0 KB
./testing wide-event-emitter.js initial 2.9 KB
./testing index.js lazy 27 KB
./testing remote-tunnel-manager.js lazy 2.5 KB
./testing utils.js lazy 1.2 KB
./tsdown index.js initial 520 B
./type-generator index.js initial 23 KB

@databricks/appkit-ui

npm tarball (packed): 367 KB (+17 KB) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 404 KB (+9.5 KB) 136 KB (+3.9 KB)
Type declarations 247 KB (+18 KB) 90 KB (+6.2 KB)
Source maps 796 KB (+29 KB) 263 KB (+10.0 KB)
CSS 16 KB 3.2 KB
Total 1.4 MB (+56 KB) 493 KB (+20 KB)
Per-entry composition (consumer bundle — deps bundled, peerDeps external)
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
./js 5.3 KB 49 KB 55 KB 208 KB 14 KB
./js/beta 1.8 KB (+1.8 KB) 0 B 1.8 KB (+1.8 KB) 0 B 4.1 KB (+4.1 KB)
./react 432 KB 49 KB 481 KB 1.3 MB 177 KB
./react/beta 1.0 KB 0 B 1.0 KB 0 B 1.9 KB

Chunks:

Entry Chunk Load Size (gz)
./js index.js initial 5.2 KB
./js chunk initial 120 B
./js apache-arrow lazy 49 KB
./js/beta beta.js initial 1.8 KB
./react index.js initial 430 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 1.0 KB

@github-actions

github-actions Bot commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

🤖 AppKit PR bot

🔬 Run evals

Start an eval for this PR from the evals-monitor app: Go to Evals Monitor →

📦 Try this PR's app template

Scaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub CLI — gh auth login — and the Databricks CLI):

gh run download 36414162330 -R databricks/appkit -n appkit-template-0.78.0-pr.983a6b7-stack-database-hooks-01-client-608 -D appkit-pr-608 \
  && unzip -o "appkit-pr-608/appkit-template-0.78.0-pr.983a6b7-stack-database-hooks-01-client-608.zip" -d "appkit-pr-608" \
  && databricks apps init --template "appkit-pr-608"

The template pins @databricks/appkit and @databricks/appkit-ui to tarballs built from this branch, so the scaffolded app runs against this PR's code.

Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi requested a review from atilafassina September 28, 2026 12:53

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant