Merge remote-tracking branch 'origin/main' into feat/jsonv-refactor

# Conflicts:
#	bun.lock
This commit is contained in:
dswbx
2025-07-05 11:11:06 +02:00
16 changed files with 114 additions and 62 deletions
+8
View File
@@ -9,10 +9,18 @@
</p>
bknd simplifies app development by providing a fully functional backend for database management, authentication, media and workflows. Being lightweight and built on Web Standards, it can be deployed nearly anywhere, including running inside your framework of choice. No more deploying multiple separate services!
* **Runtimes**: Node.js 22+, Bun 1.0+, Deno, Browser, Cloudflare Workers/Pages, Vercel, Netlify, AWS Lambda, etc.
* **Databases**:
* SQLite: LibSQL, Node SQLite, Bun SQLite, Cloudflare D1, Cloudflare Durable Objects SQLite, SQLocal
* Postgres: Vanilla Postgres, Supabase, Neon, Xata
* **Frameworks**: React, Next.js, React Router, Astro, Vite, Waku
* **Storage**: AWS S3, S3-compatible (Tigris, R2, Minio, etc.), Cloudflare R2 (binding), Cloudinary, Filesystem
**For documentation and examples, please visit https://docs.bknd.io.**
> [!WARNING]
> This project requires Node.js 22 or higher (because of `node:sqlite`).
>
> Please keep in mind that **bknd** is still under active development
> and therefore full backward compatibility is not guaranteed before reaching v1.0.0.
+1 -1
View File
@@ -3,7 +3,7 @@
"type": "module",
"sideEffects": false,
"bin": "./dist/cli/index.js",
"version": "0.15.0-rc.8",
"version": "0.15.0",
"description": "Lightweight Firebase/Supabase alternative built to run anywhere — incl. Next.js, React Router, Astro, Cloudflare, Bun, Node, AWS Lambda & more.",
"homepage": "https://bknd.io",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
import type { Connection } from "bknd/data";
import { bunSqlite } from "../bun/connection/BunSqliteConnection";
export function sqlite(config: { url: string }): Connection {
export function sqlite(config?: { url: string }): Connection {
return bunSqlite(config);
}
+1 -1
View File
@@ -1,6 +1,6 @@
import type { Connection } from "bknd/data";
import { nodeSqlite } from "../node/connection/NodeSqliteConnection";
export function sqlite(config: { url: string }): Connection {
export function sqlite(config?: { url: string }): Connection {
return nodeSqlite(config);
}
+8 -4
View File
@@ -334,9 +334,9 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
await setSignedCookie(c, "auth", token, secret, this.cookieOptions);
}
private async deleteAuthCookie(c: Context) {
private deleteAuthCookie(c: Context) {
$console.debug("deleting auth cookie");
await deleteCookie(c, "auth", this.cookieOptions);
deleteCookie(c, "auth", this.cookieOptions);
}
async logout(c: Context<ServerEnv>) {
@@ -345,9 +345,13 @@ export class Authenticator<Strategies extends Record<string, Strategy> = Record<
const cookie = await this.getAuthCookie(c);
if (cookie) {
await this.deleteAuthCookie(c);
await addFlashMessage(c, "Signed out", "info");
addFlashMessage(c, "Signed out", "info");
}
// on waku, only one cookie setting is performed
// therefore adding deleting cookie at the end
// as the flash isn't that important
this.deleteAuthCookie(c);
}
// @todo: move this to a server helper
@@ -1,4 +1,4 @@
import type { Client, Config, ResultSet } from "@libsql/client";
import type { Client, Config, InStatement, ResultSet, TransactionMode } from "@libsql/client";
import { createClient } from "libsql-stateless-easy";
import { FilterNumericKeysPlugin } from "data/plugins/FilterNumericKeysPlugin";
import {
@@ -10,7 +10,12 @@ import type { QueryResult } from "kysely";
export type LibsqlConnection = GenericSqliteConnection<Client>;
export type LibSqlCredentials = Config;
function getClient(clientOrCredentials: Client | LibSqlCredentials): Client {
export type LibsqlClientFns = {
execute: (statement: InStatement) => Promise<ResultSet>;
batch: (statements: InStatement[], mode?: TransactionMode) => Promise<ResultSet[]>;
};
function getClient(clientOrCredentials: Client | LibSqlCredentials | LibsqlClientFns): Client {
if (clientOrCredentials && "url" in clientOrCredentials) {
const { url, authToken } = clientOrCredentials;
return createClient({ url, authToken });
@@ -19,7 +24,7 @@ function getClient(clientOrCredentials: Client | LibSqlCredentials): Client {
return clientOrCredentials as Client;
}
export function libsql(config: LibSqlCredentials | Client) {
export function libsql(config: LibSqlCredentials | Client | LibsqlClientFns) {
const db = getClient(config);
return genericSqlite(
+2 -2
View File
@@ -51,7 +51,7 @@ export class AdminController extends Controller {
basepath: this._options.basepath ?? "/",
adminBasepath: this._options.adminBasepath ?? "",
assetsPath: this._options.assetsPath ?? config.server.assets_path,
theme: this._options.theme ?? "system",
//theme: this._options.theme ?? "system",
logo_return_path: this._options.logoReturnPath ?? "/",
};
}
@@ -195,7 +195,7 @@ export class AdminController extends Controller {
if (isProd) {
let manifest: any;
if (this.options.assetsPath.startsWith("http")) {
manifest = await fetch(this.options.assetsPath + "manifest.json", {
manifest = await fetch(this.options.assetsPath + ".vite/manifest.json", {
headers: {
Accept: "application/json",
},
+1 -1
View File
@@ -55,7 +55,7 @@ export default function Admin({
const Skeleton = ({ theme }: { theme?: any }) => {
const t = useTheme();
const actualTheme = theme ?? t.theme;
const actualTheme = theme && ["dark", "light"].includes(theme) ? theme : t.theme;
return (
<div id="bknd-admin" className={actualTheme + " antialiased"}>
+3 -17
View File
@@ -4,6 +4,7 @@ import { Link, useLocation } from "wouter";
import { IconButton } from "../../components/buttons/IconButton";
import { Dropdown } from "../../components/overlay/Dropdown";
import { useEvent } from "../../hooks/use-event";
import { useNavigate } from "ui/lib/routes";
type Breadcrumb = {
label: string | Element;
@@ -17,26 +18,11 @@ export type Breadcrumbs2Props = {
};
export const Breadcrumbs2 = ({ path: _path, backTo, onBack }: Breadcrumbs2Props) => {
const [_, navigate] = useLocation();
const location = window.location.pathname;
const [, , _goBack] = useNavigate();
const path = Array.isArray(_path) ? _path : [_path];
const loc = location.split("/").filter((v) => v !== "");
const hasBack = path.length > 1;
const goBack = onBack
? onBack
: useEvent(() => {
if (backTo) {
navigate(backTo, { replace: true });
return;
} else if (_path.length > 0 && _path[0]?.href) {
navigate(_path[0].href, { replace: true });
return;
}
const href = loc.slice(0, path.length + 1).join("/");
navigate(`~/${href}`, { replace: true });
});
const goBack = onBack ? onBack : () => _goBack({ fallback: backTo });
const crumbs = useMemo(
() =>
+1 -3
View File
@@ -185,9 +185,7 @@ function UserMenu() {
}
}
if (!options.theme) {
items.push(() => <UserMenuThemeToggler />);
}
items.push(() => <UserMenuThemeToggler />);
items.push(() => (
<div className="font-mono leading-none text-xs text-primary/50 text-center pb-1 pt-2 mt-1 border-t border-primary/5">
{getVersion()}
+17 -1
View File
@@ -102,13 +102,29 @@ export function useNavigate() {
}
const _url = options?.absolute ? `~/${basepath}${url}`.replace(/\/+/g, "/") : url;
const state = {
...options?.state,
referrer: location,
};
navigate(options?.query ? withQuery(_url, options?.query) : _url, {
replace: options?.replace,
state: options?.state,
state,
});
});
},
location,
(opts?: { fallback?: string }) => {
const state = window.history.state;
if (state?.referrer) {
//window.history.replaceState(state, "", state.referrer);
navigate(state.referrer, { replace: true });
} else if (opts?.fallback) {
navigate(opts.fallback, { replace: true });
} else {
window.history.back();
}
},
] as const;
}
+5 -8
View File
@@ -31,7 +31,7 @@ function DataEntityUpdateImpl({ params }) {
const entityId = params.id as PrimaryFieldType;
const [error, setError] = useState<string | null>(null);
const [navigate] = useNavigate();
const [navigate, _, _goBack] = useNavigate();
useBrowserTitle(["Data", entity.label, `#${entityId}`]);
const targetRelations = relations.listableRelationsOf(entity);
@@ -52,9 +52,8 @@ function DataEntityUpdateImpl({ params }) {
},
);
function goBack() {
window.history.go(-1);
}
const backHref = routes.data.entity.list(entity.name);
const goBack = () => _goBack({ fallback: backHref });
async function onSubmitted(changeSet?: EntityData) {
//return;
@@ -162,10 +161,8 @@ function DataEntityUpdateImpl({ params }) {
className="pl-3"
>
<Breadcrumbs2
path={[
{ label: entity.label, href: routes.data.entity.list(entity.name) },
{ label: `Edit #${entityId}` },
]}
backTo={backHref}
path={[{ label: entity.label, href: backHref }, { label: `Edit #${entityId}` }]}
/>
</AppShell.SectionHeader>
{$q.isLoading ? (
+5 -10
View File
@@ -8,13 +8,14 @@ import { useBrowserTitle } from "ui/hooks/use-browser-title";
import { useSearch } from "ui/hooks/use-search";
import * as AppShell from "ui/layouts/AppShell/AppShell";
import { Breadcrumbs2 } from "ui/layouts/AppShell/Breadcrumbs2";
import { routes } from "ui/lib/routes";
import { routes, useNavigate } from "ui/lib/routes";
import { EntityForm } from "ui/modules/data/components/EntityForm";
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
import { s } from "core/object/schema";
export function DataEntityCreate({ params }) {
const { $data } = useBkndData();
const [navigate, _, _goBack] = useNavigate();
const entity = $data.entity(params.entity as string);
if (!entity) {
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
@@ -30,9 +31,8 @@ export function DataEntityCreate({ params }) {
// @todo: use entity schema for prefilling
const search = useSearch(s.object({}), {});
function goBack() {
window.history.go(-1);
}
const backHref = routes.data.entity.list(entity.name);
const goBack = () => _goBack({ fallback: backHref });
async function onSubmitted(changeSet?: EntityData) {
console.log("create:changeSet", changeSet);
@@ -80,12 +80,7 @@ export function DataEntityCreate({ params }) {
</>
}
>
<Breadcrumbs2
path={[
{ label: entity.label, href: routes.data.entity.list(entity.name) },
{ label: "Create" },
]}
/>
<Breadcrumbs2 backTo={backHref} path={[{ label: entity.label }, { label: "Create" }]} />
</AppShell.SectionHeader>
<AppShell.Scrollable key={entity.name}>
{error && (
+4 -3
View File
@@ -15,7 +15,7 @@
},
"app": {
"name": "bknd",
"version": "0.15.0-rc.8",
"version": "0.15.0",
"bin": "./dist/cli/index.js",
"dependencies": {
"@cfworker/json-schema": "^4.1.1",
@@ -125,6 +125,7 @@
"version": "0.5.1",
"devDependencies": {
"@types/bun": "latest",
"bknd": "workspace:*",
"tsdx": "^0.14.1",
"typescript": "^5.0.0",
},
@@ -1220,7 +1221,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.20.6", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg=="],
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
"@types/bun": ["@types/bun@1.2.18", "", { "dependencies": { "bun-types": "1.2.18" } }, "sha512-Xf6RaWVheyemaThV0kUfaAUvCNokFr+bH8Jxp+tTZfx7dAPA8z9ePnP9S9+Vspzuxxx9JRAXhnyccRj3GyCMdQ=="],
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
@@ -4026,7 +4027,7 @@
"@testing-library/jest-dom/chalk": ["chalk@3.0.0", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg=="],
"@types/bun/bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"@types/bun/bun-types": ["bun-types@1.2.18", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-04+Eha5NP7Z0A9YgDAzMk5PHR16ZuLVa83b26kH5+cp1qZW4F6FmAURngE7INf4tKOvCE69vYvDEwoNl1tGiWw=="],
"@types/pg/pg-types": ["pg-types@4.0.2", "", { "dependencies": { "pg-int8": "1.0.1", "pg-numeric": "1.0.2", "postgres-array": "~3.0.1", "postgres-bytea": "~3.0.0", "postgres-date": "~2.1.0", "postgres-interval": "^3.0.0", "postgres-range": "^1.1.1" } }, "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng=="],
+41 -1
View File
@@ -36,4 +36,44 @@ docker run -p 1337:1337 -e ARGS="--db-url file:/data/data.db" bknd
To mount the data directory to the host, you can use the `-v` flag:
```bash
docker run -p 1337:1337 -v /path/to/data:/data bknd
```
```
## Docker compose example
If you want to use docker compose and build the image directly from the git repository.
```yaml compose.yml
services:
bknd:
pull_policy: build
build: https://github.com/bknd-io/bknd.git#main:docker
ports:
- 1337:1337
environment:
ARGS: "--db-url file:/data/data.db"
volumes:
- ${DATA_DIR:-.}/data:/data
```
The docker compose file can be extended to build a specific version of bknd.
Extend the `build` section with `args` and `labels`.
Inside `args`, you can pass a `VERSION` build argument, and use `labels` so the built image receives a unique identifier.
```yaml compose.yml
services:
bknd:
pull_policy: build
build:
context: https://github.com/bknd-io/bknd.git#main:docker
args:
VERSION: <version>
labels:
- x-bknd-version=<version>
ports:
- 1337:1337
environment:
ARGS: "--db-url file:/data/data.db"
volumes:
- ${DATA_DIR:-.}/data:/data
```
+8 -6
View File
@@ -107,8 +107,8 @@ import { libsql } from "bknd/data";
export default {
connection: libsql({
url: "libsql://your-database-url.turso.io",
authToken: "your-auth-token",
url: "libsql://<database>.turso.io",
authToken: "<auth-token>",
}),
} as const satisfies BkndConfig;
```
@@ -120,11 +120,13 @@ import type { BkndConfig } from "bknd";
import { libsql } from "bknd/data";
import { createClient } from "@libsql/client";
const client = createClient({
url: "libsql://<database>.turso.io",
authToken: "<auth-token>",
})
export default {
connection: libsql(createClient({
url: "libsql://your-database-url.turso.io",
authToken: "your-auth-token",
})),
connection: libsql(client),
} as const satisfies BkndConfig;
```