From 281eb9021a7297e0c7ef5b83711a8ae7e9ef3a0e Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Mon, 18 May 2026 22:08:30 +0200 Subject: [PATCH] feat: make Drizzle ORM the default for Lakebase plugin - Add `appkit.lakebase.drizzle(schema)` SDK helper with dynamic import (drizzle-orm as optional peerDep, zero cost if unused) - Rewrite template todo example to use Drizzle query builder - Add template files: db/schema.ts, db/index.ts, drizzle.config.ts - Add conditional drizzle-orm + drizzle-kit deps to template package.json - Add drizzle-kit scripts (db:push, db:generate, db:migrate) - Update Lakebase plugin docs with Drizzle section (schema, queries, migrations, OBO) - Update @databricks/lakebase README with expanded Drizzle example Signed-off-by: Pawel Kosiec --- docs/docs/plugins/lakebase.md | 105 ++++++++++++++++-- knip.json | 2 +- packages/appkit/package.json | 11 +- .../appkit/src/plugins/lakebase/lakebase.ts | 44 +++++++- packages/lakebase/README.md | 15 ++- pnpm-lock.yaml | 101 +++++++++++++++++ template/README.md | 10 ++ .../src/pages/lakebase/LakebasePage.tsx | 2 +- template/drizzle.config.ts | 26 +++++ template/package.json | 11 +- template/server/db/index.ts | 47 ++++++++ template/server/db/schema.ts | 15 +++ .../server/routes/lakebase/todo-routes.ts | 86 +++++--------- template/server/server.ts | 13 ++- 14 files changed, 409 insertions(+), 79 deletions(-) create mode 100644 template/drizzle.config.ts create mode 100644 template/server/db/index.ts create mode 100644 template/server/db/schema.ts diff --git a/docs/docs/plugins/lakebase.md b/docs/docs/plugins/lakebase.md index e6d728f9b..c95fdd8ee 100644 --- a/docs/docs/plugins/lakebase.md +++ b/docs/docs/plugins/lakebase.md @@ -39,33 +39,116 @@ await createApp({ }); ``` -## Accessing the pool +## Drizzle ORM (default) + +Apps scaffolded with the Lakebase plugin use [Drizzle ORM](https://orm.drizzle.team/) for type-safe database queries. The Lakebase plugin provides a built-in `drizzle()` helper that creates a fully-typed Drizzle instance backed by the connection pool. + +### Schema definition + +Define your tables in `server/db/schema.ts`: + +```ts +import { boolean, pgSchema, serial, text, timestamp } from "drizzle-orm/pg-core"; + +export const appSchema = pgSchema("app"); + +export const todos = appSchema.table("todos", { + id: serial("id").primaryKey(), + title: text("title").notNull(), + completed: boolean("completed").notNull().default(false), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); + +// Inferred types for insert and select operations +export type Todo = typeof todos.$inferSelect; +export type NewTodo = typeof todos.$inferInsert; +``` -After initialization, access Lakebase through the `AppKit.lakebase` object: +### Initialization ```ts +import { createApp, lakebase, server } from "@databricks/appkit"; +import * as schema from "./db/schema"; + const AppKit = await createApp({ plugins: [server(), lakebase()], + async onPluginsReady(appkit) { + const db = await appkit.lakebase.drizzle(schema); + + // Type-safe queries + const allTodos = await db.select().from(schema.todos); + }, }); +``` + +### Type-safe queries -await AppKit.lakebase.query(`CREATE SCHEMA IF NOT EXISTS app`); +```ts +import { eq, desc, not } from "drizzle-orm"; +import { todos } from "./db/schema"; + +// Select +const allTodos = await db.select().from(todos).orderBy(desc(todos.createdAt)); + +// Insert +const [created] = await db.insert(todos).values({ title: "New todo" }).returning(); -await AppKit.lakebase.query(`CREATE TABLE IF NOT EXISTS app.orders ( - id SERIAL PRIMARY KEY, - user_id VARCHAR(255) NOT NULL, - amount DECIMAL(10, 2) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -)`); +// Update (toggle boolean) +const [updated] = await db + .update(todos) + .set({ completed: not(todos.completed) }) + .where(eq(todos.id, 1)) + .returning(); + +// Delete +await db.delete(todos).where(eq(todos.id, 1)); +``` + +### Migrations with drizzle-kit + +The template includes [drizzle-kit](https://orm.drizzle.team/docs/kit-overview) for schema migrations: + +```bash +npm run db:push # Push schema directly to database (development) +npm run db:generate # Generate SQL migration files (production) +npm run db:migrate # Apply pending migrations (production) +``` + +:::note +`drizzle-kit` requires password authentication. Lakebase supports it alongside OAuth — enable it in the Lakebase UI under **Branch Overview** → **Authentication**. +::: + +### OBO with Drizzle + +On-Behalf-Of (OBO) works transparently with Drizzle. The Drizzle instance wraps AppKit's `RoutingPool`, which automatically routes queries to the per-user pool when inside `asUser(req)`: + +```ts +app.get("/api/my-todos", async (req, res) => { + const userDb = await AppKit.lakebase.asUser(req).drizzle(schema); + const myTodos = await userDb.select().from(todos); + res.json(myTodos); +}); +``` + +## Accessing the pool + +For raw SQL queries or integration with other ORMs, access the pool directly: + +```ts +const AppKit = await createApp({ + plugins: [server(), lakebase()], +}); +// Raw SQL queries const result = await AppKit.lakebase.query( "SELECT * FROM app.orders WHERE user_id = $1", [userId], ); -// Raw pg.Pool (for ORMs or advanced usage) +// Raw pg.Pool (for other ORMs or advanced usage) const pool = AppKit.lakebase.pool; -// ORM-ready config objects +// ORM-ready config objects (for TypeORM, Sequelize, etc.) const ormConfig = AppKit.lakebase.getOrmConfig(); // { host, port, database, ... } const pgConfig = AppKit.lakebase.getPgConfig(); // pg.PoolConfig ``` diff --git a/knip.json b/knip.json index b2bf14fd4..04d2d216c 100644 --- a/knip.json +++ b/knip.json @@ -29,6 +29,6 @@ "docs/**", ".github/scripts/**" ], - "ignoreDependencies": ["json-schema-to-typescript"], + "ignoreDependencies": ["json-schema-to-typescript", "drizzle-orm"], "ignoreBinaries": ["tarball", "appkit"] } diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 2349e06be..a0ec7c108 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -96,7 +96,16 @@ "@types/json-schema": "7.0.15", "@types/pg": "8.16.0", "@types/ws": "8.18.1", - "@vitejs/plugin-react": "5.1.1" + "@vitejs/plugin-react": "5.1.1", + "drizzle-orm": "0.45.2" + }, + "peerDependencies": { + "drizzle-orm": ">=0.34.0" + }, + "peerDependenciesMeta": { + "drizzle-orm": { + "optional": true + } }, "overrides": { "vite": "npm:rolldown-vite@7.1.14" diff --git a/packages/appkit/src/plugins/lakebase/lakebase.ts b/packages/appkit/src/plugins/lakebase/lakebase.ts index b8b1b16be..7dd33fd3d 100644 --- a/packages/appkit/src/plugins/lakebase/lakebase.ts +++ b/packages/appkit/src/plugins/lakebase/lakebase.ts @@ -296,12 +296,54 @@ export class LakebasePlugin extends Plugin implements ToolProvider { * Use `AppKit.lakebase.asUser(req)` to get the same API backed by a per-user pool. */ exports() { + const pool = this.pool!; return { // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API - pool: this.pool! as LakebasePool, + pool: pool as LakebasePool, query: this.query.bind(this), getOrmConfig: () => getLakebaseOrmConfig(this.activePoolConfig()), getPgConfig: () => getLakebasePgConfig(this.activePoolConfig()), + /** + * Creates a Drizzle ORM instance backed by the Lakebase connection pool. + * + * Requires `drizzle-orm` as a dependency in your project (`npm install drizzle-orm`). + * The module is loaded lazily via dynamic `import()` — zero cost if this method is never called. + * + * The returned Drizzle instance uses AppKit's {@link RoutingPool}, so queries automatically + * route to the service-principal pool or per-user pool based on `asUser(req)` context. + * + * @param schema - Optional Drizzle schema object for relational query support and type inference + * @returns A fully-typed `NodePgDatabase` instance + * + * @example + * ```ts + * import * as schema from './db/schema'; + * const db = await AppKit.lakebase.drizzle(schema); + * const todos = await db.select().from(schema.todos); + * ``` + */ + drizzle: async >( + schema?: TSchema, + ) => { + try { + const mod = await import("drizzle-orm/node-postgres"); + return mod.drizzle({ + client: pool as any, + ...(schema ? { schema } : {}), + }); + } catch (err: unknown) { + if ( + err instanceof Error && + (err.message.includes("Cannot find module") || + err.message.includes("ERR_MODULE_NOT_FOUND")) + ) { + throw new Error( + "drizzle-orm is required for appkit.lakebase.drizzle(). Install it: npm install drizzle-orm", + ); + } + throw err; + } + }, }; } } diff --git a/packages/lakebase/README.md b/packages/lakebase/README.md index 99f3beb60..02d3f6bd8 100644 --- a/packages/lakebase/README.md +++ b/packages/lakebase/README.md @@ -188,12 +188,23 @@ When used with AppKit, logging is automatically configured - see the [AppKit Int ```typescript import { drizzle } from "drizzle-orm/node-postgres"; +import { pgSchema, serial, text, timestamp } from "drizzle-orm/pg-core"; import { createLakebasePool } from "@databricks/lakebase"; +// Define schema +const appSchema = pgSchema("app"); +const users = appSchema.table("users", { + id: serial("id").primaryKey(), + name: text("name").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), +}); + +// Create Drizzle instance const pool = createLakebasePool(); -const db = drizzle(pool); +const db = drizzle({ client: pool, schema: { users } }); -const users = await db.select().from(usersTable); +// Type-safe queries +const allUsers = await db.select().from(users); ``` ### Prisma diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c576bd74a..184b17129 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -357,6 +357,9 @@ importers: '@vitejs/plugin-react': specifier: 5.1.1 version: 5.1.1(rolldown-vite@7.1.14(@types/node@25.2.3)(esbuild@0.25.10)(jiti@2.6.1)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.2)) + drizzle-orm: + specifier: 0.45.2 + version: 0.45.2(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(pg@8.18.0) packages/appkit-ui: dependencies: @@ -6770,6 +6773,98 @@ packages: sqlite3: optional: true + drizzle-orm@0.45.2: + resolution: {integrity: sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1.13' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@upstash/redis': '>=1.34.7' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@upstash/redis': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dts-resolver@2.1.3: resolution: {integrity: sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw==} engines: {node: '>=20.19.0'} @@ -19287,6 +19382,12 @@ snapshots: '@types/pg': 8.16.0 pg: 8.18.0 + drizzle-orm@0.45.2(@opentelemetry/api@1.9.0)(@types/pg@8.16.0)(pg@8.18.0): + optionalDependencies: + '@opentelemetry/api': 1.9.0 + '@types/pg': 8.16.0 + pg: 8.18.0 + dts-resolver@2.1.3(oxc-resolver@11.19.1): optionalDependencies: oxc-resolver: 11.19.1 diff --git a/template/README.md b/template/README.md index 10c238ff4..96324e53d 100644 --- a/template/README.md +++ b/template/README.md @@ -42,6 +42,10 @@ DATABRICKS_APP_PORT=8000 #### Lakebase Configuration The Lakebase plugin requires additional environment variables for PostgreSQL connectivity. To learn how to configure the Lakebase plugin, see the [Lakebase plugin documentation](https://www.databricks.com/devhub/docs/appkit/v0/plugins/lakebase). + +This template uses [Drizzle ORM](https://orm.drizzle.team/) for type-safe database queries. Schema definitions are in `server/db/schema.ts`. For migrations: +- **Development:** `npm run db:push` (pushes schema directly to database) +- **Production:** `npm run db:generate && npm run db:migrate` (versioned migrations) {{- end}} ### CLI Authentication @@ -190,12 +194,18 @@ databricks bundle deploy -t prod * public/ # Static assets * server/ # Express backend * server.ts # Server entry point +{{- if .plugins.lakebase}} + * db/ # Database schema and client (Drizzle ORM) +{{- end}} * routes/ # Routes * shared/ # Shared types {{- if .plugins.analytics}} * config/ # Configuration * queries/ # SQL query files {{- end}} +{{- if .plugins.lakebase}} +* drizzle.config.ts # Drizzle Kit migration config +{{- end}} * databricks.yml # Bundle configuration * app.yaml # App configuration * .env.example # Environment variables example diff --git a/template/client/src/pages/lakebase/LakebasePage.tsx b/template/client/src/pages/lakebase/LakebasePage.tsx index 06c02922d..b354b2786 100644 --- a/template/client/src/pages/lakebase/LakebasePage.tsx +++ b/template/client/src/pages/lakebase/LakebasePage.tsx @@ -15,7 +15,7 @@ interface Todo { id: number; title: string; completed: boolean; - created_at: string; + createdAt: string; } export function LakebasePage() { diff --git a/template/drizzle.config.ts b/template/drizzle.config.ts new file mode 100644 index 000000000..45df1cfca --- /dev/null +++ b/template/drizzle.config.ts @@ -0,0 +1,26 @@ +{{if .plugins.lakebase -}} +// Drizzle Kit configuration for schema migrations. +// drizzle-kit requires password auth — Lakebase supports it alongside OAuth. +// Enable password auth in the Lakebase UI under Branch Overview → Authentication. +// +// Usage: +// npm run db:push — Push schema directly to database (dev) +// npm run db:generate — Generate SQL migration files (production) +// npm run db:migrate — Apply pending migrations (production) + +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './server/db/schema.ts', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { + host: process.env.PGHOST!, + port: Number(process.env.PGPORT || 5432), + database: process.env.PGDATABASE!, + user: process.env.PGUSER!, + password: process.env.PGPASSWORD!, + ssl: process.env.PGSSLMODE === 'require' ? 'require' : undefined, + }, +}); +{{- end}} diff --git a/template/package.json b/template/package.json index eb2359d32..8887e8631 100644 --- a/template/package.json +++ b/template/package.json @@ -25,7 +25,10 @@ "predev": "npm run sync && npm run typegen", "sync": "appkit plugin sync --write --silent", "typegen": "appkit generate-types", - "setup": "appkit setup --write" + "setup": "appkit setup --write"{{if .plugins.lakebase}}, + "db:push": "drizzle-kit push", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate"{{end}} }, "keywords": [], "author": "", @@ -46,7 +49,8 @@ "tailwind-merge": "3.3.1", "tw-animate-css": "1.4.0", "tailwindcss-animate": "1.0.7", - "zod": "4.3.6" + "zod": "4.3.6"{{if .plugins.lakebase}}, + "drizzle-orm": "0.45.2"{{end}} }, "devDependencies": { "@ast-grep/napi": "0.37.0", @@ -76,7 +80,8 @@ "typescript": "5.9.3", "typescript-eslint": "8.48.0", "vite": "npm:rolldown-vite@7.1.14", - "vitest": "4.0.14" + "vitest": "4.0.14"{{if .plugins.lakebase}}, + "drizzle-kit": "0.31.10"{{end}} }, "overrides": { "vite": "npm:rolldown-vite@7.1.14" diff --git a/template/server/db/index.ts b/template/server/db/index.ts new file mode 100644 index 000000000..a89f5c3e8 --- /dev/null +++ b/template/server/db/index.ts @@ -0,0 +1,47 @@ +{{if .plugins.lakebase -}} +// Database initialization and schema setup. +// +// Schema setup uses raw SQL for the initial CREATE SCHEMA/TABLE (needed for first deploy). +// For subsequent schema changes, use drizzle-kit: +// npm run db:push (dev — push schema directly) +// npm run db:generate (production — generate migration files) +// npm run db:migrate (production — apply migrations) + +import type { LakebasePool } from '@databricks/appkit'; +import * as schema from './schema'; + +export { schema }; + +export type Database = Awaited>; + +const TABLE_EXISTS_SQL = ` + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'app' AND table_name = 'todos' +`; + +const SETUP_SCHEMA_SQL = `CREATE SCHEMA IF NOT EXISTS app`; + +const CREATE_TABLE_SQL = ` + CREATE TABLE IF NOT EXISTS app.todos ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL, + completed BOOLEAN NOT NULL DEFAULT false, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ) +`; + +export async function ensureSchema(pool: LakebasePool): Promise { + const { rows } = await pool.query(TABLE_EXISTS_SQL); + if (rows.length > 0) { + console.log('[lakebase] Table app.todos already exists, skipping setup'); + return; + } + await pool.query(SETUP_SCHEMA_SQL); + await pool.query(CREATE_TABLE_SQL); + console.log('[lakebase] Created schema and table app.todos'); +} + +export async function initDb(appkit: { lakebase: { drizzle: (schema: typeof import('./schema')) => Promise } }) { + return appkit.lakebase.drizzle(schema); +} +{{- end}} diff --git a/template/server/db/schema.ts b/template/server/db/schema.ts new file mode 100644 index 000000000..9d08c8d18 --- /dev/null +++ b/template/server/db/schema.ts @@ -0,0 +1,15 @@ +{{if .plugins.lakebase -}} +import { boolean, pgSchema, serial, text, timestamp } from 'drizzle-orm/pg-core'; + +export const appSchema = pgSchema('app'); + +export const todos = appSchema.table('todos', { + id: serial('id').primaryKey(), + title: text('title').notNull(), + completed: boolean('completed').notNull().default(false), + createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), +}); + +export type Todo = typeof todos.$inferSelect; +export type NewTodo = typeof todos.$inferInsert; +{{- end}} diff --git a/template/server/routes/lakebase/todo-routes.ts b/template/server/routes/lakebase/todo-routes.ts index 416e2182d..a3332a72f 100644 --- a/template/server/routes/lakebase/todo-routes.ts +++ b/template/server/routes/lakebase/todo-routes.ts @@ -1,60 +1,31 @@ {{if .plugins.lakebase -}} +// Todo routes using Drizzle ORM for type-safe database queries. // For per-user connections (OBO) with Row-Level Security, see: // https://www.databricks.com/devhub/docs/appkit/v0/plugins/lakebase#on-behalf-of-obo--per-user-connections +import { eq, desc, not } from 'drizzle-orm'; import { z } from 'zod'; -import { Application } from 'express'; +import type { Application } from 'express'; +import type { Database } from '../../db'; +import { todos } from '../../db/schema'; -interface AppKitWithLakebase { - lakebase: { - query(text: string, params?: unknown[]): Promise<{ rows: Record[] }>; - }; +interface AppKitWithServer { server: { extend(fn: (app: Application) => void): void; }; } -const TABLE_EXISTS_SQL = ` - SELECT 1 FROM information_schema.tables - WHERE table_schema = 'app' AND table_name = 'todos' -`; - -const SETUP_SCHEMA_SQL = `CREATE SCHEMA IF NOT EXISTS app`; - -const CREATE_TABLE_SQL = ` - CREATE TABLE IF NOT EXISTS app.todos ( - id SERIAL PRIMARY KEY, - title TEXT NOT NULL, - completed BOOLEAN NOT NULL DEFAULT false, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() - ) -`; - const CreateTodoBody = z.object({ title: z.string().min(1) }); -export async function setupSampleLakebaseRoutes(appkit: AppKitWithLakebase) { - try { - const { rows } = await appkit.lakebase.query(TABLE_EXISTS_SQL); - if (rows.length > 0) { - console.log('[lakebase] Table app.todos already exists, skipping setup'); - } else { - await appkit.lakebase.query(SETUP_SCHEMA_SQL); - await appkit.lakebase.query(CREATE_TABLE_SQL); - console.log('[lakebase] Created schema and table app.todos'); - } - } catch (err) { - console.warn('[lakebase] Database setup failed:', (err as Error).message); - console.warn('[lakebase] Routes will be registered but may return errors'); - console.warn('[lakebase] See https://www.databricks.com/devhub/docs/appkit/v0/plugins/lakebase#database-permissions for troubleshooting'); - } - +export function setupTodoRoutes(appkit: AppKitWithServer, db: Database) { appkit.server.extend((app) => { app.get('/api/lakebase/todos', async (_req, res) => { try { - const result = await appkit.lakebase.query( - 'SELECT id, title, completed, created_at FROM app.todos ORDER BY created_at DESC', - ); - res.json(result.rows); + const result = await db + .select() + .from(todos) + .orderBy(desc(todos.createdAt)); + res.json(result); } catch (err) { console.error('Failed to list todos:', err); res.status(500).json({ error: 'Failed to list todos' }); @@ -68,11 +39,11 @@ export async function setupSampleLakebaseRoutes(appkit: AppKitWithLakebase) { res.status(400).json({ error: 'title is required' }); return; } - const result = await appkit.lakebase.query( - 'INSERT INTO app.todos (title) VALUES ($1) RETURNING id, title, completed, created_at', - [parsed.data.title.trim()], - ); - res.status(201).json(result.rows[0]); + const [created] = await db + .insert(todos) + .values({ title: parsed.data.title.trim() }) + .returning(); + res.status(201).json(created); } catch (err) { console.error('Failed to create todo:', err); res.status(500).json({ error: 'Failed to create todo' }); @@ -86,15 +57,16 @@ export async function setupSampleLakebaseRoutes(appkit: AppKitWithLakebase) { res.status(400).json({ error: 'Invalid id' }); return; } - const result = await appkit.lakebase.query( - 'UPDATE app.todos SET completed = NOT completed WHERE id = $1 RETURNING id, title, completed, created_at', - [id], - ); - if (result.rows.length === 0) { + const [updated] = await db + .update(todos) + .set({ completed: not(todos.completed) }) + .where(eq(todos.id, id)) + .returning(); + if (!updated) { res.status(404).json({ error: 'Todo not found' }); return; } - res.json(result.rows[0]); + res.json(updated); } catch (err) { console.error('Failed to update todo:', err); res.status(500).json({ error: 'Failed to update todo' }); @@ -108,11 +80,11 @@ export async function setupSampleLakebaseRoutes(appkit: AppKitWithLakebase) { res.status(400).json({ error: 'Invalid id' }); return; } - const result = await appkit.lakebase.query( - 'DELETE FROM app.todos WHERE id = $1 RETURNING id', - [id], - ); - if (result.rows.length === 0) { + const [deleted] = await db + .delete(todos) + .where(eq(todos.id, id)) + .returning({ id: todos.id }); + if (!deleted) { res.status(404).json({ error: 'Todo not found' }); return; } diff --git a/template/server/server.ts b/template/server/server.ts index 47f8f9c1c..356d40eaf 100644 --- a/template/server/server.ts +++ b/template/server/server.ts @@ -13,7 +13,8 @@ import { createApp{{range $name, $p := .plugins}}{{if ne $p.Stability "beta"}}, import { {{$betaImports}} } from '@databricks/appkit/beta'; {{- end}} {{- if .plugins.lakebase}} -import { setupSampleLakebaseRoutes } from './routes/lakebase/todo-routes'; +import { setupTodoRoutes } from './routes/lakebase/todo-routes'; +import { ensureSchema, initDb } from './db'; {{- end}} {{- if .plugins.agents}} import { helper } from './agents/helper'; @@ -31,7 +32,15 @@ createApp({ ], {{- if .plugins.lakebase}} async onPluginsReady(appkit) { - await setupSampleLakebaseRoutes(appkit); + try { + await ensureSchema(appkit.lakebase.pool); + } catch (err) { + console.warn('[lakebase] Database setup failed:', (err as Error).message); + console.warn('[lakebase] Routes will be registered but may return errors'); + console.warn('[lakebase] See https://www.databricks.com/devhub/docs/appkit/v0/plugins/lakebase#database-permissions for troubleshooting'); + } + const db = await initDb(appkit); + setupTodoRoutes(appkit, db); }, {{- end}} }).catch(console.error);