Skip to content

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

Merged
ditadi merged 9 commits into
mainfrom
stack/database-hooks/01-client
Oct 7, 2026
Merged

ditadi merged 9 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.

@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 (+34 B) — gzipped download (dist + bin; excludes release-only docs/NOTICE).

dist raw gzip
JS (runtime) 1.2 MB (+86 B) 447 KB (+9 B)
Type declarations 455 KB 165 KB (+2 B)
Source maps 2.5 MB (+20 B) 837 KB (+13 B)
Other 11 KB 3.7 KB
Total 4.1 MB (+106 B) 1.4 MB (+24 B)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 102 KB (-1 B) 2.5 KB (-1 B) 105 KB (-2 B) external 331 KB
./beta 98 KB 484 B 99 KB external 297 KB
./testing 42 KB 32 KB (+3 B) 74 KB (+3 B) external 215 KB
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 23 KB 0 B 23 KB external 66 KB

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 97 KB
. utils.js initial 4.6 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 81 KB
./beta stream-manager.js initial 5.9 KB
./beta service-context.js initial 4.2 KB
./beta databricks.js initial 3.3 KB
./beta wide-event-emitter.js initial 3.2 KB
./beta client.js initial 594 B
./beta index.js initial 20 B
./beta supervisor-api.js lazy 192 B
./beta databricks.js lazy 178 B
./beta index.js lazy 114 B
./testing manifest.js initial 29 KB
./testing index.js initial 10 KB
./testing wide-event-emitter.js initial 2.9 KB
./testing index.js lazy 28 KB
./testing remote-tunnel-manager.js lazy 2.5 KB
./testing utils.js lazy 1.8 KB
./tsdown index.js initial 520 B
./type-generator index.js initial 23 KB

@databricks/appkit-ui

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

dist raw gzip
JS (runtime) 406 KB 137 KB
Type declarations 247 KB 90 KB (-1 B)
Source maps 798 KB 264 KB
CSS 16 KB 3.2 KB
Total 1.4 MB 494 KB (-1 B)
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 2.1 KB 0 B 2.1 KB 0 B 4.9 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 2.1 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 37657551065 -R databricks/appkit -n appkit-template-0.85.0-pr.62035b9-stack-database-hooks-01-client-608 -D appkit-pr-608 \
  && unzip -o "appkit-pr-608/appkit-template-0.85.0-pr.62035b9-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.

@ditadi
ditadi requested a review from atilafassina September 28, 2026 12:53
@ditadi
ditadi force-pushed the stack/database-hooks/01-client branch from 73e8abb to d9c15c5 Compare October 7, 2026 14:39
ditadi and others added 9 commits October 7, 2026 18:51
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>
Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
Acknowledge the new typed database client: the packed tarball grows by
19,271 bytes (5.4%). The js/beta consumer entry is 2,113 gzip bytes with
no bundled dependencies.

Generated from a clean build. The 5% and 10 KiB budget gates are unchanged.

Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi force-pushed the stack/database-hooks/01-client branch from d513e09 to de99f54 Compare October 7, 2026 17:14

@atilafassina atilafassina left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at de99f54 against main. The earlier findings are addressed: non-finite numbers are rejected on writes, dynamic select arrays now type as Partial, and boolean/JSON primary keys no longer publish routes. Empty where and empty update still compile but now fail with a clear INVALID_REQUEST. Database tests (355) and the appkit-ui and shared typechecks pass.

@ditadi
ditadi merged commit a0ab1b7 into main Oct 7, 2026
12 checks passed
@ditadi
ditadi deleted the stack/database-hooks/01-client branch October 7, 2026 20:08
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.

2 participants