From e56fc9c368ad7093ef9be4622be3bac6104e9cd1 Mon Sep 17 00:00:00 2001 From: dswbx Date: Tue, 2 Dec 2025 14:03:41 +0100 Subject: [PATCH] finalized sqlocal, added `BkndBrowserApp`, updated react example --- app/__test__/data/postgres.test.ts | 4 +- app/build.ts | 5 + app/package.json | 6 + app/src/adapter/browser/BkndBrowserApp.tsx | 152 ++++++++++ app/src/adapter/browser/OpfsStorageAdapter.ts | 2 +- app/src/adapter/browser/index.ts | 2 + app/src/data/connection/Connection.ts | 3 - app/src/data/connection/postgres/index.ts | 5 - .../sqlite/sqlocal}/SQLocalConnection.ts | 4 + app/src/index.ts | 3 + app/src/plugins/auth/email-otp.plugin.ts | 2 +- bun.lock | 23 +- examples/react/package.json | 1 - examples/react/src/App.tsx | 155 +++------- examples/react/src/OpfsStorageAdapter.ts | 265 ------------------ examples/react/src/routes/_index.tsx | 15 +- 16 files changed, 232 insertions(+), 415 deletions(-) create mode 100644 app/src/adapter/browser/BkndBrowserApp.tsx delete mode 100644 app/src/data/connection/postgres/index.ts rename {packages/sqlocal/src => app/src/data/connection/sqlite/sqlocal}/SQLocalConnection.ts (90%) delete mode 100644 examples/react/src/OpfsStorageAdapter.ts diff --git a/app/__test__/data/postgres.test.ts b/app/__test__/data/postgres.test.ts index f9016cd..1560204 100644 --- a/app/__test__/data/postgres.test.ts +++ b/app/__test__/data/postgres.test.ts @@ -1,8 +1,8 @@ import { describe, beforeAll, afterAll, test } from "bun:test"; -import type { PostgresConnection } from "data/connection/postgres"; +import type { PostgresConnection } from "data/connection/postgres/PostgresConnection"; import { pg, postgresJs } from "bknd"; import { Pool } from "pg"; -import postgres from 'postgres' +import postgres from "postgres"; import { disableConsoleLog, enableConsoleLog, $waitUntil } from "bknd/utils"; import { $ } from "bun"; import { connectionTestSuite } from "data/connection/connection-test-suite"; diff --git a/app/build.ts b/app/build.ts index 599ce82..9c01af2 100644 --- a/app/build.ts +++ b/app/build.ts @@ -267,6 +267,11 @@ async function buildAdapters() { // specific adatpers tsup.build(baseConfig("react-router")), + tsup.build( + baseConfig("browser", { + external: [/^sqlocal\/?.*?/, "wouter"], + }), + ), tsup.build( baseConfig("bun", { external: [/^bun\:.*/], diff --git a/app/package.json b/app/package.json index 8e36a10..19e625b 100644 --- a/app/package.json +++ b/app/package.json @@ -129,6 +129,7 @@ "react-icons": "5.5.0", "react-json-view-lite": "^2.5.0", "sql-formatter": "^15.6.10", + "sqlocal": "^0.16.0", "tailwind-merge": "^3.0.2", "tailwindcss": "^4.1.16", "tailwindcss-animate": "^1.0.7", @@ -257,6 +258,11 @@ "import": "./dist/adapter/aws/index.js", "require": "./dist/adapter/aws/index.js" }, + "./adapter/browser": { + "types": "./dist/types/adapter/browser/index.d.ts", + "import": "./dist/adapter/browser/index.js", + "require": "./dist/adapter/browser/index.js" + }, "./dist/main.css": "./dist/ui/main.css", "./dist/styles.css": "./dist/ui/styles.css", "./dist/manifest.json": "./dist/static/.vite/manifest.json", diff --git a/app/src/adapter/browser/BkndBrowserApp.tsx b/app/src/adapter/browser/BkndBrowserApp.tsx new file mode 100644 index 0000000..8d9cc16 --- /dev/null +++ b/app/src/adapter/browser/BkndBrowserApp.tsx @@ -0,0 +1,152 @@ +import { + createContext, + lazy, + Suspense, + useContext, + useEffect, + useState, + type ReactNode, +} from "react"; +import { checksum } from "bknd/utils"; +import { App, registries, sqlocal, type BkndConfig } from "bknd"; +import { Route, Router, Switch } from "wouter"; +import { type Api, ClientProvider } from "bknd/client"; +import { SQLocalKysely } from "sqlocal/kysely"; +import type { ClientConfig, DatabasePath } from "sqlocal"; +import { OpfsStorageAdapter } from "bknd/adapter/browser"; +import type { BkndAdminConfig } from "bknd/ui"; + +const Admin = lazy(() => + Promise.all([ + import("bknd/ui"), + // @ts-ignore + import("bknd/dist/styles.css"), + ]).then(([mod]) => ({ + default: mod.Admin, + })), +); + +function safeViewTransition(fn: () => void) { + if (document.startViewTransition) { + document.startViewTransition(fn); + } else { + fn(); + } +} + +export type BrowserBkndConfig = Omit< + BkndConfig, + "connection" | "app" +> & { + adminConfig?: BkndAdminConfig; + connection?: ClientConfig | DatabasePath; +}; + +export type BkndBrowserAppProps = { + children: ReactNode; + loading?: ReactNode; + notFound?: ReactNode; +} & BrowserBkndConfig; + +const BkndBrowserAppContext = createContext<{ + app: App; + hash: string; +}>(undefined!); + +export function BkndBrowserApp({ + children, + adminConfig, + loading, + notFound, + ...config +}: BkndBrowserAppProps) { + const [app, setApp] = useState(undefined); + const [api, setApi] = useState(undefined); + const [hash, setHash] = useState(""); + const adminRoutePath = (adminConfig?.basepath ?? "") + "/*?"; + + async function onBuilt(app: App) { + safeViewTransition(async () => { + setApp(app); + setApi(app.getApi()); + setHash(await checksum(app.toJSON())); + }); + } + + useEffect(() => { + setup({ ...config, adminConfig }) + .then((app) => onBuilt(app as any)) + .catch(console.error); + }, []); + + if (!app || !api) { + return ( + loading ?? ( +
+ Loading... +
+ ) + ); + } + + return ( + + + + + {children} + + + + + + + + {notFound ?? ( +
404
+ )} +
+
+
+
+
+ ); +} + +export function useApp() { + return useContext(BkndBrowserAppContext); +} + +const Center = (props: React.HTMLAttributes) => ( +
+); + +let initialized = false; +async function setup(config: BrowserBkndConfig = {}) { + if (initialized) return; + initialized = true; + + registries.media.register("opfs", OpfsStorageAdapter); + + const app = App.create({ + ...config, + // @ts-ignore + connection: sqlocal(new SQLocalKysely(config.connection ?? ":localStorage:")), + }); + + await config.beforeBuild?.(app); + await app.build({ sync: true }); + await config.onBuilt?.(app); + + return app; +} diff --git a/app/src/adapter/browser/OpfsStorageAdapter.ts b/app/src/adapter/browser/OpfsStorageAdapter.ts index af2c891..693451a 100644 --- a/app/src/adapter/browser/OpfsStorageAdapter.ts +++ b/app/src/adapter/browser/OpfsStorageAdapter.ts @@ -4,7 +4,7 @@ import { parse, s, isFile, isBlob } from "bknd/utils"; export const opfsAdapterConfig = s.object( { - root: s.string({ default: "" }), + root: s.string({ default: "" }).optional(), }, { title: "OPFS", diff --git a/app/src/adapter/browser/index.ts b/app/src/adapter/browser/index.ts index e69de29..46607dc 100644 --- a/app/src/adapter/browser/index.ts +++ b/app/src/adapter/browser/index.ts @@ -0,0 +1,2 @@ +export * from "./OpfsStorageAdapter"; +export * from "./BkndBrowserApp"; diff --git a/app/src/data/connection/Connection.ts b/app/src/data/connection/Connection.ts index 0328b35..c9353aa 100644 --- a/app/src/data/connection/Connection.ts +++ b/app/src/data/connection/Connection.ts @@ -6,15 +6,12 @@ import { type CompiledQuery, type DatabaseIntrospector, type Dialect, - type Expression, type Kysely, type KyselyPlugin, type OnModifyForeignAction, type QueryResult, - type RawBuilder, type SelectQueryBuilder, type SelectQueryNode, - type Simplify, sql, } from "kysely"; import type { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite"; diff --git a/app/src/data/connection/postgres/index.ts b/app/src/data/connection/postgres/index.ts deleted file mode 100644 index 011270e..0000000 --- a/app/src/data/connection/postgres/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { pg, PgPostgresConnection, type PgPostgresConnectionConfig } from "./PgPostgresConnection"; -export { PostgresIntrospector } from "./PostgresIntrospector"; -export { PostgresConnection, type QB, plugins } from "./PostgresConnection"; -export { postgresJs, PostgresJsConnection, type PostgresJsConfig } from "./PostgresJsConnection"; -export { createCustomPostgresConnection } from "./custom"; diff --git a/packages/sqlocal/src/SQLocalConnection.ts b/app/src/data/connection/sqlite/sqlocal/SQLocalConnection.ts similarity index 90% rename from packages/sqlocal/src/SQLocalConnection.ts rename to app/src/data/connection/sqlite/sqlocal/SQLocalConnection.ts index d3ad04d..b32a9d7 100644 --- a/packages/sqlocal/src/SQLocalConnection.ts +++ b/app/src/data/connection/sqlite/sqlocal/SQLocalConnection.ts @@ -42,3 +42,7 @@ export class SQLocalConnection extends SqliteConnection { this.initialized = true; } } + +export function sqlocal(instance: InstanceType): SQLocalConnection { + return new SQLocalConnection(instance); +} diff --git a/app/src/index.ts b/app/src/index.ts index 81b4c9a..c158502 100644 --- a/app/src/index.ts +++ b/app/src/index.ts @@ -152,6 +152,9 @@ export { SqliteConnection } from "data/connection/sqlite/SqliteConnection"; export { SqliteIntrospector } from "data/connection/sqlite/SqliteIntrospector"; export { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection"; +// data sqlocal +export { SQLocalConnection, sqlocal } from "data/connection/sqlite/sqlocal/SQLocalConnection"; + // data postgres export { pg, diff --git a/app/src/plugins/auth/email-otp.plugin.ts b/app/src/plugins/auth/email-otp.plugin.ts index 13f9a93..db00012 100644 --- a/app/src/plugins/auth/email-otp.plugin.ts +++ b/app/src/plugins/auth/email-otp.plugin.ts @@ -126,7 +126,7 @@ export function emailOTP({ ...entityConfig, }, "generated", - ), + ) as any, }, ({ index }, schema) => { const otp = schema[entityName]!; diff --git a/bun.lock b/bun.lock index 1a3d426..620c2d1 100644 --- a/bun.lock +++ b/bun.lock @@ -100,6 +100,7 @@ "react-icons": "5.5.0", "react-json-view-lite": "^2.5.0", "sql-formatter": "^15.6.10", + "sqlocal": "^0.16.0", "tailwind-merge": "^3.0.2", "tailwindcss": "^4.1.16", "tailwindcss-animate": "^1.0.7", @@ -155,10 +156,10 @@ "name": "@bknd/sqlocal", "version": "0.0.1", "dependencies": { - "sqlocal": "^0.14.0", + "sqlocal": "^0.16.0", }, "devDependencies": { - "@types/node": "^22.13.10", + "@types/node": "^24.10.1", "@vitest/browser": "^3.0.8", "@vitest/ui": "^3.0.8", "bknd": "workspace:*", @@ -1163,7 +1164,7 @@ "@speed-highlight/core": ["@speed-highlight/core@1.2.7", "", {}, "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g=="], - "@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.48.0-build4", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-hI6twvUkzOmyGZhQMza1gpfqErZxXRw6JEsiVjUbo7tFanVD+8Oil0Ih3l2nGzHdxPI41zFmfUQG7GHqhciKZQ=="], + "@sqlite.org/sqlite-wasm": ["@sqlite.org/sqlite-wasm@3.50.4-build1", "", { "bin": { "sqlite-wasm": "bin/index.js" } }, "sha512-Qig2Wso7gPkU1PtXwFzndh+CTRzrIFxVGqv6eCetjU7YqxlHItj+GvQYwYTppCRgAPawtRN/4AJcEgB9xDHGug=="], "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], @@ -1287,7 +1288,7 @@ "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "@types/node": ["@types/node@22.19.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-xpr/lmLPQEj+TUnHmR+Ab91/glhJvsqcjB+yY0Ix9GO70H6Lb4FHH5GeqdOE5btAx7eIMwuHkp4H2MSkLcqWbA=="], + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], @@ -3323,7 +3324,7 @@ "sql-formatter": ["sql-formatter@15.6.10", "", { "dependencies": { "argparse": "^2.0.1", "nearley": "^2.20.1" }, "bin": { "sql-formatter": "bin/sql-formatter-cli.cjs" } }, "sha512-0bJOPQrRO/JkjQhiThVayq0hOKnI1tHI+2OTkmT7TGtc6kqS+V7kveeMzRW+RNQGxofmTmet9ILvztyuxv0cJQ=="], - "sqlocal": ["sqlocal@0.14.0", "", { "dependencies": { "@sqlite.org/sqlite-wasm": "^3.48.0-build4", "coincident": "^1.2.3" }, "peerDependencies": { "drizzle-orm": "*", "kysely": "*" }, "optionalPeers": ["drizzle-orm", "kysely"] }, "sha512-qhkjWXrvh0aG0IwMeuTznCozNTJvlp2hr+JlSGOx2c1vpjgWoXYvZEf4jcynJ5n/pm05mdKNcDFzb/9KJL/IHQ=="], + "sqlocal": ["sqlocal@0.16.0", "", { "dependencies": { "@sqlite.org/sqlite-wasm": "^3.50.4-build1", "coincident": "^1.2.3" }, "peerDependencies": { "@angular/core": ">=17.0.0", "drizzle-orm": "*", "kysely": "*", "react": ">=18.0.0", "vite": ">=4.0.0", "vue": ">=3.0.0" }, "optionalPeers": ["@angular/core", "drizzle-orm", "kysely", "react", "vite", "vue"] }, "sha512-iK9IAnPGW+98Pw0dWvhPZlapEZ9NaAKMEhRsbY1XlXPpAnRXblF6hP3NGtfLcW2dErWRJ79xzX3tAVZ2jNwqCg=="], "sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="], @@ -3541,7 +3542,7 @@ "undici": ["undici@5.28.5", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA=="], - "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], @@ -4189,8 +4190,6 @@ "base/pascalcase": ["pascalcase@0.1.1", "", {}, "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw=="], - "bknd/@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="], - "bknd/kysely": ["kysely@0.28.8", "", {}, "sha512-QUOgl5ZrS9IRuhq5FvOKFSsD/3+IA6MLE81/bOOTRA/YQpKDza2sFdN5g6JCB9BOpqMJDGefLCQ9F12hRS13TA=="], "bknd/miniflare": ["miniflare@4.20251011.2", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "sharp": "^0.33.5", "stoppable": "1.1.0", "undici": "7.14.0", "workerd": "1.20251011.0", "ws": "8.18.0", "youch": "4.1.0-beta.10", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-5oAaz6lqZus4QFwzEJiNtgpjZR2TBVwBeIhOW33V4gu+l23EukpKja831tFIX2o6sOD/hqZmKZHplOrWl3YGtQ=="], @@ -4843,10 +4842,10 @@ "@types/graceful-fs/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], - "@types/pg/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "@types/resolve/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + "@types/responselike/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@types/ws/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], "@types/yauzl/@types/node/undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], @@ -4909,8 +4908,6 @@ "bknd-cli/@libsql/client/libsql": ["libsql@0.4.7", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.4.7", "@libsql/darwin-x64": "0.4.7", "@libsql/linux-arm64-gnu": "0.4.7", "@libsql/linux-arm64-musl": "0.4.7", "@libsql/linux-x64-gnu": "0.4.7", "@libsql/linux-x64-musl": "0.4.7", "@libsql/win32-x64-msvc": "0.4.7" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ] }, "sha512-T9eIRCs6b0J1SHKYIvD8+KCJMcWZ900iZyxdnSCdqxN12Z1ijzT+jY5nrk72Jw4B0HGzms2NgpryArlJqvc3Lw=="], - "bknd/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "bknd/miniflare/undici": ["undici@7.14.0", "", {}, "sha512-Vqs8HTzjpQXZeXdpsfChQTlafcMQaaIwnGwLam1wudSSjlJeQ3bw1j+TLPePgrCnCpUXx7Ba5Pdpf5OBih62NQ=="], "bknd/miniflare/workerd": ["workerd@1.20251011.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20251011.0", "@cloudflare/workerd-darwin-arm64": "1.20251011.0", "@cloudflare/workerd-linux-64": "1.20251011.0", "@cloudflare/workerd-linux-arm64": "1.20251011.0", "@cloudflare/workerd-windows-64": "1.20251011.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Dq35TLPEJAw7BuYQMkN3p9rge34zWMU2Gnd4DSJFeVqld4+DAO2aPG7+We2dNIAyM97S8Y9BmHulbQ00E0HC7Q=="], @@ -5479,8 +5476,6 @@ "wrangler/miniflare/youch/cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], - "@bknd/plasmic/@types/bun/bun-types/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], - "@vitejs/plugin-react/@babel/core/@babel/generator/@jridgewell/gen-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], "@vitejs/plugin-react/@babel/core/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], diff --git a/examples/react/package.json b/examples/react/package.json index 3d87404..06ea8de 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -10,7 +10,6 @@ "preview": "vite preview" }, "dependencies": { - "@bknd/sqlocal": "file:../../packages/sqlocal", "bknd": "file:../../app", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/examples/react/src/App.tsx b/examples/react/src/App.tsx index 14f3f93..9c9a330 100644 --- a/examples/react/src/App.tsx +++ b/examples/react/src/App.tsx @@ -1,61 +1,7 @@ -import { lazy, Suspense, useEffect, useState } from "react"; -import { checksum } from "bknd/utils"; -import { App, boolean, em, entity, text, registries } from "bknd"; -import { SQLocalConnection } from "@bknd/sqlocal"; -import { Route, Router, Switch } from "wouter"; +import { boolean, em, entity, text } from "bknd"; +import { Route } from "wouter"; import IndexPage from "~/routes/_index"; -import { Center } from "~/components/Center"; -import { type Api, ClientProvider } from "bknd/client"; -import { SQLocalKysely } from "sqlocal/kysely"; -import { OpfsStorageAdapter } from "~/OpfsStorageAdapter"; - -const Admin = lazy(() => import("~/routes/admin")); - -export default function () { - const [app, setApp] = useState(undefined); - const [api, setApi] = useState(undefined); - const [hash, setHash] = useState(""); - - async function onBuilt(app: App) { - document.startViewTransition(async () => { - setApp(app); - setApi(app.getApi()); - setHash(await checksum(app.toJSON())); - }); - } - - useEffect(() => { - setup({ onBuilt }) - .then((app) => console.log("setup", app?.version())) - .catch(console.error); - }, []); - - if (!app || !api) - return ( -
- Loading... -
- ); - - return ( - - - - } /> - - - - - - - -
404
-
-
-
-
- ); -} +import { BkndBrowserApp, type BrowserBkndConfig } from "bknd/adapter/browser"; const schema = em({ todos: entity("todos", { @@ -70,66 +16,41 @@ declare module "bknd" { interface DB extends Database {} } -let initialized = false; -async function setup(opts?: { - beforeBuild?: (app: App) => Promise; - onBuilt?: (app: App) => Promise; -}) { - if (initialized) return; - initialized = true; +const config = { + config: { + data: schema.toJSON(), + auth: { + enabled: true, + jwt: { + secret: "secret", + }, + }, + }, + adminConfig: { + basepath: "/admin", + logo_return_path: "/../", + }, + options: { + // the seed option is only executed if the database was empty + seed: async (ctx) => { + await ctx.em.mutator("todos").insertMany([ + { title: "Learn bknd", done: true }, + { title: "Build something cool", done: false }, + ]); - const connection = new SQLocalConnection( - new SQLocalKysely({ - databasePath: ":localStorage:", - verbose: true, - }), + // @todo: auth is currently not working due to POST request + await ctx.app.module.auth.createUser({ + email: "test@bknd.io", + password: "12345678", + }); + }, + }, +} satisfies BrowserBkndConfig; + +export default function App() { + return ( + + + ); - - registries.media.register("opfs", OpfsStorageAdapter); - - const app = App.create({ - connection, - // an initial config is only applied if the database is empty - config: { - data: schema.toJSON(), - auth: { - enabled: true, - jwt: { - secret: "secret", - }, - }, - }, - options: { - // the seed option is only executed if the database was empty - seed: async (ctx) => { - await ctx.em.mutator("todos").insertMany([ - { title: "Learn bknd", done: true }, - { title: "Build something cool", done: false }, - ]); - - // @todo: auth is currently not working due to POST request - await ctx.app.module.auth.createUser({ - email: "test@bknd.io", - password: "12345678", - }); - }, - }, - }); - - if (opts?.onBuilt) { - app.emgr.onEvent( - App.Events.AppBuiltEvent, - async () => { - await opts.onBuilt?.(app); - // @ts-ignore - window.sql = app.connection.client.sql; - }, - "sync", - ); - } - - await opts?.beforeBuild?.(app); - await app.build({ sync: true }); - - return app; } diff --git a/examples/react/src/OpfsStorageAdapter.ts b/examples/react/src/OpfsStorageAdapter.ts deleted file mode 100644 index 693451a..0000000 --- a/examples/react/src/OpfsStorageAdapter.ts +++ /dev/null @@ -1,265 +0,0 @@ -import type { FileBody, FileListObject, FileMeta, FileUploadPayload } from "bknd"; -import { StorageAdapter, guessMimeType } from "bknd"; -import { parse, s, isFile, isBlob } from "bknd/utils"; - -export const opfsAdapterConfig = s.object( - { - root: s.string({ default: "" }).optional(), - }, - { - title: "OPFS", - description: "Origin Private File System storage", - additionalProperties: false, - }, -); -export type OpfsAdapterConfig = s.Static; - -/** - * Storage adapter for OPFS (Origin Private File System) - * Provides browser-based file storage using the File System Access API - */ -export class OpfsStorageAdapter extends StorageAdapter { - private config: OpfsAdapterConfig; - private rootPromise: Promise; - - constructor(config: Partial = {}) { - super(); - this.config = parse(opfsAdapterConfig, config); - this.rootPromise = this.initializeRoot(); - } - - private async initializeRoot(): Promise { - const opfsRoot = await navigator.storage.getDirectory(); - if (!this.config.root) { - return opfsRoot; - } - - // navigate to or create nested directory structure - const parts = this.config.root.split("/").filter(Boolean); - let current = opfsRoot; - for (const part of parts) { - current = await current.getDirectoryHandle(part, { create: true }); - } - return current; - } - - getSchema() { - return opfsAdapterConfig; - } - - getName(): string { - return "opfs"; - } - - async listObjects(prefix?: string): Promise { - const root = await this.rootPromise; - const files: FileListObject[] = []; - - for await (const [name, handle] of root.entries()) { - if (handle.kind === "file") { - if (!prefix || name.startsWith(prefix)) { - const file = await (handle as FileSystemFileHandle).getFile(); - files.push({ - key: name, - last_modified: new Date(file.lastModified), - size: file.size, - }); - } - } - } - - return files; - } - - private async computeEtagFromArrayBuffer(buffer: ArrayBuffer): Promise { - const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join(""); - - // wrap the hex string in quotes for ETag format - return `"${hashHex}"`; - } - - async putObject(key: string, body: FileBody): Promise { - if (body === null) { - throw new Error("Body is empty"); - } - - const root = await this.rootPromise; - const fileHandle = await root.getFileHandle(key, { create: true }); - const writable = await fileHandle.createWritable(); - - try { - let contentBuffer: ArrayBuffer; - - if (isFile(body)) { - contentBuffer = await body.arrayBuffer(); - await writable.write(contentBuffer); - } else if (body instanceof ReadableStream) { - const chunks: Uint8Array[] = []; - const reader = body.getReader(); - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - await writable.write(value); - } - } finally { - reader.releaseLock(); - } - // compute total size and combine chunks for etag - const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0); - const combined = new Uint8Array(totalSize); - let offset = 0; - for (const chunk of chunks) { - combined.set(chunk, offset); - offset += chunk.length; - } - contentBuffer = combined.buffer; - } else if (isBlob(body)) { - contentBuffer = await (body as Blob).arrayBuffer(); - await writable.write(contentBuffer); - } else { - // body is ArrayBuffer or ArrayBufferView - if (ArrayBuffer.isView(body)) { - const view = body as ArrayBufferView; - contentBuffer = view.buffer.slice( - view.byteOffset, - view.byteOffset + view.byteLength, - ) as ArrayBuffer; - } else { - contentBuffer = body as ArrayBuffer; - } - await writable.write(body); - } - - await writable.close(); - return await this.computeEtagFromArrayBuffer(contentBuffer); - } catch (error) { - await writable.abort(); - throw error; - } - } - - async deleteObject(key: string): Promise { - try { - const root = await this.rootPromise; - await root.removeEntry(key); - } catch { - // file doesn't exist, which is fine - } - } - - async objectExists(key: string): Promise { - try { - const root = await this.rootPromise; - await root.getFileHandle(key); - return true; - } catch { - return false; - } - } - - private parseRangeHeader( - rangeHeader: string, - fileSize: number, - ): { start: number; end: number } | null { - // parse "bytes=start-end" format - const match = rangeHeader.match(/^bytes=(\d*)-(\d*)$/); - if (!match) return null; - - const [, startStr, endStr] = match; - let start = startStr ? Number.parseInt(startStr, 10) : 0; - let end = endStr ? Number.parseInt(endStr, 10) : fileSize - 1; - - // handle suffix-byte-range-spec (e.g., "bytes=-500") - if (!startStr && endStr) { - start = Math.max(0, fileSize - Number.parseInt(endStr, 10)); - end = fileSize - 1; - } - - // validate range - if (start < 0 || end >= fileSize || start > end) { - return null; - } - - return { start, end }; - } - - async getObject(key: string, headers: Headers): Promise { - try { - const root = await this.rootPromise; - const fileHandle = await root.getFileHandle(key); - const file = await fileHandle.getFile(); - const fileSize = file.size; - const mimeType = guessMimeType(key); - - const responseHeaders = new Headers({ - "Accept-Ranges": "bytes", - "Content-Type": mimeType || "application/octet-stream", - }); - - const rangeHeader = headers.get("range"); - - if (rangeHeader) { - const range = this.parseRangeHeader(rangeHeader, fileSize); - - if (!range) { - // invalid range - return 416 Range Not Satisfiable - responseHeaders.set("Content-Range", `bytes */${fileSize}`); - return new Response("", { - status: 416, - headers: responseHeaders, - }); - } - - const { start, end } = range; - const arrayBuffer = await file.arrayBuffer(); - const chunk = arrayBuffer.slice(start, end + 1); - - responseHeaders.set("Content-Range", `bytes ${start}-${end}/${fileSize}`); - responseHeaders.set("Content-Length", chunk.byteLength.toString()); - - return new Response(chunk, { - status: 206, // Partial Content - headers: responseHeaders, - }); - } else { - // normal request - return entire file - const content = await file.arrayBuffer(); - responseHeaders.set("Content-Length", content.byteLength.toString()); - - return new Response(content, { - status: 200, - headers: responseHeaders, - }); - } - } catch { - // handle file reading errors - return new Response("", { status: 404 }); - } - } - - getObjectUrl(_key: string): string { - throw new Error("Method not implemented."); - } - - async getObjectMeta(key: string): Promise { - const root = await this.rootPromise; - const fileHandle = await root.getFileHandle(key); - const file = await fileHandle.getFile(); - - return { - type: guessMimeType(key) || "application/octet-stream", - size: file.size, - }; - } - - toJSON(_secrets?: boolean) { - return { - type: this.getName(), - config: this.config, - }; - } -} diff --git a/examples/react/src/routes/_index.tsx b/examples/react/src/routes/_index.tsx index ae905ec..79cd2e5 100644 --- a/examples/react/src/routes/_index.tsx +++ b/examples/react/src/routes/_index.tsx @@ -1,10 +1,11 @@ import { Center } from "~/components/Center"; -import type { App } from "bknd"; import { useEntityQuery } from "bknd/client"; -import type { SQLocalConnection } from "@bknd/sqlocal/src"; +import type { SQLocalConnection } from "bknd"; +import { useApp } from "bknd/adapter/browser"; import { useEffect, useState } from "react"; +import { Link } from "wouter"; -export default function IndexPage({ app }: { app: App }) { +export default function IndexPage() { //const user = app.getApi().getUser(); const limit = 5; const { data: todos, ...$q } = useEntityQuery("todos", undefined, { @@ -80,7 +81,7 @@ export default function IndexPage({ app }: { app: App }) {
- Go to Admin ➝ + Go to Admin ➝ {/*
{user ? (

@@ -91,12 +92,13 @@ export default function IndexPage({ app }: { app: App }) { )}

*/}
- + ); } -function Debug({ app }: { app: App }) { +function Debug() { + const { app } = useApp(); const [info, setInfo] = useState(); const connection = app.em.connection as SQLocalConnection; @@ -128,6 +130,7 @@ function Debug({ app }: { app: App }) { return (