Skip to content

feat(appkit): add database mutations and transactional hooks - #528

Open
ditadi wants to merge 6 commits into
mainfrom
stack/database-mvp/04-mutations-hooks
Open

feat(appkit): add database mutations and transactional hooks#528
ditadi wants to merge 6 commits into
mainfrom
stack/database-mvp/04-mutations-hooks

Conversation

@ditadi

@ditadi ditadi commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Follow-up status

The api rename, CRUD-on default, configuration diagnostics, transaction predicate fix, and pool timeout forwarding described below are implemented and validated locally, but have not been pushed yet. The current GitHub head may still show crudRoutes and opt-in writes.

Stack

This PR now targets main.

What

Adds create, update, upsert, and delete to the typed entity API, plus transactional before/after hooks. database({ schema }) generates full HTTP CRUD by default. No separate write opt-in is required.

The public configuration option is api, replacing crudRoutes:

// Full generated CRUD for every declared table.
database({ schema });

// Disable generated routes, preserving the server-side client.
database({ schema, api: false });

// Read-only routes.
database({ schema, api: { writes: false } });

// Full CRUD for selected tables.
database({ schema, api: { tables: ["notes"] } });

// Reads, create, and update without delete.
database({ schema, api: { writes: { operations: ["create", "update"] } } });

Access model

The database plugin executes as the app's service principal in deployed Databricks Apps. OBO is not supported and is not planned for this plugin. Generated routes do not implement per-user or per-row authorization. Every caller who can reach them receives every enabled operation on every exposed row.

CRUD-on is the intended AppKit default, consistent with its opinionated, plug-and-play application model. It does not imply row-level isolation. Applications with narrower access requirements must restrict or disable generated routes and implement authorization in custom server routes. App admission and least-privilege database grants remain application/deployment responsibilities.

Restrictions reject invalid configuration rather than silently enabling more routes. Unknown keys, invalid values, undeclared tables, duplicate names, and unsupported operations fail setup before pool creation. The removed crudRoutes option is rejected with migration guidance so an old opt-out cannot silently turn into full CRUD.

Changes

Hooks share the mutation's transaction

A hooked mutation opens its transaction before running before*, the write, and after*. Writes through ctx.app.database join the same transaction and commit or roll back together. This does not extend transactions to other plugins or external services.

An instance-owned AsyncLocalStorage keeps concurrent requests and plugin instances isolated. A shared budget caps each transaction at 100 database operations, including direct SQL. Nested mutations reject repeated entity/operation pairs and depth beyond 8 frames.

Transactions have a 30-second callback deadline. PostgreSQL also receives a 30-second statement_timeout and a 30-second idle_in_transaction_session_timeout through the Lakebase pool configuration. The JavaScript deadline is not cancellation of arbitrary hook code or external side effects.

Before-hook replacement values are revalidated against the trusted schema before persistence. An update or delete that matches no row throws inside the mutation transaction so hook writes roll back before the generated route returns 404.

Deliberate validation failures

DatabaseValidationError produces HTTP 422. Only issues naming public columns are returned, with at most 50 issues. Other hook failures remain opaque server errors.

HTTP writes and row integrity

Generated write allowlists exclude private columns and server-owned fields. Generated primary keys, including UUID primary keys with defaultRandom, cannot be supplied by HTTP callers. Natural primary keys may be supplied on create, but no primary key can be updated over HTTP.

Programmatic upsert excludes primary keys, server-generated fields, and the conflict target from the update values. There is no generated upsert route.

Unknown or read-only write fields are rejected rather than silently discarded. Rejections identify a field only when it is a public column; other fields use the generic ["body"] path.

Query state survives transaction redirection

Redirecting an entity mutation to a transaction client now clones the caller's query state. where() predicates remain attached to update/delete through hooks and explicit transactions without mutating the shared transaction client. Tests cover all five transaction paths identified in review.

create and upsert reject accumulated predicates rather than silently ignoring them.

Diagnostics and documentation

Invalid route names and configuration restrictions now produce actionable server-side setup errors. Errors name the problematic key or table and explain how to fix it. Non-routable tables are not silently omitted: rename them, select a routable subset with api.tables, or disable generated routes with api: false.

Client messages remain generic, and request boundaries strip setup diagnostics. The new database plugin guide documents the access model, defaults, restrictions, private columns, hooks, and timeout limitations. API references include the exported API configuration types.

Bounded responses

Success and error responses share the encoded byte budget. Error details are dropped if they exceed it. The row sanitizer uses null-prototype objects so JSON keys such as __proto__ remain data.

