feat(appkit): add database mutations and transactional hooks - #528
Open
ditadi wants to merge 6 commits into
Open
feat(appkit): add database mutations and transactional hooks#528ditadi wants to merge 6 commits into
ditadi wants to merge 6 commits into
Conversation
This was referenced Aug 10, 2026
ditadi
force-pushed
the
stack/database-mvp/04-mutations-hooks
branch
from
August 28, 2026 15:11
fe60ed6 to
d75540a
Compare
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
force-pushed
the
stack/database-mvp/04-mutations-hooks
branch
from
August 31, 2026 10:57
3da27b2 to
60ea7e3
Compare
Contributor
📦 Bundle size reportCompared against
|
| 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 |
Contributor
🤖 AppKit PR bot🔬 Run evalsStart an eval for this PR from the evals-monitor app: Go to Evals Monitor → 📦 Try this PR's app templateScaffolds a new app from this PR's SDK build. Run it in any folder (requires the GitHub 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 |
MarioCadenas
reviewed
Sep 4, 2026
Signed-off-by: ditadi <victordperd@gmail.com>
Signed-off-by: ditadi <victordperd@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up status
The
apirename, 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 showcrudRoutesand opt-in writes.Stack
This PR now targets
main.What
Adds
create,update,upsert, anddeleteto 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, replacingcrudRoutes: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
crudRoutesoption 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, andafter*. Writes throughctx.app.databasejoin the same transaction and commit or roll back together. This does not extend transactions to other plugins or external services.An instance-owned
AsyncLocalStoragekeeps 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_timeoutand a 30-secondidle_in_transaction_session_timeoutthrough 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
DatabaseValidationErrorproduces 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.createandupsertreject 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 withapi: 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.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.No commit or push has been made for the local follow-up yet.