updated README and documentation (first part) to match 0.6

This commit is contained in:
dswbx
2025-01-21 18:30:01 +01:00
parent 24542f30cb
commit f64e5dac03
21 changed files with 484 additions and 227 deletions

66
docs/usage/cli.mdx Normal file
View File

@@ -0,0 +1,66 @@
---
title: 'Using the CLI'
description: 'How to start a bknd instance using the CLI.'
---
Instead of running **bknd** using a framework, you can also use the CLI to quickly spin up a
full functional instance. To see all available options, run:
```
npx bknd
```
Here is the output:
```
$ npx bknd
Usage: bknd [options] [command]
bknd cli
Options:
-V, --version output the version number
-h, --help display help for command
Commands:
user <action> create and update user (auth)
schema [options] get schema
run [options]
config [options] get default config
help [command] display help for command
```
## Starting an instance (`run`)
To see all available `run` options, execute `npx bknd run --help`.
```
$ npx bknd run --help
Usage: bknd run [options]
Options:
-p, --port <port> port to run on (default: 1337, env: PORT)
-c, --config <config> config file
--db-url <db> database url, can be any valid libsql url
--db-token <db> database token
--server <server> server type (choices: "node", "bun", default: "node")
-h, --help display help for command
```
### In-memory database
To start an instance with an ephemeral in-memory database, run the following:
```
npx bknd run
```
Keep in mind that the database is not persisted and will be lost when the process is terminated.
### File-based database
To start an instance with a file-based database, run the following:
```
npx bknd run --db-url file:data.db
```
### Turso/LibSQL database
To start an instance with a Turso/LibSQL database, run the following:
```
npx bknd run --db-url libsql://your-db.turso.io --db-token <your-token>
```
The `--db-token` option is optional and only required if the database is protected.

195
docs/usage/database.mdx Normal file
View File