Local verification

  • pnpm exec vitest run --maxWorkers=4: 4,778 passed, 1 skipped.
  • Focused database and Lakebase suites: 255 passed.
  • pnpm build && pnpm docs:build: passed.
  • pnpm check:fix && pnpm -r typecheck: passed; lint reports four existing unused-import/variable warnings outside these changes. Untracked local scratch directories were excluded from formatting.
  • The initial test run with default concurrency hit a five-second timeout in an existing type-generator test. The full suite passed with four workers.

No commit or push has been made for the local follow-up yet.

Comment thread packages/appkit/src/plugins/database/crud/routes.ts
Comment thread packages/appkit/src/plugins/database/lifecycle.ts
Comment thread packages/appkit/src/plugins/database/scope.ts
Comment thread packages/appkit/src/plugins/database/entity-client.ts Outdated
Comment thread packages/appkit/src/plugins/database/crud/contract.ts Outdated
Comment thread packages/appkit/src/plugins/database/entity-client.ts
@ditadi
ditadi force-pushed the stack/database-mvp/04-mutations-hooks branch from fe60ed6 to d75540a Compare August 28, 2026 15:11
Base automatically changed from stack/database-mvp/03-crud-reads to main August 31, 2026 09:52
ditadi added 3 commits August 31, 2026 12:55
Extend the typed entity API and the generated routes with create, update, upsert, and
delete, and let an entity declare before/after hooks that run inside the mutation's own
transaction, so writes a hook issues commit or roll back with it. Keep the HTTP write
allowlist narrower than trusted code's: a key, a generated identity, and a materialized
stamp stay server-owned. Answer a hook's DatabaseValidationError with 422 carrying only
the issues that name a public column, and leave every other hook failure opaque.

Keyed mutations narrow by the accumulated predicate as find(id) already does, and an
insert that would silently drop one is rejected, so no terminal operation ignores
fluent state.

Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi force-pushed the stack/database-mvp/04-mutations-hooks branch from 3da27b2 to 60ea7e3 Compare August 31, 2026 10:57
@github-actions

github-actions Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size report

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

@databricks/appkit

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

dist raw gzip
JS (runtime) 1.1 MB (+21 KB) 404 KB (+6.9 KB)
Type declarations 412 KB (+6.2 KB) 148 KB (+2.4 KB)
Source maps 2.2 MB (+43 KB) 758 KB (+13 KB)
Other 11 KB 3.7 KB
Total 3.8 MB (+71 KB) 1.3 MB (+22 KB)
Per-entry composition (own code — deps external (as shipped))
Entry Initial (gz) Lazy (gz) Total (gz) node_modules (min) Own code (min)
. 95 KB (+119 B) 2.5 KB 98 KB (+119 B) external 313 KB (+321 B)
./beta 84 KB (+2.7 KB) 457 B 84 KB (+2.7 KB) external 251 KB (+9.4 KB)
./testing 17 KB (+32 B) 0 B 17 KB (+32 B) external 52 KB (+112 B)
./tsdown 520 B 0 B 520 B external 813 B
./type-generator 23 KB 0 B 23 KB external 65 KB

Chunks:

Entry Chunk Load Size (gz)
. index.js initial 91 KB
. utils.js initial 4.0 KB
. remote-tunnel-manager.js lazy 2.5 KB
./beta beta.js initial 67 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.1 KB
./beta service-context.js initial 1.3 KB
./beta client.js initial 434 B
./beta client-options.js initial 220 B
./beta supervisor-api.js lazy 192 B
./beta databricks.js lazy 142 B
./beta index.js lazy 123 B
./testing index.js initial 17 KB
./tsdown index.js initial 520 B
./type-generator index.js initial 23 KB

@databricks/appkit-ui

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

dist raw gzip
JS (runtime) 395 KB 132 KB
Type declarations 229 KB 84 KB (+1 B)
Source maps 766 KB 253 KB
CSS 16 KB 3.2 KB
Total 1.4 MB 473 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 20 B 0 B 20 B 0 B 0 B
./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 20 B
./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 Aug 31, 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 33734923500 -R databricks/appkit -n appkit-template-0.70.0-pr.0048d19-stack-database-mvp-04-mutations-hooks-528 -D appkit-pr-528 \
  && unzip -o "appkit-pr-528/appkit-template-0.70.0-pr.0048d19-stack-database-mvp-04-mutations-hooks-528.zip" -d "appkit-pr-528" \
  && databricks apps init --template "appkit-pr-528"

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.

Comment thread packages/appkit/src/plugins/database/entity-client.ts
Signed-off-by: ditadi <victordperd@gmail.com>
@ditadi
ditadi requested a review from MarioCadenas September 5, 2026 14:58
Signed-off-by: ditadi <victordperd@gmail.com>
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