diff --git a/README.md b/README.md index d473e360..ab86304e 100644 --- a/README.md +++ b/README.md @@ -9,10 +9,18 @@

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. diff --git a/app/package.json b/app/package.json index e0361b4f..4165bdb1 100644 --- a/app/package.json +++ b/app/package.json @@ -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": { diff --git a/app/src/adapter/sqlite/bun.ts b/app/src/adapter/sqlite/bun.ts index eaf02c4b..6d549181 100644 --- a/app/src/adapter/sqlite/bun.ts +++ b/app/src/adapter/sqlite/bun.ts @@ -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); } diff --git a/app/src/adapter/sqlite/node.ts b/app/src/adapter/sqlite/node.ts index 7bc6ebda..f14a8569 100644 --- a/app/src/adapter/sqlite/node.ts +++ b/app/src/adapter/sqlite/node.ts @@ -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); } diff --git a/app/src/auth/authenticate/Authenticator.ts b/app/src/auth/authenticate/Authenticator.ts index b2c5097d..f7a3e401 100644 --- a/app/src/auth/authenticate/Authenticator.ts +++ b/app/src/auth/authenticate/Authenticator.ts @@ -334,9 +334,9 @@ export class Authenticator = 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) { @@ -345,9 +345,13 @@ export class Authenticator = 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 diff --git a/app/src/data/connection/sqlite/libsql/LibsqlConnection.ts b/app/src/data/connection/sqlite/libsql/LibsqlConnection.ts index c99ad590..32de8fc4 100644 --- a/app/src/data/connection/sqlite/libsql/LibsqlConnection.ts +++ b/app/src/data/connection/sqlite/libsql/LibsqlConnection.ts @@ -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; export type LibSqlCredentials = Config; -function getClient(clientOrCredentials: Client | LibSqlCredentials): Client { +export type LibsqlClientFns = { + execute: (statement: InStatement) => Promise; + batch: (statements: InStatement[], mode?: TransactionMode) => Promise; +}; + +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( diff --git a/app/src/modules/server/AdminController.tsx b/app/src/modules/server/AdminController.tsx index 35ebc819..5cb66d6c 100644 --- a/app/src/modules/server/AdminController.tsx +++ b/app/src/modules/server/AdminController.tsx @@ -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", }, diff --git a/app/src/ui/Admin.tsx b/app/src/ui/Admin.tsx index 86b4e8ed..df142acd 100644 --- a/app/src/ui/Admin.tsx +++ b/app/src/ui/Admin.tsx @@ -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 (
diff --git a/app/src/ui/layouts/AppShell/Breadcrumbs2.tsx b/app/src/ui/layouts/AppShell/Breadcrumbs2.tsx index b495b435..2b7c5621 100644 --- a/app/src/ui/layouts/AppShell/Breadcrumbs2.tsx +++ b/app/src/ui/layouts/AppShell/Breadcrumbs2.tsx @@ -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( () => diff --git a/app/src/ui/layouts/AppShell/Header.tsx b/app/src/ui/layouts/AppShell/Header.tsx index 0b313c5a..629d5374 100644 --- a/app/src/ui/layouts/AppShell/Header.tsx +++ b/app/src/ui/layouts/AppShell/Header.tsx @@ -185,9 +185,7 @@ function UserMenu() { } } - if (!options.theme) { - items.push(() => ); - } + items.push(() => ); items.push(() => (
{getVersion()} diff --git a/app/src/ui/lib/routes.ts b/app/src/ui/lib/routes.ts index b94e550f..42b896ba 100644 --- a/app/src/ui/lib/routes.ts +++ b/app/src/ui/lib/routes.ts @@ -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; } diff --git a/app/src/ui/routes/data/data.$entity.$id.tsx b/app/src/ui/routes/data/data.$entity.$id.tsx index 978573ab..e531452d 100644 --- a/app/src/ui/routes/data/data.$entity.$id.tsx +++ b/app/src/ui/routes/data/data.$entity.$id.tsx @@ -31,7 +31,7 @@ function DataEntityUpdateImpl({ params }) { const entityId = params.id as PrimaryFieldType; const [error, setError] = useState(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" > {$q.isLoading ? ( diff --git a/app/src/ui/routes/data/data.$entity.create.tsx b/app/src/ui/routes/data/data.$entity.create.tsx index bb83eaf6..07b2bb58 100644 --- a/app/src/ui/routes/data/data.$entity.create.tsx +++ b/app/src/ui/routes/data/data.$entity.create.tsx @@ -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 ; @@ -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 }) { } > - + {error && ( diff --git a/bun.lock b/bun.lock index 6e309474..8d730fcd 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/docs/integration/docker.mdx b/docs/integration/docker.mdx index 387c2516..9b5bc5e2 100644 --- a/docs/integration/docker.mdx +++ b/docs/integration/docker.mdx @@ -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 -``` \ No newline at end of file +``` + +## 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: + labels: + - x-bknd-version= + ports: + - 1337:1337 + environment: + ARGS: "--db-url file:/data/data.db" + volumes: + - ${DATA_DIR:-.}/data:/data +``` diff --git a/docs/usage/database.mdx b/docs/usage/database.mdx index dc520d21..8a43de33 100644 --- a/docs/usage/database.mdx +++ b/docs/usage/database.mdx @@ -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://.turso.io", + authToken: "", }), } 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://.turso.io", + authToken: "", +}) + export default { - connection: libsql(createClient({ - url: "libsql://your-database-url.turso.io", - authToken: "your-auth-token", - })), + connection: libsql(client), } as const satisfies BkndConfig; ```