Skip to content

feat(appkit-ui): call named database operations from the client and hooks - #691

Draft
ditadi wants to merge 1 commit into
stack/database-stable/04-operation-generationfrom
stack/database-stable/05-client-operations
Draft

ditadi wants to merge 1 commit into
stack/database-stable/04-operation-generationfrom
stack/database-stable/05-client-operations

Conversation

@ditadi

@ditadi ditadi commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What did you change, and why?

Why: Browser code needs to call operations by name with generated types, and its state must not outlive a session.

Change (additive):

  • Adds databaseApi.execute(name, input, { signal }), useDatabaseQuery for GET operations, and useDatabaseMutation for writes.
  • A name the server did not publish fails with NOT_EXPOSED without a request. A changed caller or a 401 clears hook state, and a late answer fails with SESSION_CHANGED. A write without a usable answer reports OUTCOME_UNKNOWN. A mutation never retries and restarts the mounted queries.
  • entity-client.ts is the previous client.ts, moved verbatim, and the entity hooks import it from there. databaseApi keeps its entity calls until the next PR.
  • Safe to deploy alone: no server publishes operations yet, and the entity client and hooks are unchanged.

Interface

New in @databricks/appkit-ui/js/beta and @databricks/appkit-ui/react/beta. Names, inputs, and outputs come from the generated DatabricksAppKitDatabaseOperations.

databaseApi.execute<N extends DatabaseOperationName>(
  name: N,
  input: DatabaseOperationInput<N>,
  options?: { signal?: AbortSignal },
): Promise<DatabaseOperationOutput<N>>;

// GET operations only.
function useDatabaseQuery<N extends DatabaseQueryName>(
  name: N,
  input: DatabaseOperationInput<N>,
  options?: { enabled?: boolean },
): {
  data: DatabaseOperationOutput<N> | undefined;
  isLoading: boolean;
  error: DatabaseApiError | null;
  refetch: () => void;
};

// POST, PATCH, PUT, and DELETE operations. Mounting sends nothing.
function useDatabaseMutation<N extends DatabaseMutationName>(name: N): {
  // Never rejects and never retries.
  mutate: (input: DatabaseOperationInput<N>) => Promise<
    | { data: DatabaseOperationOutput<N>; error: null }
    | { data: undefined; error: DatabaseApiError }
  >;
  data: DatabaseOperationOutput<N> | undefined;
  isLoading: boolean;
  error: DatabaseApiError | null;
  reset: () => void;
};

type DatabaseApiErrorCode =
  | DatabaseErrorCategory
  | "NOT_EXPOSED" // the server did not publish the name; nothing was sent
  | "UNAUTHENTICATED"
  | "SESSION_CHANGED" // the answer belongs to an earlier actor
  | "OUTCOME_UNKNOWN"; // a write may have committed without a usable answer

Example, as the playground uses it in the last PR of this stack:

const notes = useDatabaseQuery("notes.list", { board_id: boardId });
const createNote = useDatabaseMutation("notes.create");

async function add(body: string) {
  const outcome = await createNote.mutate({ board_id: boardId, body });
  if (outcome.error === null) setDraft(""); // mounted queries reload after a success
}

useDatabaseMutation("notes.list"); // type error: a GET operation is not a mutation

How do you know it works?

Testing: The client unit and HTTP tests, the query and mutation hook tests, and the hook type tests cover the new API over a generated contract. The entity client and hook suites pass unchanged apart from the moved import. appkit-ui passes (37 files, 769 tests), and the playground's typecheck:database passes against the built package.


Stack created with GitHub Stacks CLI • Give Feedback 💬

…ooks

Add databaseApi.execute, useDatabaseQuery, and useDatabaseMutation.

A name the server did not publish fails with NOT_EXPOSED and sends

nothing. Answers carry the actor discriminator, so a changed session

clears every query, and a mutation restarts mounted queries.

The entity client and hooks stay until the plugin serves operations.

Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi added this pull request to stack #693 October 23, 2026 15:01
@github-actions

Copy link
Copy Markdown
Contributor

📦 Bundle size report

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

@databricks/appkit

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

dist raw gzip
JS (runtime) 1.3 MB (+21 KB) 466 KB (+8.7 KB)
Type declarations 478 KB (+14 KB) 173 KB (+4.7 KB)
Source maps 2.6 MB (+59 KB) 876 KB (+20 KB)
Other 11 KB 3.7 KB
Total 4.3 MB (+95 KB) 1.5 MB (+33 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 111 KB (+4.6 KB) 2.5 KB (+1 B) 113 KB (+4.6 KB) external 357 KB (+13 KB)
./beta 102 KB (-567 B) 485 B 102 KB (-567 B) external 306 KB (-2.4 KB)
./testing 45 KB (+271 B) 37 KB (+4.3 KB) 82 KB (+4.6 KB) external 237 KB (+13 KB)
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 30 KB (+4.2 KB) 0 B 30 KB (+4.2 KB) external 87 KB (+12 KB)

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 106 KB
. utils.js initial 4.6 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 82 KB
./beta modular.js initial 6.0 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 client.js initial 647 B
./beta index.js initial 20 B
./beta supervisor-api.js lazy 192 B
./beta databricks.js lazy 178 B
./beta index.js lazy 115 B
./testing manifest.js initial 32 KB
./testing index.js initial 10 KB
./testing wide-event-emitter.js initial 2.9 KB
./testing index.js lazy 32 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 30 KB

@databricks/appkit-ui

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

dist raw gzip
JS (runtime) 446 KB (+17 KB) 153 KB (+6.5 KB)
Type declarations 276 KB (+8.4 KB) 102 KB (+3.5 KB)
Source maps 888 KB (+35 KB) 298 KB (+13 KB)
CSS 16 KB 3.2 KB
Total 1.6 MB (+60 KB) 557 KB (+23 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 3.4 KB (+1.3 KB) 0 B 3.4 KB (+1.3 KB) 0 B 9.2 KB (+4.2 KB)
./react 433 KB 49 KB 481 KB 1.3 MB 177 KB
./react/beta 6.7 KB (+1.9 KB) 0 B 6.7 KB (+1.9 KB) 0 B 19 KB (+6.2 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 3.4 KB
./react index.js initial 431 KB
./react tslib initial 2.1 KB
./react apache-arrow lazy 49 KB
./react/beta beta.js initial 6.7 KB

@github-actions

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 39974615992 -R databricks/appkit -n appkit-template-0.89.0-pr.c0900cd-stack-database-stable-05-client-operations-691 -D appkit-pr-691 \
  && unzip -o "appkit-pr-691/appkit-template-0.89.0-pr.c0900cd-stack-database-stable-05-client-operations-691.zip" -d "appkit-pr-691" \
  && databricks apps init --template "appkit-pr-691"

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.

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