mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
finalized sqlocal, added BkndBrowserApp, updated react example
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import { describe, beforeAll, afterAll, test } from "bun:test";
|
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 { pg, postgresJs } from "bknd";
|
||||||
import { Pool } from "pg";
|
import { Pool } from "pg";
|
||||||
import postgres from 'postgres'
|
import postgres from "postgres";
|
||||||
import { disableConsoleLog, enableConsoleLog, $waitUntil } from "bknd/utils";
|
import { disableConsoleLog, enableConsoleLog, $waitUntil } from "bknd/utils";
|
||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
import { connectionTestSuite } from "data/connection/connection-test-suite";
|
||||||
|
|||||||
@@ -267,6 +267,11 @@ async function buildAdapters() {
|
|||||||
|
|
||||||
// specific adatpers
|
// specific adatpers
|
||||||
tsup.build(baseConfig("react-router")),
|
tsup.build(baseConfig("react-router")),
|
||||||
|
tsup.build(
|
||||||
|
baseConfig("browser", {
|
||||||
|
external: [/^sqlocal\/?.*?/, "wouter"],
|
||||||
|
}),
|
||||||
|
),
|
||||||
tsup.build(
|
tsup.build(
|
||||||
baseConfig("bun", {
|
baseConfig("bun", {
|
||||||
external: [/^bun\:.*/],
|
external: [/^bun\:.*/],
|
||||||
|
|||||||
@@ -129,6 +129,7 @@
|
|||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
"react-json-view-lite": "^2.5.0",
|
"react-json-view-lite": "^2.5.0",
|
||||||
"sql-formatter": "^15.6.10",
|
"sql-formatter": "^15.6.10",
|
||||||
|
"sqlocal": "^0.16.0",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -257,6 +258,11 @@
|
|||||||
"import": "./dist/adapter/aws/index.js",
|
"import": "./dist/adapter/aws/index.js",
|
||||||
"require": "./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/main.css": "./dist/ui/main.css",
|
||||||
"./dist/styles.css": "./dist/ui/styles.css",
|
"./dist/styles.css": "./dist/ui/styles.css",
|
||||||
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
"./dist/manifest.json": "./dist/static/.vite/manifest.json",
|
||||||
|
|||||||
152
app/src/adapter/browser/BkndBrowserApp.tsx
Normal file
152
app/src/adapter/browser/BkndBrowserApp.tsx
Normal file
@@ -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<Args = ImportMetaEnv> = Omit<
|
||||||
|
BkndConfig<Args>,
|
||||||
|
"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<App | undefined>(undefined);
|
||||||
|
const [api, setApi] = useState<Api | undefined>(undefined);
|
||||||
|
const [hash, setHash] = useState<string>("");
|
||||||
|
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 ?? (
|
||||||
|
<Center>
|
||||||
|
<span style={{ opacity: 0.2 }}>Loading...</span>
|
||||||
|
</Center>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BkndBrowserAppContext.Provider value={{ app, hash }}>
|
||||||
|
<ClientProvider api={api}>
|
||||||
|
<Router key={hash}>
|
||||||
|
<Switch>
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<Route path={adminRoutePath}>
|
||||||
|
<Suspense>
|
||||||
|
<Admin config={adminConfig} />
|
||||||
|
</Suspense>
|
||||||
|
</Route>
|
||||||
|
<Route path="*">
|
||||||
|
{notFound ?? (
|
||||||
|
<Center style={{ fontSize: "48px", fontFamily: "monospace" }}>404</Center>
|
||||||
|
)}
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</Router>
|
||||||
|
</ClientProvider>
|
||||||
|
</BkndBrowserAppContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApp() {
|
||||||
|
return useContext(BkndBrowserAppContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Center = (props: React.HTMLAttributes<HTMLDivElement>) => (
|
||||||
|
<div
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
minHeight: "100vh",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
...(props.style ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import { parse, s, isFile, isBlob } from "bknd/utils";
|
|||||||
|
|
||||||
export const opfsAdapterConfig = s.object(
|
export const opfsAdapterConfig = s.object(
|
||||||
{
|
{
|
||||||
root: s.string({ default: "" }),
|
root: s.string({ default: "" }).optional(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "OPFS",
|
title: "OPFS",
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./OpfsStorageAdapter";
|
||||||
|
export * from "./BkndBrowserApp";
|
||||||
|
|||||||
@@ -6,15 +6,12 @@ import {
|
|||||||
type CompiledQuery,
|
type CompiledQuery,
|
||||||
type DatabaseIntrospector,
|
type DatabaseIntrospector,
|
||||||
type Dialect,
|
type Dialect,
|
||||||
type Expression,
|
|
||||||
type Kysely,
|
type Kysely,
|
||||||
type KyselyPlugin,
|
type KyselyPlugin,
|
||||||
type OnModifyForeignAction,
|
type OnModifyForeignAction,
|
||||||
type QueryResult,
|
type QueryResult,
|
||||||
type RawBuilder,
|
|
||||||
type SelectQueryBuilder,
|
type SelectQueryBuilder,
|
||||||
type SelectQueryNode,
|
type SelectQueryNode,
|
||||||
type Simplify,
|
|
||||||
sql,
|
sql,
|
||||||
} from "kysely";
|
} from "kysely";
|
||||||
import type { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite";
|
import type { jsonArrayFrom, jsonBuildObject, jsonObjectFrom } from "kysely/helpers/sqlite";
|
||||||
|
|||||||
@@ -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";
|
|
||||||
@@ -42,3 +42,7 @@ export class SQLocalConnection extends SqliteConnection<SQLocalKysely> {
|
|||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sqlocal(instance: InstanceType<typeof SQLocalKysely>): SQLocalConnection {
|
||||||
|
return new SQLocalConnection(instance);
|
||||||
|
}
|
||||||
@@ -152,6 +152,9 @@ export { SqliteConnection } from "data/connection/sqlite/SqliteConnection";
|
|||||||
export { SqliteIntrospector } from "data/connection/sqlite/SqliteIntrospector";
|
export { SqliteIntrospector } from "data/connection/sqlite/SqliteIntrospector";
|
||||||
export { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
export { SqliteLocalConnection } from "data/connection/sqlite/SqliteLocalConnection";
|
||||||
|
|
||||||
|
// data sqlocal
|
||||||
|
export { SQLocalConnection, sqlocal } from "data/connection/sqlite/sqlocal/SQLocalConnection";
|
||||||
|
|
||||||
// data postgres
|
// data postgres
|
||||||
export {
|
export {
|
||||||
pg,
|
pg,
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export function emailOTP({
|
|||||||
...entityConfig,
|
...entityConfig,
|
||||||
},
|
},
|
||||||
"generated",
|
"generated",
|
||||||
),
|
) as any,
|
||||||
},
|
},
|
||||||
({ index }, schema) => {
|
({ index }, schema) => {
|
||||||
const otp = schema[entityName]!;
|
const otp = schema[entityName]!;
|
||||||
|
|||||||
23
bun.lock
23
bun.lock
@@ -100,6 +100,7 @@
|
|||||||
"react-icons": "5.5.0",
|
"react-icons": "5.5.0",
|
||||||
"react-json-view-lite": "^2.5.0",
|
"react-json-view-lite": "^2.5.0",
|
||||||
"sql-formatter": "^15.6.10",
|
"sql-formatter": "^15.6.10",
|
||||||
|
"sqlocal": "^0.16.0",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.1.16",
|
"tailwindcss": "^4.1.16",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -155,10 +156,10 @@
|
|||||||
"name": "@bknd/sqlocal",
|
"name": "@bknd/sqlocal",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sqlocal": "^0.14.0",
|
"sqlocal": "^0.16.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.13.10",
|
"@types/node": "^24.10.1",
|
||||||
"@vitest/browser": "^3.0.8",
|
"@vitest/browser": "^3.0.8",
|
||||||
"@vitest/ui": "^3.0.8",
|
"@vitest/ui": "^3.0.8",
|
||||||
"bknd": "workspace:*",
|
"bknd": "workspace:*",
|
||||||
@@ -1163,7 +1164,7 @@
|
|||||||
|
|
||||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.7", "", {}, "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g=="],
|
"@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=="],
|
"@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/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=="],
|
"@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=="],
|
"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=="],
|
"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": ["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=="],
|
"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=="],
|
"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/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=="],
|
"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/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/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/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=="],
|
"@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-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/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=="],
|
"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=="],
|
"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/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=="],
|
"@vitejs/plugin-react/@babel/core/@babel/generator/@jridgewell/trace-mapping/@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bknd/sqlocal": "file:../../packages/sqlocal",
|
|
||||||
"bknd": "file:../../app",
|
"bknd": "file:../../app",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
|||||||
@@ -1,61 +1,7 @@
|
|||||||
import { lazy, Suspense, useEffect, useState } from "react";
|
import { boolean, em, entity, text } from "bknd";
|
||||||
import { checksum } from "bknd/utils";
|
import { Route } from "wouter";
|
||||||
import { App, boolean, em, entity, text, registries } from "bknd";
|
|
||||||
import { SQLocalConnection } from "@bknd/sqlocal";
|
|
||||||
import { Route, Router, Switch } from "wouter";
|
|
||||||
import IndexPage from "~/routes/_index";
|
import IndexPage from "~/routes/_index";
|
||||||
import { Center } from "~/components/Center";
|
import { BkndBrowserApp, type BrowserBkndConfig } from "bknd/adapter/browser";
|
||||||
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<App | undefined>(undefined);
|
|
||||||
const [api, setApi] = useState<Api | undefined>(undefined);
|
|
||||||
const [hash, setHash] = useState<string>("");
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Center>
|
|
||||||
<span className="opacity-20">Loading...</span>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClientProvider api={api}>
|
|
||||||
<Router key={hash}>
|
|
||||||
<Switch>
|
|
||||||
<Route path="/" component={() => <IndexPage app={app} />} />
|
|
||||||
|
|
||||||
<Route path="/admin/*?">
|
|
||||||
<Suspense>
|
|
||||||
<Admin config={{ basepath: "/admin", logo_return_path: "/../" }} />
|
|
||||||
</Suspense>
|
|
||||||
</Route>
|
|
||||||
<Route path="*">
|
|
||||||
<Center className="font-mono text-4xl">404</Center>
|
|
||||||
</Route>
|
|
||||||
</Switch>
|
|
||||||
</Router>
|
|
||||||
</ClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const schema = em({
|
const schema = em({
|
||||||
todos: entity("todos", {
|
todos: entity("todos", {
|
||||||
@@ -70,26 +16,7 @@ declare module "bknd" {
|
|||||||
interface DB extends Database {}
|
interface DB extends Database {}
|
||||||
}
|
}
|
||||||
|
|
||||||
let initialized = false;
|
const config = {
|
||||||
async function setup(opts?: {
|
|
||||||
beforeBuild?: (app: App) => Promise<void>;
|
|
||||||
onBuilt?: (app: App) => Promise<void>;
|
|
||||||
}) {
|
|
||||||
if (initialized) return;
|
|
||||||
initialized = true;
|
|
||||||
|
|
||||||
const connection = new SQLocalConnection(
|
|
||||||
new SQLocalKysely({
|
|
||||||
databasePath: ":localStorage:",
|
|
||||||
verbose: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
registries.media.register("opfs", OpfsStorageAdapter);
|
|
||||||
|
|
||||||
const app = App.create({
|
|
||||||
connection,
|
|
||||||
// an initial config is only applied if the database is empty
|
|
||||||
config: {
|
config: {
|
||||||
data: schema.toJSON(),
|
data: schema.toJSON(),
|
||||||
auth: {
|
auth: {
|
||||||
@@ -99,6 +26,10 @@ async function setup(opts?: {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
adminConfig: {
|
||||||
|
basepath: "/admin",
|
||||||
|
logo_return_path: "/../",
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
// the seed option is only executed if the database was empty
|
// the seed option is only executed if the database was empty
|
||||||
seed: async (ctx) => {
|
seed: async (ctx) => {
|
||||||
@@ -114,22 +45,12 @@ async function setup(opts?: {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
} satisfies BrowserBkndConfig;
|
||||||
|
|
||||||
if (opts?.onBuilt) {
|
export default function App() {
|
||||||
app.emgr.onEvent(
|
return (
|
||||||
App.Events.AppBuiltEvent,
|
<BkndBrowserApp {...config}>
|
||||||
async () => {
|
<Route path="/" component={IndexPage} />
|
||||||
await opts.onBuilt?.(app);
|
</BkndBrowserApp>
|
||||||
// @ts-ignore
|
|
||||||
window.sql = app.connection.client.sql;
|
|
||||||
},
|
|
||||||
"sync",
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
await opts?.beforeBuild?.(app);
|
|
||||||
await app.build({ sync: true });
|
|
||||||
|
|
||||||
return app;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<typeof opfsAdapterConfig>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<FileSystemDirectoryHandle>;
|
|
||||||
|
|
||||||
constructor(config: Partial<OpfsAdapterConfig> = {}) {
|
|
||||||
super();
|
|
||||||
this.config = parse(opfsAdapterConfig, config);
|
|
||||||
this.rootPromise = this.initializeRoot();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async initializeRoot(): Promise<FileSystemDirectoryHandle> {
|
|
||||||
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<FileListObject[]> {
|
|
||||||
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<string> {
|
|
||||||
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<string | FileUploadPayload> {
|
|
||||||
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<void> {
|
|
||||||
try {
|
|
||||||
const root = await this.rootPromise;
|
|
||||||
await root.removeEntry(key);
|
|
||||||
} catch {
|
|
||||||
// file doesn't exist, which is fine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async objectExists(key: string): Promise<boolean> {
|
|
||||||
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<Response> {
|
|
||||||
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<FileMeta> {
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Center } from "~/components/Center";
|
import { Center } from "~/components/Center";
|
||||||
import type { App } from "bknd";
|
|
||||||
import { useEntityQuery } from "bknd/client";
|
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 { 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 user = app.getApi().getUser();
|
||||||
const limit = 5;
|
const limit = 5;
|
||||||
const { data: todos, ...$q } = useEntityQuery("todos", undefined, {
|
const { data: todos, ...$q } = useEntityQuery("todos", undefined, {
|
||||||
@@ -80,7 +81,7 @@ export default function IndexPage({ app }: { app: App }) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col items-center gap-1">
|
<div className="flex flex-col items-center gap-1">
|
||||||
<a href="/admin">Go to Admin ➝</a>
|
<Link to="/admin">Go to Admin ➝</Link>
|
||||||
{/*<div className="opacity-50 text-sm">
|
{/*<div className="opacity-50 text-sm">
|
||||||
{user ? (
|
{user ? (
|
||||||
<p>
|
<p>
|
||||||
@@ -91,12 +92,13 @@ export default function IndexPage({ app }: { app: App }) {
|
|||||||
)}
|
)}
|
||||||
</div>*/}
|
</div>*/}
|
||||||
</div>
|
</div>
|
||||||
<Debug app={app} />
|
<Debug />
|
||||||
</Center>
|
</Center>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Debug({ app }: { app: App }) {
|
function Debug() {
|
||||||
|
const { app } = useApp();
|
||||||
const [info, setInfo] = useState<any>();
|
const [info, setInfo] = useState<any>();
|
||||||
const connection = app.em.connection as SQLocalConnection;
|
const connection = app.em.connection as SQLocalConnection;
|
||||||
|
|
||||||
@@ -128,6 +130,7 @@ function Debug({ app }: { app: App }) {
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 items-center">
|
<div className="flex flex-col gap-2 items-center">
|
||||||
<button
|
<button
|
||||||
|
type="button"
|
||||||
className="bg-foreground/20 leading-none py-2 px-3.5 rounded-lg text-sm hover:bg-foreground/30 transition-colors cursor-pointer"
|
className="bg-foreground/20 leading-none py-2 px-3.5 rounded-lg text-sm hover:bg-foreground/30 transition-colors cursor-pointer"
|
||||||
onClick={download}
|
onClick={download}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user