diff --git a/app/build.ts b/app/build.ts index ee6e428..3fd35ef 100644 --- a/app/build.ts +++ b/app/build.ts @@ -308,6 +308,11 @@ async function buildAdapters() { platform: "node", }), + tsup.build({ + ...baseConfig("sveltekit"), + platform: "node", + }), + tsup.build({ ...baseConfig("node"), platform: "node", diff --git a/app/package.json b/app/package.json index b3fa2f2..577e19a 100644 --- a/app/package.json +++ b/app/package.json @@ -253,6 +253,11 @@ "import": "./dist/adapter/astro/index.js", "require": "./dist/adapter/astro/index.js" }, + "./adapter/sveltekit": { + "types": "./dist/types/adapter/sveltekit/index.d.ts", + "import": "./dist/adapter/sveltekit/index.js", + "require": "./dist/adapter/sveltekit/index.js" + }, "./adapter/aws": { "types": "./dist/types/adapter/aws/index.d.ts", "import": "./dist/adapter/aws/index.js", @@ -280,6 +285,7 @@ "adapter/react-router": ["./dist/types/adapter/react-router/index.d.ts"], "adapter/bun": ["./dist/types/adapter/bun/index.d.ts"], "adapter/node": ["./dist/types/adapter/node/index.d.ts"], + "adapter/sveltekit": ["./dist/types/adapter/sveltekit/index.d.ts"], "adapter/sqlite": ["./dist/types/adapter/sqlite/edge.d.ts"] } }, @@ -309,6 +315,8 @@ "remix", "react-router", "astro", + "sveltekit", + "svelte", "bun", "node" ] diff --git a/app/src/adapter/sveltekit/index.ts b/app/src/adapter/sveltekit/index.ts new file mode 100644 index 0000000..8f0f38e --- /dev/null +++ b/app/src/adapter/sveltekit/index.ts @@ -0,0 +1 @@ +export * from "./sveltekit.adapter"; diff --git a/app/src/adapter/sveltekit/sveltekit.adapter.spec.ts b/app/src/adapter/sveltekit/sveltekit.adapter.spec.ts new file mode 100644 index 0000000..17da756 --- /dev/null +++ b/app/src/adapter/sveltekit/sveltekit.adapter.spec.ts @@ -0,0 +1,15 @@ +import { afterAll, beforeAll, describe } from "bun:test"; +import * as sveltekit from "./sveltekit.adapter"; +import { disableConsoleLog, enableConsoleLog } from "core/utils"; +import { adapterTestSuite } from "adapter/adapter-test-suite"; +import { bunTestRunner } from "adapter/bun/test"; + +beforeAll(disableConsoleLog); +afterAll(enableConsoleLog); + +describe("sveltekit adapter", () => { + adapterTestSuite(bunTestRunner, { + makeApp: (c, a) => sveltekit.getApp(c, a ?? ({} as any)), + makeHandler: (c, a) => (request: Request) => sveltekit.serve(c, a ?? ({} as any))({ request }), + }); +}); diff --git a/app/src/adapter/sveltekit/sveltekit.adapter.ts b/app/src/adapter/sveltekit/sveltekit.adapter.ts new file mode 100644 index 0000000..415fc33 --- /dev/null +++ b/app/src/adapter/sveltekit/sveltekit.adapter.ts @@ -0,0 +1,33 @@ +import { createRuntimeApp, type RuntimeBkndConfig } from "bknd/adapter"; + +type TSvelteKit = { + request: Request; +}; + +export type SvelteKitBkndConfig = Pick, "adminOptions">; + +/** + * Get bknd app instance + * @param config - bknd configuration + * @param args - environment variables (use $env/dynamic/private for universal runtime support) + */ +export async function getApp( + config: SvelteKitBkndConfig = {} as SvelteKitBkndConfig, + args: Env, +) { + return await createRuntimeApp(config, args); +} + +/** + * Create request handler for hooks.server.ts + * @param config - bknd configuration + * @param args - environment variables (use $env/dynamic/private for universal runtime support) + */ +export function serve( + config: SvelteKitBkndConfig = {} as SvelteKitBkndConfig, + args: Env, +) { + return async (fnArgs: TSvelteKit) => { + return (await getApp(config, args)).fetch(fnArgs.request); + }; +} diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json index 82151fc..6fb7bfc 100644 --- a/docs/content/docs/(documentation)/integration/(frameworks)/meta.json +++ b/docs/content/docs/(documentation)/integration/(frameworks)/meta.json @@ -1,3 +1,3 @@ { - "pages": ["nextjs", "react-router", "astro", "vite"] + "pages": ["nextjs", "react-router", "astro", "sveltekit", "vite"] } diff --git a/docs/content/docs/(documentation)/integration/(frameworks)/sveltekit.mdx b/docs/content/docs/(documentation)/integration/(frameworks)/sveltekit.mdx new file mode 100644 index 0000000..5ef07bf --- /dev/null +++ b/docs/content/docs/(documentation)/integration/(frameworks)/sveltekit.mdx @@ -0,0 +1,204 @@ +--- +title: "SvelteKit" +description: "Run bknd inside SvelteKit" +tags: ["documentation"] +--- + +## Installation + +To get started with SvelteKit and bknd, create a new SvelteKit project by following the [official guide](https://svelte.dev/docs/kit/creating-a-project), and then install bknd as a dependency: + + + +```bash tab="npm" +npm install bknd +``` + +```bash tab="pnpm" +pnpm install bknd +``` + +```bash tab="yarn" +yarn add bknd +``` + +```bash tab="bun" +bun add bknd +``` + + + +## Configuration + + + When run with Node.js, a version of 22 (LTS) or higher is required. Please + verify your version by running `node -v`, and + [upgrade](https://nodejs.org/en/download/) if necessary. + + +Now create a `bknd.config.ts` file in the root of your project: + +```typescript title="bknd.config.ts" +import type { SvelteKitBkndConfig } from "bknd/adapter/sveltekit"; + +export default { + connection: { + url: "file:data.db", + }, +} satisfies SvelteKitBkndConfig; +``` + +See [bknd.config.ts](/extending/config) for more information on how to configure bknd. The `SvelteKitBkndConfig` type extends the base config type with the following properties: + +```typescript +export type SvelteKitBkndConfig = Pick, "adminOptions">; +``` + +## Serve the API + +The SvelteKit adapter uses SvelteKit's hooks mechanism to handle API requests. Create a `src/hooks.server.ts` file: + +```typescript title="src/hooks.server.ts" +import type { Handle } from "@sveltejs/kit"; +import { serve } from "bknd/adapter/sveltekit"; +import { env } from "$env/dynamic/private"; +import config from "../bknd.config"; + +const bkndHandler = serve(config, env); + +export const handle: Handle = async ({ event, resolve }) => { + // handle bknd API requests + const pathname = event.url.pathname; + if (pathname.startsWith("/api/")) { + const res = await bkndHandler(event); + if (res.status !== 404) { + return res; + } + } + + return resolve(event); +}; +``` + +For more information about the connection object, refer to the [Database](/usage/database) guide. + + + The adapter uses `$env/dynamic/private` to access environment variables, making it runtime-agnostic + and compatible with any deployment target (Node.js, Bun, Cloudflare, etc.). + + +## Enabling the Admin UI + +The SvelteKit adapter supports serving the Admin UI statically. First, copy the required assets to your `static` folder by adding a postinstall script to your `package.json`: + +```json title="package.json" +{ + "scripts": { + "postinstall": "bknd copy-assets --out static" // [!code highlight] + } +} +``` + +Then update your `bknd.config.ts` to configure the admin base path: + +```typescript title="bknd.config.ts" +import type { SvelteKitBkndConfig } from "bknd/adapter/sveltekit"; + +export default { + connection: { + url: "file:data.db", + }, + adminOptions: { // [!code highlight] + adminBasepath: "/admin" // [!code highlight] + }, // [!code highlight] +} satisfies SvelteKitBkndConfig; +``` + +Finally, update your `hooks.server.ts` to also handle admin routes: + +```typescript title="src/hooks.server.ts" +import type { Handle } from "@sveltejs/kit"; +import { serve } from "bknd/adapter/sveltekit"; +import { env } from "$env/dynamic/private"; +import config from "../bknd.config"; + +const bkndHandler = serve(config, env); + +export const handle: Handle = async ({ event, resolve }) => { + // handle bknd API and admin requests + const pathname = event.url.pathname; + if (pathname.startsWith("/api/") || pathname.startsWith("/admin")) { // [!code highlight] + const res = await bkndHandler(event); + if (res.status !== 404) { + return res; + } + } + + return resolve(event); +}; +``` + +## Example usage of the API + +You can use the `getApp` function to access the bknd API in your server-side load functions: + +```typescript title="src/routes/+page.server.ts" +import type { PageServerLoad } from "./$types"; +import { getApp } from "bknd/adapter/sveltekit"; +import { env } from "$env/dynamic/private"; +import config from "../../bknd.config"; + +export const load: PageServerLoad = async () => { + const app = await getApp(config, env); + const api = app.getApi(); + + const todos = await api.data.readMany("todos"); + + return { + todos: todos.data ?? [], + }; +}; +``` + +Then display the data in your Svelte component: + +```svelte title="src/routes/+page.svelte" + + +

