mirror of
https://github.com/bknd-io/bknd/
synced 2026-08-02 08:06:00 +00:00
Merge remote-tracking branch 'origin/main' into feat/jsonv-refactor
# Conflicts: # bun.lock
This commit is contained in:
@@ -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
@@ -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,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,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);
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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(
|
||||
() =>
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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=="],
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user