From 0e682f3a9a75adea0c2a9e828aa398c2325df099 Mon Sep 17 00:00:00 2001 From: Pawel Kosiec Date: Tue, 10 Feb 2026 19:30:15 +0100 Subject: [PATCH 1/3] chore: add Lakebase Autoscaling example for `dev-playground` app --- .../components/lakebase/ActivityLogsPanel.tsx | 259 +++++++++ .../src/components/lakebase/OrdersPanel.tsx | 524 ++++++++++++++++++ .../src/components/lakebase/ProductsPanel.tsx | 331 +++++++++++ .../src/components/lakebase/TasksPanel.tsx | 401 ++++++++++++++ .../client/src/components/lakebase/index.ts | 4 + .../client/src/hooks/use-lakebase-data.ts | 144 +++++ .../client/src/routeTree.gen.ts | 21 + .../client/src/routes/__root.tsx | 8 + .../client/src/routes/index.tsx | 19 + .../client/src/routes/lakebase.route.tsx | 64 +++ apps/dev-playground/package.json | 4 + apps/dev-playground/server/index.ts | 3 + .../server/lakebase-examples-plugin.ts | 75 +++ .../lakebase-examples/drizzle-example.ts | 192 +++++++ .../lakebase-examples/raw-driver-example.ts | 226 ++++++++ .../lakebase-examples/sequelize-example.ts | 272 +++++++++ .../lakebase-examples/typeorm-example.ts | 195 +++++++ apps/dev-playground/tsdown.config.ts | 15 + .../prepared-files/root-tsconfig.json | 6 +- tsconfig.json | 2 + 20 files changed, 2764 insertions(+), 1 deletion(-) create mode 100644 apps/dev-playground/client/src/components/lakebase/ActivityLogsPanel.tsx create mode 100644 apps/dev-playground/client/src/components/lakebase/OrdersPanel.tsx create mode 100644 apps/dev-playground/client/src/components/lakebase/ProductsPanel.tsx create mode 100644 apps/dev-playground/client/src/components/lakebase/TasksPanel.tsx create mode 100644 apps/dev-playground/client/src/components/lakebase/index.ts create mode 100644 apps/dev-playground/client/src/hooks/use-lakebase-data.ts create mode 100644 apps/dev-playground/client/src/routes/lakebase.route.tsx create mode 100644 apps/dev-playground/server/lakebase-examples-plugin.ts create mode 100644 apps/dev-playground/server/lakebase-examples/drizzle-example.ts create mode 100644 apps/dev-playground/server/lakebase-examples/raw-driver-example.ts create mode 100644 apps/dev-playground/server/lakebase-examples/sequelize-example.ts create mode 100644 apps/dev-playground/server/lakebase-examples/typeorm-example.ts create mode 100644 apps/dev-playground/tsdown.config.ts diff --git a/apps/dev-playground/client/src/components/lakebase/ActivityLogsPanel.tsx b/apps/dev-playground/client/src/components/lakebase/ActivityLogsPanel.tsx new file mode 100644 index 00000000..10bb6406 --- /dev/null +++ b/apps/dev-playground/client/src/components/lakebase/ActivityLogsPanel.tsx @@ -0,0 +1,259 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, +} from "@databricks/appkit-ui/react"; +import { Activity, Loader2 } from "lucide-react"; +import { useId, useState } from "react"; +import { useLakebaseData, useLakebasePost } from "@/hooks/use-lakebase-data"; + +interface ActivityLog { + id: number; + userId: string; + action: string; + metadata: Record | null; + timestamp: string; +} + +interface Stats { + totalLogs: number; + uniqueUsers: number; + recentActivity: number; +} + +export function ActivityLogsPanel() { + const userIdFieldId = useId(); + const actionFieldId = useId(); + + const { + data: logs, + loading: logsLoading, + error: logsError, + refetch, + } = useLakebaseData("/api/lakebase-examples/drizzle/activity"); + + const { data: stats } = useLakebaseData( + "/api/lakebase-examples/drizzle/stats", + ); + + const { post, loading: creating } = useLakebasePost< + Partial, + ActivityLog + >("/api/lakebase-examples/drizzle/activity"); + + const generateRandomActivity = () => { + const users = ["alice", "bob", "charlie", "diana", "eve"]; + const actions = [ + "login", + "logout", + "view_dashboard", + "create_report", + "export_data", + "update_settings", + "share_document", + "delete_item", + ]; + + return { + userId: users[Math.floor(Math.random() * users.length)], + action: actions[Math.floor(Math.random() * actions.length)], + }; + }; + + const [formData, setFormData] = useState(generateRandomActivity()); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await post({ + userId: formData.userId, + action: formData.action, + metadata: { + source: "web", + timestamp: new Date().toISOString(), + }, + }); + + if (result) { + setFormData(generateRandomActivity()); + refetch(); + } + }; + + return ( +
+ {/* Header */} + + +
+
+ +
+
+ Drizzle ORM Example + + Type-safe queries with schema definitions and automatic type + inference + +
+
+
+
+ + {/* Statistics */} + {stats && ( +
+ + + Total Logs + {stats.totalLogs} + + + + + + Unique Users + + {stats.uniqueUsers} + + + + + + Last 24 Hours + + {stats.recentActivity} + + +
+ )} + + {/* Create log form */} + + + Log Activity + + +
+
+
+ + + setFormData({ ...formData, userId: e.target.value }) + } + placeholder="alice" + required + /> +
+
+ + + setFormData({ ...formData, action: e.target.value }) + } + placeholder="view_dashboard" + required + /> +
+
+ +
+
+
+ + {/* Activity logs */} + + +
+ Activity Logs + +
+
+ + {logsLoading && ( +
+
+ Loading activity logs... +
+ )} + + {logsError && ( +
+ Error: {logsError.message} +
+ )} + + {logs && logs.length === 0 && ( +
+ +

No activity logs yet. Log your first activity above.

+
+ )} + + {logs && logs.length > 0 && ( +
+ {logs.map((log) => ( +
+
+
+ + {log.userId} + + {log.action} +
+ + {new Date(log.timestamp).toLocaleString()} + +
+ {log.metadata && ( +
+ + View metadata + +
+                        {JSON.stringify(log.metadata, null, 2)}
+                      
+
+ )} +
+ ))} +
+ )} + + +
+ ); +} diff --git a/apps/dev-playground/client/src/components/lakebase/OrdersPanel.tsx b/apps/dev-playground/client/src/components/lakebase/OrdersPanel.tsx new file mode 100644 index 00000000..8730bb04 --- /dev/null +++ b/apps/dev-playground/client/src/components/lakebase/OrdersPanel.tsx @@ -0,0 +1,524 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, +} from "@databricks/appkit-ui/react"; +import { + CheckCircle, + Circle, + Loader2, + Package, + ShoppingCart, + Truck, +} from "lucide-react"; +import { useId, useState } from "react"; +import { + useLakebaseData, + useLakebasePatch, + useLakebasePost, +} from "@/hooks/use-lakebase-data"; + +interface Order { + id: number; + orderNumber: string; + customerName: string; + productName: string; + amount: number; + status: "pending" | "processing" | "shipped" | "delivered"; + createdAt: string; + updatedAt: string; +} + +interface OrderStats { + total: number; + pending: number; + processing: number; + shipped: number; + delivered: number; +} + +export function OrdersPanel() { + const orderNumberId = useId(); + const customerNameId = useId(); + const productNameId = useId(); + const amountId = useId(); + + const { + data: orders, + loading: ordersLoading, + error: ordersError, + refetch, + } = useLakebaseData("/api/lakebase-examples/sequelize/orders"); + + const { data: stats } = useLakebaseData( + "/api/lakebase-examples/sequelize/stats", + ); + + const { post, loading: creating } = useLakebasePost, Order>( + "/api/lakebase-examples/sequelize/orders", + ); + + const { patch, loading: updating } = useLakebasePatch< + { status: string }, + Order + >("/api/lakebase-examples/sequelize/orders"); + + const generateRandomOrder = () => { + const customers = [ + "Alice Johnson", + "Bob Smith", + "Carol Williams", + "David Brown", + "Emma Davis", + "Frank Miller", + "Grace Wilson", + "Henry Moore", + ]; + const products = [ + "Wireless Bluetooth Headphones", + "USB-C Charging Cable", + "Laptop Stand - Ergonomic", + "Mechanical Keyboard - RGB", + "4K Webcam with Microphone", + "Portable SSD 1TB", + "Wireless Mouse - Ergonomic", + "Monitor Arm Mount", + "Noise Cancelling Earbuds", + "Desk Lamp - LED", + ]; + + const orderNum = `ORD-${new Date().getFullYear()}-${String(Math.floor(Math.random() * 9999) + 1).padStart(4, "0")}`; + const customer = customers[Math.floor(Math.random() * customers.length)]; + const product = products[Math.floor(Math.random() * products.length)]; + const amount = (Math.random() * 200 + 10).toFixed(2); + + return { + orderNumber: orderNum, + customerName: customer, + productName: product, + amount, + }; + }; + + const [formData, setFormData] = useState(generateRandomOrder()); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await post({ + orderNumber: formData.orderNumber, + customerName: formData.customerName, + productName: formData.productName, + amount: Number.parseFloat(formData.amount), + status: "pending", + }); + + if (result) { + setFormData(generateRandomOrder()); + refetch(); + } + }; + + const handleStatusUpdate = async (id: number, status: Order["status"]) => { + const result = await patch(id, { status }); + if (result) { + refetch(); + } + }; + + const getStatusBadge = (status: Order["status"]) => { + switch (status) { + case "pending": + return ( + + + Pending + + ); + case "processing": + return ( + + + Processing + + ); + case "shipped": + return ( + + + Shipped + + ); + case "delivered": + return ( + + + Delivered + + ); + } + }; + + const ordersByStatus = orders + ? { + pending: orders.filter((o) => o.status === "pending"), + processing: orders.filter((o) => o.status === "processing"), + shipped: orders.filter((o) => o.status === "shipped"), + delivered: orders.filter((o) => o.status === "delivered"), + } + : { pending: [], processing: [], shipped: [], delivered: [] }; + + return ( +
+ {/* Header */} + + +
+
+ +
+
+ Sequelize Example + + Model-based ORM with intuitive API and automatic timestamps + +
+
+
+
+ + {/* Statistics */} + {stats && ( +
+ + + + Total Orders + + {stats.total} + + + + + Pending + {stats.pending} + + + + + Processing + {stats.processing} + + + + + Shipped + {stats.shipped} + + + + + Delivered + {stats.delivered} + + +
+ )} + + {/* Create order form */} + + + Create Order + + +
+
+
+ + + setFormData({ ...formData, orderNumber: e.target.value }) + } + placeholder="ORD-2024-0001" + required + /> +
+
+ + + setFormData({ ...formData, customerName: e.target.value }) + } + placeholder="John Doe" + required + /> +
+
+
+
+ + + setFormData({ ...formData, productName: e.target.value }) + } + placeholder="Wireless Headphones" + required + /> +
+
+ + + setFormData({ ...formData, amount: e.target.value }) + } + placeholder="99.99" + required + /> +
+
+ +
+
+
+ + {/* Order board */} + + +
+ Order Board + +
+
+ + {ordersLoading && ( +
+
+ Loading orders... +
+ )} + + {ordersError && ( +
+ Error:{" "} + {ordersError.message} +
+ )} + + {orders && orders.length === 0 && ( +
+ +

No orders yet. Add an order to get started.

+
+ )} + + {orders && orders.length > 0 && ( +
+ {/* Pending column */} +
+
+ Pending ({ordersByStatus.pending.length}) +
+
+ {ordersByStatus.pending.map((order) => ( + +
+
{getStatusBadge(order.status)}
+

+ {order.orderNumber} +

+

+ {order.customerName} +

+

+ {order.productName} +

+

+ ${Number(order.amount).toFixed(2)} +

+ +
+
+ ))} +
+
+ + {/* Processing column */} +
+
+ Processing ({ordersByStatus.processing.length}) +
+
+ {ordersByStatus.processing.map((order) => ( + +
+
{getStatusBadge(order.status)}
+

+ {order.orderNumber} +

+

+ {order.customerName} +

+

+ {order.productName} +

+

+ ${Number(order.amount).toFixed(2)} +

+
+ + +
+
+
+ ))} +
+
+ + {/* Shipped column */} +
+
+ Shipped ({ordersByStatus.shipped.length}) +
+
+ {ordersByStatus.shipped.map((order) => ( + +
+
{getStatusBadge(order.status)}
+

+ {order.orderNumber} +

+

+ {order.customerName} +

+

+ {order.productName} +

+

+ ${Number(order.amount).toFixed(2)} +

+ +
+
+ ))} +
+
+ + {/* Delivered column */} +
+
+ Delivered ({ordersByStatus.delivered.length}) +
+
+ {ordersByStatus.delivered.map((order) => ( + +
+
{getStatusBadge(order.status)}
+

+ {order.orderNumber} +

+

+ {order.customerName} +

+

+ {order.productName} +

+

+ ${Number(order.amount).toFixed(2)} +

+
+
+ ))} +
+
+
+ )} + + +
+ ); +} diff --git a/apps/dev-playground/client/src/components/lakebase/ProductsPanel.tsx b/apps/dev-playground/client/src/components/lakebase/ProductsPanel.tsx new file mode 100644 index 00000000..69b9402b --- /dev/null +++ b/apps/dev-playground/client/src/components/lakebase/ProductsPanel.tsx @@ -0,0 +1,331 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Checkbox, + Input, +} from "@databricks/appkit-ui/react"; +import { Database, Loader2, Package } from "lucide-react"; +import { useId, useState } from "react"; +import { useLakebaseData, useLakebasePost } from "@/hooks/use-lakebase-data"; + +interface Product { + id: number; + name: string; + category: string; + price: number | string; // PostgreSQL DECIMAL returns as string + stock: number; + created_by?: string; + created_at: string; +} + +interface CreateProductRequest { + name: string; + category: string; + price: number; + stock: number; + useObo?: boolean; +} + +interface HealthStatus { + status: string; + connected: boolean; + message: string; +} + +export function ProductsPanel() { + const nameId = useId(); + const categoryId = useId(); + const priceId = useId(); + const stockId = useId(); + const oboCheckboxId = useId(); + + const { + data: products, + loading: productsLoading, + error: productsError, + refetch, + } = useLakebaseData("/api/lakebase-examples/raw/products"); + + const { data: health } = useLakebaseData( + "/api/lakebase-examples/raw/health", + ); + + const { post, loading: creating } = useLakebasePost< + CreateProductRequest, + Product + >("/api/lakebase-examples/raw/products"); + + const generateRandomProduct = () => { + const products = [ + "Ergonomic Keyboard", + "Wireless Mouse", + "USB-C Hub", + "Laptop Stand", + "Monitor Arm", + "Mechanical Keyboard", + "Gaming Headset", + "Webcam HD", + ]; + const categories = ["Electronics", "Accessories", "Peripherals", "Office"]; + const price = (Math.random() * (199.99 - 29.99) + 29.99).toFixed(2); + const stock = Math.floor(Math.random() * (500 - 50) + 50); + + return { + name: products[Math.floor(Math.random() * products.length)], + category: categories[Math.floor(Math.random() * categories.length)], + price, + stock: String(stock), + }; + }; + + const [formData, setFormData] = useState(generateRandomProduct()); + const [useObo, setUseObo] = useState(false); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await post({ + name: formData.name, + category: formData.category, + price: Number(formData.price), + stock: Number(formData.stock), + useObo, + }); + + if (result) { + setFormData(generateRandomProduct()); + refetch(); + } + }; + + return ( +
+ {/* Header with connection status */} + + +
+
+
+ +
+
+ Raw Driver Example + + Direct PostgreSQL connection using pg.Pool with automatic + OAuth token refresh + +
+
+ {health && ( + + {health.connected ? "Connected" : "Disconnected"} + + )} +
+
+
+ + {/* Create product form */} + + + Create Product + + +
+
+
+ + + setFormData({ ...formData, name: e.target.value }) + } + placeholder="Wireless Mouse" + required + /> +
+
+ + + setFormData({ ...formData, category: e.target.value }) + } + placeholder="Electronics" + required + /> +
+
+ + + setFormData({ ...formData, price: e.target.value }) + } + placeholder="29.99" + required + /> +
+
+ + + setFormData({ ...formData, stock: e.target.value }) + } + placeholder="100" + required + /> +
+
+
+ setUseObo(checked === true)} + disabled + /> + +
+ +
+
+
+ + {/* Products list */} + + +
+ Products Catalog + +
+
+ + {productsLoading && ( +
+
+ Loading products... +
+ )} + + {productsError && ( +
+ Error:{" "} + {productsError.message} +
+ )} + + {products && products.length === 0 && ( +
+ +

No products available. Create your first product above.

+
+ )} + + {products && products.length > 0 && ( +
+ + + + + + + + + + + + + {products.map((product) => ( + + + + + + + + + ))} + +
+ ID + + Name + + Category + + Price + + Stock + + Created By +
{product.id}{product.name} + {product.category} + + ${Number(product.price).toFixed(2)} + + {product.stock} + + {product.created_by || "unknown"} +
+
+ )} + + +
+ ); +} diff --git a/apps/dev-playground/client/src/components/lakebase/TasksPanel.tsx b/apps/dev-playground/client/src/components/lakebase/TasksPanel.tsx new file mode 100644 index 00000000..fdc5c19a --- /dev/null +++ b/apps/dev-playground/client/src/components/lakebase/TasksPanel.tsx @@ -0,0 +1,401 @@ +import { + Badge, + Button, + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + Input, + Textarea, +} from "@databricks/appkit-ui/react"; +import { CheckCircle, Circle, ListTodo, Loader2 } from "lucide-react"; +import { useId, useState } from "react"; +import { + useLakebaseData, + useLakebasePatch, + useLakebasePost, +} from "@/hooks/use-lakebase-data"; + +interface Task { + id: number; + title: string; + status: "pending" | "in_progress" | "completed"; + description: string | null; + createdAt: string; +} + +interface TaskStats { + total: number; + pending: number; + inProgress: number; + completed: number; +} + +export function TasksPanel() { + const titleId = useId(); + const descriptionId = useId(); + + const { + data: tasks, + loading: tasksLoading, + error: tasksError, + refetch, + } = useLakebaseData("/api/lakebase-examples/typeorm/tasks"); + + const { data: stats } = useLakebaseData( + "/api/lakebase-examples/typeorm/stats", + ); + + const { post, loading: creating } = useLakebasePost, Task>( + "/api/lakebase-examples/typeorm/tasks", + ); + + const { patch, loading: updating } = useLakebasePatch< + { status: string }, + Task + >("/api/lakebase-examples/typeorm/tasks"); + + const generateRandomTask = () => { + const tasks = [ + { + title: "Implement user authentication", + description: "Add OAuth2 authentication flow with JWT tokens", + }, + { + title: "Write API documentation", + description: "Document all REST endpoints with examples", + }, + { + title: "Set up CI/CD pipeline", + description: "Configure GitHub Actions for automated testing", + }, + { + title: "Add error monitoring", + description: "Integrate error tracking and alerting system", + }, + { + title: "Optimize database queries", + description: "Add indexes and analyze slow queries", + }, + { + title: "Implement data validation", + description: "Add schema validation for all API requests", + }, + { + title: "Set up development environment", + description: "Configure local development tools and dependencies", + }, + { + title: "Design database schema", + description: "Create ERD and define table relationships", + }, + ]; + + const task = tasks[Math.floor(Math.random() * tasks.length)]; + return { + title: task.title, + description: task.description, + }; + }; + + const [formData, setFormData] = useState(generateRandomTask()); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await post({ + title: formData.title, + description: formData.description || null, + status: "pending", + }); + + if (result) { + setFormData(generateRandomTask()); + refetch(); + } + }; + + const handleStatusUpdate = async (id: number, status: Task["status"]) => { + const result = await patch(id, { status }); + if (result) { + refetch(); + } + }; + + const getStatusBadge = (status: Task["status"]) => { + switch (status) { + case "pending": + return ( + + + Pending + + ); + case "in_progress": + return ( + + + In Progress + + ); + case "completed": + return ( + + + Completed + + ); + } + }; + + const tasksByStatus = tasks + ? { + pending: tasks.filter((t) => t.status === "pending"), + in_progress: tasks.filter((t) => t.status === "in_progress"), + completed: tasks.filter((t) => t.status === "completed"), + } + : { pending: [], in_progress: [], completed: [] }; + + return ( +
+ {/* Header */} + + +
+
+ +
+
+ TypeORM Example + + Entity-based data access with decorators and repository pattern + +
+
+
+
+ + {/* Statistics */} + {stats && ( +
+ + + Total Tasks + {stats.total} + + + + + Pending + {stats.pending} + + + + + In Progress + {stats.inProgress} + + + + + Completed + {stats.completed} + + +
+ )} + + {/* Create task form */} + + + Create Task + + +
+
+ + + setFormData({ ...formData, title: e.target.value }) + } + placeholder="Implement feature X" + required + /> +
+
+ +