Todos

+
    + {#each data.todos as todo (todo.id)} +
  • {todo.title}
  • + {/each} +
+``` + +### Using authentication + +To use authentication in your load functions, pass the request headers to the API: + +```typescript title="src/routes/+page.server.ts" +import type { PageServerLoad } from "./$types"; +import { getApp } from "bknd/adapter/sveltekit"; +import { env } from "$env/dynamic/private"; +import config from "../../bknd.config"; + +export const load: PageServerLoad = async ({ request }) => { + const app = await getApp(config, env); + const api = app.getApi({ headers: request.headers }); + await api.verifyAuth(); + + const todos = await api.data.readMany("todos"); + + return { + todos: todos.data ?? [], + user: api.getUser(), + }; +}; +``` + +Check the [SvelteKit repository example](https://github.com/bknd-io/bknd/tree/main/examples/sveltekit) for more implementation details. diff --git a/docs/content/docs/(documentation)/integration/introduction.mdx b/docs/content/docs/(documentation)/integration/introduction.mdx index 6330208..7119efd 100644 --- a/docs/content/docs/(documentation)/integration/introduction.mdx +++ b/docs/content/docs/(documentation)/integration/introduction.mdx @@ -27,6 +27,12 @@ bknd seamlessly integrates with popular frameworks, allowing you to use what you href="/integration/astro" /> +} + title="SvelteKit" + href="/integration/sveltekit" +/> + Create a new issue to request a guide for your framework. diff --git a/docs/content/docs/(documentation)/start.mdx b/docs/content/docs/(documentation)/start.mdx index 91bfdd7..86ca8ce 100644 --- a/docs/content/docs/(documentation)/start.mdx +++ b/docs/content/docs/(documentation)/start.mdx @@ -103,6 +103,12 @@ Start by using the integration guide for these popular frameworks/runtimes. Ther href="/integration/deno" /> +} + title="SvelteKit" + href="/integration/sveltekit" +/> + } title="AWS Lambda" diff --git a/examples/sveltekit/.gitignore b/examples/sveltekit/.gitignore new file mode 100644 index 0000000..1380082 --- /dev/null +++ b/examples/sveltekit/.gitignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +*.db +static/manifest.json +static/assets \ No newline at end of file diff --git a/examples/sveltekit/README.md b/examples/sveltekit/README.md new file mode 100644 index 0000000..9a5d66f --- /dev/null +++ b/examples/sveltekit/README.md @@ -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` diff --git a/examples/sveltekit/bknd.config.ts b/examples/sveltekit/bknd.config.ts new file mode 100644 index 0000000..d36c862 --- /dev/null +++ b/examples/sveltekit/bknd.config.ts @@ -0,0 +1,56 @@ +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, + }, + }, + }, + }, + adminOptions: { + // this path must be the same as in `hooks.server.ts` + adminBasepath: "/admin" + }, + 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; diff --git a/examples/sveltekit/package.json b/examples/sveltekit/package.json new file mode 100644 index 0000000..53fbf94 --- /dev/null +++ b/examples/sveltekit/package.json @@ -0,0 +1,24 @@ +{ + "name": "bknd-sveltekit-example", + "version": "0.0.1", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "postinstall": "node node_modules/.bin/bknd copy-assets --out static" + }, + "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" +} diff --git a/examples/sveltekit/src/app.html b/examples/sveltekit/src/app.html new file mode 100644 index 0000000..84ffad1 --- /dev/null +++ b/examples/sveltekit/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/examples/sveltekit/src/hooks.server.ts b/examples/sveltekit/src/hooks.server.ts new file mode 100644 index 0000000..66f3ef4 --- /dev/null +++ b/examples/sveltekit/src/hooks.server.ts @@ -0,0 +1,19 @@ +import type { Handle } from "@sveltejs/kit"; +import { serve } from "bknd/adapter/sveltekit"; +import { env } from "$env/dynamic/private"; +import config from "../bknd.config"; + +const bkndHandler = serve(config, env); + +export const handle: Handle = async ({ event, resolve }) => { + // Handle bknd API requests + const pathname = event.url.pathname; + if (pathname.startsWith("/api/") || pathname.startsWith("/admin")) { + const res = await bkndHandler(event); + if (res.status !== 404) { + return res; + } + } + + return resolve(event); +}; diff --git a/examples/sveltekit/src/routes/+page.server.ts b/examples/sveltekit/src/routes/+page.server.ts new file mode 100644 index 0000000..8efbda8 --- /dev/null +++ b/examples/sveltekit/src/routes/+page.server.ts @@ -0,0 +1,15 @@ +import type { PageServerLoad } from "./$types"; +import { getApp } from "bknd/adapter/sveltekit"; +import { env } from "$env/dynamic/private"; +import config from "../../bknd.config"; + +export const load: PageServerLoad = async () => { + const app = await getApp(config, env); + const api = app.getApi(); + + const todos = await api.data.readMany("todos"); + + return { + todos: todos.data ?? [], + }; +}; diff --git a/examples/sveltekit/src/routes/+page.svelte b/examples/sveltekit/src/routes/+page.svelte new file mode 100644 index 0000000..8973d68 --- /dev/null +++ b/examples/sveltekit/src/routes/+page.svelte @@ -0,0 +1,24 @@ + + +

bknd + SvelteKit Example

+ +

Todos

+
    + {#each data.todos as todo (todo.id)} +
  • {todo.title} - {todo.done === "true" ? "Done" : "Pending"}
  • + {/each} +
+ +

API Endpoints

+ + +

+ Login credentials: admin@example.com / password +

diff --git a/examples/sveltekit/static/favicon.ico b/examples/sveltekit/static/favicon.ico new file mode 100644 index 0000000..c1a946d Binary files /dev/null and b/examples/sveltekit/static/favicon.ico differ diff --git a/examples/sveltekit/static/robots.txt b/examples/sveltekit/static/robots.txt new file mode 100644 index 0000000..2c5308f --- /dev/null +++ b/examples/sveltekit/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: \ No newline at end of file diff --git a/examples/sveltekit/svelte.config.js b/examples/sveltekit/svelte.config.js new file mode 100644 index 0000000..3fc56b9 --- /dev/null +++ b/examples/sveltekit/svelte.config.js @@ -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; diff --git a/examples/sveltekit/tsconfig.json b/examples/sveltekit/tsconfig.json new file mode 100644 index 0000000..4344710 --- /dev/null +++ b/examples/sveltekit/tsconfig.json @@ -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" + } +} diff --git a/examples/sveltekit/vite.config.ts b/examples/sveltekit/vite.config.ts new file mode 100644 index 0000000..80864b9 --- /dev/null +++ b/examples/sveltekit/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [sveltekit()], +});