From b1fc04995ce5dad17e22d1c5cf1a3266eda43fae Mon Sep 17 00:00:00 2001 From: ditadi Date: Tue, 5 May 2026 00:43:35 +0100 Subject: [PATCH 1/2] docs(database): add plugin guide and template schema stub --- .../Function.createLakebasePostgrestClient.md | 19 +++ docs/docs/api/appkit/Function.enumeration.md | 20 +++ ...Interface.LakebasePostgrestClientConfig.md | 46 ++++++ .../TypeAlias.LakebasePostgrestClient.md | 5 + .../appkit/TypeAlias.LakebaseTokenResolver.md | 15 ++ docs/docs/plugins/database.md | 153 ++++++++++++++++++ docs/static/appkit-ui/styles.gen.css | 15 ++ template/config/database/schema.ts | 22 +++ 8 files changed, 295 insertions(+) create mode 100644 docs/docs/api/appkit/Function.createLakebasePostgrestClient.md create mode 100644 docs/docs/api/appkit/Function.enumeration.md create mode 100644 docs/docs/api/appkit/Interface.LakebasePostgrestClientConfig.md create mode 100644 docs/docs/api/appkit/TypeAlias.LakebasePostgrestClient.md create mode 100644 docs/docs/api/appkit/TypeAlias.LakebaseTokenResolver.md create mode 100644 docs/docs/plugins/database.md create mode 100644 template/config/database/schema.ts diff --git a/docs/docs/api/appkit/Function.createLakebasePostgrestClient.md b/docs/docs/api/appkit/Function.createLakebasePostgrestClient.md new file mode 100644 index 000000000..34efd4487 --- /dev/null +++ b/docs/docs/api/appkit/Function.createLakebasePostgrestClient.md @@ -0,0 +1,19 @@ +# Function: createLakebasePostgrestClient() + +```ts +function createLakebasePostgrestClient(config: LakebasePostgrestClientConfig): unknown; +``` + +Create a Lakebase PostgREST client. + +## Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `config` | [`LakebasePostgrestClientConfig`](Interface.LakebasePostgrestClientConfig.md) | Configuration for creating a Lakebase PostgREST client. | + +## Returns + +`unknown` + +A Lakebase PostgREST client. diff --git a/docs/docs/api/appkit/Function.enumeration.md b/docs/docs/api/appkit/Function.enumeration.md new file mode 100644 index 000000000..2dfd97681 --- /dev/null +++ b/docs/docs/api/appkit/Function.enumeration.md @@ -0,0 +1,20 @@ +# Function: enumeration() + +```ts +function enumeration(name: string, values: readonly string[]): AppKitColumnChain; +``` + +Create an enum column. + +## Parameters + +| Parameter | Type | Description | +| ------ | ------ | ------ | +| `name` | `string` | The name of the enum. | +| `values` | readonly `string`[] | The values of the enum. | + +## Returns + +[`AppKitColumnChain`](Interface.AppKitColumnChain.md) + +The wrapped column chain. diff --git a/docs/docs/api/appkit/Interface.LakebasePostgrestClientConfig.md b/docs/docs/api/appkit/Interface.LakebasePostgrestClientConfig.md new file mode 100644 index 000000000..72e9e8a6c --- /dev/null +++ b/docs/docs/api/appkit/Interface.LakebasePostgrestClientConfig.md @@ -0,0 +1,46 @@ +# Interface: LakebasePostgrestClientConfig + +Configuration for creating a Lakebase PostgREST client. + +## Properties + +### dataApiUrl? + +```ts +optional dataApiUrl: string; +``` + +*** + +### fetch()? + +```ts +optional fetch: (input: string | URL | Request, init?: RequestInit) => Promise; +``` + +#### Parameters + +| Parameter | Type | +| ------ | ------ | +| `input` | `string` \| `URL` \| `Request` | +| `init?` | `RequestInit` | + +#### Returns + +`Promise`\<`Response`\> + +*** + +### resolveToken + +```ts +resolveToken: LakebaseTokenResolver; +``` + +*** + +### schema? + +```ts +optional schema: string; +``` diff --git a/docs/docs/api/appkit/TypeAlias.LakebasePostgrestClient.md b/docs/docs/api/appkit/TypeAlias.LakebasePostgrestClient.md new file mode 100644 index 000000000..382906a89 --- /dev/null +++ b/docs/docs/api/appkit/TypeAlias.LakebasePostgrestClient.md @@ -0,0 +1,5 @@ +# Type Alias: LakebasePostgrestClient + +```ts +type LakebasePostgrestClient = unknown; +``` diff --git a/docs/docs/api/appkit/TypeAlias.LakebaseTokenResolver.md b/docs/docs/api/appkit/TypeAlias.LakebaseTokenResolver.md new file mode 100644 index 000000000..728197252 --- /dev/null +++ b/docs/docs/api/appkit/TypeAlias.LakebaseTokenResolver.md @@ -0,0 +1,15 @@ +# Type Alias: LakebaseTokenResolver() + +```ts +type LakebaseTokenResolver = () => Promise; +``` + +A function that resolves a Lakebase token. + +The default `database` plugin runtime no longer uses the Data API path, +so this is reserved for callers that opt into the PostgREST client +directly via `createLakebasePostgrestClient`. + +## Returns + +`Promise`\<`string` \| `null`\> diff --git a/docs/docs/plugins/database.md b/docs/docs/plugins/database.md new file mode 100644 index 000000000..a4a82d041 --- /dev/null +++ b/docs/docs/plugins/database.md @@ -0,0 +1,153 @@ +--- +sidebar_position: 4 +--- + +# Database plugin (beta) + +The **database plugin** is the application-level layer over Lakebase. It owns +schema declaration, type generation, drift detection, auto-mounted CRUD +routes, and a typed `db` browser client — all driven by a single +`config/database/schema.ts`. + +> **Beta:** the manifest declares `stability: "beta"`. The CLI and runtime +> APIs are stable enough for non-critical workloads but may change before GA. + +**Key features:** + +- Single source of truth: `config/database/schema.ts` declares tables once. +- Auto-mounted REST surface at `/api/database/` per table. +- Typed `db.` browser client (no hand-written types). +- Live schema drift detection at boot — fail-closed in production. +- On-Behalf-Of (OBO) execution: `appkit.database..asUser(req)`. +- Optional Row-Level Security helpers via `appkit db rls`. + +## Basic usage + +```ts +// server +import { createApp, server } from "@databricks/appkit"; +import { database } from "@databricks/appkit/beta"; + +const app = await createApp({ plugins: [server(), database()] }); +const cases = await app.database.cases.where({ status: "New" }).limit(50).toArray(); +``` + +```ts +// browser (types come from the generated DatabaseRegistry) +import { db } from "@databricks/appkit-ui/js"; +const cases = await db.cases.where({ status: "New" }).limit(50).toArray(); +``` + +## Convention + +The plugin auto-loads `config/database/schema.ts` (one of these paths is +probed: `config/database/schema.ts`, `config/database/schema/index.ts`, or +the `dist/` build artifacts). + +```ts +// config/database/schema.ts +import { defineSchema, id, text, timestamp } from "@databricks/appkit"; + +export default defineSchema(({ table }) => ({ + user: table("user", { + id: id(), + email: text().notNull(), + createdAt: timestamp().defaultNow().notNull(), + }), +})); +``` + +## Auto-mounted routes + +Each table gets six conventional routes plus a metadata pair: + +| Method | Path | Purpose | +|--------|----------------------------------|------------------------------------| +| GET | `/api/database/` | List rows (filters, order, paging) | +| GET | `/api/database//count` | Count rows matching filters | +| GET | `/api/database//:id` | Find one row by primary key | +| POST | `/api/database/` | Create a row (upsert via `Prefer`) | +| PATCH | `/api/database//:id` | Update by primary key | +| DELETE | `/api/database//:id` | Delete by primary key | +| GET | `/api/database//_columns` | Public column metadata for forms | +| GET | `/api/database/_entities` | Discovery — list of entities | +| GET | `/api/database/_healthz` | Readiness probe (`SELECT 1`) | + +By default every verb runs OBO (on-behalf-of the forwarded user). Override +per-entity via the `http` config: + +```ts +database({ + http: { + user: { + list: "service", // service-principal + delete: false, // disable the DELETE route entirely + columns: "service" // override the metadata gate + }, + }, +}); +``` + +## CLI lifecycle + +```bash +npx appkit db init # one-command Lakebase onboarding +npx appkit db generate # scaffold a table (greenfield) +npx appkit db introspect # pull existing schema (brownfield) +npx appkit db migration generate # author a new SQL migration +npx appkit db migrate up # apply migrations (advisory-locked) +npx appkit db verify # detect drift between schema.ts and DB +npx appkit db rls # scaffold a Row-Level Security policy +``` + +`db migrate up` takes a Postgres advisory lock so two concurrent deploys +cannot race the same migration. The flag `--dry-run` prints the plan +without applying. + +`db init` prints an env-diff before touching `.env`, backs up the existing +file to `.env.bak`, and refuses to drop a branch under `--from reset` +without an interactive confirmation. + +## Hooks + +Add per-entity lifecycle hooks via `database({ hooks: { ... } })`: + +```ts +database({ + hooks: { + user: { + beforeCreate: async (data, ctx) => ({ ...data, createdBy: ctx.userId }), + afterCreate: async (row) => audit(row.id, "created"), + }, + }, +}); +``` + +`upsert` is a separate channel from `create` and `update` — `beforeUpsert` +does **not** fan out into `beforeCreate` / `beforeUpdate`. Use a shared +helper if you need the same logic in both branches. + +## OBO and forwarded headers + +Per-user execution reads `x-forwarded-email` and `x-forwarded-access-token` +from the request. The Databricks Apps gateway strips inbound copies and +injects authentic values, so the plugin trusts these headers in production. +In dev the same headers are accepted from anywhere so the local loop stays +unblocked. + +## Pool sizing + +The service-principal (SP) pool defaults to 10 connections. Per-user (OBO) +pools default to 4 connections each, and the registry caps at 25 distinct +users. Worst-case fan-out is therefore `(1 + 25) × 4 + 10 = 114` connections +per app instance — tune via `connection.max` and `oboPoolMax` for your +Lakebase tier. + +## Drift detection + +Boot fails closed in production when `schema.ts` and the live DB disagree on +column types or declared-but-missing tables. Additive drift (live-only +columns/tables) is logged as a warning so blue/green deploys aren't blocked. + +Customize with `database({ checkDrift: false })` to skip the check, or +`tolerateSetupFailure: true` to log-and-continue on schema-load errors. diff --git a/docs/static/appkit-ui/styles.gen.css b/docs/static/appkit-ui/styles.gen.css index a2192039d..77e7c7c6e 100644 --- a/docs/static/appkit-ui/styles.gen.css +++ b/docs/static/appkit-ui/styles.gen.css @@ -678,6 +678,9 @@ .max-h-80 { max-height: calc(var(--spacing) * 80); } + .max-h-\[85vh\] { + max-height: 85vh; + } .max-h-\[300px\] { max-height: 300px; } @@ -831,6 +834,9 @@ .max-w-\[calc\(100\%-2rem\)\] { max-width: calc(100% - 2rem); } + .max-w-full { + max-width: 100%; + } .max-w-max { max-width: max-content; } @@ -870,6 +876,9 @@ .min-w-\[var\(--radix-select-trigger-width\)\] { min-width: var(--radix-select-trigger-width); } + .min-w-full { + min-width: 100%; + } .flex-1 { flex: 1; } @@ -1462,6 +1471,9 @@ .py-2 { padding-block: calc(var(--spacing) * 2); } + .py-2\.5 { + padding-block: calc(var(--spacing) * 2.5); + } .py-3 { padding-block: calc(var(--spacing) * 3); } @@ -1540,6 +1552,9 @@ .align-middle { vertical-align: middle; } + .align-top { + vertical-align: top; + } .font-mono { font-family: var(--font-mono); } diff --git a/template/config/database/schema.ts b/template/config/database/schema.ts new file mode 100644 index 000000000..7c3e893d6 --- /dev/null +++ b/template/config/database/schema.ts @@ -0,0 +1,22 @@ +import { defineSchema } from "@databricks/appkit"; + +/** + * Application database schema. The database plugin auto-loads this file + * (see config/database/) and uses it as the single source of truth for + * - the typed `db.` browser client, + * - the auto-mounted `/api/database/` REST routes, + * - and runtime drift detection against the live Lakebase DB. + * + * Add tables under the returned object and run: + * npx appkit db migration generate + * npx appkit db migrate up + * + * Example: + * user: table("user", { + * id: id(), + * email: text().notNull(), + * createdAt: timestamp().defaultNow().notNull(), + * }), + */ +// biome-ignore lint/correctness/noEmptyPattern: schema is intentionally empty in the starter template. +export default defineSchema(({}) => ({})); From 97c45859711af358b096fab1e718b3ec45d3b6f3 Mon Sep 17 00:00:00 2001 From: ditadi Date: Thu, 7 May 2026 00:07:45 +0100 Subject: [PATCH 2/2] docs(database): tighten plugin guide and CLAUDE.md; small public-API polish --- CLAUDE.md | 14 ++ docs/docs/api/appkit/Interface.DataPath.md | 4 +- docs/docs/plugins/database.md | 121 +++++++++++++----- .../appkit/src/database/runtime/data-path.ts | 4 +- .../database/schema-builder/define-schema.ts | 12 +- .../src/plugins/database/entity-proxy.ts | 8 +- template/config/database/schema.ts | 25 ++-- 7 files changed, 133 insertions(+), 55 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index eaf504796..41579cde8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -267,6 +267,20 @@ const result = await pool.query('SELECT * FROM users'); **ORM Integration:** Works with Drizzle, Sequelize, TypeORM - see the `@databricks/lakebase` README and `apps/dev-playground/server/lakebase-examples/` for examples. +### Database Plugin + +Application-level layer over Lakebase (beta). Owns schema declaration, type generation, drift detection, auto-mounted CRUD routes, and a typed `db` browser client — all driven by `config/database/schema.ts`. See [`docs/docs/plugins/database.md`](./docs/docs/plugins/database.md) for the full guide. + +```typescript +import { createApp, server } from '@databricks/appkit'; +import { database } from '@databricks/appkit/beta'; + +const app = await createApp({ plugins: [server(), database()] }); +const cases = await app.database.cases.where({ status: 'New' }).limit(50).toArray(); +``` + +CLI: `npx appkit db init | introspect | migration generate | migrate up | rls | seed | setup:dev | types generate | verify`. + ### Frontend-Backend Interaction ``` diff --git a/docs/docs/api/appkit/Interface.DataPath.md b/docs/docs/api/appkit/Interface.DataPath.md index cba52e63c..46313e3b7 100644 --- a/docs/docs/api/appkit/Interface.DataPath.md +++ b/docs/docs/api/appkit/Interface.DataPath.md @@ -120,8 +120,8 @@ raw(strings: TemplateStringsArray, ...values: unknown[]): Promise; ``` Tagged-template SQL escape hatch. Values are bound as parameters; column -and identifier interpolation is intentionally not supported here — use -`getDrizzle()` from the plugin's exports for that case. +and identifier interpolation is intentionally not supported here — drop +to `appkit.database.getPool().query(...)` if you need that. #### Type Parameters diff --git a/docs/docs/plugins/database.md b/docs/docs/plugins/database.md index a4a82d041..8bb808fe3 100644 --- a/docs/docs/plugins/database.md +++ b/docs/docs/plugins/database.md @@ -4,6 +4,12 @@ sidebar_position: 4 # Database plugin (beta) + +:::warning Beta plugin +This plugin is currently **beta**. APIs may change between minor releases. Import from `@databricks/appkit/beta`. See [Plugin Stability Tiers](./stability.md). +::: + + The **database plugin** is the application-level layer over Lakebase. It owns schema declaration, type generation, drift detection, auto-mounted CRUD routes, and a typed `db` browser client — all driven by a single @@ -11,6 +17,7 @@ routes, and a typed `db` browser client — all driven by a single > **Beta:** the manifest declares `stability: "beta"`. The CLI and runtime > APIs are stable enough for non-critical workloads but may change before GA. +> See [Known limitations](#known-limitations-beta) for what is not yet covered. **Key features:** @@ -59,7 +66,7 @@ export default defineSchema(({ table }) => ({ ## Auto-mounted routes -Each table gets six conventional routes plus a metadata pair: +Each table gets six conventional routes plus discovery and health metadata: | Method | Path | Purpose | |--------|----------------------------------|------------------------------------| @@ -69,7 +76,6 @@ Each table gets six conventional routes plus a metadata pair: | POST | `/api/database/` | Create a row (upsert via `Prefer`) | | PATCH | `/api/database//:id` | Update by primary key | | DELETE | `/api/database//:id` | Delete by primary key | -| GET | `/api/database//_columns` | Public column metadata for forms | | GET | `/api/database/_entities` | Discovery — list of entities | | GET | `/api/database/_healthz` | Readiness probe (`SELECT 1`) | @@ -82,7 +88,6 @@ database({ user: { list: "service", // service-principal delete: false, // disable the DELETE route entirely - columns: "service" // override the metadata gate }, }, }); @@ -91,13 +96,16 @@ database({ ## CLI lifecycle ```bash -npx appkit db init # one-command Lakebase onboarding -npx appkit db generate # scaffold a table (greenfield) -npx appkit db introspect # pull existing schema (brownfield) -npx appkit db migration generate # author a new SQL migration -npx appkit db migrate up # apply migrations (advisory-locked) -npx appkit db verify # detect drift between schema.ts and DB -npx appkit db rls
# scaffold a Row-Level Security policy +npx appkit db init # one-command Lakebase onboarding +npx appkit db introspect # pull existing schema (brownfield) +npx appkit db migration generate # author a new SQL migration +npx appkit db migrate up # apply migrations (advisory-locked) +npx appkit db migrate status # list applied vs pending migrations +npx appkit db verify # detect drift between schema.ts and DB +npx appkit db rls # scaffold a Row-Level Security policy +npx appkit db seed # apply config/database/seed.sql +npx appkit db setup:dev # provision a per-user dev branch +npx appkit db types generate # regenerate typed client artifacts ``` `db migrate up` takes a Postgres advisory lock so two concurrent deploys @@ -110,44 +118,99 @@ without an interactive confirmation. ## Hooks -Add per-entity lifecycle hooks via `database({ hooks: { ... } })`: +`ctx.userId` is the forwarded email — a label, not authz; `undefined` under +SP. Guard before writing it as audit metadata: ```ts database({ hooks: { user: { - beforeCreate: async (data, ctx) => ({ ...data, createdBy: ctx.userId }), + beforeCreate: async (data, ctx) => ({ + ...data, + ...(ctx.userId ? { createdBy: ctx.userId } : {}), + }), afterCreate: async (row) => audit(row.id, "created"), }, }, }); ``` -`upsert` is a separate channel from `create` and `update` — `beforeUpsert` -does **not** fan out into `beforeCreate` / `beforeUpdate`. Use a shared -helper if you need the same logic in both branches. +`upsert` is its own channel — `beforeUpsert` / `afterUpsert` fire on +`create({ upsert: true })`; `beforeCreate` / `beforeUpdate` do **not**. + +## Row-Level Security + +`appkit db rls ` writes a numbered migration, registers it +in `meta/_journal.json`, and emits `ENABLE` + `FORCE ROW LEVEL SECURITY` +(Postgres bypasses RLS for table owners by default — `FORCE` covers the SP +pool). The first run also emits a helpers migration with `current_user_email()`, +which reads the `app.user_id` GUC AppKit `SET`s on every OBO connection +(rename via [`rls.sessionVariable`](#configuration)). + +```bash +npx appkit db rls case "owner_email:owner_email" # SELECT/UPDATE/DELETE +npx appkit db rls case "owner_email:owner_email" --action insert +npx appkit db rls case "tenant_id = current_setting('app.tenant_id')::uuid" +``` + +`owner_email:` expands to `= current_user_email()`. Anything else +is raw SQL (rejected on semicolons, comments, unbalanced parens). Use +`--dry-run` to preview without writing. + +`--action select,update` emits one policy per verb with derived names +(`_select`, `_update`); `all` is exclusive. ## OBO and forwarded headers -Per-user execution reads `x-forwarded-email` and `x-forwarded-access-token` -from the request. The Databricks Apps gateway strips inbound copies and -injects authentic values, so the plugin trusts these headers in production. -In dev the same headers are accepted from anywhere so the local loop stays -unblocked. +OBO reads `x-forwarded-email` and `x-forwarded-access-token`. The Databricks +Apps gateway strips inbound copies and injects authentic values; the plugin +trusts them in production. Dev accepts them from anywhere — **don't expose +the dev server beyond loopback** unless you front it with the same trust +boundary. ## Pool sizing -The service-principal (SP) pool defaults to 10 connections. Per-user (OBO) -pools default to 4 connections each, and the registry caps at 25 distinct -users. Worst-case fan-out is therefore `(1 + 25) × 4 + 10 = 114` connections -per app instance — tune via `connection.max` and `oboPoolMax` for your -Lakebase tier. +SP pool: 10. OBO pools: 2 connections each, registry capped at 100 users +(LRU). Worst-case fan-out per instance: `(1 + 100) × 2 + 10 = 212`. Tune via +`connection.max` and `oboPoolMax`. Lakebase's PgBouncer multiplexes client +connections, so effective headroom is larger than the raw tier limit. ## Drift detection Boot fails closed in production when `schema.ts` and the live DB disagree on column types or declared-but-missing tables. Additive drift (live-only -columns/tables) is logged as a warning so blue/green deploys aren't blocked. - -Customize with `database({ checkDrift: false })` to skip the check, or -`tolerateSetupFailure: true` to log-and-continue on schema-load errors. +columns/tables) is logged. Policies are not compared. + +`database({ checkDrift: false })` skips the check; +`tolerateSetupFailure: true` logs schema-load errors instead of throwing. + +## Configuration + +| Key | Default | Notes | +|----------------------------------|----------------|------------------------------------------------------------| +| `connection.max` | 10 | SP pool max connections | +| `oboPoolMax` | 100 | Distinct OBO pools kept alive (LRU evicts beyond this) | +| `statementTimeoutMs` | 15_000 | Server-side `statement_timeout` per pooled connection | +| `checkDrift` | `true` | Run drift introspection at boot | +| `tolerateSetupFailure` | `false` | Log instead of throw on schema-load / drift errors | +| `healthCheck` | enabled | Set `false` to suppress `/api/database/_healthz` | +| `entitiesDiscovery` | enabled | Set `false` to suppress `/api/database/_entities` | +| `rls.sessionVariable` | `"app.user_id"` | GUC name AppKit `SET`s on OBO connect (RLS reads it) | + +## `column.private()` — partial + +Filters the typegen registry, but row payloads from +`select`/`find`/`update().returning()` still include the value. **Treat as a +"hide from forms" hint, not authz** — keep true secrets in a separate table +with stricter ACLs. + +## Known limitations (beta) + +- **`column.private()` is a UX hint, not authz** — see above. +- **No policy drift detection** — `db verify` doesn't compare `pg_policies`. +- **Browser 404 semantics** — `db..find(missingId)` and + `update(missingId, ...)` return `null` (not throw). +- **`in` lists capped** — URL builder bounds `in` to stay under proxy + limits; partition large lists client-side. +- **Dev mode trusts forwarded headers from any source** — see *OBO and + forwarded headers*. diff --git a/packages/appkit/src/database/runtime/data-path.ts b/packages/appkit/src/database/runtime/data-path.ts index cabb16663..f2c716a0c 100644 --- a/packages/appkit/src/database/runtime/data-path.ts +++ b/packages/appkit/src/database/runtime/data-path.ts @@ -165,8 +165,8 @@ export interface DataPath { /** * Tagged-template SQL escape hatch. Values are bound as parameters; column - * and identifier interpolation is intentionally not supported here — use - * `getDrizzle()` from the plugin's exports for that case. + * and identifier interpolation is intentionally not supported here — drop + * to `appkit.database.getPool().query(...)` if you need that. */ raw( strings: TemplateStringsArray, diff --git a/packages/appkit/src/database/schema-builder/define-schema.ts b/packages/appkit/src/database/schema-builder/define-schema.ts index 02887b81b..910d1adb7 100644 --- a/packages/appkit/src/database/schema-builder/define-schema.ts +++ b/packages/appkit/src/database/schema-builder/define-schema.ts @@ -18,16 +18,16 @@ export interface DefineSchemaOptions { } /** - * Define a schema. This is used to build the schema for the database. - * @param build - A function that builds the schema. - * @param options - Options for defining the schema. - * @returns The defined schema. + * Define a schema. Single source of truth for tables, types, and routes. + * + * @param build - Receives `{ table, enum }`. + * @param options - `schemaName` defaults to `"app"`. */ export function defineSchema>( build: (ctx: SchemaBuilderContext) => T, - options: DefineSchemaOptions = {}, + options?: DefineSchemaOptions, ): Schema { - const schemaName = options.schemaName ?? "app"; + const schemaName = options?.schemaName ?? "app"; const schemaInstance = schemaName === "public" ? { table: pgTable } : pgSchema(schemaName); diff --git a/packages/appkit/src/plugins/database/entity-proxy.ts b/packages/appkit/src/plugins/database/entity-proxy.ts index ad646a5ad..ae60c2707 100644 --- a/packages/appkit/src/plugins/database/entity-proxy.ts +++ b/packages/appkit/src/plugins/database/entity-proxy.ts @@ -216,15 +216,17 @@ export function makeEntityClient< /** * Thin immutable wrapper around `DataPath`. Terminators go through - * `this.run(action, fn)` → `Plugin#execute`, so telemetry, retry, cache, - * and timeout flow consistently per action. + * `this.run(action, fn)` → `Plugin#execute` so telemetry/retry/cache/timeout + * flow per action. `implements EntityClient` catches drift at the declaration + * site instead of via the factory's `as unknown as` cast. */ class EntityClientImpl< TRow extends Row = Row, TInsert = TRow, TUpdate = Partial, TIncludes = Record, -> { +> implements EntityClient +{ constructor( private readonly deps: EntityClientDeps, private readonly state: EntityClientState, diff --git a/template/config/database/schema.ts b/template/config/database/schema.ts index 7c3e893d6..ffe405391 100644 --- a/template/config/database/schema.ts +++ b/template/config/database/schema.ts @@ -1,22 +1,21 @@ import { defineSchema } from "@databricks/appkit"; /** - * Application database schema. The database plugin auto-loads this file - * (see config/database/) and uses it as the single source of truth for - * - the typed `db.` browser client, - * - the auto-mounted `/api/database/` REST routes, - * - and runtime drift detection against the live Lakebase DB. + * Application database schema. Source of truth for the typed browser client, + * `/api/database/` routes, and drift detection. * - * Add tables under the returned object and run: - * npx appkit db migration generate - * npx appkit db migrate up + * Add tables, then `npx appkit db migration generate ` + `migrate up`. * * Example: - * user: table("user", { - * id: id(), - * email: text().notNull(), - * createdAt: timestamp().defaultNow().notNull(), - * }), + * import { defineSchema, id, text, timestamp } from "@databricks/appkit"; + * + * export default defineSchema(({ table }) => ({ + * user: table("user", { + * id: id(), + * email: text().notNull(), + * createdAt: timestamp().defaultNow().notNull(), + * }), + * })); */ // biome-ignore lint/correctness/noEmptyPattern: schema is intentionally empty in the starter template. export default defineSchema(({}) => ({}));