@@ -0,0 +1,195 @@
---
title: 'Database'
description: 'Choosing the right database configuration'
---
In order to use **bknd**, you need to prepare access information to your database and install
the dependencies.
<Note>
Connections to the database are managed using Kysely. Therefore, all its dialects are
theoretically supported. However, only the `SQLite` dialect is implemented as of now.
</Note>
## Database
### SQLite in-memory
The easiest to get started is using SQLite in-memory. When serving the API in the "Integrations",
the function accepts an object with connection details. To use an in-memory database, you can either omit the object completely or explicitly use it as follows:
```json
{
"type": "libsql",
"config": {
"url": ":memory:"
}
}
```
### SQLite as file
Just like the in-memory option, using a file is just as easy:
```json
{
"type": "libsql",
"config": {
"url": "file:<path/to/your/database.db>"
}
}
```
Please note that using SQLite as a file is only supported in server environments.
### SQLite using LibSQL
Turso offers a SQLite-fork called LibSQL that runs a server around your SQLite database. To
point **bknd** to a local instance of LibSQL, [install Turso's CLI](https://docs.turso.tech/cli/introduction) and run the following command:
```bash
turso dev
```
The command will yield a URL. Use it in the connection object:
```json
{
"type": "libsql",
"config": {
"url": "http://localhost:8080"
}
}
```
### SQLite using LibSQL on Turso
If you want to use LibSQL on Turso, [sign up for a free account](https://turso.tech/), create a database and point your
connection object to your new database:
```json
{
"type": "libsql",
"config": {
"url": "libsql://your-database-url.turso.io",
"authToken": "your-auth-token"
}
}
```
### Custom Connection
<Note>
Follow the progress of custom connections on its [Github Issue](https://github.com/bknd-io/bknd/issues/24).
If you're interested, make sure to upvote so it can be prioritized.
</Note>
Any bknd app instantiation accepts as connection either `undefined`, a connection object like
described above, or an class instance that extends from `Connection`:
```ts
import { createApp } from "bknd";
import { Connection } from "bknd/data";
class CustomConnection extends Connection {
constructor() {
const kysely = new Kysely(/* ... */);
super(kysely);
}
}
const connection = new CustomConnection();
// e.g. and then, create an instance
const app = createApp({ connection })
```
## Initial Structure
To provide an initial database structure, you can pass `initialConfig` to the creation of an app. This will only be used if there isn't an existing configuration found in the database given. Here is a quick example:
```ts
import { em, entity, text, number } from "bknd/data";
const schema = em({
posts: entity("posts", {
// "id" is automatically added
title: text().required(),
slug: text().required(),
content: text(),
views: number()
}),
comments: entity("comments", {
content: text()
})
// relations and indices are defined separately.
// the first argument are the helper functions, the second the entities.
}, ({ relation, index }, { posts, comments }) => {
relation(comments).manyToOne(posts);
// relation as well as index can be chained!
index(posts).on(["title"]).on(["slug"], true);
});
// to get a type from your schema, use:
type Database = (typeof schema)["DB"];
// type Database = {
// posts: {
// id: number;
// title: string;
// content: string;
// views: number;
// },
// comments: {
// id: number;
// content: string;
// }
// }
// pass the schema to the app
const app = createApp({
connection: { /* ... */ },
initialConfig: {
data: schema.toJSON()
}
});
```
Note that we didn't add relational fields directly to the entity, but instead defined them afterwards. That is because the relations are managed outside the entity scope to have an unified expierence for all kinds of relations (e.g. many-to-many).
<Note>
Defined relations are currently not part of the produced types for the structure. We're working on that, but in the meantime, you can define them manually.
</Note>
### Type completion
All entity related functions use the types defined in `DB` from `bknd/core`. To get type completion, you can extend that interface with your own schema:
```ts
import { em } from "bknd/data";
import { Api } from "bknd";
// const schema = em({ ... });
type Database = (typeof schema)["DB"];
declare module "bknd/core" {
interface DB extends Database {}
}
const api = new Api({ /* ... */ });
const { data: posts } = await api.data.readMany("posts", {})
// `posts` is now typed as Database["posts"]
```
The type completion is available for the API as well as all provided [React hooks](/usage/react).
### Seeding the database
To seed your database with initial data, you can pass a `seed` function to the configuration. It
provides the `ModuleBuildContext` ([reference](/usage/introduction#modulebuildcontext)) as the first argument.
Note that the seed function will only be executed on app's first boot. If a configuration
already exists in the database, it will not be executed.
```ts
import { createApp, type ModuleBuildContext } from "bknd";
const app = createApp({
connection: { /* ... */ },
initialConfig: { /* ... */ },
options: {
seed: async (ctx: ModuleBuildContext) => {
await ctx.em.mutator("posts").insertMany([
{ title: "First post", slug: "first-post", content: "..." },
{ title: "Second post", slug: "second-post" }
]);
}
}
});
```

25
docs/usage/elements.mdx Normal file
View File

@@ -0,0 +1,25 @@
---
title: "React Elements"
description: "Speed up your frontend development"
---
<Note>
The documentation is currently a work in progress and not complete.
</Note>
Not only creating and maintaing a backend is time-consuming, but also integrating it into your frontend can be a hassle. With `bknd/elements`, you can easily add media uploads and authentication forms to your app without having to figure out API details.
## Media uploads
```tsx
import { Media } from "bknd/elements"
export function UserAvatar() {
return <Media.Dropzone
entity={{ name: "users", id: 1, field: "avatar" }}
maxItems={1}
overwrite
/>
}
```

205
docs/usage/introduction.mdx Normal file
View File

@@ -0,0 +1,205 @@
---
title: 'Introduction'
description: 'Setting up bknd'
---
There are several methods to get **bknd** up and running. You can choose between these options:
1. [Run it using the CLI](/usage/cli): That's the easiest and fastest way to get started.
2. Use a runtime like [Node](/integration/node), [Bun](/integration/bun) or
[Cloudflare](/integration/cloudflare) (workerd). This will run the API and UI in the runtime's
native server and serves the UI assets statically from `node_modules`.
3. Run it inside your React framework of choice like [Next.js](/integration/nextjs),
[Astro](/integration/astro) or [Remix](/integration/remix).
There is also a fourth option, which is running it inside a
[Docker container](/integration/docker). This is essentially a wrapper around the CLI.
## Basic setup
Regardless of the method you choose, at the end all adapters come down to the actual
instantiation of the `App`, which in raw looks like this:
```ts
import { createApp, type CreateAppConfig } from "bknd";
// create the app
const config = { /* ... */ } satisfies CreateAppConfig;
const app = createApp(config);
// build the app
await app.build();
// export for Web API compliant envs
export default app;
```
In Web API compliant environments, all you have to do is to default exporting the app, as it
implements the `Fetch` API.
## Configuration (`CreateAppConfig`)
The `CreateAppConfig` type is the main configuration object for the `createApp` function. It has
the following properties:
```ts
import type { Connection } from "bknd/data";
import type { Config } from "@libsql/client";
type AppPlugin = (app: App) => Promise<void> | void;
type LibSqlCredentials = Config;
type CreateAppConfig = {
connection?:
| Connection
| {
type: "libsql";
config: LibSqlCredentials;
};
initialConfig?: InitialModuleConfigs;
plugins?: AppPlugin[];
options?: {
basePath?: string;
trustFetched?: boolean;
onFirstBoot?: () => Promise<void>;
seed?: (ctx: ModuleBuildContext) => Promise<void>;
};
};
```
### `connection`
The `connection` property is the main connection object to the database. It can be either an
object with a type specifier (only `libsql` is supported at the moment) and the actual
`Connection` class. The `libsql` connection object looks like this:
```ts
const connection = {
type: "libsql",
config: {
url: string;
authToken?: string;
};
}
```
Alternatively, you can pass an instance of a `Connection` class directly,
see [Custom Connection](/usage/database#custom-connection) as a reference.
If the connection object is omitted, the app will try to use an in-memory database.
### `initialConfig`
As initial configuration, you can either pass a partial configuration object or a complete one
with a version number. The version number is used to automatically migrate the configuration up
to the latest version upon boot. The default configuration looks like this:
```json
{
"server": {
"admin": {
"basepath": "",
"color_scheme": "light",
"logo_return_path": "/"
},
"cors": {
"origin": "*",
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE" ],
"allow_headers": ["Content-Type", "Content-Length", "Authorization", "Accept"]
}
},
"data": {
"basepath": "/api/data",
"entities": {},
"relations": {},
"indices": {}
},
"auth": {
"enabled": false,
"basepath": "/api/auth",
"entity_name": "users",
"allow_register": true,
"jwt": {
"secret": "",
"alg": "HS256",
"fields": ["id", "email", "role"]
},
"cookie": {
"path": "/",
"sameSite": "lax",
"secure": true,
"httpOnly": true,
"expires": 604800,
"renew": true,
"pathSuccess": "/",
"pathLoggedOut": "/"
},
"strategies": {
"password": {
"type": "password",
"config": {
"hashing": "sha256"
}
}
},
"roles": {}
},
"media": {
"enabled": false,
"basepath": "/api/media",
"entity_name": "media",
"storage": {}
},
"flows": {
"basepath": "/api/flows",
"flows": {}
}
}
```
You can use the CLI to get the default configuration:
```sh
npx bknd config --pretty
```
To validate your configuration against a JSON schema, you can also dump the schema using the CLI:
```sh
npx bknd schema
```
To create an initial data structure, you can use helpers [described here](/usage/database#initial-structure).
### `plugins`
The `plugins` property is an array of functions that are called after the app has been built,
but before its event is emitted. This is useful for adding custom routes or other functionality.
A simple plugin that adds a custom route looks like this:
```ts
export const myPlugin: AppPlugin = (app) => {
app.server.get("/hello", (c) => c.json({ hello: "world" }));
};
```
Since each plugin has full access to the `app` instance, it can add routes, modify the database
structure, add custom middlewares, respond to or add events, etc. Plugins are very powerful, so
make sure to only run trusted ones.
### `options`
This object is passed to the `ModuleManager` which is responsible for:
- validating and maintaining configuration of all modules
- building all modules (data, auth, media, flows)
- maintaining the `ModuleBuildContext` used by the modules
The `options` object has the following properties:
- `basePath` (`string`): The base path for the Hono instance. This is used to prefix all routes.
- `trustFetched` (`boolean`): If set to `true`, the app will not perform any validity checks for
the given or fetched configuration.
- `onFirstBoot` (`() => Promise<void>`): A function that is called when the app is booted for
the first time.
- `seed` (`(ctx: ModuleBuildContext) => Promise<void>`): A function that is called when the app is
booted for the first time and an initial partial configuration is provided.
## `ModuleBuildContext`
```ts
type ModuleBuildContext = {
connection: Connection;
server: Hono;
em: EntityManager;
emgr: EventManager;
guard: Guard;
};
```

198
docs/usage/react.mdx Normal file
View File

@@ -0,0 +1,198 @@
---
title: 'SDK (React)'
description: 'Use the bknd SDK for React'
---
bknd exports 4 useful hooks to work with your backend:
1. simple hooks which are solely based on the [API](/usage/sdk):
- [`useApi`](#useapi)
- [`useEntity`](#useentity)
2. query hooks that wraps the API in [SWR](https://swr.vercel.app/):
- [`useApiQuery`](#useapiquery)
- [`useEntityQuery`](#useentityquery)
## `useApi()`
To use the simple hook that returns the Api, you can use:
```tsx
import { useApi } from "bknd/client";
export default function App() {
const api = useApi();
// ...
}
```
## `useApiQuery()`
This hook wraps the API class in an SWR hook for convenience. You can use any API endpoint
supported, like so:
```tsx
import { useApiQuery } from "bknd/client";
export default function App() {
const { data, ...swr } = useApiQuery((api) => api.data.readMany("comments"));
if (swr.error) return <div>Error</div>
if (swr.isLoading) return <div>Loading...</div>
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
```
### Props
* `selector: (api: Api) => FetchPromise`
The first parameter is a selector function that provides an Api instance and expects an
endpoint function to be returned.
* `options`: optional object that inherits from `SWRConfiguration`
```ts
type Options <Data> = import("swr").SWRConfiguration & {
enabled? : boolean;
refine? : (data: Data) => Data | any;
}
```
* `enabled`: Determines whether this hook should trigger a fetch of the data or not.
* `refine`: Optional refinement that is called after a response from the API has been
received. Useful to omit irrelevant data from the response (see example below).
### Using mutations
To query and mutate data using this hook, you can leverage the parameters returned. In the
following example we'll also use a `refine` function as well as `revalidateOnFocus` (option from
`SWRConfiguration`) so that our data keeps updating on window focus change.
```tsx
import { useEffect, useState } from "react";
import { useApiQuery } from "bknd/client";
export default function App() {
const [text, setText] = useState("");
const { data, api, mutate, ...q } = useApiQuery(
(api) => api.data.readOne("comments", 1),
{
// filter to a subset of the response
refine: (data) => data.data,
revalidateOnFocus: true
}
);
const comment = data ? data : null;
useEffect(() => {
setText(comment?.content ?? "");
}, [comment]);
if (q.error) return <div>Error</div>
if (q.isLoading) return <div>Loading...</div>
return (
<form
onSubmit={async (e) => {
e.preventDefault();
if (!comment) return;
// this will automatically revalidate the query
await mutate(async () => {
const res = await api.data.updateOne("comments", comment.id, {
content: text
});
return res.data;
});
return false;
}}
>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">Update</button>
</form>
);
}
```
## `useEntity()`
This hook wraps the endpoints of `DataApi` and returns CRUD options as parameters:
```tsx
import { useState, useEffect } from "react";
import { useEntity } from "bknd/client";
export default function App() {
const [data, setData] = useState<any>();
const { create, read, update, _delete } = useEntity("comments", 1);
useEffect(() => {
read().then(setData);
}, []);
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
```
If you only supply the entity name as string without an ID, the `read` method will fetch a list
of entities instead of a single entry.
### Props
Following props are available when using `useEntityQuery([entity], [id?])`:
- `entity: string`: Specify the table name of the entity
- `id?: number | string`: If an id given, it will fetch a single entry, otherwise a list
### Returned actions
The following actions are returned from this hook:
- `create: (input: object)`: Create a new entry
- `read: (query: Partial<RepoQuery> = {})`: If an id was given,
it returns a single item, otherwise a list
- `update: (input: object, id?: number | string)`: If an id was given, the id parameter is
optional. Updates the given entry partially.
- `_delete: (id?: number | string)`: If an id was given, the id parameter is
optional. Deletes the given entry.
## `useEntityQuery()`
This hook wraps the actions from `useEntity` around `SWR`. The previous example would look like
this:
```tsx
import { useEntityQuery } from "bknd/client";
export default function App() {
const { data } = useEntityQuery("comments", 1);
return <pre>{JSON.stringify(data, null, 2)}</pre>
}
```
### Using mutations
All actions returned from `useEntityQuery` are conveniently wrapped around the `mutate` function,
so you don't have think about this:
```tsx
import { useState, useEffect } from "react";
import { useEntityQuery } from "bknd/client";
export default function App() {
const [text, setText] = useState("");
const { data, update, ...q } = useEntityQuery("comments", 1);
const comment = data ? data : null;
useEffect(() => {
setText(comment?.content ?? "");
}, [comment]);
if (q.error) return <div>Error</div>
if (q.isLoading) return <div>Loading...</div>
return (
<form
onSubmit={async (e) => {
e.preventDefault();
if (!comment) return;
// this will automatically revalidate the query
await update({ content: text });
return false;
}}
>
<input type="text" value={text} onChange={(e) => setText(e.target.value)} />
<button type="submit">Update</button>
</form>
);
}
```

110
docs/usage/sdk.mdx Normal file
View File

@@ -0,0 +1,110 @@
---
title: 'SDK (TypeScript)'
description: 'Use the bknd SDK in TypeScript'
---
To start using the bknd API, start by creating a new API instance:
```ts
import { Api } from "bknd";
const api = new Api({
host: "..." // point to your bknd instance
});
// make sure to verify auth
await api.verifyAuth();
```
The `Api` class is the main entry point for interacting with the bknd API. It provides methods
for all available modules described below.
## Data (`api.data`)
Access the `Data` specific API methods at `api.data`.
### `data.readMany([entity], [query])`
To retrieve a list of records from an entity, use the `readMany` method:
```ts
const { data } = await api.data.readMany("posts");
```
You can also add additional query instructions:
```ts
const { data } = await api.data.readMany("posts", {
limit: 10,
offset: 0,
select: ["id", "title", "views"],
with: ["comments"],
where: {
title: "Hello, World!",
views: {
$gt: 100
}
},
sort: { by: "views", order: "desc" }
});
```
The `with` property automatically adds the related entries to the response.
### `data.readOne([entity], [id])`
To retrieve a single record from an entity, use the `readOne` method:
```ts
const { data } = await api.data.readOne("posts", 1);
```
### `data.createOne([entity], [data])`
To create a single record of an entity, use the `createOne` method:
```ts
const { data } = await api.data.createOne("posts", {
title: "Hello, World!",
content: "This is a test post.",
views: 0
});
```
### `data.updateOne([entity], [id], [data])`
To update a single record of an entity, use the `updateOne` method:
```ts
const { data } = await api.data.updateOne("posts", 1, {
views: 1
});
```
### `data.deleteOne([entity], [id])`
To delete a single record of an entity, use the `deleteOne` method:
```ts
const { data } = await api.data.deleteOne("posts", 1);
```
## Auth (`api.auth`)
Access the `Auth` specific API methods at `api.auth`. If there is successful authentication, the
API will automatically save the token and use it for subsequent requests.
### `auth.loginWithPassword([input])`
To log in with a password, use the `loginWithPassword` method:
```ts
const { data } = await api.auth.loginWithPassword({
email: "...",
password: "..."
});
```
### `auth.registerWithPassword([input])`
To register with a password, use the `registerWithPassword` method:
```ts
const { data } = await api.auth.registerWithPassword({
email: "...",
password: "..."
});
```
### `auth.me()`
To retrieve the current user, use the `me` method:
```ts
const { data } = await api.auth.me();
```
### `auth.strategies()`
To retrieve the available authentication strategies, use the `strategies` method:
```ts
const { data } = await api.auth.strategies();
```