diff --git a/template/.env.example.tmpl b/template/.env.example.tmpl index 0a7c80ec..7ab495cb 100644 --- a/template/.env.example.tmpl +++ b/template/.env.example.tmpl @@ -1,7 +1,15 @@ DATABRICKS_HOST=https://... -{{- if .dotenv_example}} -{{.dotenv_example}} +{{- if .dotEnv.example}} +{{.dotEnv.example}} +{{- end}} +{{- if .plugins.lakebase}} +PGHOST=your-lakebase-host.databricks.com +PGDATABASE=databricks_postgres +# Run: databricks postgres list-endpoints projects/{project-id}/branches/{branch-id} +LAKEBASE_ENDPOINT=projects//branches//endpoints/ +# PGUSER=your_user # optional, defaults to DATABRICKS_CLIENT_ID +PGSSLMODE=require {{- end}} DATABRICKS_APP_PORT=8000 -DATABRICKS_APP_NAME={{.project_name}} +DATABRICKS_APP_NAME={{.projectName}} FLASK_RUN_HOST=0.0.0.0 diff --git a/template/.env.tmpl b/template/.env.tmpl index 31d0d997..693834f8 100644 --- a/template/.env.tmpl +++ b/template/.env.tmpl @@ -1,7 +1,18 @@ -{{if ne .profile ""}}DATABRICKS_CONFIG_PROFILE={{.profile}}{{else}}DATABRICKS_HOST={{.workspace_host}}{{end}} -{{- if .dotenv}} -{{.dotenv}} +{{- if ne .profile ""}} +DATABRICKS_CONFIG_PROFILE={{.profile}} +{{- else}} +DATABRICKS_HOST={{.workspaceHost}} +{{- end}} +{{- if .dotEnv.content}} +{{.dotEnv.content}} +{{- end}} +{{- if .plugins.lakebase}} +PGHOST='' # Copy from the Lakebase Postgres UI +PGDATABASE='databricks_postgres' # Copy from the Lakebase Postgres UI +LAKEBASE_ENDPOINT='' # Run: databricks postgres list-endpoints projects/{project-id}/branches/{branch-id} +# PGUSER='' # optional, defaults to DATABRICKS_CLIENT_ID +PGSSLMODE=require {{- end}} DATABRICKS_APP_PORT=8000 -DATABRICKS_APP_NAME={{.project_name}} +DATABRICKS_APP_NAME={{.projectName}} FLASK_RUN_HOST=0.0.0.0 diff --git a/template/app.yaml.tmpl b/template/app.yaml.tmpl index a4e7b27d..f4ac5ed4 100644 --- a/template/app.yaml.tmpl +++ b/template/app.yaml.tmpl @@ -1,5 +1,17 @@ command: ['npm', 'run', 'start'] -{{- if .app_env}} env: -{{.app_env}} +{{- if .appEnv}} +{{.appEnv}} {{- end}} +{{- if .plugins.lakebase}} + - name: PGHOST + value: "" # Copy from the Lakebase Postgres UI + - name: PGDATABASE + value: "databricks_postgres" # Copy from the Lakebase Postgres UI + - name: LAKEBASE_ENDPOINT + value: "" # Run: databricks postgres list-endpoints projects/{project-id}/branches/{branch-id} + - name: PGSSLMODE + value: "require" +# - name: PGUSER +# value: "" # optional, defaults to DATABRICKS_CLIENT_ID +{{- end}} \ No newline at end of file diff --git a/template/client/index.html b/template/client/index.html index 3c0f326f..a3da7c2b 100644 --- a/template/client/index.html +++ b/template/client/index.html @@ -9,7 +9,7 @@ - {{.project_name}} + {{.projectName}}
diff --git a/template/client/public/site.webmanifest b/template/client/public/site.webmanifest index 03106ced..64190b64 100644 --- a/template/client/public/site.webmanifest +++ b/template/client/public/site.webmanifest @@ -1,6 +1,6 @@ { - "name": "{{.project_name}}", - "short_name": "{{.project_name}}", + "name": "{{.projectName}}", + "short_name": "{{.projectName}}", "icons": [ { "src": "/favicon-192x192.png", diff --git a/template/client/src/App.tsx b/template/client/src/App.tsx index 3075436b..d6497de7 100644 --- a/template/client/src/App.tsx +++ b/template/client/src/App.tsx @@ -1,167 +1,114 @@ -/** - * ⚠️ BEFORE MODIFYING THIS FILE: - * - * 1. Create SQL files in config/queries/ - * 2. Run `npm run typegen` to generate query types - * 3. Check appKitTypes.d.ts for available types - * - * Common Mistakes: - * - DataTable does NOT accept `data` or `columns` props - * - Charts use `xKey` and `yKey`, NOT `seriesKey`/`nameKey`/`valueKey` - * - useAnalyticsQuery has no `enabled` option - use conditional rendering - */ +import { createBrowserRouter, RouterProvider, NavLink, Outlet } from 'react-router'; import { - useAnalyticsQuery, - AreaChart, - LineChart, - RadarChart, Card, CardContent, CardHeader, CardTitle, - Skeleton, - Label, - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, } from '@databricks/appkit-ui/react'; -import { sql } from "@databricks/appkit-ui/js"; -import { useState, useEffect } from 'react'; +{{- if .plugins.analytics}} +import { AnalyticsPage } from './pages/analytics/AnalyticsPage'; +{{- end}} +{{- if .plugins.lakebase}} +import { LakebasePage } from './pages/lakebase/LakebasePage'; +{{- end}} -function App() { - const { data, loading, error } = useAnalyticsQuery('hello_world', { - message: sql.string('hello world'), - }); +const navLinkClass = ({ isActive }: { isActive: boolean }) => + `px-3 py-1.5 rounded-md text-sm font-medium transition-colors ${ + isActive + ? 'bg-primary text-primary-foreground' + : 'text-muted-foreground hover:bg-muted hover:text-foreground' + }`; - const [health, setHealth] = useState<{ - status: string; - timestamp: string; - } | null>(null); - const [healthError, setHealthError] = useState(null); +function Layout() { + return ( +
+
+

{{.projectName}}

+ +
- useEffect(() => { - fetch('/health') - .then((response) => response.json()) - .then((data: { status: string }) => setHealth({ ...data, timestamp: new Date().toISOString() })) - .catch((error: { message: string }) => setHealthError(error.message)); - }, []); +
+ +
+
+ ); +} - const [maxMonthNum, setMaxMonthNum] = useState(12); +const router = createBrowserRouter([ + { + element: , + children: [ + { path: '/', element: }, +{{- if .plugins.analytics}} + { path: '/analytics', element: }, +{{- end}} +{{- if .plugins.lakebase}} + { path: '/lakebase', element: }, +{{- end}} + ], + }, +]); - const salesParameters = { max_month_num: sql.number(maxMonthNum) }; +export default function App() { + return ; +} +function HomePage() { return ( -
-
-

Minimal Databricks App

-

A minimal Databricks App powered by Databricks AppKit

+
+
+

+ Welcome to your Databricks App +

+

+ Powered by Databricks AppKit +

-
- - - SQL Query Result - - - {loading && ( -
- - -
- )} - {error &&
Error: {error}
} - {data && data.length > 0 && ( -
-
Query: SELECT :message AS value
-
{data[0].value}
-
- )} - {data && data.length === 0 &&
No results
} -
-
- - - - Health Check - - - {!health && !healthError && ( -
- - -
- )} - {healthError && ( -
Error: {healthError}
- )} - {health && ( -
-
-
-
{health.status.toUpperCase()}
-
-
- Last checked: {new Date(health.timestamp).toLocaleString()} -
-
- )} -
-
- - - - Sales Data Filter - - -
-
- - -
-
-
-
- - - - Sales Trend Area Chart - - - - - - - - Sales Trend Custom Line Chart - - - - - - - - Sales Trend Radar Chart - - - - - -
+ + + Getting Started + + +

Your app is ready. Explore the resources below to continue building.

+ +
+
); } - -export default App; diff --git a/template/client/src/pages/analytics/AnalyticsPage.tsx b/template/client/src/pages/analytics/AnalyticsPage.tsx new file mode 100644 index 00000000..e1ca7a9d --- /dev/null +++ b/template/client/src/pages/analytics/AnalyticsPage.tsx @@ -0,0 +1,108 @@ +{{if .plugins.analytics -}} +import { + useAnalyticsQuery, + AreaChart, + LineChart, + RadarChart, + Card, + CardContent, + CardHeader, + CardTitle, + Skeleton, + Label, + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@databricks/appkit-ui/react'; +import { sql } from "@databricks/appkit-ui/js"; +import { useState } from 'react'; + +export function AnalyticsPage() { + const { data, loading, error } = useAnalyticsQuery('hello_world', { + message: sql.string('hello world'), + }); + + const [maxMonthNum, setMaxMonthNum] = useState(12); + const salesParameters = { max_month_num: sql.number(maxMonthNum) }; + + return ( +
+
+ + + SQL Query Result + + + {loading && ( +
+ + +
+ )} + {error &&
Error: {error}
} + {data && data.length > 0 && ( +
+
Query: SELECT :message AS value
+
{data[0].value}
+
+ )} + {data && data.length === 0 &&
No results
} +
+
+ + + + Sales Data Filter + + +
+
+ + +
+
+
+
+ + + + Sales Trend Area Chart + + + + + + + + Sales Trend Line Chart + + + + + + + + Sales Trend Radar Chart + + + + + +
+
+ ); +} +{{- end}} diff --git a/template/client/src/pages/lakebase/LakebasePage.tsx b/template/client/src/pages/lakebase/LakebasePage.tsx new file mode 100644 index 00000000..06c02922 --- /dev/null +++ b/template/client/src/pages/lakebase/LakebasePage.tsx @@ -0,0 +1,178 @@ +{{if .plugins.lakebase -}} +import { + Card, + CardContent, + CardHeader, + CardTitle, + Button, + Input, + Skeleton, +} from '@databricks/appkit-ui/react'; +import { useState, useEffect } from 'react'; +import { Check, X } from 'lucide-react'; + +interface Todo { + id: number; + title: string; + completed: boolean; + created_at: string; +} + +export function LakebasePage() { + const [todos, setTodos] = useState([]); + const [newTitle, setNewTitle] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + fetch('/api/lakebase/todos') + .then((res) => { + if (!res.ok) throw new Error(`Failed to fetch todos: ${res.statusText}`); + return res.json() as Promise; + }) + .then(setTodos) + .catch((err) => setError(err instanceof Error ? err.message : 'Failed to load todos')) + .finally(() => setLoading(false)); + }, []); + + const addTodo = async (e: React.FormEvent) => { + e.preventDefault(); + const title = newTitle.trim(); + if (!title) return; + + setSubmitting(true); + try { + const res = await fetch('/api/lakebase/todos', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title }), + }); + if (!res.ok) throw new Error(`Failed to create todo: ${res.statusText}`); + const created = (await res.json()) as Todo; + setTodos((prev) => [created, ...prev]); + setNewTitle(''); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to add todo'); + } finally { + setSubmitting(false); + } + }; + + const toggleTodo = async (id: number) => { + try { + const res = await fetch(`/api/lakebase/todos/${id}`, { method: 'PATCH' }); + if (!res.ok) throw new Error(`Failed to update todo: ${res.statusText}`); + const updated = (await res.json()) as Todo; + setTodos((prev) => prev.map((t) => (t.id === id ? updated : t))); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to update todo'); + } + }; + + const deleteTodo = async (id: number) => { + try { + const res = await fetch(`/api/lakebase/todos/${id}`, { method: 'DELETE' }); + if (!res.ok) throw new Error(`Failed to delete todo: ${res.statusText}`); + setTodos((prev) => prev.filter((t) => t.id !== id)); + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to delete todo'); + } + }; + + const completedCount = todos.filter((t) => t.completed).length; + + return ( +
+ + + Todo List + + +

+ A simple CRUD example powered by Databricks Lakebase (PostgreSQL). +

+ +
+ setNewTitle(e.target.value)} + disabled={submitting} + className="flex-1" + /> + +
+ + {error && ( +
+ {error} +
+ )} + + {loading && ( +
+ {Array.from({ length: 3 }, (_, i) => ( +
+ + +
+ ))} +
+ )} + + {!loading && todos.length === 0 && ( +

+ No todos yet. Add one above to get started. +

+ )} + + {!loading && todos.length > 0 && ( +
+ {todos.map((todo) => ( +
+ + + + {todo.title} + + + +
+ ))} + +

+ {completedCount} of {todos.length} completed +

+
+ )} +
+
+
+ ); +} +{{- end}} diff --git a/template/config/queries/hello_world.sql b/template/config/queries/hello_world.sql index 31e480f7..2ee26aa5 100644 --- a/template/config/queries/hello_world.sql +++ b/template/config/queries/hello_world.sql @@ -1 +1,3 @@ +{{if .plugins.analytics -}} SELECT :message AS value; +{{- end}} diff --git a/template/config/queries/mocked_sales.sql b/template/config/queries/mocked_sales.sql index 868e9472..526b5fd8 100644 --- a/template/config/queries/mocked_sales.sql +++ b/template/config/queries/mocked_sales.sql @@ -1,3 +1,4 @@ +{{if .plugins.analytics -}} WITH mock_data AS ( SELECT 'January' AS month, 1 AS month_num, 65000 AS revenue, 45000 AS expenses, 850 AS customers UNION ALL SELECT 'February', 2, 72000, 48000, 920 @@ -16,3 +17,4 @@ SELECT * FROM mock_data WHERE month_num <= :max_month_num ORDER BY month_num; +{{- end}} diff --git a/template/databricks.yml.tmpl b/template/databricks.yml.tmpl index a4cace29..4e610b44 100644 --- a/template/databricks.yml.tmpl +++ b/template/databricks.yml.tmpl @@ -1,35 +1,35 @@ bundle: - name: {{.project_name}} -{{if .variables}} + name: {{.projectName}} +{{- if .bundle.variables}} variables: -{{.variables}} +{{.bundle.variables}} {{- end}} resources: apps: app: - name: "{{.project_name}}" - description: "{{.app_description}}" + name: "{{.projectName}}" + description: "{{.appDescription}}" source_code_path: ./ # Uncomment to enable on behalf of user API scopes. Available scopes: sql, dashboards.genie, files.files # user_api_scopes: # - sql -{{if .resources}} +{{- if .bundle.resources}} # The resources which this app has access to. resources: -{{.resources}} +{{.bundle.resources}} {{- end}} targets: default: default: true workspace: - host: {{.workspace_host}} -{{if .target_variables}} + host: {{.workspaceHost}} +{{- if .bundle.targetVariables}} variables: -{{.target_variables}} +{{.bundle.targetVariables}} {{- end}} diff --git a/template/package-lock.json b/template/package-lock.json index f3c40c78..d37deaaf 100644 --- a/template/package-lock.json +++ b/template/package-lock.json @@ -1,11 +1,11 @@ { - "name": "{{.project_name}}", + "name": "{{.projectName}}", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "{{.project_name}}", + "name": "{{.projectName}}", "version": "1.0.0", "hasInstallScript": true, "license": "Unlicensed", @@ -20,7 +20,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-resizable-panels": "^3.0.6", - "superjson": "^2.2.5", + "react-router": "^7.13.0", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.4.0", @@ -33,7 +33,7 @@ "@playwright/test": "^1.57.0", "@tailwindcss/postcss": "^4.1.17", "@tailwindcss/vite": "^4.1.17", - "@types/express": "^5.0.5", + "@types/express": "^5.0.5"t, "@types/node": "^24.6.0", "@types/react": "^19.1.16", "@types/react-dom": "^19.1.9", @@ -7731,21 +7731,6 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, - "node_modules/copy-anything": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", - "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", - "license": "MIT", - "dependencies": { - "is-what": "^5.2.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -10240,18 +10225,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-what": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", - "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -11771,6 +11744,41 @@ "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -12174,6 +12182,12 @@ "node": ">= 0.8.0" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -12628,18 +12642,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/superjson": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", - "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", - "license": "MIT", - "dependencies": { - "copy-anything": "^4" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/template/package.json b/template/package.json index e9199925..0f3a4ea6 100644 --- a/template/package.json +++ b/template/package.json @@ -1,8 +1,6 @@ { - "name": "{{.project_name}}", + "name": "{{.projectName}}", "version": "1.0.0", - "main": "build/index.js", - "type": "module", "scripts": { "start": "NODE_ENV=production node --env-file-if-exists=./.env ./dist/server/server.js", "dev": "NODE_ENV=development tsx watch --tsconfig ./tsconfig.server.json --env-file-if-exists=./.env ./server/server.ts", @@ -30,7 +28,7 @@ "keywords": [], "author": "", "license": "Unlicensed", - "description": "{{.app_description}}", + "description": "{{.appDescription}}", "dependencies": { "@databricks/appkit": "0.11.0", "@databricks/appkit-ui": "0.11.0", @@ -42,7 +40,7 @@ "react": "^19.1.1", "react-dom": "^19.1.1", "react-resizable-panels": "^3.0.6", - "superjson": "^2.2.5", + "react-router": "^7.13.0", "tailwind-merge": "^3.3.1", "tw-animate-css": "^1.4.0", "tailwindcss-animate": "^1.0.7", diff --git a/template/server/routes/lakebase/todo-routes.ts b/template/server/routes/lakebase/todo-routes.ts new file mode 100644 index 00000000..16007a17 --- /dev/null +++ b/template/server/routes/lakebase/todo-routes.ts @@ -0,0 +1,100 @@ +{{if .plugins.lakebase -}} +import { z } from 'zod'; +import type { LakebasePlugin, ServerPlugin } from '@databricks/appkit'; + +interface AppKitWithLakebase { + lakebase: ReturnType; + server: ReturnType; +} + +const CREATE_TABLE_SQL = ` + CREATE TABLE IF NOT EXISTS 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) { + await appkit.lakebase.query(CREATE_TABLE_SQL); + + 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 todos ORDER BY created_at DESC', + ); + res.json(result.rows); + } catch (err) { + console.error('Failed to list todos:', err); + res.status(500).json({ error: 'Failed to list todos' }); + } + }); + + app.post('/api/lakebase/todos', async (req, res) => { + try { + const parsed = CreateTodoBody.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: 'title is required' }); + return; + } + const result = await appkit.lakebase.query( + 'INSERT INTO todos (title) VALUES ($1) RETURNING id, title, completed, created_at', + [parsed.data.title.trim()], + ); + res.status(201).json(result.rows[0]); + } catch (err) { + console.error('Failed to create todo:', err); + res.status(500).json({ error: 'Failed to create todo' }); + } + }); + + app.patch('/api/lakebase/todos/:id', async (req, res) => { + try { + const id = parseInt(req.params.id, 10); + if (isNaN(id)) { + res.status(400).json({ error: 'Invalid id' }); + return; + } + const result = await appkit.lakebase.query( + 'UPDATE todos SET completed = NOT completed WHERE id = $1 RETURNING id, title, completed, created_at', + [id], + ); + if (result.rows.length === 0) { + res.status(404).json({ error: 'Todo not found' }); + return; + } + res.json(result.rows[0]); + } catch (err) { + console.error('Failed to update todo:', err); + res.status(500).json({ error: 'Failed to update todo' }); + } + }); + + app.delete('/api/lakebase/todos/:id', async (req, res) => { + try { + const id = parseInt(req.params.id, 10); + if (isNaN(id)) { + res.status(400).json({ error: 'Invalid id' }); + return; + } + const result = await appkit.lakebase.query( + 'DELETE FROM todos WHERE id = $1 RETURNING id', + [id], + ); + if (result.rows.length === 0) { + res.status(404).json({ error: 'Todo not found' }); + return; + } + res.status(204).send(); + } catch (err) { + console.error('Failed to delete todo:', err); + res.status(500).json({ error: 'Failed to delete todo' }); + } + }); + }); +} +{{- end}} diff --git a/template/server/server.ts b/template/server/server.ts index e5f3b323..a44a4626 100644 --- a/template/server/server.ts +++ b/template/server/server.ts @@ -1,7 +1,26 @@ -import { createApp, {{.plugin_imports}} } from '@databricks/appkit'; +import { createApp{{range $name, $_ := .plugins}}, {{$name}}{{end}} } from '@databricks/appkit'; +{{- if .plugins.lakebase}} +import { setupSampleLakebaseRoutes } from './routes/lakebase/todo-routes'; +{{- end}} createApp({ plugins: [ - {{.plugin_usages}} +{{- if .plugins.lakebase}} + server({ autoStart: false }), +{{- range $name, $_ := .plugins}} +{{- if ne $name "server"}} + {{$name}}(), +{{- end}} +{{- end}} +{{- else}} +{{- range $name, $_ := .plugins}} + {{$name}}(), +{{- end}} +{{- end}} ], +}).then(async (appkit) => { +{{- if .plugins.lakebase}} + await setupSampleLakebaseRoutes(appkit); + await appkit.server.start(); +{{- end}} }).catch(console.error); diff --git a/template/tsconfig.server.json b/template/tsconfig.server.json index 8cdada22..19ce069e 100644 --- a/template/tsconfig.server.json +++ b/template/tsconfig.server.json @@ -5,6 +5,10 @@ "target": "ES2020", "lib": ["ES2020"], + /* Modules */ + "module": "CommonJS", + "moduleResolution": "node", + /* Emit */ "outDir": "./dist", "rootDir": "./",