feat: add SvelteKit adapter

Add framework adapter for SvelteKit with:
- `getApp()` - get bknd app instance
- `serve()` - request handler for hooks.server.ts

Usage in hooks.server.ts:
```typescript
import { serve } from "bknd/adapter/sveltekit";
import config from "../bknd.config";

const bkndHandler = serve(config);

export const handle = async ({ event, resolve }) => {
  if (event.url.pathname.startsWith("/api/")) {
    return bkndHandler(event);
  }
  return resolve(event);
};
```

Includes:
- Adapter implementation (app/src/adapter/sveltekit/)
- Test suite
- Working example (examples/sveltekit/)
- Package exports and types
This commit is contained in:
Szymon Rączka
2025-12-26 16:55:10 +01:00
parent 81f3914e7f
commit 90b4de7093
16 changed files with 260 additions and 0 deletions

11
examples/sveltekit/.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.DS_Store
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
*.db

View File

@@ -0,0 +1,26 @@
# bknd + SvelteKit Example
This example shows how to integrate bknd with SvelteKit.
## Setup
```bash
bun install
bun run dev
```
## How it works
1. **`bknd.config.ts`** - bknd configuration with database connection, schema, and seed data
2. **`src/hooks.server.ts`** - Routes `/api/*` requests to bknd
3. **`src/routes/+page.server.ts`** - Uses `getApp()` to fetch data server-side
## API Endpoints
- `GET /api/data/entity/todos` - List todos (requires auth)
- `POST /api/auth/password/login` - Login
## Test Credentials
- Email: `admin@example.com`
- Password: `password`

View File

@@ -0,0 +1,52 @@
import type { SvelteKitBkndConfig } from "bknd/adapter/sveltekit";
import { em, entity, text, libsql } from "bknd";
import { createClient } from "@libsql/client";
const schema = em({
todos: entity("todos", {
title: text().required(),
done: text(),
}),
});
export default {
connection: libsql(
createClient({
url: "file:data.db",
})
),
config: {
data: schema.toJSON(),
auth: {
enabled: true,
allow_register: true,
jwt: {
issuer: "bknd-sveltekit-example",
secret: "dev-secret-change-in-production-1234567890abcdef",
},
roles: {
admin: {
implicit_allow: true,
},
default: {
permissions: ["data.entity.read", "data.entity.create"],
is_default: true,
},
},
},
},
options: {
seed: async (ctx) => {
await ctx.app.module.auth.createUser({
email: "admin@example.com",
password: "password",
role: "admin",
});
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: "true" },
{ title: "Build with SvelteKit", done: "false" },
]);
},
},
} as const satisfies SvelteKitBkndConfig;

View File

@@ -0,0 +1,23 @@
{
"name": "bknd-sveltekit-example",
"version": "0.0.1",
"private": true,
"scripts": {
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^7.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^6.0.0",
"svelte": "^5.0.0",
"typescript": "^5.0.0",
"vite": "^7.0.0"
},
"dependencies": {
"bknd": "file:../../app",
"@libsql/client": "^0.15.0"
},
"type": "module"
}

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>

View File

@@ -0,0 +1,14 @@
import type { Handle } from "@sveltejs/kit";
import { serve } from "bknd/adapter/sveltekit";
import config from "../bknd.config";
const bkndHandler = serve(config);
export const handle: Handle = async ({ event, resolve }) => {
// Handle bknd API requests
if (event.url.pathname.startsWith("/api/")) {
return bkndHandler(event);
}
return resolve(event);
};

View File

@@ -0,0 +1,14 @@
import type { PageServerLoad } from "./$types";
import { getApp } from "bknd/adapter/sveltekit";
import config from "../../bknd.config";
export const load: PageServerLoad = async () => {
const app = await getApp(config);
const api = app.getApi();
const todos = await api.data.readMany("todos");
return {
todos: todos.data ?? [],
};
};

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
</script>
<h1>bknd + SvelteKit Example</h1>
<h2>Todos</h2>
<ul>
{#each data.todos as todo (todo.id)}
<li>{todo.title} - {todo.done === "true" ? "Done" : "Pending"}</li>
{/each}
</ul>
<h2>API Endpoints</h2>
<ul>
<li><a href="/api/data/entity/todos">/api/data/entity/todos</a></li>
<li><a href="/api/auth/password/login">/api/auth/password/login</a> (POST)</li>
</ul>
<p>
Login credentials: <code>admin@example.com</code> / <code>password</code>
</p>

View File

@@ -0,0 +1,12 @@
import adapter from "@sveltejs/adapter-auto";
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
/** @type {import('@sveltejs/kit').Config} */
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
},
};
export default config;

View File

@@ -0,0 +1,14 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true,
"moduleResolution": "bundler"
}
}

View File

@@ -0,0 +1,6 @@
import { sveltekit } from "@sveltejs/kit/vite";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [sveltekit()],
});